YouTube Full Dates (v2)

Replace "1 year ago" with exact dates. Three-tier color badges: Green (this week), Purple (this year - toggleable), Yellow (old years)

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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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         YouTube Full Dates (v2)
// @namespace    YouTube Full Dates
// @version      2
// @description  Replace "1 year ago" with exact dates. Three-tier color badges: Green (this week), Purple (this year - toggleable), Yellow (old years)
// @author       Solomon (improved from InMirrors)
// @match        https://www.youtube.com/*
// @icon         https://www.youtube.com/s/desktop/814d40a6/img/favicon_144x144.png
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

// Previous Features (v1):
// - Exact upload dates replacing relative timestamps
// - 11 language support
// - Custom date format tokens
// - Three-tier color badges (green/purple/yellow)
// - Smart year hiding for current year
// - Settings panel with toggles
// - Works on all YouTube pages

// New in v2:
// - Added toggle to disable purple "this year" badges (shows plain text instead)
// - User request: "can we please get a toggle for the purple badges? I'd rather see plain text"

(function() {
    'use strict';

    const LANGUAGES = {
        en: { name: 'English', monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], monthsFull: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], agoKeywords: ['ago', 'Streamed'], dateKeywords: ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'] },
        es: { name: 'Español', monthsShort: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'], monthsFull: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'], agoKeywords: ['hace'], dateKeywords: ['segundo', 'minuto', 'hora', 'día', 'semana', 'mes', 'año'] },
        fr: { name: 'Français', monthsShort: ['janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', 'sept', 'oct', 'nov', 'déc'], monthsFull: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], agoKeywords: ['il y a'], dateKeywords: ['seconde', 'minute', 'heure', 'jour', 'semaine', 'mois', 'an', 'année'] },
        de: { name: 'Deutsch', monthsShort: ['Jan', 'Feb', 'März', 'Apr', 'Mai', 'Juni', 'Juli', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], monthsFull: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], agoKeywords: ['vor'], dateKeywords: ['Sekunde', 'Minute', 'Stunde', 'Tag', 'Woche', 'Monat', 'Jahr'] },
        pt: { name: 'Português', monthsShort: ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'], monthsFull: ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'], agoKeywords: ['há'], dateKeywords: ['segundo', 'minuto', 'hora', 'dia', 'semana', 'mês', 'ano'] },
        it: { name: 'Italiano', monthsShort: ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'], monthsFull: ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'], agoKeywords: ['fa'], dateKeywords: ['secondo', 'minuto', 'ora', 'giorno', 'settimana', 'mese', 'anno'] },
        ru: { name: 'Русский', monthsShort: ['янв', 'февр', 'март', 'апр', 'май', 'июнь', 'июль', 'авг', 'сент', 'окт', 'нояб', 'дек'], monthsFull: ['январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'], agoKeywords: ['назад'], dateKeywords: ['секунд', 'минут', 'час', 'день', 'дней', 'недел', 'месяц', 'год', 'лет'] },
        zh: { name: '中文', monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], monthsFull: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], agoKeywords: ['前'], dateKeywords: ['秒', '分', '时', '時', '天', '日', '周', '週', '月', '年'] },
        ja: { name: '日本語', monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], monthsFull: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], agoKeywords: ['前'], dateKeywords: ['秒', '分', '時間', '日', '週間', 'か月', '年'] },
        ko: { name: '한국어', monthsShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], monthsFull: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], agoKeywords: ['전'], dateKeywords: ['초', '분', '시간', '일', '주', '개월', '년'] },
        ar: { name: 'العربية', monthsShort: ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'], monthsFull: ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'], agoKeywords: ['قبل', 'منذ'], dateKeywords: ['ثانية', 'دقيقة', 'ساعة', 'يوم', 'أسبوع', 'شهر', 'سنة'] }
    };

    // v2: Added thisYearBadge setting (default true for backward compatibility)
    const DEFAULT_CONFIG = { dateFormat: 'MMMM dd yy', language: 'en', smartYear: true, highlightOldVideos: true, thisWeekEmoji: true, thisWeekBadge: '🆕', thisYearBadge: true, debugMode: false };
    const savedSettings = GM_getValue('settings', {});
    const SETTINGS = { ...DEFAULT_CONFIG, ...savedSettings };

    const PROCESSED_ATTR = 'data-ytfd-done';
    const dateCache = new Map();
    let isProcessing = false, pendingRequests = 0, lastUrl = window.location.href;
    const MAX_CONCURRENT = 8, requestQueue = [];

    const log = (...args) => SETTINGS.debugMode && console.log('📅 [v2]', ...args);

    function isWithinLastWeek(date) {
        const diffDays = (new Date() - new Date(date)) / (1000 * 60 * 60 * 24);
        return diffDays >= 0 && diffDays < 7;
    }

    function formatDate(date) {
        const d = new Date(date);
        if (isNaN(d.getTime())) return { text: '', tier: 'none' };

        const lang = LANGUAGES[SETTINGS.language] || LANGUAGES.en;
        const pad = (n) => String(n).padStart(2, '0');
        const now = new Date(), currentYear = now.getFullYear(), videoYear = d.getFullYear();
        const isThisWeek = isWithinLastWeek(d);
        const isThisYear = currentYear === videoYear && !isThisWeek;
        const isOldYear = videoYear < currentYear;

        // Tier 1: This week - green with emoji
        if (SETTINGS.thisWeekEmoji && isThisWeek) {
            return {
                text: SETTINGS.thisWeekBadge + ' ' + lang.monthsShort[d.getMonth()] + ' ' + d.getDate(),
                tier: 'thisWeek'
            };
        }

        // Build date string
        const tokens = {
            yyyy: d.getFullYear(),
            yy: String(d.getFullYear()).slice(-2),
            MMMM: lang.monthsFull[d.getMonth()],
            MMM: lang.monthsShort[d.getMonth()],
            MM: pad(d.getMonth() + 1),
            dd: pad(d.getDate())
        };

        let result = SETTINGS.dateFormat;

        // Tier 2: This year - no year shown
        if (SETTINGS.smartYear && currentYear === videoYear) {
            result = result.replace(/\s*yyyy\s*/g, ' ').replace(/\s*yy\s*/g, ' ');
        }

        // Replace tokens and clean up
        result = result.replace(/yyyy|yy|MMMM|MMM|MM|dd/g, m => tokens[m]);
        result = result.replace(/,/g, '').replace(/\s+/g, ' ').trim();

        if (isThisYear) return { text: result, tier: 'thisYear' };
        if (isOldYear) return { text: result, tier: 'oldYear' };
        return { text: result, tier: 'none' };
    }

    function getVideoId(url) {
        if (!url) return null;
        let m = url.match(/\/shorts\/([^/?&]+)/) || url.match(/[?&]v=([^&]+)/) || url.match(/\/embed\/([^/?&]+)/);
        return m ? m[1] : null;
    }

    function hasRelativeDate(text) {
        if (!text) return false;
        const agoKw = Object.values(LANGUAGES).flatMap(l => l.agoKeywords);
        const dateKw = Object.values(LANGUAGES).flatMap(l => l.dateKeywords);
        const t = text.toLowerCase();
        return agoKw.some(k => t.includes(k.toLowerCase())) && dateKw.some(k => t.includes(k.toLowerCase()));
    }

    function hasYouTubeFullDate(text) {
        if (!text) return false;
        return /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|June|July|August|September|October|November|December)[a-z]*\.?\s+\d{1,2}/i.test(text);
    }

    function extractDateFromText(text) {
        if (!text) return null;
        const monthMap = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11, january: 0, february: 1, march: 2, april: 3, june: 5, july: 6, august: 7, september: 8, october: 9, november: 10, december: 11 };

        let m = text.match(/\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December)[a-z]*\.?\s+(\d{1,2}),?\s*(\d{4})?\b/i);
        if (m) {
            const mo = monthMap[m[1].toLowerCase().replace(/[^a-z]/g, '').substring(0, 3)];
            if (mo !== undefined) {
                const year = m[3] ? parseInt(m[3]) : new Date().getFullYear();
                return new Date(year, mo, parseInt(m[2]));
            }
        }
        return null;
    }

    function needsProcessing(el) {
        if (el.hasAttribute(PROCESSED_ATTR)) return false;
        const text = el.textContent;
        return text && (hasRelativeDate(text) || hasYouTubeFullDate(text));
    }

    async function fetchUploadDate(videoId) {
        if (dateCache.has(videoId)) return dateCache.get(videoId);
        try {
            const res = await fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ context: { client: { clientName: 'WEB', clientVersion: '2.20240416.01.00' } }, videoId })
            });
            if (!res.ok) throw new Error('Network error');
            const data = await res.json(), info = data?.microformat?.playerMicroformatRenderer;
            let uploadDate = info?.liveBroadcastDetails?.isLiveNow ? info.liveBroadcastDetails.startTimestamp : (info?.publishDate || info?.uploadDate);
            if (uploadDate) dateCache.set(videoId, uploadDate);
            return uploadDate;
        } catch (e) { log('❌ Fetch error:', e); return null; }
    }

    async function processQueue() {
        while (requestQueue.length > 0 && pendingRequests < MAX_CONCURRENT) {
            const task = requestQueue.shift();
            pendingRequests++;
            try { await task(); } catch (e) { log('❌ Task error:', e); }
            pendingRequests--;
        }
    }

    function applyDateToElement(element, result) {
        element.textContent = result.text;
        element.setAttribute(PROCESSED_ATTR, 'true');
        element.classList.remove('ytfd-old-video', 'ytfd-this-week', 'ytfd-this-year');

        if (result.tier === 'thisWeek') {
            element.classList.add('ytfd-this-week');
        } else if (result.tier === 'oldYear' && SETTINGS.highlightOldVideos) {
            element.classList.add('ytfd-old-video');
        } else if (result.tier === 'thisYear' && SETTINGS.thisYearBadge) {
            // v2: Only apply purple badge if thisYearBadge is enabled
            element.classList.add('ytfd-this-year');
        }
        // v2: If thisYearBadge is OFF, "thisYear" dates get no badge class = plain text

        log('✅', result.tier, result.text);
    }

    function queueDateUpdate(videoId, element, originalText) {
        element.setAttribute(PROCESSED_ATTR, 'pending');

        requestQueue.push(async () => {
            let uploadDate = null;

            if (hasYouTubeFullDate(originalText)) {
                const extracted = extractDateFromText(originalText);
                if (extracted) {
                    uploadDate = extracted.toISOString();
                    log('📅 Extracted:', originalText, '→', extracted);
                }
            }

            if (!uploadDate) {
                uploadDate = await fetchUploadDate(videoId);
            }

            if (!uploadDate) {
                element.removeAttribute(PROCESSED_ATTR);
                return;
            }

            const result = formatDate(uploadDate);
            if (!result.text) {
                element.removeAttribute(PROCESSED_ATTR);
                return;
            }

            applyDateToElement(element, result);
        });
        processQueue();
    }

    function findDateElement(container) {
        const selectors = ['#metadata-line > span:first-child', '#metadata-line > span', '.inline-metadata-item', '.yt-core-attributed-string--link-inherit-color', 'span.yt-formatted-string'];
        for (const selector of selectors) {
            const elements = container.querySelectorAll(selector);
            for (const el of elements) {
                if (needsProcessing(el)) return el;
            }
        }
        return null;
    }

    function findVideoLink(container) {
        const selectors = ['a#thumbnail', 'a#video-title-link', 'h3 > a', '.yt-lockup-view-model__content-image', 'a[href*="watch"]', 'a[href*="shorts"]'];
        for (const selector of selectors) {
            const el = container.querySelector(selector);
            if (el?.href) return el.href;
        }
        return null;
    }

    function processAllVideos() {
        const containerSelectors = ['ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytd-playlist-video-renderer', 'ytd-grid-video-renderer', 'ytd-rich-grid-media', 'yt-lockup-view-model'];
        containerSelectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(container => {
                const dateEl = findDateElement(container);
                if (!dateEl) return;
                const href = findVideoLink(container);
                const videoId = getVideoId(href);
                if (!videoId) return;
                const originalText = dateEl.textContent.trim();
                queueDateUpdate(videoId, dateEl, originalText);
            });
        });
    }

    function clearAllMarkers() {
        log('🔄 Clearing all markers...');
        document.querySelectorAll('[' + PROCESSED_ATTR + ']').forEach(el => {
            el.removeAttribute(PROCESSED_ATTR);
            el.classList.remove('ytfd-old-video', 'ytfd-this-week', 'ytfd-this-year');
        });
    }

    function runProcessors() {
        if (isProcessing) return;
        isProcessing = true;
        try { processAllVideos(); } catch (e) { log('❌ Error:', e); }
        isProcessing = false;
    }

    GM_addStyle(`
        .ytfd-old-video { background-color: #ffeb3b !important; padding: 2px 6px !important; border-radius: 4px !important; color: #000 !important; font-weight: 600 !important; display: inline !important; line-height: 1.2 !important; }
        html[dark] .ytfd-old-video, [dark] .ytfd-old-video { background-color: #ffd600 !important; }
        .ytfd-this-week { background-color: #81c784 !important; padding: 2px 6px !important; border-radius: 4px !important; color: #000 !important; font-weight: 600 !important; display: inline !important; line-height: 1.2 !important; }
        html[dark] .ytfd-this-week, [dark] .ytfd-this-week { background-color: #66bb6a !important; }
        .ytfd-this-year { background-color: #b39ddb !important; padding: 2px 6px !important; border-radius: 4px !important; color: #000 !important; font-weight: 600 !important; display: inline !important; line-height: 1.2 !important; }
        html[dark] .ytfd-this-year, [dark] .ytfd-this-year { background-color: #9575cd !important; color: #fff !important; }

        .ytfd-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); z-index: 99999; width: 360px; max-height: 80vh; overflow: hidden; opacity: 0; visibility: hidden; transition: all 0.2s ease; }
        html[dark] .ytfd-panel, [dark] .ytfd-panel { background: #212121; color: #fff; }
        .ytfd-panel.visible { opacity: 1; visibility: visible; }
        .ytfd-header { background: #cc0000; color: #fff; padding: 14px; display: flex; justify-content: space-between; align-items: center; }
        .ytfd-header h2 { margin: 0; font-size: 15px; }
        .ytfd-close { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 26px; height: 26px; border-radius: 50%; cursor: pointer; font-size: 14px; }
        .ytfd-content { padding: 14px; max-height: 50vh; overflow-y: auto; }
        .ytfd-section { margin-bottom: 14px; }
        .ytfd-section-title { font-size: 11px; font-weight: 600; color: #666; margin-bottom: 6px; text-transform: uppercase; }
        html[dark] .ytfd-section-title { color: #aaa; }
        .ytfd-input-group { margin-bottom: 8px; }
        .ytfd-input-group label { display: block; font-size: 12px; margin-bottom: 3px; color: #333; }
        html[dark] .ytfd-input-group label { color: #ddd; }
        .ytfd-input-group input, .ytfd-input-group select { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; box-sizing: border-box; }
        html[dark] .ytfd-input-group input, html[dark] .ytfd-input-group select { background: #333; border-color: #444; color: #fff; }
        .ytfd-toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; }
        .ytfd-toggle-label { font-size: 12px; color: #333; }
        html[dark] .ytfd-toggle-label { color: #ddd; }
        .ytfd-toggle { width: 36px; height: 18px; background: #ccc; border-radius: 9px; position: relative; cursor: pointer; }
        .ytfd-toggle::after { content: ''; position: absolute; width: 14px; height: 14px; background: #fff; border-radius: 50%; top: 2px; left: 2px; transition: transform 0.2s; }
        .ytfd-toggle.on { background: #cc0000; }
        .ytfd-toggle.on::after { transform: translateX(18px); }
        .ytfd-footer { padding: 10px 14px; background: #f5f5f5; display: flex; justify-content: flex-end; gap: 6px; }
        html[dark] .ytfd-footer { background: #1a1a1a; }
        .ytfd-btn { padding: 6px 12px; border: none; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
        .ytfd-btn-primary { background: #cc0000; color: #fff; }
        .ytfd-btn-secondary { background: #ddd; color: #333; }
        html[dark] .ytfd-btn-secondary { background: #444; color: #fff; }
        .ytfd-help { font-size: 10px; color: #888; line-height: 1.4; }
    `);

    function createSettingsPanel() {
        const panel = document.createElement('div');
        panel.className = 'ytfd-panel';
        const langOpts = Object.entries(LANGUAGES).map(([c, l]) => '<option value="' + c + '"' + (c === SETTINGS.language ? ' selected' : '') + '>' + l.name + '</option>').join('');

        panel.innerHTML = '<div class="ytfd-header"><h2>📅 YouTube Full Dates v2</h2><button class="ytfd-close">✕</button></div><div class="ytfd-content"><div class="ytfd-section"><div class="ytfd-section-title">Language & Format</div><div class="ytfd-input-group"><label>Language</label><select id="ytfd-language">' + langOpts + '</select></div><div class="ytfd-input-group"><label>Date Format</label><input type="text" id="ytfd-format" value="' + SETTINGS.dateFormat + '"></div><div class="ytfd-help">MMMM=January, MMM=Jan, dd=05, yy=25, yyyy=2025</div></div><div class="ytfd-section"><div class="ytfd-section-title">Display Options</div><div class="ytfd-toggle-row"><span class="ytfd-toggle-label">Smart Year (hide if current)</span><div class="ytfd-toggle' + (SETTINGS.smartYear ? ' on' : '') + '" data-key="smartYear"></div></div><div class="ytfd-toggle-row"><span class="ytfd-toggle-label">🟡 Yellow for old years</span><div class="ytfd-toggle' + (SETTINGS.highlightOldVideos ? ' on' : '') + '" data-key="highlightOldVideos"></div></div><div class="ytfd-toggle-row"><span class="ytfd-toggle-label">🆕 Green for this week</span><div class="ytfd-toggle' + (SETTINGS.thisWeekEmoji ? ' on' : '') + '" data-key="thisWeekEmoji"></div></div><div class="ytfd-toggle-row"><span class="ytfd-toggle-label">🟣 Purple for this year</span><div class="ytfd-toggle' + (SETTINGS.thisYearBadge ? ' on' : '') + '" data-key="thisYearBadge"></div></div></div><div class="ytfd-section"><div class="ytfd-section-title">Color Guide</div><div class="ytfd-help">🟢 Green = This week (&lt;7 days)<br>🟣 Purple = This year (7+ days) — toggle off for plain text<br>🟡 Yellow = Previous years</div></div></div><div class="ytfd-footer"><button class="ytfd-btn ytfd-btn-secondary" id="ytfd-reset">Reset</button><button class="ytfd-btn ytfd-btn-primary" id="ytfd-save">Save</button></div>';

        document.body.appendChild(panel);
        panel.querySelector('.ytfd-close').addEventListener('click', () => panel.classList.remove('visible'));
        panel.querySelectorAll('.ytfd-toggle').forEach(t => t.addEventListener('click', () => t.classList.toggle('on')));
        panel.querySelector('#ytfd-save').addEventListener('click', () => {
            GM_setValue('settings', {
                dateFormat: panel.querySelector('#ytfd-format').value,
                language: panel.querySelector('#ytfd-language').value,
                smartYear: panel.querySelector('[data-key="smartYear"]').classList.contains('on'),
                highlightOldVideos: panel.querySelector('[data-key="highlightOldVideos"]').classList.contains('on'),
                thisWeekEmoji: panel.querySelector('[data-key="thisWeekEmoji"]').classList.contains('on'),
                thisYearBadge: panel.querySelector('[data-key="thisYearBadge"]').classList.contains('on'),
                thisWeekBadge: SETTINGS.thisWeekBadge,
                debugMode: SETTINGS.debugMode
            });
            alert('Saved! Refresh to apply.');
            panel.classList.remove('visible');
        });
        panel.querySelector('#ytfd-reset').addEventListener('click', () => { GM_setValue('settings', {}); alert('Reset! Refresh to apply.'); panel.classList.remove('visible'); });
        document.addEventListener('keydown', e => { if (e.key === 'Escape') panel.classList.remove('visible'); });
        return panel;
    }

    const settingsPanel = createSettingsPanel();
    GM_registerMenuCommand('⚙️ Settings', () => settingsPanel.classList.add('visible'));

    let debounceTimer = null;
    function debouncedRun(delay = 200) { clearTimeout(debounceTimer); debounceTimer = setTimeout(runProcessors, delay); }

    document.addEventListener('click', e => {
        const chip = e.target.closest('yt-chip-cloud-chip-renderer, yt-formatted-string[class*="chip"], [role="tab"], iron-selector yt-formatted-string, tp-yt-paper-item, ytd-feed-filter-chip-bar-renderer yt-chip-cloud-chip-renderer');
        if (chip) {
            log('🎯 Tab/filter clicked');
            setTimeout(() => { clearAllMarkers(); runProcessors(); }, 500);
            setTimeout(() => { clearAllMarkers(); runProcessors(); }, 1000);
            setTimeout(() => { clearAllMarkers(); runProcessors(); }, 2000);
        }
    }, true);

    const observer = new MutationObserver((mutations) => {
        let shouldRun = false;
        for (const m of mutations) {
            if (m.addedNodes.length > 0) {
                for (const n of m.addedNodes) {
                    if (n.nodeType === 1 && (n.matches?.('ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, #contents') || n.querySelector?.('ytd-rich-item-renderer, ytd-video-renderer'))) {
                        shouldRun = true;
                        break;
                    }
                }
            }
        }
        if (shouldRun) debouncedRun(100);
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setInterval(() => { if (window.location.href !== lastUrl) { log('🔄 URL changed'); lastUrl = window.location.href; clearAllMarkers(); debouncedRun(300); } }, 500);
    window.addEventListener('yt-navigate-finish', () => { log('🧭 Navigation finished'); dateCache.clear(); lastUrl = window.location.href; clearAllMarkers(); debouncedRun(300); });
    window.addEventListener('scroll', () => debouncedRun(150), { passive: true });

    setInterval(runProcessors, 3000);
    setTimeout(runProcessors, 300);
    setTimeout(runProcessors, 1000);
    setTimeout(runProcessors, 2000);

    console.log('📅 YouTube Full Dates v2 loaded!');
})();