YouTube Filter & Sorter (Compact UI)

STABLE & FINAL FIX. Live filtering is now reliable. Filter & Sort YouTube with a compact UI. Save your favorite settings as the new default.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         YouTube Filter & Sorter (Compact UI)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  STABLE & FINAL FIX. Live filtering is now reliable. Filter & Sort YouTube with a compact UI. Save your favorite settings as the new default.
// @author       Opita04
// @license      MIT
// @match        *://www.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    // --- --- CONFIGURATION (loaded once at start) --- ---
    let filtersGloballyEnabled = GM_getValue('filtersGloballyEnabled', true);
    const initialValues = {
        viewCount: GM_getValue('viewCountThreshold', 1000), viewType: GM_getValue('viewFilterType', 'greater'),
        ageEnabled: GM_getValue('ageFilterEnabled', false), ageVal: GM_getValue('ageValue', 1), ageUnit: GM_getValue('ageUnit', 'years'), ageType: GM_getValue('ageFilterType', 'newer'),
        durEnabled: GM_getValue('durationFilterEnabled', false), durVal: GM_getValue('durationValue', 10), durUnit: GM_getValue('durationUnit', 'minutes'), durType: GM_getValue('durationFilterType', 'longer'),
        sort: GM_getValue('sortOrder', 'default'), panelVisible: GM_getValue('isPanelVisible', true)
    };

    // --- --- PARSING FUNCTIONS --- ---
    function parseViews(v) { if (!v) return 0; const c = v.toLowerCase().replace(/views|,/g, '').trim(), n = parseFloat(c); if (c.includes('k')) return n * 1e3; if (c.includes('m')) return n * 1e6; if (c.includes('b')) return n * 1e9; return parseInt(c, 10) || 0; }
    function parseAgeToDays(a) { if (!a) return Infinity; const c = a.toLowerCase(), n = parseInt(c.match(/\d+/)) || 0; if (c.includes('year')) return n * 365; if (c.includes('month')) return n * 30; if (c.includes('week')) return n * 7; if (c.includes('day')) return n; if (c.includes('hour') || c.includes('minute') || c.includes('second')) return 0; return Infinity; }
    function parseDurationToSeconds(d) { if (!d) return null; const p = d.trim().split(':').map(Number); if (p.some(isNaN)) return null; let s = 0; if (p.length === 3) s = p[0] * 3600 + p[1] * 60 + p[2]; else if (p.length === 2) s = p[0] * 60 + p[1]; else if (p.length === 1) s = p[0]; return s; }

    // --- --- CORE LOGIC --- ---
    function getVideoElements() { return document.querySelectorAll('ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer, ytd-playlist-video-renderer'); }
    function showAllVideos() { getVideoElements().forEach(v => { if (v.style.display === 'none') v.style.display = ''; }); }

    function filterVideos() {
        const viewCount = parseInt(document.getElementById('view-input').value, 10) || 0;
        const viewType = document.getElementById('view-select').value;
        const ageEnabled = document.getElementById('age-check').checked;
        const ageVal = parseInt(document.getElementById('age-input').value, 10) || 0;
        const ageUnit = document.getElementById('age-unit-select').value;
        const ageType = document.getElementById('age-type-select').value;
        const durEnabled = document.getElementById('duration-check').checked;
        const durVal = parseInt(document.getElementById('duration-input').value, 10) || 0;
        const durUnit = document.getElementById('duration-unit-select').value;
        const durType = document.getElementById('duration-type-select').value;
        const videos = getVideoElements();
        const ageThreshold = ageEnabled ? (ageUnit === 'years' ? ageVal * 365 : (ageUnit === 'months' ? ageVal * 30 : ageVal)) : 0;
        const durationThreshold = durEnabled ? (durUnit === 'hours' ? durVal * 3600 : durVal * 60) : 0;
        videos.forEach(video => {
            const metadata = video.querySelectorAll('#metadata-line span');
            const durationEl = video.querySelector('#time-status.ytd-thumbnail-overlay-time-status-renderer');
            let viewTxt = '', ageTxt = '';
            metadata.forEach(span => { const txt = span.textContent.toLowerCase(); if (txt.includes('view')) viewTxt = txt; else if (txt.includes('ago')) ageTxt = txt; });
            const views = parseViews(viewTxt), ageDays = parseAgeToDays(ageTxt), durationSecs = durationEl ? parseDurationToSeconds(durationEl.textContent) : null;
            let hide = false;
            if (views > 0 && ( (viewType === 'greater' && views < viewCount) || (viewType === 'less' && views > viewCount) )) hide = true;
            if (!hide && ageEnabled && ageDays !== Infinity && ( (ageType === 'newer' && ageDays > ageThreshold) || (ageType === 'older' && ageDays < ageThreshold) )) hide = true;
            if (!hide && durEnabled && durationSecs !== null && ( (durType === 'longer' && durationSecs < durationThreshold) || (durType === 'shorter' && durationSecs > durationThreshold) )) hide = true;
            if (video.style.display !== (hide ? 'none' : '')) video.style.display = hide ? 'none' : '';
        });
    }

    function sortVideos() {
        const selectedOrder = document.getElementById('sort-order-select').value;
        GM_setValue('sortOrder', selectedOrder);
        if (selectedOrder === 'default') return;
        const container = document.querySelector('#contents.ytd-rich-grid-renderer, #contents.ytd-item-section-renderer, #primary #contents');
        if (!container) { console.warn("Filter Script: Could not find video container to sort."); return; }
        const videos = Array.from(container.children).filter(el => el.tagName.match(/YTD-(RICH-ITEM|VIDEO)-RENDERER/));
        const videosData = videos.map(video => {
            const metadata = video.querySelectorAll('#metadata-line span');
            const durationEl = video.querySelector('#time-status.ytd-thumbnail-overlay-time-status-renderer');
            let viewTxt = '', ageTxt = '';
            metadata.forEach(span => { const txt = span.textContent.toLowerCase(); if (txt.includes('view')) viewTxt = txt; else if (txt.includes('ago')) ageTxt = txt; });
            return { element: video, views: parseViews(viewTxt), ageDays: parseAgeToDays(ageTxt), durationSeconds: durationEl ? parseDurationToSeconds(durationEl.textContent) : -1 };
        });
        videosData.sort((a, b) => {
            switch (selectedOrder) {
                case 'views_desc': return b.views - a.views; case 'views_asc': return a.views - b.views;
                case 'age_asc': return a.ageDays - b.ageDays; case 'age_desc': return b.ageDays - a.ageDays;
                case 'duration_desc': return b.durationSeconds - a.durationSeconds; case 'duration_asc': return a.durationSeconds - b.durationSeconds;
                default: return 0;
            }
        });
        videosData.forEach(vid => container.appendChild(vid.element));
    }

    function isFilterablePage() { return !window.location.pathname.startsWith('/watch'); }

    // --- --- UI & MAIN LOOP --- ---
    function createMenu() {
        const container = document.createElement('div'); container.id = 'view-filter-container'; document.body.appendChild(container);
        const quickToggleButton = document.createElement('button'); quickToggleButton.id = 'quick-toggle-filter-btn'; container.appendChild(quickToggleButton);
        function updateQuickToggleButton() { if (filtersGloballyEnabled) { quickToggleButton.textContent = 'Filters On'; quickToggleButton.className = 'filters-on'; } else { quickToggleButton.textContent = 'Filters Off'; quickToggleButton.className = 'filters-off'; } }
        quickToggleButton.addEventListener('click', () => {
            filtersGloballyEnabled = !filtersGloballyEnabled;
            GM_setValue('filtersGloballyEnabled', filtersGloballyEnabled);
            updateQuickToggleButton();
            mainLogic();
        });
        const toggleButton = document.createElement('button'); toggleButton.id = 'view-filter-toggle-btn'; container.appendChild(toggleButton);
        const panel = document.createElement('div'); panel.id = 'view-filter-panel'; container.appendChild(panel);
        panel.innerHTML = `
            <h3>View Count Filter</h3><div class="input-row"><input type="number" id="view-input" value="${initialValues.viewCount}"><select id="view-select"><option value="greater" ${initialValues.viewType === 'greater' ? 'selected' : ''}>Greater Than</option><option value="less" ${initialValues.viewType === 'less' ? 'selected' : ''}>Less Than</option></select></div><hr>
            <h3>Video Age Filter</h3><label class="filter-label"><input type="checkbox" id="age-check" ${initialValues.ageEnabled ? 'checked' : ''}> Enable Age Filter</label><div class="input-row"><input type="number" id="age-input" value="${initialValues.ageVal}"><select id="age-unit-select"><option value="days" ${initialValues.ageUnit === 'days' ? 'selected' : ''}>Days</option><option value="months" ${initialValues.ageUnit === 'months' ? 'selected' : ''}>Months</option><option value="years" ${initialValues.ageUnit === 'years' ? 'selected' : ''}>Years</option></select></div><select id="age-type-select"><option value="newer" ${initialValues.ageType === 'newer' ? 'selected' : ''}>Newer Than</option><option value="older" ${initialValues.ageType === 'older' ? 'selected' : ''}>Older Than</option></select><hr>
            <h3>Video Duration Filter</h3><label class="filter-label"><input type="checkbox" id="duration-check" ${initialValues.durEnabled ? 'checked' : ''}> Enable Duration Filter</label><div class="input-row"><input type="number" id="duration-input" value="${initialValues.durVal}"><select id="duration-unit-select"><option value="minutes" ${initialValues.durUnit === 'minutes' ? 'selected' : ''}>Minutes</option><option value="hours" ${initialValues.durUnit === 'hours' ? 'selected' : ''}>Hours</option></select></div><select id="duration-type-select"><option value="longer" ${initialValues.durType === 'longer' ? 'selected' : ''}>Longer Than</option><option value="shorter" ${initialValues.durType === 'shorter' ? 'selected' : ''}>Shorter Than</option></select><hr>
            <h3>Sort Order</h3><div class="input-row"><select id="sort-order-select"><option value="default" ${initialValues.sort === 'default' ? 'selected' : ''}>Default</option><option value="views_desc" ${initialValues.sort === 'views_desc' ? 'selected' : ''}>Views (High-Low)</option><option value="views_asc" ${initialValues.sort === 'views_asc' ? 'selected' : ''}>Views (Low-High)</option><option value="age_asc" ${initialValues.sort === 'age_asc' ? 'selected' : ''}>Age (New-Old)</option><option value="age_desc" ${initialValues.sort === 'age_desc' ? 'selected' : ''}>Age (Old-New)</option><option value="duration_desc" ${initialValues.sort === 'duration_desc' ? 'selected' : ''}>Duration (Long-Short)</option><option value="duration_asc" ${initialValues.sort === 'duration_asc' ? 'selected' : ''}>Duration (Short-Long)</option></select><button id="sort-now-btn">Sort Now</button></div><hr>
            <button id="save-btn">Save as Default</button>
        `;
        const saveButton = document.getElementById('save-btn');
        document.getElementById('sort-now-btn').addEventListener('click', sortVideos);
        saveButton.addEventListener('click', () => {
            GM_setValue('viewCountThreshold', parseInt(document.getElementById('view-input').value, 10)); GM_setValue('viewFilterType', document.getElementById('view-select').value);
            GM_setValue('ageFilterEnabled', document.getElementById('age-check').checked); GM_setValue('ageValue', parseInt(document.getElementById('age-input').value, 10)); GM_setValue('ageUnit', document.getElementById('age-unit-select').value); GM_setValue('ageFilterType', document.getElementById('age-type-select').value);
            GM_setValue('durationFilterEnabled', document.getElementById('duration-check').checked); GM_setValue('durationValue', parseInt(document.getElementById('duration-input').value, 10)); GM_setValue('durationUnit', document.getElementById('duration-unit-select').value); GM_setValue('durationFilterType', document.getElementById('duration-type-select').value);
            GM_setValue('sortOrder', document.getElementById('sort-order-select').value);
            const originalText = saveButton.textContent; saveButton.textContent = 'Saved!'; saveButton.style.backgroundColor = '#27813a'; saveButton.disabled = true;
            setTimeout(() => { saveButton.textContent = originalText; saveButton.style.backgroundColor = '#3ea6ff'; saveButton.disabled = false; }, 1500);
        });
        let isPanelVisible = initialValues.panelVisible;
        function updatePanelVisibility() { if (isPanelVisible) { panel.style.display = 'block'; toggleButton.textContent = 'Hide Controls'; } else { panel.style.display = 'none'; toggleButton.textContent = 'Show Controls'; } }
        toggleButton.addEventListener('click', () => { isPanelVisible = !isPanelVisible; GM_setValue('isPanelVisible', isPanelVisible); updatePanelVisibility(); });
        updateQuickToggleButton(); updatePanelVisibility();
        // Start the main logic loop ONLY AFTER the menu is created to prevent race conditions
        setInterval(mainLogic, 500);
    }

    function mainLogic() {
        const menuContainer = document.getElementById('view-filter-container');
        if (!menuContainer) return;
        if (isFilterablePage()) {
            if (menuContainer.style.display !== 'flex') menuContainer.style.display = 'flex';
            if (filtersGloballyEnabled) { filterVideos(); } else { showAllVideos(); }
        } else {
            if (menuContainer.style.display !== 'none') menuContainer.style.display = 'none';
            showAllVideos();
        }
    }

    GM_addStyle(`
        #view-filter-container { position: fixed; top: 80px; right: 20px; z-index: 9999; display: flex; flex-direction: column; align-items: flex-end; }
        #quick-toggle-filter-btn, #view-filter-toggle-btn { border: 1px solid #3f3f3f; padding: 5px 10px; cursor: pointer; border-radius: 5px; margin-bottom: 5px; order: -1; font-weight: bold; }
        #quick-toggle-filter-btn.filters-on { background-color: #27813a; color: white; }
        #quick-toggle-filter-btn.filters-off { background-color: #555; color: #ddd; }
        #view-filter-toggle-btn { background-color: #0f0f0f; color: white; }
        #view-filter-panel { background-color: #282828; color: white; border: 1px solid #3f3f3f; padding: 15px; border-radius: 8px; width: 220px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
        #view-filter-panel h3 { margin: 10px 0; font-size: 16px; text-align: center; border-bottom: 1px solid #444; padding-bottom: 5px; }
        #view-filter-panel h3:first-of-type { margin-top: 0; }
        #view-filter-panel input, #view-filter-panel select, #view-filter-panel button { display: block; width: 100%; box-sizing: border-box; padding: 8px; border-radius: 4px; border: 1px solid #555; background-color: #1e1e1e; color: white; }
        #view-filter-panel hr { border: none; border-top: 1px solid #444; margin: 20px 0; }
        .filter-label { display: flex; align-items: center; margin-bottom: 10px; }
        .filter-label input[type="checkbox"] { width: auto; margin-right: 10px; }
        .input-row { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
        .input-row > * { margin-bottom: 0 !important; }
        .input-row > input, .input-row > select { flex: 1; }
        #sort-now-btn { flex: 0 1 auto; }
        #save-btn { background-color: #3ea6ff; color: black; font-weight: bold; margin-top: 10px; transition: background-color 0.2s; }
        #sort-now-btn { background-color: #da860a; color: white; font-weight: bold; }
        #save-btn:hover, #sort-now-btn:hover { opacity: 0.9; }
    `);

    // Wait until the page body is ready before creating the menu and starting the loop
    if (document.body) {
        createMenu();
    } else {
        document.addEventListener('DOMContentLoaded', createMenu);
    }
})();