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)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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!');
})();