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.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         HTTP Index Sorter
// @namespace    https://greasyfork.org/en/users/1574063-primetime43
// @version      1.0
// @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://greasyfork.org/en/users/1574063-primetime43
// @supportURL   https://greasyfork.org/en/users/1574063-primetime43
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/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 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 ---

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

    function saveUrls(urls) {
        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 ---

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

    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');
            const textNode = node.nextSibling;
            const meta = (textNode && textNode.nodeType === 3) ? textNode.textContent : '';

            if (href === '../' || href === '/') {
                parentEntry = { element: node, 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, 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) {
            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 };
            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) {
            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 };
            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) return -1;
        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);
    }

    // --- 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.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.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 createDateHeader(label) {
        const header = document.createElement('div');
        header.className = 'http-index-sorter-date-header';
        header.textContent = '\u2500\u2500 ' + label + ' \u2500\u2500';
        header.style.cssText = `
            font-family: monospace;
            font-size: 12px;
            color: #888;
            padding: 6px 0 2px 0;
            font-weight: bold;
        `;
        return header;
    }

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

    // --- 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 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';
        }
    }

    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 idx = i;
                removeBtn.addEventListener('click', () => {
                    const u = loadUrls();
                    u.splice(idx, 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);
    }

})();