HTTP Index Sorter

Enhances pre-formatted HTTP directory listings (Nginx, lighttpd, seedbox indexes, etc.) with sortable columns (name, date, size), file type icons, date grouping, and per-site preferences. Configure which sites to activate on via URL patterns.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         HTTP Index Sorter
// @namespace    https://greatest.deepsurf.us/en/users/1574063-primetime43
// @version      1.3
// @description  Enhances pre-formatted HTTP directory listings (Nginx, lighttpd, seedbox indexes, etc.) with sortable columns (name, date, size), file type icons, date grouping, and per-site preferences. Configure which sites to activate on via URL patterns.
// @author       primetime43
// @match        *://*/*
// @homepageURL  https://greatest.deepsurf.us/en/users/1574063-primetime43
// @supportURL   https://greatest.deepsurf.us/en/users/1574063-primetime43
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // --- File Type Icons ---

    const FILE_ICONS = {
        // Video
        video: { icon: '\uD83C\uDFA5', extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v', 'mpg', 'mpeg', 'ts'] },
        // Audio
        audio: { icon: '\uD83C\uDFB5', extensions: ['mp3', 'flac', 'wav', 'aac', 'ogg', 'wma', 'm4a', 'opus'] },
        // Images
        image: { icon: '\uD83D\uDDBC\uFE0F', extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif'] },
        // Archives
        archive: { icon: '\uD83D\uDCE6', extensions: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'zst', 'tgz', 'tar.gz'] },
        // Documents
        document: { icon: '\uD83D\uDCC4', extensions: ['pdf', 'doc', 'docx', 'odt', 'rtf', 'txt', 'epub', 'mobi'] },
        // Code
        code: { icon: '\uD83D\uDCDD', extensions: ['js', 'ts', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'go', 'rs', 'rb', 'php', 'html', 'css', 'json', 'xml', 'yaml', 'yml', 'sh', 'bat'] },
        // Disk images
        disk: { icon: '\uD83D\uDCBF', extensions: ['iso', 'img', 'dmg', 'bin', 'cue', 'nrg'] },
        // Executables
        exe: { icon: '\u2699\uFE0F', extensions: ['exe', 'msi', 'apk', 'deb', 'rpm', 'appimage'] },
        // Subtitles
        subtitle: { icon: '\uD83D\uDCAC', extensions: ['srt', 'sub', 'ass', 'ssa', 'vtt'] },
        // Torrent
        torrent: { icon: '\uD83E\uDDF2', extensions: ['torrent'] },
        // NFO
        nfo: { icon: '\u2139\uFE0F', extensions: ['nfo', 'nzb'] },
    };

    const MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };

    const FOLDER_ICON = '\uD83D\uDCC1';
    const DEFAULT_FILE_ICON = '\uD83D\uDCC3';

    function getFileIcon(name) {
        if (name.endsWith('/')) return FOLDER_ICON;
        const lower = name.toLowerCase();
        for (const category of Object.values(FILE_ICONS)) {
            if (category.extensions.some(ext => lower.endsWith('.' + ext))) {
                return category.icon;
            }
        }
        return DEFAULT_FILE_ICON;
    }

    // --- URL Pattern Management ---

    let _urlsCache = null;

    function loadUrls() {
        if (_urlsCache !== null) return _urlsCache;
        const stored = GM_getValue('urls', null);
        if (stored === null) {
            GM_setValue('urls', JSON.stringify([]));
            _urlsCache = [];
            return [];
        }
        try { _urlsCache = JSON.parse(stored); } catch { _urlsCache = []; }
        return _urlsCache;
    }

    function saveUrls(urls) {
        _urlsCache = urls;
        _sortKeyCache = null; // invalidate since matching pattern may change
        GM_setValue('urls', JSON.stringify(urls));
    }

    function globToRegex(pattern) {
        const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
        const withWildcards = escaped.replace(/\*/g, '.*');
        return new RegExp('^' + withWildcards + '$');
    }

    function urlMatches(url, patterns) {
        return patterns.some(p => globToRegex(p).test(url));
    }

    // --- Per-Site Sort Preferences ---

    let _sortKeyCache = null;

    function getSortKey() {
        if (_sortKeyCache !== null) return _sortKeyCache;
        const patterns = loadUrls();
        for (const p of patterns) {
            if (globToRegex(p).test(location.href)) {
                _sortKeyCache = 'sort_' + p;
                return _sortKeyCache;
            }
        }
        _sortKeyCache = 'sort_global';
        return _sortKeyCache;
    }

    function loadSort() {
        const key = getSortKey();
        try { return JSON.parse(GM_getValue(key, 'null')); } catch { return null; }
    }

    function saveSort(column, ascending) {
        const key = getSortKey();
        GM_setValue(key, JSON.stringify({ column, ascending }));
    }

    function loadGroupByDate() {
        const key = getSortKey() + '_group';
        return GM_getValue(key, false);
    }

    function saveGroupByDate(enabled) {
        const key = getSortKey() + '_group';
        GM_setValue(key, enabled);
    }

    // --- Directory Listing Detection ---

    function isDirectoryListing() {
        const pre = document.querySelector('pre');
        if (!pre) return false;
        const links = pre.querySelectorAll('a[href]');
        if (links.length < 1) return false;
        const text = pre.textContent || '';
        return /\d{2}-[A-Za-z]{3}-\d{4}/.test(text) || /\d{4}-\d{2}-\d{2}/.test(text);
    }

    // --- Parsing ---

    function parseEntries() {
        const pre = document.querySelector('pre');
        const children = Array.from(pre.childNodes);
        const entries = [];
        let parentEntry = null;

        for (let i = 0; i < children.length; i++) {
            const node = children[i];
            if (node.nodeName !== 'A') continue;

            const href = node.getAttribute('href');
            // Skip past any copy button spans to find the text node
            let sibling = node.nextSibling;
            let copyBtnNode = null;
            if (sibling && sibling.nodeType === 1 && sibling.classList &&
                sibling.classList.contains('http-index-sorter-copy-btn')) {
                copyBtnNode = sibling;
                sibling = sibling.nextSibling;
            }
            const textNode = sibling;
            const meta = (textNode && textNode.nodeType === 3) ? textNode.textContent : '';

            if (href === '../' || href === '/') {
                parentEntry = { element: node, copyBtn: copyBtnNode, textNode: textNode, name: '../', date: null, size: -1, isParent: true };
                continue;
            }

            const name = node.textContent.trim();
            const date = parseDate(meta);
            const size = parseSize(meta);

            entries.push({ element: node, copyBtn: copyBtnNode, textNode: textNode, name, date, size, isParent: false });
        }

        return { entries, parentEntry, pre };
    }

    function parseDate(text) {
        // Format: 02-Jan-2024 12:34
        let m = text.match(/(\d{2})-([A-Za-z]{3})-(\d{4})\s+(\d{2}):(\d{2})/);
        if (m) return new Date(+m[3], MONTHS[m[2]] ?? 0, +m[1], +m[4], +m[5]);
        // Format: 2024-01-02 12:34
        m = text.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);
        if (m) return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]);
        // Date only
        m = text.match(/(\d{2})-([A-Za-z]{3})-(\d{4})/);
        if (m) return new Date(+m[3], MONTHS[m[2]] ?? 0, +m[1]);
        return null;
    }

    function parseSize(text) {
        const m = text.match(/([\d.]+)\s*([BKMGT]i?B?|[BKMGT])\b/i);
        if (m) {
            const val = parseFloat(m[1]);
            const unit = m[2].charAt(0).toUpperCase();
            const multipliers = { B: 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
            return val * (multipliers[unit] || 1);
        }
        // Plain byte count with no unit suffix (e.g. "1234")
        const plain = text.match(/\b(\d+)\b/);
        if (plain) return parseInt(plain[1], 10);
        return -1;
    }

    // --- Sorting ---

    function sortEntries(entries, column, ascending) {
        const dir = ascending ? 1 : -1;
        entries.sort((a, b) => {
            switch (column) {
                case 'name':
                    return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
                case 'date': {
                    const da = a.date ? a.date.getTime() : 0;
                    const db = b.date ? b.date.getTime() : 0;
                    return dir * (da - db);
                }
                case 'size':
                    return dir * (a.size - b.size);
                default:
                    return 0;
            }
        });
    }

    function applySort(column, ascending) {
        const { entries, parentEntry, pre } = parseEntries();
        removeDateHeaders(pre);
        sortEntries(entries, column, ascending);

        // Rebuild content
        while (pre.firstChild) pre.removeChild(pre.firstChild);

        if (parentEntry) {
            pre.appendChild(parentEntry.element);
            if (parentEntry.copyBtn) pre.appendChild(parentEntry.copyBtn);
            if (parentEntry.textNode) pre.appendChild(parentEntry.textNode);
        }

        let lastDateLabel = null;
        for (const entry of entries) {
            if (groupByDate) {
                const label = getDateLabel(entry.date);
                if (label !== lastDateLabel) {
                    pre.appendChild(createDateHeader(label));
                    pre.appendChild(document.createTextNode('\n'));
                    lastDateLabel = label;
                }
            }
            pre.appendChild(entry.element);
            if (entry.copyBtn) pre.appendChild(entry.copyBtn);
            if (entry.textNode) pre.appendChild(entry.textNode);
        }

        // Save per-site preference
        saveSort(column, ascending);
        updateToolbarArrows(column, ascending);
        updateItemCount();
    }

    // --- Date Grouping ---

    let groupByDate = false;

    function getDateLabel(date) {
        if (!date) return 'Unknown Date';
        const now = new Date();
        const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
        const yesterday = new Date(today.getTime() - 86400000);
        const entryDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());

        if (entryDay.getTime() === today.getTime()) return 'Today';
        if (entryDay.getTime() === yesterday.getTime()) return 'Yesterday';

        const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
        return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
    }

    function createSectionActionBtn(text, title) {
        const btn = document.createElement('span');
        btn.textContent = text;
        btn.title = title;
        btn.style.cssText = `
            cursor: pointer; margin-left: 8px; font-size: 11px;
            padding: 1px 6px; border: 1px solid #aaa; border-radius: 3px;
            background: #f0f0f0; color: #555; user-select: none;
            font-weight: normal;
        `;
        btn.onmouseenter = () => { btn.style.background = '#e0e0e0'; };
        btn.onmouseleave = () => { btn.style.background = '#f0f0f0'; };
        return btn;
    }

    function getLinksInSection(header) {
        const links = [];
        let node = header.nextSibling;
        while (node) {
            if (node.classList && node.classList.contains('http-index-sorter-date-header')) break;
            if (node.nodeName === 'A') {
                const href = node.getAttribute('href');
                if (href && href !== '../' && href !== '/') links.push(node);
            }
            node = node.nextSibling;
        }
        return links;
    }

    function getLinksInSectionDeep(header) {
        const links = [];
        let node = header.nextSibling;
        while (node) {
            if (node.classList && node.classList.contains('http-index-sorter-date-header')) break;
            if (node.nodeName === 'A') {
                const href = node.getAttribute('href');
                if (href && href !== '../' && href !== '/') links.push(node);
            } else if (node.classList && node.classList.contains('http-index-sorter-expansion')) {
                const subLinks = node.querySelectorAll('a[href]');
                for (const a of subLinks) {
                    const href = a.getAttribute('href');
                    if (href && href !== '../' && href !== '/') links.push(a);
                }
            }
            node = node.nextSibling;
        }
        return links;
    }

    function showCopyByTypeMenu(header, anchorBtn) {
        const existing = document.getElementById('http-index-sorter-type-menu');
        if (existing) { existing.remove(); return; }

        const links = getLinksInSectionDeep(header);
        const byExt = new Map();
        for (const a of links) {
            const href = a.getAttribute('href') || '';
            if (href.endsWith('/')) continue;
            const name = decodeURIComponent(href.split('/').pop().split('?')[0]);
            const m = name.match(/\.([a-z0-9]+)$/i);
            const ext = m ? m[1].toLowerCase() : '(no ext)';
            if (!byExt.has(ext)) byExt.set(ext, []);
            byExt.get(ext).push(resolveHref(a));
        }

        if (byExt.size === 0) return;

        const entries = Array.from(byExt.entries()).sort((a, b) => b[1].length - a[1].length);

        const menu = document.createElement('div');
        menu.id = 'http-index-sorter-type-menu';
        const rect = anchorBtn.getBoundingClientRect();
        menu.style.cssText = `
            position: absolute;
            top: ${rect.bottom + window.scrollY + 4}px;
            left: ${rect.left + window.scrollX}px;
            background: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            font-family: monospace;
            font-size: 12px;
            z-index: 9999;
            min-width: 160px;
            padding: 4px 0;
        `;

        // "All files" option at top
        const allUrls = entries.filter(([ext]) => ext !== '(no ext)' || true)
                               .flatMap(([, urls]) => urls);
        const allItem = document.createElement('div');
        allItem.style.cssText = `
            padding: 4px 12px; cursor: pointer; display: flex;
            justify-content: space-between; gap: 16px; color: #333;
            border-bottom: 1px solid #eee;
        `;
        const allName = document.createElement('span');
        allName.textContent = 'All files';
        allName.style.fontWeight = 'bold';
        const allCount = document.createElement('span');
        allCount.textContent = allUrls.length;
        allCount.style.color = '#888';
        allItem.appendChild(allName);
        allItem.appendChild(allCount);
        allItem.onmouseenter = () => { allItem.style.background = '#f0f0f0'; };
        allItem.onmouseleave = () => { allItem.style.background = '#fff'; };
        allItem.addEventListener('click', () => {
            navigator.clipboard.writeText(allUrls.join('\n'));
            allName.textContent = '\u2713 copied ' + allUrls.length;
            setTimeout(() => menu.remove(), 700);
        });
        menu.appendChild(allItem);

        for (const [ext, urls] of entries) {
            const item = document.createElement('div');
            item.style.cssText = `
                padding: 4px 12px; cursor: pointer; display: flex;
                justify-content: space-between; gap: 16px; color: #333;
            `;
            const nameSpan = document.createElement('span');
            nameSpan.textContent = ext === '(no ext)' ? '(no extension)' : '.' + ext;
            const countSpan = document.createElement('span');
            countSpan.textContent = urls.length;
            countSpan.style.color = '#888';
            item.appendChild(nameSpan);
            item.appendChild(countSpan);
            item.onmouseenter = () => { item.style.background = '#f0f0f0'; };
            item.onmouseleave = () => { item.style.background = '#fff'; };
            item.addEventListener('click', () => {
                navigator.clipboard.writeText(urls.join('\n'));
                nameSpan.textContent = '\u2713 copied ' + urls.length;
                setTimeout(() => menu.remove(), 700);
            });
            menu.appendChild(item);
        }

        document.body.appendChild(menu);

        setTimeout(() => {
            const dismissHandler = (e) => {
                if (!menu.contains(e.target) && e.target !== anchorBtn) {
                    menu.remove();
                    document.removeEventListener('click', dismissHandler);
                }
            };
            document.addEventListener('click', dismissHandler);
        }, 0);
    }

    function createDateHeader(label) {
        const header = document.createElement('div');
        header.className = 'http-index-sorter-date-header';
        header.style.cssText = `
            font-family: monospace;
            font-size: 12px;
            color: #888;
            padding: 6px 0 2px 0;
            font-weight: bold;
            display: flex;
            align-items: center;
        `;

        const labelSpan = document.createElement('span');
        labelSpan.textContent = '\u2500\u2500 ' + label + ' \u2500\u2500';
        header.appendChild(labelSpan);

        // Expand folders button — fetches and displays each subfolder's contents inline
        const expandBtn = createSectionActionBtn('\uD83D\uDCC2 Expand folders', 'Fetch and display all folder contents inline');
        let expanded = false;
        expandBtn.addEventListener('click', async () => {
            const folders = getLinksInSection(header).filter(a => a.getAttribute('href').endsWith('/'));
            if (folders.length === 0) return;

            if (expanded) {
                folders.forEach(collapseFolderInline);
                expanded = false;
                expandBtn.textContent = '\uD83D\uDCC2 Expand folders';
                return;
            }

            expanded = true;
            expandBtn.textContent = '\uD83D\uDCC1 Collapse folders';
            await Promise.all(folders.map(expandFolderInline));
        });
        header.appendChild(expandBtn);

        // Copy URLs button (includes expanded contents if any)
        const copyBtn = createSectionActionBtn('\uD83D\uDCCB Copy URLs', 'Copy all URLs in this section (including expanded folder contents)');
        copyBtn.addEventListener('click', () => {
            const urls = getLinksInSectionDeep(header).map(a => resolveHref(a));
            navigator.clipboard.writeText(urls.join('\n'));
            showCopyFeedback(copyBtn, '\uD83D\uDCCB Copy URLs');
        });
        header.appendChild(copyBtn);

        // Copy by type button
        const copyTypeBtn = createSectionActionBtn('\uD83C\uDFF7\uFE0F Copy by type', 'Copy URLs filtered by file extension');
        copyTypeBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            showCopyByTypeMenu(header, copyTypeBtn);
        });
        header.appendChild(copyTypeBtn);

        return header;
    }

    function removeDateHeaders(pre) {
        const headers = pre.querySelectorAll('.http-index-sorter-date-header');
        for (const h of headers) h.remove();
    }

    // --- Inline Folder Expansion ---

    async function expandFolderInline(folderLink) {
        if (folderLink._expansionContainer) return;

        const url = resolveHref(folderLink);
        const container = document.createElement('div');
        container.className = 'http-index-sorter-expansion';
        container.style.cssText = `
            padding: 4px 0 4px 24px;
            margin: 2px 0 4px 0;
            border-left: 2px solid #8ab4f8;
            background: #fafbff;
            color: #555;
            font-family: monospace;
            font-size: 12px;
            white-space: pre;
        `;
        container.textContent = 'Loading...';

        // Insert after the folder's metadata text node
        let cursor = folderLink.nextSibling;
        if (cursor && cursor.nodeType === 1 &&
            cursor.classList && cursor.classList.contains('http-index-sorter-copy-btn')) {
            cursor = cursor.nextSibling;
        }
        const insertBefore = (cursor && cursor.nodeType === 3) ? cursor.nextSibling : cursor;
        folderLink.parentNode.insertBefore(container, insertBefore);
        folderLink._expansionContainer = container;

        try {
            const resp = await fetch(url);
            if (!resp.ok) throw new Error('HTTP ' + resp.status);
            const html = await resp.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const pre = doc.querySelector('pre');
            if (!pre) throw new Error('no directory listing at target');

            container.innerHTML = '';
            const nodes = Array.from(pre.childNodes);
            let count = 0;
            for (let i = 0; i < nodes.length; i++) {
                const node = nodes[i];
                if (node.nodeName !== 'A') continue;
                const href = node.getAttribute('href');
                if (href === '../' || href === '/') continue;

                const newA = document.createElement('a');
                newA.href = new URL(href, url).href;
                const iconSpan = document.createElement('span');
                iconSpan.textContent = getFileIcon(href) + ' ';
                iconSpan.style.marginRight = '2px';
                newA.appendChild(iconSpan);
                newA.appendChild(document.createTextNode(node.textContent.trim()));
                container.appendChild(newA);
                container.appendChild(createHoverCopyButton(newA));

                const next = nodes[i + 1];
                if (next && next.nodeType === 3) {
                    container.appendChild(document.createTextNode(next.textContent));
                    i++;
                } else {
                    container.appendChild(document.createTextNode('\n'));
                }
                count++;
            }

            if (count === 0) {
                container.textContent = '(empty)';
                container.style.fontStyle = 'italic';
            }
        } catch (err) {
            container.textContent = 'Failed to load: ' + (err.message || err);
            container.style.color = '#c00';
            container.style.borderLeftColor = '#c00';
        }
    }

    function collapseFolderInline(folderLink) {
        if (folderLink._expansionContainer) {
            folderLink._expansionContainer.remove();
            folderLink._expansionContainer = null;
        }
    }

    // --- UI: Toolbar ---

    let toolbarButtons = {};
    let itemCountSpan = null;
    let groupBtn = null;

    function updateItemCount() {
        if (!itemCountSpan) return;
        const pre = document.querySelector('pre');
        const allLinks = pre.querySelectorAll('a[href]');
        let total = 0;
        for (const link of allLinks) {
            const href = link.getAttribute('href');
            if (href === '../' || href === '/') continue;
            total++;
        }
        itemCountSpan.textContent = total + ' items';
    }

    function updateToolbarArrows(activeColumn, ascending) {
        for (const [col, btn] of Object.entries(toolbarButtons)) {
            const arrow = btn.querySelector('.sort-arrow');
            if (col === activeColumn) {
                arrow.textContent = ascending ? ' \u25B2' : ' \u25BC';
            } else {
                arrow.textContent = '';
            }
        }
    }

    function resolveHref(link) {
        return new URL(link.getAttribute('href'), location.href).href;
    }

    function showCopyFeedback(btn, originalText) {
        btn.textContent = '\u2713';
        setTimeout(() => { btn.textContent = originalText; }, 1000);
    }

    function createHoverCopyButton(link) {
        const copyBtn = document.createElement('span');
        copyBtn.className = 'http-index-sorter-copy-btn';
        copyBtn.textContent = '\uD83D\uDCCB';
        copyBtn.title = 'Copy URL';
        copyBtn.style.cssText = `
            cursor: pointer; margin-left: 4px; font-size: 12px;
            opacity: 0; transition: opacity 0.15s;
            vertical-align: middle; user-select: none;
        `;
        copyBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            navigator.clipboard.writeText(resolveHref(link));
            showCopyFeedback(copyBtn, '\uD83D\uDCCB');
        });
        link.addEventListener('mouseenter', () => { copyBtn.style.opacity = '1'; });
        link.addEventListener('mouseleave', (e) => {
            if (e.relatedTarget !== copyBtn) copyBtn.style.opacity = '0';
        });
        copyBtn.addEventListener('mouseenter', () => { copyBtn.style.opacity = '1'; });
        copyBtn.addEventListener('mouseleave', () => { copyBtn.style.opacity = '0'; });
        return copyBtn;
    }

    function addFileIcons() {
        const pre = document.querySelector('pre');
        const links = pre.querySelectorAll('a[href]');
        for (const link of links) {
            const href = link.getAttribute('href');
            if (href === '../' || href === '/') continue;
            if (link.dataset.iconAdded) continue;
            const icon = getFileIcon(href);
            const iconSpan = document.createElement('span');
            iconSpan.textContent = icon + ' ';
            iconSpan.style.cssText = 'font-style: normal; margin-right: 2px;';
            link.insertBefore(iconSpan, link.firstChild);
            link.dataset.iconAdded = 'true';

            const copyBtn = createHoverCopyButton(link);
            if (link.nextSibling) {
                link.parentNode.insertBefore(copyBtn, link.nextSibling);
            } else {
                link.parentNode.appendChild(copyBtn);
            }
        }
    }

    function createToolbar() {
        const pre = document.querySelector('pre');
        const toolbar = document.createElement('div');
        toolbar.id = 'http-index-sorter-toolbar';
        toolbar.style.cssText = `
            font-family: monospace;
            font-size: 13px;
            padding: 6px 8px;
            margin-bottom: 4px;
            background: #f5f5f5;
            border: 1px solid #ddd;
            border-radius: 3px;
            display: flex;
            align-items: center;
            gap: 4px;
            color: #333;
        `;

        const label = document.createElement('span');
        label.textContent = 'Sort:';
        label.style.cssText = 'margin-right: 4px; color: #666;';
        toolbar.appendChild(label);

        let currentSort = loadSort();

        const columns = [
            { key: 'name', label: 'Name' },
            { key: 'date', label: 'Date' },
            { key: 'size', label: 'Size' },
        ];

        for (const col of columns) {
            const btn = document.createElement('button');
            btn.style.cssText = `
                font-family: monospace;
                font-size: 13px;
                padding: 2px 8px;
                border: 1px solid #ccc;
                border-radius: 3px;
                background: #fff;
                cursor: pointer;
                color: #333;
            `;
            btn.onmouseenter = () => { btn.style.background = '#e8e8e8'; };
            btn.onmouseleave = () => { btn.style.background = '#fff'; };

            const textSpan = document.createElement('span');
            textSpan.textContent = col.label;
            btn.appendChild(textSpan);

            const arrow = document.createElement('span');
            arrow.className = 'sort-arrow';
            arrow.textContent = '';
            btn.appendChild(arrow);

            let ascending = true;
            if (currentSort && currentSort.column === col.key) {
                ascending = currentSort.ascending;
            }

            btn.addEventListener('click', () => {
                const parsed = loadSort();
                if (parsed && parsed.column === col.key) {
                    ascending = !parsed.ascending;
                } else {
                    ascending = true;
                }
                applySort(col.key, ascending);
            });

            toolbarButtons[col.key] = btn;
            toolbar.appendChild(btn);
        }

        // Separator
        const sep = document.createElement('span');
        sep.textContent = '|';
        sep.style.cssText = 'color: #ccc; margin: 0 2px;';
        toolbar.appendChild(sep);

        // Group by Date toggle
        groupBtn = document.createElement('button');
        groupBtn.title = 'Group entries by date';
        groupBtn.style.cssText = `
            font-family: monospace;
            font-size: 13px;
            padding: 2px 8px;
            border: 1px solid #ccc;
            border-radius: 3px;
            background: #fff;
            cursor: pointer;
            color: #333;
        `;
        function updateGroupBtnLabel() {
            groupBtn.textContent = groupByDate ? 'Group: ON' : 'Group: OFF';
            groupBtn.style.background = groupByDate ? '#e0ecff' : '#fff';
            groupBtn.style.borderColor = groupByDate ? '#8ab4f8' : '#ccc';
        }
        updateGroupBtnLabel();
        groupBtn.onmouseenter = () => { if (!groupByDate) groupBtn.style.background = '#e8e8e8'; };
        groupBtn.onmouseleave = () => { groupBtn.style.background = groupByDate ? '#e0ecff' : '#fff'; };
        groupBtn.addEventListener('click', () => {
            groupByDate = !groupByDate;
            saveGroupByDate(groupByDate);
            updateGroupBtnLabel();
            // When enabling grouping, force sort by date descending
            if (groupByDate) {
                applySort('date', false);
            } else {
                const parsed = loadSort();
                if (parsed && parsed.column) {
                    applySort(parsed.column, parsed.ascending);
                } else {
                    applySort('name', true);
                }
            }
        });
        toolbar.appendChild(groupBtn);

        // Spacer
        const spacer = document.createElement('span');
        spacer.style.flex = '1';
        toolbar.appendChild(spacer);

        // Item count
        itemCountSpan = document.createElement('span');
        itemCountSpan.style.cssText = 'color: #888; font-size: 12px; margin-right: 8px;';
        toolbar.appendChild(itemCountSpan);

        // Settings gear
        const gear = document.createElement('button');
        gear.textContent = '\u2699';
        gear.title = 'URL pattern settings';
        gear.style.cssText = `
            font-size: 16px;
            padding: 2px 6px;
            border: 1px solid #ccc;
            border-radius: 3px;
            background: #fff;
            cursor: pointer;
            color: #666;
        `;
        gear.onmouseenter = () => { gear.style.background = '#e8e8e8'; };
        gear.onmouseleave = () => { gear.style.background = '#fff'; };
        gear.addEventListener('click', openSettings);
        toolbar.appendChild(gear);

        pre.parentNode.insertBefore(toolbar, pre);

        return currentSort;
    }

    // --- UI: Settings Panel ---

    function openSettings() {
        if (document.getElementById('http-index-sorter-settings')) return;

        const overlay = document.createElement('div');
        overlay.id = 'http-index-sorter-settings';
        overlay.style.cssText = `
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.4); z-index: 10000;
            display: flex; align-items: center; justify-content: center;
        `;

        const panel = document.createElement('div');
        panel.style.cssText = `
            background: #fff; border-radius: 6px; padding: 20px;
            min-width: 420px; max-width: 600px; max-height: 80vh;
            font-family: monospace; font-size: 13px; color: #333;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            display: flex; flex-direction: column; gap: 12px;
        `;

        const title = document.createElement('div');
        title.textContent = 'HTTP Index Sorter \u2014 URL Patterns';
        title.style.cssText = 'font-size: 15px; font-weight: bold; margin-bottom: 4px;';
        panel.appendChild(title);

        const desc = document.createElement('div');
        desc.textContent = 'The script activates on pages matching these patterns. Use * as a wildcard.';
        desc.style.cssText = 'color: #666; margin-bottom: 8px;';
        panel.appendChild(desc);

        const urls = loadUrls();
        const list = document.createElement('div');
        list.style.cssText = 'display: flex; flex-direction: column; gap: 6px; max-height: 300px; overflow-y: auto;';

        function renderList() {
            list.innerHTML = '';
            const currentUrls = loadUrls();
            for (let i = 0; i < currentUrls.length; i++) {
                const row = document.createElement('div');
                row.style.cssText = 'display: flex; align-items: center; gap: 6px;';

                const input = document.createElement('input');
                input.type = 'text';
                input.value = currentUrls[i];
                input.style.cssText = `
                    flex: 1; font-family: monospace; font-size: 12px;
                    padding: 4px 6px; border: 1px solid #ccc; border-radius: 3px;
                `;
                input.readOnly = true;
                row.appendChild(input);

                const removeBtn = document.createElement('button');
                removeBtn.textContent = '\u2715';
                removeBtn.title = 'Remove';
                removeBtn.style.cssText = `
                    padding: 4px 8px; border: 1px solid #ccc; border-radius: 3px;
                    background: #fff; cursor: pointer; color: #c00; font-weight: bold;
                `;
                const pattern = currentUrls[i];
                removeBtn.addEventListener('click', () => {
                    const u = loadUrls();
                    const removeIdx = u.indexOf(pattern);
                    if (removeIdx !== -1) {
                        u.splice(removeIdx, 1);
                        saveUrls(u);
                    }
                    renderList();
                });
                row.appendChild(removeBtn);

                list.appendChild(row);
            }
        }

        renderList();
        panel.appendChild(list);

        // Add current URL button
        const addCurrentRow = document.createElement('div');
        addCurrentRow.style.cssText = 'display: flex; gap: 6px;';
        const addCurrentBtn = document.createElement('button');
        addCurrentBtn.textContent = '+ Add Current Page URL';
        addCurrentBtn.title = 'Add a pattern matching the current page';
        addCurrentBtn.style.cssText = `
            flex: 1; padding: 6px 12px; border: 1px solid #5a9e5a; border-radius: 3px;
            background: #5a9e5a; color: #fff; cursor: pointer; font-family: monospace;
            font-size: 12px;
        `;
        addCurrentBtn.onmouseenter = () => { addCurrentBtn.style.background = '#4a8e4a'; };
        addCurrentBtn.onmouseleave = () => { addCurrentBtn.style.background = '#5a9e5a'; };
        addCurrentBtn.addEventListener('click', () => {
            // Generate a pattern from the current URL: replace the last path segment with *
            const url = location.href.replace(/\/[^/]*$/, '/*');
            const u = loadUrls();
            if (!u.includes(url)) {
                u.push(url);
                saveUrls(u);
                location.reload();
            }
        });
        addCurrentRow.appendChild(addCurrentBtn);
        panel.appendChild(addCurrentRow);

        // Add new URL row
        const addRow = document.createElement('div');
        addRow.style.cssText = 'display: flex; gap: 6px;';
        const addInput = document.createElement('input');
        addInput.type = 'text';
        addInput.placeholder = 'https://example.com/files/*';
        addInput.style.cssText = `
            flex: 1; font-family: monospace; font-size: 12px;
            padding: 4px 6px; border: 1px solid #ccc; border-radius: 3px;
        `;
        addRow.appendChild(addInput);

        const addBtn = document.createElement('button');
        addBtn.textContent = 'Add';
        addBtn.style.cssText = `
            padding: 4px 12px; border: 1px solid #4a90d9; border-radius: 3px;
            background: #4a90d9; color: #fff; cursor: pointer; font-family: monospace;
        `;
        addBtn.addEventListener('click', () => {
            const val = addInput.value.trim();
            if (!val) return;
            const u = loadUrls();
            if (!u.includes(val)) {
                u.push(val);
                saveUrls(u);
            }
            addInput.value = '';
            renderList();
        });
        addRow.appendChild(addBtn);
        panel.appendChild(addRow);

        // Close button
        const closeRow = document.createElement('div');
        closeRow.style.cssText = 'text-align: right; margin-top: 4px;';
        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close';
        closeBtn.style.cssText = `
            padding: 4px 16px; border: 1px solid #ccc; border-radius: 3px;
            background: #f5f5f5; cursor: pointer; font-family: monospace;
        `;
        closeBtn.addEventListener('click', () => overlay.remove());
        closeRow.appendChild(closeBtn);
        panel.appendChild(closeRow);

        overlay.appendChild(panel);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
        document.body.appendChild(overlay);
    }

    // --- Menu Command ---

    GM_registerMenuCommand('HTTP Index Sorter Settings', openSettings);

    // --- Init ---

    const urls = loadUrls();
    if (!urlMatches(location.href, urls)) return;
    if (!isDirectoryListing()) return;

    addFileIcons();
    groupByDate = loadGroupByDate();
    const lastSort = createToolbar();
    updateItemCount();

    if (groupByDate) {
        applySort('date', false);
    } else if (lastSort && lastSort.column) {
        applySort(lastSort.column, lastSort.ascending);
    }

})();