以絕對日期顯示 YouTube 的影片上傳日期 (yyyy-mm-dd 或其他自訂格式)

顯示絕對日期,而不是「2 週前」、「1 年前」等相對日期。可自訂日期和時間格式。

// ==UserScript==
// @version           0.7.1
// @name:en           Display YouTube video upload dates as absolute dates (yyyy-mm-dd or other custom formats)
// @name              以绝对时间显示 YouTube 的视频上传日期 (yyyy-mm-dd 或其他自定义格式)
// @name:zh-TW        以絕對日期顯示 YouTube 的影片上傳日期 (yyyy-mm-dd 或其他自訂格式)
// @name:ja-JP        絶対日付を YouTube の動画アップロード日付として表示します (yyyy-mm-dd または他のカスタム形式)
// @description:en    Show full upload dates, instead of "1 year ago", "2 weeks ago", etc. You can customize the date and time format.
// @description       显示具体日期而不是“2 星期前”,“1 年前”这种相对日期。可自定义日期和时间格式。
// @description:zh-TW 顯示絕對日期,而不是「2 週前」、「1 年前」等相對日期。可自訂日期和時間格式。
// @description:ja-JP YouTubeの動画アップロード日付を「1 年前」などの相対的な表現ではなく、具体的な日付を表示します。表示形式をカスタマイズできます。
// @author            InMirrors
// @namespace         https://greasyfork.org/users/518374
// @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==

(function () {
    'use strict';

    const PROCESSED_MARKER = '\u200B'; // Zero-Width Space
    const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';

    // Keywords for identifying relative date strings.
    const DATE_TIME_KEYWORDS_EN = 'second minute hour day week month year';
    const DATE_TIME_KEYWORDS_ZH = '秒 分 时 時 天 日 周 週 月 年'
    const DEFAULT_DATE_TIME_KEYWORDS = [DATE_TIME_KEYWORDS_EN, DATE_TIME_KEYWORDS_ZH].join(' ').split(' ');
    const DEFAULT_AGO_KEYWORDS = ['ago', '前'];
    const DEFAULT_OLD_UPLOAD_KEYWORDS = ['day', 'week', 'month', 'year', '天', '日', '周', '週', '月', '年'];
    const DEFAULT_MONTH_NAMES = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC';
    const DEFAULT_DAY_NAMES = 'Sun Mon Tue Wed Thu Fri Sat';

    // Get basic settings from storage
    const SETTINGS = GM_getValue("basic", {});
    const DATE_FORMAT = SETTINGS.dateFormat || DEFAULT_DATE_FORMAT;
    const DATE_TIME_KEYWORDS = SETTINGS.dateTimeKeywords?.length ? SETTINGS.dateTimeKeywords : DEFAULT_DATE_TIME_KEYWORDS;
    const AGO_KEYWORDS = SETTINGS.agoKeywords?.length ? SETTINGS.agoKeywords : DEFAULT_AGO_KEYWORDS;
    const OLD_UPLOAD_KEYWORDS = SETTINGS.oldUploadKeywords?.length ? SETTINGS.oldUploadKeywords : DEFAULT_OLD_UPLOAD_KEYWORDS;
    const MONTH_NAMES = SETTINGS.monthNames?.length ? SETTINGS.monthNames : DEFAULT_MONTH_NAMES.split(' ');
    const DAY_NAMES = SETTINGS.dayNames?.length ? SETTINGS.dayNames : DEFAULT_DAY_NAMES.split(' ');
    const PREPEND_DATES_ENABLED = SETTINGS.prependDatesEnabled ?? false;

    // Get advanced settings from storage
    let timerModeEnabled = GM_getValue("timerModeEnabled", false);
    let fomattingTimer = null; // for timer mode
    let useAllConfigsEnabled = GM_getValue("useAllConfigsEnabled", false);
    // Test all configs to find valid ones for the current page
    let findValidConfigEnable = GM_getValue("findValidConfigEnable", false);

    // === Debug Mode ===

    let debugModeEnabled = GM_getValue("debugModeEnabled", false);
    let debugVidsArrayLength = GM_getValue("debugVidsArrayLength", 4);

    if (debugModeEnabled) {
        GM_registerMenuCommand(`Toggle Debug mode ${debugModeEnabled ? "OFF" : "ON"}`, () => {
            debugModeEnabled = !debugModeEnabled;
            GM_setValue("debugModeEnabled", debugModeEnabled);
            alert(`Debug mode is now ${debugModeEnabled ? "ON" : "OFF"}`);
        });
        GM_registerMenuCommand("Set vids array length", () => {
            let input = prompt("Please enter a number, 0 to disable slicing:", debugVidsArrayLength);
            if (input == null) { // input canceled
                return;
            }
            if (input.trim() !== "" && !isNaN(input)) { // valid input
                debugVidsArrayLength = Number(input);
                GM_setValue("debugVidsArrayLength", debugVidsArrayLength);
                alert("Value updated to: " + debugVidsArrayLength);
            } else {
                alert("Invalid input. Please enter a number.");
            }
        });
        GM_registerMenuCommand(`Toggle all configs mode ${useAllConfigsEnabled ? "OFF" : "ON"}`, () => {
            useAllConfigsEnabled = !useAllConfigsEnabled;
            GM_setValue("useAllConfigsEnabled", useAllConfigsEnabled);
            alert(`All configs mode is now ${useAllConfigsEnabled ? "ON" : "OFF"}`);
        });
        GM_registerMenuCommand(`Toggle all configs mode ${useAllConfigsEnabled ? "OFF" : "ON"}`, () => {
            useAllConfigsEnabled = !useAllConfigsEnabled;
            GM_setValue("useAllConfigsEnabled", useAllConfigsEnabled);
            alert(`All configs mode is now ${useAllConfigsEnabled ? "ON" : "OFF"}`);
        });
        GM_registerMenuCommand(`Toggle find valid config mode ${findValidConfigEnable ? "OFF" : "ON"}`, () => {
            findValidConfigEnable = !findValidConfigEnable;
            if (findValidConfigEnable) {
                useAllConfigsEnabled = true;
                GM_setValue("useAllConfigsEnabled", useAllConfigsEnabled);
            }
            GM_setValue("findValidConfigEnable", findValidConfigEnable);
            alert(`Find valid config mode is now ${findValidConfigEnable ? "ON" : "OFF"}`);
        });
    }

    /**
     * Validate configs array
     * @param {Array} configs - array of config objects
     * @returns {boolean} - true if all configs are valid, false otherwise
     */
    function validateConfigs(configs) {
        const errors = [];

        configs.forEach(config => {
            // Get id and set a default value if necessary
            const { id = '[no id]' } = config;

            // Properties that must be non-empty strings
            const stringProps = [
                "id",
                "videoContainerSelector",
                "metaSpansSelector",
                "vidLinkSelector",
            ];

            // string properties must be non-empty strings
            stringProps.forEach(prop => {
                if (typeof config[prop] !== "string" || config[prop].trim() === "") {
                    errors.push(`${id}: "${prop}" must be a non-empty string`);
                }
            });

            // urlPattern must be RegExp
            if (!(config.urlPattern instanceof RegExp)) {
                errors.push(`${id}: "urlPattern" must be a RegExp`);
            }

            // shouldCreateDateSpan must be boolean
            if (typeof config.shouldCreateDateSpan !== "boolean") {
                errors.push(`${id}: "shouldCreateDateSpan" must be a boolean`);
            }

            // Conditional validation
            if (config.shouldCreateDateSpan === true) {
                if (typeof config.insertAfterIndex !== "number") {
                    errors.push(`${id}: "insertAfterIndex" must be a number when shouldCreateDateSpan is true`);
                }
            }
        });

        if (errors.length > 0) {
            console.log('[YTDF] Validation errors:');
            errors.forEach(err => console.log(" - " + err));
            return false;
        } else {
            console.log('[YTDF] All configs are valid!');
            return true;
        }
    }

    // === Setting UI ===

    const translations = {
        en: {
            menuTitle: 'Open Settings Panel',
            panelTitle: 'Settings',
            saveBtn: 'Save',
            resetBtn: 'Reset',
            languageLabel: 'Language',
            prependDatesLabel: 'Prepend dates',
            prependDatesHelp: 'Insert absolute dates before the original relative dates. Default is after.',
            dateFormatLabel: 'Date format',
            dateFormatHelp: 'The format for the displayed date. You can get syntax help from the script\'s readme.',
            oldUploadKeywordsLabel: 'Keywords for old uploads',
            oldUploadKeywordsHelp: 'Keywords to identify old uploads. Only formatted dates will be shown for these.',
            dateTimeKeywordsLabel: 'Date and time keywords *',
            dateTimeKeywordsHelp: 'Keywords used to identify relative date strings.',
            agoKeywordsLabel: 'Ago keyword *',
            agoKeywordsHelp: 'Keywords used to identify the "ago" part of relative dates, such as "ago" in "1 day ago".',
            monthNamesLabel: 'Month names',
            monthNamesHelp: 'The names of the months used in the date format (MMM).',
            dayNamesLabel: 'Day of the week names',
            dayNamesHelp: 'The names of the days of the week used in the date format (ww).',
            stringsFooterBasic: 'Click the ? icon to use the example.',
            stringsFooterI18n: 'You must complete these keywords settings if your YouTube language is not English, 中文 or 日本語.',
            helpTooltip: '{desc} Example:\n{example}',
            inputPlaceholder: 'Enter value...',
            alertSaved: 'Settings saved, please refresh the page to apply changes',
            confirmReset: 'Reset to defaults?'
        },
        zh: {
            menuTitle: '打开设置面板',
            panelTitle: '设置',
            saveBtn: '保存',
            resetBtn: '重置',
            languageLabel: '语言',
            prependDatesLabel: '插入日期到前面',
            prependDatesHelp: '插入绝对日期到原始的相对日期前,默认在后面。',
            dateFormatLabel: '日期格式',
            dateFormatHelp: '显示日期的格式。详细语法请查阅脚本的帮助文档。',
            oldUploadKeywordsLabel: '旧视频关键字',
            oldUploadKeywordsHelp: '用于识别旧视频的关键字。匹配的视频只显示格式化后的日期,不显示原始文本。',
            dateTimeKeywordsLabel: '日期和时间关键字 *',
            dateTimeKeywordsHelp: '用于识别相对日期字符串的关键字。',
            agoKeywordsLabel: '“前”的关键字 *',
            agoKeywordsHelp: '相对日期中的关键字,例如 "1天前" 中的 "前"。',
            monthNamesLabel: '月份名称',
            monthNamesHelp: '日期格式中 "MMM" 对应的月份名称。',
            dayNamesLabel: '周几的名称',
            dayNamesHelp: '日期格式中 "ww" 对应的周几的名称。',
            stringsFooterBasic: '点击 ? 图标以使用示例',
            stringsFooterI18n: '中文用户不用管下面的。',
            helpTooltip: '{desc}示例:\n{example}',
            inputPlaceholder: '请输入...',
            alertSaved: '设置已保存,请刷新页面以应用更改',
            confirmReset: '要重置为默认值吗?'
        }
    };

    let currentStrings = (() => {
        let currentLang = SETTINGS.language || 'en';
        return translations[currentLang] || translations.en;
    })();

    const panelCSS = `
:root { --ytdf-panel-bg:#fff; --ytdf-accent:#1e88e5; }
#ytdf-panel {
    width: 520px;
    margin: 40px auto;
    border-radius: 10px;
    background: var(--ytdf-panel-bg);
    box-shadow: 0 6px 24px rgba(0,0,0,0.12);
    font-family: Arial, sans-serif;
    color: black;
    overflow: hidden;
    position: fixed;
    top: 50px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 9999;
    display: none; /* Initially hidden */
}
.ytdf-header { display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid #eef2f7; }
.ytdf-header h1 { font-size:18px; font-weight:bolder; margin:0; }
.ytdf-header .ytdf-buttons { display:flex; gap:8px; }
.ytdf-button { background:var(--ytdf-accent); color:#fff; border:none; padding:8px 12px; border-radius:7px; cursor:pointer; }
.ytdf-button.ytdf-secondary { background:#edf2f7; color:#111; border:1px solid #d1dae8; }
.ytdf-container { padding:18px; display:grid; gap:14px; }
.ytdf-section { padding:12px; border-radius:8px; box-shadow: 0px 0px 6px lightgray; }
.ytdf-section-title { font-size:18px; font-weight:bolder; margin-block:10px; }
.ytdf-section.ytdf-selects       { background:hsl(110,60%,99%); border-left:4px solid hsl(110,50%,70%); }
.ytdf-section.ytdf-switches      { background:hsl(190,60%,99%); border-left:4px solid hsl(190,50%,70%); }
.ytdf-section.ytdf-strings-basic { background:hsl(250,60%,99%); border-left:4px solid hsl(250,70%,70%); }
.ytdf-section.ytdf-strings-i18n  { background:hsl(340,60%,99%); border-left:4px solid hsl(340,70%,70%); }
.ytdf-switch-row { display:flex; align-items:center; gap:10px; padding:6px 0; }
.ytdf-switch-row label { flex:1; display:flex; align-items:center; gap:8px; font-size:14px; }
.ytdf-toggle { width:40px; height:22px; background:#cbd5e1; border-radius:20px; position:relative; cursor:pointer; flex:0 0 auto; }
.ytdf-toggle .ytdf-knob { position:absolute; top:2px; left:2px; width:18px; height:18px; background:white; border-radius:50%; transition:all 0.18s; }
.ytdf-toggle.ytdf-on { background:#4fbe79; }
.ytdf-toggle.ytdf-on .ytdf-knob { left:20px; }
.ytdf-string-label-help, .ytdf-select-row { display:flex; align-items:center; gap:10px; padding:8px 0; border-top:1px dashed rgba(0,0,0,0.04); }
.ytdf-string-row:first-child, .ytdf-select-row:first-child { border-top:none; }
.ytdf-select-label { width:140px; font-size:14px; }
.ytdf-string-label { font-size:14px; }
.ytdf-string-input, .ytdf-select-input { flex:1; }
.ytdf-string-input input[type="text"], .ytdf-select-input select { width:100%; padding:8px; border-radius:6px; border:1px solid #cbd5e1; }
.ytdf-help { margin-left:auto; cursor:pointer; user-select:none; font-size:12px; color:#0366d6; }
.ytdf-footer-note { font-size:12px; color:#718096; margin-top:6px; }
@media (max-width:600px) { #ytdf-panel { width:92%; top: 20px; left: 4%; transform: none; } }
    `;

    // Inject HTML and CSS into the page
    GM_addStyle(panelCSS);

    // Function to create the panel element using DOM manipulation
    // Use this approach to bypass YouTube's TrustedHTML restriction
    function createPanelElement() {
        const panel = document.createElement('div');
        panel.id = 'ytdf-panel';

        // Header
        const header = document.createElement('div');
        header.className = 'ytdf-header';
        const h1 = document.createElement('h1');
        h1.dataset.langKey = 'panelTitle';
        h1.textContent = 'Settings';
        const buttonsDiv = document.createElement('div');
        buttonsDiv.className = 'ytdf-buttons'; const resetButton = document.createElement('button');
        resetButton.className = 'ytdf-button ytdf-secondary';
        resetButton.id = 'ytdf-reset-btn';
        resetButton.dataset.langKey = 'resetBtn';
        resetButton.textContent = 'Reset'; const saveButton = document.createElement('button');
        saveButton.className = 'ytdf-button';
        saveButton.id = 'ytdf-save-btn';
        saveButton.dataset.langKey = 'saveBtn';
        saveButton.textContent = 'Save'; buttonsDiv.appendChild(resetButton);
        buttonsDiv.appendChild(saveButton);
        header.appendChild(h1);
        header.appendChild(buttonsDiv);
        panel.appendChild(header);

        // Container
        const container = document.createElement('div');
        container.className = 'ytdf-container';

        // Section: Selects (Language)
        const selectsSection = document.createElement('div');
        selectsSection.className = 'ytdf-section ytdf-selects';
        const selectRow = document.createElement('div');
        selectRow.className = 'ytdf-select-row';
        const selectLabel = document.createElement('div');
        selectLabel.className = 'ytdf-select-label';
        selectLabel.dataset.langKey = 'languageLabel';
        selectLabel.textContent = 'Language';
        const selectInput = document.createElement('div');
        selectInput.className = 'ytdf-select-input';
        const langSelect = document.createElement('select');
        langSelect.id = 'ytdf-lang-select';
        const optionEn = document.createElement('option');
        optionEn.value = 'en';
        optionEn.textContent = 'English';
        const optionZh = document.createElement('option');
        optionZh.value = 'zh';
        optionZh.textContent = '中文';
        langSelect.appendChild(optionEn);
        langSelect.appendChild(optionZh);
        selectInput.appendChild(langSelect);
        selectRow.appendChild(selectLabel);
        selectRow.appendChild(selectInput);
        selectsSection.appendChild(selectRow);

        // Section: Switches (Prepend dates)
        const switchesSection = document.createElement('div');
        switchesSection.className = 'ytdf-section ytdf-switches';
        const switchRow = document.createElement('div');
        switchRow.className = 'ytdf-switch-row';
        const label = document.createElement('label');
        const toggleDiv = document.createElement('div');
        toggleDiv.className = 'ytdf-toggle';
        toggleDiv.dataset.key = 'prependDatesEnabled';
        toggleDiv.setAttribute('role', 'switch');
        toggleDiv.setAttribute('tabindex', '0');
        const knobDiv = document.createElement('div');
        knobDiv.className = 'ytdf-knob';
        toggleDiv.appendChild(knobDiv);
        const spanPrependDates = document.createElement('span');
        spanPrependDates.dataset.langKey = 'prependDatesLabel';
        spanPrependDates.textContent = 'Prepend dates';
        const helpDiv = document.createElement('div');
        helpDiv.className = 'ytdf-help';
        helpDiv.dataset.langKey = 'prependDatesHelp';
        helpDiv.dataset.desc = 'Add absolute dates before or after the original dates.';
        helpDiv.textContent = '❓';
        label.appendChild(toggleDiv);
        label.appendChild(spanPrependDates);
        label.appendChild(helpDiv);
        switchRow.appendChild(label);
        switchesSection.appendChild(switchRow);

        // Section: Strings Basic (Date format, Old upload keywords)
        const stringsBasicSection = document.createElement('div');
        stringsBasicSection.className = 'ytdf-section ytdf-strings-basic';
        const footerNoteBasic = document.createElement('div');
        footerNoteBasic.className = 'ytdf-footer-note';
        footerNoteBasic.dataset.langKey = 'stringsFooterBasic';
        footerNoteBasic.textContent = 'Click the ? icon to use the example.';
        stringsBasicSection.appendChild(footerNoteBasic);

        // Date format row
        const dateFormatRow = document.createElement('div');
        dateFormatRow.className = 'ytdf-string-row';
        dateFormatRow.dataset.key = 'dateFormat';
        const dateFormatLabelHelp = document.createElement('div');
        dateFormatLabelHelp.className = 'ytdf-string-label-help';
        const dateFormatLabel = document.createElement('div');
        dateFormatLabel.className = 'ytdf-string-label';
        dateFormatLabel.dataset.langKey = 'dateFormatLabel';
        dateFormatLabel.textContent = 'Date format';
        const dateFormatHelp = document.createElement('div');
        dateFormatHelp.className = 'ytdf-help';
        dateFormatHelp.dataset.langKey = 'dateFormatHelp';
        dateFormatHelp.dataset.example = 'yyyy-MM-dd';
        dateFormatHelp.dataset.desc = 'Date format help';
        dateFormatHelp.textContent = '❓';
        dateFormatLabelHelp.appendChild(dateFormatLabel);
        dateFormatLabelHelp.appendChild(dateFormatHelp);
        const dateFormatInputDiv = document.createElement('div');
        dateFormatInputDiv.className = 'ytdf-string-input';
        const dateFormatInput = document.createElement('input');
        dateFormatInput.type = 'text';
        dateFormatInput.id = 'ytdf-dateFormat';
        dateFormatInput.setAttribute('placeholder', '');
        dateFormatInputDiv.appendChild(dateFormatInput);
        dateFormatRow.appendChild(dateFormatLabelHelp);
        dateFormatRow.appendChild(dateFormatInputDiv);
        stringsBasicSection.appendChild(dateFormatRow);

        // Old upload keywords row
        const oldUploadKeywordsRow = document.createElement('div');
        oldUploadKeywordsRow.className = 'ytdf-string-row';
        oldUploadKeywordsRow.dataset.key = 'oldUploadKeywords';
        const oldUploadKeywordsLabelHelp = document.createElement('div');
        oldUploadKeywordsLabelHelp.className = 'ytdf-string-label-help';
        const oldUploadKeywordsLabel = document.createElement('div');
        oldUploadKeywordsLabel.className = 'ytdf-string-label';
        oldUploadKeywordsLabel.dataset.langKey = 'oldUploadKeywordsLabel';
        oldUploadKeywordsLabel.textContent = 'Old upload keywords';
        const oldUploadKeywordsHelp = document.createElement('div');
        oldUploadKeywordsHelp.className = 'ytdf-help';
        oldUploadKeywordsHelp.dataset.langKey = 'oldUploadKeywordsHelp';
        oldUploadKeywordsHelp.dataset.example = 'day week month year';
        oldUploadKeywordsHelp.dataset.desc = 'Old upload keywords help';
        oldUploadKeywordsHelp.textContent = '❓';
        oldUploadKeywordsLabelHelp.appendChild(oldUploadKeywordsLabel);
        oldUploadKeywordsLabelHelp.appendChild(oldUploadKeywordsHelp);
        const oldUploadKeywordsInputDiv = document.createElement('div');
        oldUploadKeywordsInputDiv.className = 'ytdf-string-input';
        const oldUploadKeywordsInput = document.createElement('input');
        oldUploadKeywordsInput.type = 'text';
        oldUploadKeywordsInput.id = 'ytdf-oldUploadKeywords';
        oldUploadKeywordsInput.setAttribute('placeholder', '');
        oldUploadKeywordsInputDiv.appendChild(oldUploadKeywordsInput);
        oldUploadKeywordsRow.appendChild(oldUploadKeywordsLabelHelp);
        oldUploadKeywordsRow.appendChild(oldUploadKeywordsInputDiv);
        stringsBasicSection.appendChild(oldUploadKeywordsRow);

        // Section: Strings I18n (Date and time keywords, Ago keywords, Month names)
        const stringsI18nSection = document.createElement('div');
        stringsI18nSection.className = 'ytdf-section ytdf-strings-i18n';
        const footerNoteI18n = document.createElement('div');
        footerNoteI18n.className = 'ytdf-footer-note';
        footerNoteI18n.dataset.langKey = 'stringsFooterI18n';
        footerNoteI18n.textContent = 'You must complete these two keywords settings if your YouTube language is not English, 中文 or 日本語.';
        stringsI18nSection.appendChild(footerNoteI18n);

        // Date and time keywords row
        const dateTimeKeywordsRow = document.createElement('div');
        dateTimeKeywordsRow.className = 'ytdf-string-row';
        dateTimeKeywordsRow.dataset.key = 'dateTimeKeywords';
        const dateTimeKeywordsLabelHelp = document.createElement('div');
        dateTimeKeywordsLabelHelp.className = 'ytdf-string-label-help';
        const dateTimeKeywordsLabel = document.createElement('div');
        dateTimeKeywordsLabel.className = 'ytdf-string-label';
        dateTimeKeywordsLabel.dataset.langKey = 'dateTimeKeywordsLabel';
        dateTimeKeywordsLabel.textContent = 'Date and time keywords';
        const dateTimeKeywordsHelp = document.createElement('div');
        dateTimeKeywordsHelp.className = 'ytdf-help';
        dateTimeKeywordsHelp.dataset.langKey = 'dateTimeKeywordsHelp';
        dateTimeKeywordsHelp.dataset.example = 'second minute hour day week month year';
        dateTimeKeywordsHelp.dataset.desc = 'Date time keywords help';
        dateTimeKeywordsHelp.textContent = '❓';
        dateTimeKeywordsLabelHelp.appendChild(dateTimeKeywordsLabel);
        dateTimeKeywordsLabelHelp.appendChild(dateTimeKeywordsHelp);
        const dateTimeKeywordsInputDiv = document.createElement('div');
        dateTimeKeywordsInputDiv.className = 'ytdf-string-input';
        const dateTimeKeywordsInput = document.createElement('input');
        dateTimeKeywordsInput.type = 'text';
        dateTimeKeywordsInput.id = 'ytdf-dateTimeKeywords';
        dateTimeKeywordsInput.setAttribute('placeholder', '');
        dateTimeKeywordsInputDiv.appendChild(dateTimeKeywordsInput);
        dateTimeKeywordsRow.appendChild(dateTimeKeywordsLabelHelp);
        dateTimeKeywordsRow.appendChild(dateTimeKeywordsInputDiv);
        stringsI18nSection.appendChild(dateTimeKeywordsRow);

        // Ago keywords row
        const agoKeywordsRow = document.createElement('div');
        agoKeywordsRow.className = 'ytdf-string-row';
        agoKeywordsRow.dataset.key = 'agoKeywords';
        const agoKeywordsLabelHelp = document.createElement('div');
        agoKeywordsLabelHelp.className = 'ytdf-string-label-help';
        const agoKeywordsLabel = document.createElement('div');
        agoKeywordsLabel.className = 'ytdf-string-label';
        agoKeywordsLabel.dataset.langKey = 'agoKeywordsLabel';
        agoKeywordsLabel.textContent = 'Ago keyword';
        const agoKeywordsHelp = document.createElement('div');
        agoKeywordsHelp.className = 'ytdf-help';
        agoKeywordsHelp.dataset.langKey = 'agoKeywordsHelp';
        agoKeywordsHelp.dataset.example = 'ago';
        agoKeywordsHelp.dataset.desc = 'Ago keyword help';
        agoKeywordsHelp.textContent = '❓';
        agoKeywordsLabelHelp.appendChild(agoKeywordsLabel);
        agoKeywordsLabelHelp.appendChild(agoKeywordsHelp);
        const agoKeywordsInputDiv = document.createElement('div');
        agoKeywordsInputDiv.className = 'ytdf-string-input';
        const agoKeywordsInput = document.createElement('input');
        agoKeywordsInput.type = 'text';
        agoKeywordsInput.id = 'ytdf-agoKeywords';
        agoKeywordsInput.setAttribute('placeholder', '');
        agoKeywordsInputDiv.appendChild(agoKeywordsInput);
        agoKeywordsRow.appendChild(agoKeywordsLabelHelp);
        agoKeywordsRow.appendChild(agoKeywordsInputDiv);
        stringsI18nSection.appendChild(agoKeywordsRow);

        // Month names row
        const monthNamesRow = document.createElement('div');
        monthNamesRow.className = 'ytdf-string-row';
        monthNamesRow.dataset.key = 'monthNames';
        const monthNamesLabelHelp = document.createElement('div');
        monthNamesLabelHelp.className = 'ytdf-string-label-help';
        const monthNamesLabel = document.createElement('div');
        monthNamesLabel.className = 'ytdf-string-label';
        monthNamesLabel.dataset.langKey = 'monthNamesLabel';
        monthNamesLabel.textContent = 'Month names';
        const monthNamesHelp = document.createElement('div');
        monthNamesHelp.className = 'ytdf-help';
        monthNamesHelp.dataset.langKey = 'monthNamesHelp';
        monthNamesHelp.dataset.example = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC';
        monthNamesHelp.dataset.desc = 'Month names help';
        monthNamesHelp.textContent = '❓';
        monthNamesLabelHelp.appendChild(monthNamesLabel);
        monthNamesLabelHelp.appendChild(monthNamesHelp);
        const monthNamesInputDiv = document.createElement('div');
        monthNamesInputDiv.className = 'ytdf-string-input';
        const monthNamesInput = document.createElement('input');
        monthNamesInput.type = 'text';
        monthNamesInput.id = 'ytdf-monthNames';
        monthNamesInput.setAttribute('placeholder', '');
        monthNamesInputDiv.appendChild(monthNamesInput);
        monthNamesRow.appendChild(monthNamesLabelHelp);
        monthNamesRow.appendChild(monthNamesInputDiv);
        stringsI18nSection.appendChild(monthNamesRow);

        // Day names row
        const dayNamesRow = document.createElement('div');
        dayNamesRow.className = 'ytdf-string-row';
        dayNamesRow.dataset.key = 'dayNames';
        const dayNamesLabelHelp = document.createElement('div');
        dayNamesLabelHelp.className = 'ytdf-string-label-help';
        const dayNamesLabel = document.createElement('div');
        dayNamesLabel.className = 'ytdf-string-label';
        dayNamesLabel.dataset.langKey = 'dayNamesLabel';
        dayNamesLabel.textContent = 'Day names';
        const dayNamesHelp = document.createElement('div');
        dayNamesHelp.className = 'ytdf-help';
        dayNamesHelp.dataset.langKey = 'dayNamesHelp';
        dayNamesHelp.dataset.example = 'Sun Mon Tue Wed Thu Fri Sat';
        dayNamesHelp.dataset.desc = 'Day names help';
        dayNamesHelp.textContent = '❓';
        dayNamesLabelHelp.appendChild(dayNamesLabel);
        dayNamesLabelHelp.appendChild(dayNamesHelp);
        const dayNamesInputDiv = document.createElement('div');
        dayNamesInputDiv.className = 'ytdf-string-input';
        const dayNamesInput = document.createElement('input');
        dayNamesInput.type = 'text';
        dayNamesInput.id = 'ytdf-dayNames';
        dayNamesInput.setAttribute('placeholder', '');
        dayNamesInputDiv.appendChild(dayNamesInput);
        dayNamesRow.appendChild(dayNamesLabelHelp);
        dayNamesRow.appendChild(dayNamesInputDiv);
        stringsI18nSection.appendChild(dayNamesRow);

        container.appendChild(selectsSection);
        container.appendChild(switchesSection);
        container.appendChild(stringsBasicSection);
        container.appendChild(stringsI18nSection);

        panel.appendChild(container);
        return panel;
    }

    const panelElementToInsert = createPanelElement();

    if (panelElementToInsert) {
        document.body.appendChild(panelElementToInsert);
    } else {
        console.error('[YTDF] Failed to create panel element.');
    }

    // === Setting UI Logic ===

    // Corresponding runtime values are constants at the top of the file
    // Only local settings stored in the GM storage are modified by the UI

    const panelElement = document.getElementById('ytdf-panel');

    function saveSettings(obj) {
        GM_setValue("basic", obj);
    }

    // Update all text in the UI based on the selected language
    function updateUIText(lang) {
        currentStrings = translations[lang] || translations.en;
        document.querySelectorAll('[data-lang-key]').forEach(el => {
            const key = el.dataset.langKey;
            if (currentStrings[key]) {
                // Help elements
                if (el.dataset.desc) {
                    el.dataset.desc = currentStrings[key];
                }
                else {
                    el.innerText = currentStrings[key];
                }
            }
        });

        // Update placeholders and titles which are not covered by the generic loop
        document.querySelectorAll('.ytdf-string-input input').forEach(input => {
            input.setAttribute('placeholder', currentStrings.inputPlaceholder);
        });
        attachHelpHandlers(); // Re-attach to update tooltips
    }

    function setToggle(el, value) {
        if (value) {
            el.classList.add('ytdf-on');
        } else {
            el.classList.remove('ytdf-on');
        }
        el.setAttribute('aria-checked', !!value);
    }

    function applySettingsToUI(settings) {
        const langSelect = document.getElementById('ytdf-lang-select');
        langSelect.value = settings.language || 'en';
        updateUIText(langSelect.value);

        const arrayToStr = (array) => Array.isArray(array) ? array.join(' ') : '';

        setToggle(document.querySelector('.ytdf-toggle[data-key="prependDatesEnabled"]'), settings.prependDatesEnabled);
        document.getElementById('ytdf-dateFormat').value = settings.dateFormat || '';
        document.getElementById('ytdf-oldUploadKeywords').value = arrayToStr(settings.oldUploadKeywords);
        document.getElementById('ytdf-dateTimeKeywords').value = arrayToStr(settings.dateTimeKeywords);
        document.getElementById('ytdf-agoKeywords').value = arrayToStr(settings.agoKeywords);
        document.getElementById('ytdf-monthNames').value = arrayToStr(settings.monthNames);
        document.getElementById('ytdf-dayNames').value = arrayToStr(settings.dayNames);
    }

    function readUIToSettings() {
        const stringToArray = (str) => str.split(' ').filter(Boolean);
        return {
            language: document.getElementById('ytdf-lang-select').value,
            prependDatesEnabled: document.querySelector('.ytdf-toggle[data-key="prependDatesEnabled"]').classList.contains('ytdf-on'),
            dateFormat: document.getElementById('ytdf-dateFormat').value,
            oldUploadKeywords: stringToArray(document.getElementById('ytdf-oldUploadKeywords').value),
            dateTimeKeywords: stringToArray(document.getElementById('ytdf-dateTimeKeywords').value),
            agoKeywords: stringToArray(document.getElementById('ytdf-agoKeywords').value),
            monthNames: stringToArray(document.getElementById('ytdf-monthNames').value),
            dayNames: stringToArray(document.getElementById('ytdf-dayNames').value),
        };
    }

    function attachToggleHandlers() {
        document.querySelectorAll('.ytdf-toggle').forEach(t => {
            t.addEventListener('click', () => {
                const newState = !t.classList.contains('ytdf-on');
                setToggle(t, newState);
            });
            t.addEventListener('keydown', (ev) => {
                if (ev.key === ' ' || ev.key === 'Enter') {
                    ev.preventDefault();
                    t.click();
                }
            });
        });
    }

    function attachHelpHandlers() {
        document.querySelectorAll('.ytdf-string-row, .ytdf-switch-row').forEach(row => {
            const help = row.querySelector('.ytdf-help');
            if (!help) return;
            const example = help.dataset.example || '';
            const desc = help.dataset.desc || '';
            const key = row.dataset.key;

            // Switch rows
            if (!key) {
                help.setAttribute('title', desc);
                return;
            }

            const titleText = currentStrings.helpTooltip
                .replace('{desc}', desc)
                .replace('{example}', example);
            help.setAttribute('title', titleText);

            if (example) {
                help.addEventListener('click', () => {
                    const input = document.getElementById('ytdf-' + key);
                    if (input) {
                        input.value = example;
                        input.focus();
                    }
                });
            }
        });
    }

    // --- Panel Visibility Controls ---
    function showPanel() {
        if (panelElement) panelElement.style.display = 'block';
    }

    function hidePanel() {
        if (panelElement) panelElement.style.display = 'none';
    }

    GM_registerMenuCommand(currentStrings.menuTitle, showPanel);

    document.addEventListener('keydown', (ev) => {
        if (ev.key === 'Escape') {
            hidePanel();
        }
    });

    document.addEventListener('click', (e) => {
        // Hide only if the panel is visible and the click is outside the panel
        if (panelElement.style.display === 'block' && !panelElement.contains(e.target)) {
            // And also ensure the click is not on a Tampermonkey menu item
            if (!e.target.closest('.GM-style-mt')) {
                hidePanel();
            }
        }
    }, true); // Use capture to catch the click early

    // --- Handlers ---
    // save button
    document.getElementById('ytdf-save-btn').addEventListener('click', () => {
        saveSettings(readUIToSettings());
        alert(currentStrings.alertSaved);
    });
    // reset button
    document.getElementById('ytdf-reset-btn').addEventListener('click', () => {
        if (!confirm(currentStrings.confirmReset)) return;
        // Saving default values, but keeping the language setting
        const currentLang = document.getElementById('ytdf-lang-select').value;
        const settingsToSave = { language: currentLang };
        saveSettings(settingsToSave);
        alert(currentStrings.alertSaved);
        applySettingsToUI(settingsToSave);
    });
    // language dropdown
    document.getElementById('ytdf-lang-select').addEventListener('change', (e) => {
        updateUIText(e.target.value);
    });

    // --- Initialize the UI ---
    try {
        applySettingsToUI(SETTINGS);
        attachToggleHandlers();
    }
    catch (e) {
        console.error('[YTDF] Error initializing UI:', e);
    }

    // === Date Formatting ===

    /**
     * Find elements in a NodeList containing any of the specified keywords.
     *
     * @param {NodeList | Array} nodeList - A NodeList or array of elements to search.
     * @param {string[]} keywords - Array of keywords to search for in element textContent.
     * @param {boolean} [findAll=false] - If true, return all matches; if false, return the first match.
     * @returns {Element | Element[] | undefined} - A single element, an array of elements, or undefined if no match.
     */
    function findElementsByKeywords(nodeList, keywords, findAll = false) {
        // Convert NodeList to a real Array (if it isn't already)
        const elements = Array.from(nodeList);

        if (findAll) {
            // Return all matching elements as an array
            return elements.filter(el =>
                keywords.some(keyword => el.textContent.includes(keyword))
            );
        } else {
            // Return only the first matching element
            return elements.find(el =>
                keywords.some(keyword => el.textContent.includes(keyword))
            );
        }
    }

    /**
     * Format a date (string, number, or Date) into a string with a custom template.
     *
     * @param {string|number|Date} date - Date string, timestamp, or Date object
     * @param {string} [template="yyyy-MM-dd HH:mm:ss"] - Template string (e.g., "yyyy-MM-dd HH:mm:ss")
     * @param {boolean} [useLocal=true] - Whether to use local time (true) or UTC (false)
     * @param {string[]} [months] - Uppercase month names (default: English JAN-DEC)
     * @param {string[]} [days] - Day names (default: English Sun-Sat)
     * @returns {string} Formatted date string, or "" if invalid
     *
     * Supported tokens:
     * - yyyy: 4-digit year
     * - yy: 2-digit year
     * - MMM: uppercase month name from given array
     * - MM: 2-digit month
     * - dd: 2-digit day
     * - ww: day of week
     * - HH: 24-hour format (00-23)
     * - hh: 12-hour format (01-12)
     * - ap: AM/PM
     * - mm: 2-digit minutes
     * - ss: 2-digit seconds
     */
    function getDateStr(date, template = "yyyy-MM-dd HH:mm:ss", useLocal = true, months, days) {
        const defaultMonths = [
            "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
            "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
        ];
        const defaultDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
        const monthNames = months || defaultMonths;
        const dayNames = days || defaultDays;

        const dt = new Date(date);

        // If invalid date, return empty string
        if (isNaN(dt.getTime())) {
            return "";
        }

        // Get local or UTC info automatically
        const getMethod = (key) => useLocal ? dt[`get${key}`]() : dt[`getUTC${key}`]();

        const pad = (num, size = 2) => String(num).padStart(size, "0");

        const map = {
            yyyy: String(getMethod("FullYear")),
            yy: String(getMethod("FullYear")).slice(-2),
            MMM: monthNames[getMethod("Month")],
            MM: pad(getMethod("Month") + 1),
            dd: pad(getMethod("Date")),
            ww: dayNames[getMethod("Day")],
            HH: pad(getMethod("Hours")),
            hh: pad((getMethod("Hours") % 12) || 12),
            ap: getMethod("Hours") < 12 ? "AM" : "PM",
            mm: pad(getMethod("Minutes")),
            ss: pad(getMethod("Seconds")),
        };
        return template.replaceAll(/(yy(yy)?|MMM?|dd|ww|HH|hh|mm|ss|ap)/g, n => map[n]);
    }

    function getUploadDate() {
        let el = document.body.querySelector('player-microformat-renderer script');
        if (el) {
            let parts = el.textContent.split('"startDate":"', 2);
            if (parts.length == 2) {
                return parts[1].split('"', 1)[0];
            }
            parts = el.textContent.split('"uploadDate":"', 2);
            if (parts.length == 2) {
                return parts[1].split('"', 1)[0];
            }
        }

        return null;
    }

    // Check if the video is a live broadcast
    function getIsLiveBroadcast() {
        let el = document.body.querySelector('player-microformat-renderer script');
        if (!el) {
            return null;
        }

        let parts = el.textContent.split('"isLiveBroadcast":', 2);
        if (parts.length != 2) {
            return false;
        }

        let isLiveBroadcast = !!parts[1].split(',', 1)[0];
        if (!isLiveBroadcast) {
            return false;
        }

        parts = el.textContent.split('"endDate":"', 2);
        if (parts.length == 2) {
            return false;
        }

        return true;
    }

    // Update the upload date in the video description
    function processDescription() {
        let uploadDate = getUploadDate();
        if (uploadDate) {
            // Format the date and check if it's a live broadcast
            uploadDate = getDateStr(uploadDate, DATE_FORMAT, true, MONTH_NAMES, DAY_NAMES);
            let isLiveBroadcast = getIsLiveBroadcast();

            if (isLiveBroadcast) {
                document.body.classList.add('ytud-description-live');
            } else {
                document.body.classList.remove('ytud-description-live');
            }

            // Update the upload date in the video description
            let el = document.querySelector('#info-container > #info > b');
            if (!el) {
                let span = document.querySelector('#info-container > #info > span:nth-child(1)');
                if (span) {
                    el = document.createElement('b');
                    el.textContent = uploadDate;
                    span.insertAdjacentElement('afterend', el);
                }
            } else {
                if (el.parentNode.children[1] !== el) {
                    let container = el.parentNode;
                    el = container.removeChild(el);
                    container.children[0].insertAdjacentElement('afterend', el);
                }
                if (el.firstChild.nodeValue !== uploadDate) {
                    el.firstChild.nodeValue = uploadDate;
                }
            }
        }
    }

    // This section for the topic sidebar is too different and is kept separate.
    /* search list - topic in sidebar */
    function processTopicSidebar() {
        let vids = document.querySelectorAll('#contents > ytd-universal-watch-card-renderer > #sections > ytd-watch-card-section-sequence-renderer > #lists > ytd-vertical-watch-card-list-renderer > #items > ytd-watch-card-compact-video-renderer');
        if (vids.length > 0) {
            vids.forEach((el) => {
                let holders = el.querySelectorAll('div.text-wrapper > yt-formatted-string.subtitle');
                if (holders.length === 0) {
                    return;
                }

                let holder = holders[0];
                let separator = ' • ';
                let parts = holder.firstChild.nodeValue.split(separator, 2);
                if (parts.length < 2) {
                    return;
                }
                let dateText = parts[1];
                let text = el.getAttribute('date-text');

                if (text !== null && text === dateText) {
                    return;
                }

                el.setAttribute('date-text', dateText);
                let link = el.querySelector('a#thumbnail').getAttribute('href');
                let videoId = urlToVideoId(link);
                fetchAndUpdateUploadDate(videoId, holder, dateText);
            });
        }
    }

    // Extract video id from the URL
    function urlToVideoId(url) {
        let parts = url.split('/shorts/', 2);
        if (parts.length === 2) {
            url = parts[1];
        } else {
            url = parts[0];
        }

        parts = url.split('v=', 2);
        if (parts.length === 2) {
            url = parts[1];
        } else {
            url = parts[0];
        }

        return url.split('&', 1)[0];
    }

    // Retrieve the upload date from a remote source using the video id and invoke the callback with the result
    function getRemoteUploadDate(videoId, callback) {
        let body = { "context": { "client": { "clientName": "WEB", "clientVersion": "2.20240416.01.00" } }, "videoId": videoId };

        fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(body)
        })
            .then(response => {
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }

                return response.json();
            })
            .then(data => {
                let object = data.microformat.playerMicroformatRenderer;

                if (object.liveBroadcastDetails?.isLiveNow) {
                    callback(object.liveBroadcastDetails.startTimestamp);
                    return;
                } else if (object.publishDate) {
                    callback(object.publishDate);
                    return;
                }

                callback(object.uploadDate);
            })
            .catch(error => {
                console.error('[YTDF] There was a problem with the fetch operation:', error);
            });
    }

    /**
     * Process and update upload date info for a video element
     * @param {string} videoId - YouTube video ID
     * @param {HTMLElement} dateElem - DOM element that holds the date
     * @param {string} originalDateText - The original relative time text
     */
    function fetchAndUpdateUploadDate(videoId, dateElem, originalDateText) {
        getRemoteUploadDate(videoId, (uploadDate) => {
            const formattedDate = getDateStr(uploadDate, DATE_FORMAT, true, MONTH_NAMES, DAY_NAMES) + PROCESSED_MARKER;
            let displayText;
            // Show only formatted date for old uploads
            if (OLD_UPLOAD_KEYWORDS.some(keyword => originalDateText.includes(keyword))) {
                displayText = formattedDate;
            }
            // Keep original + formatted date for recent uploads
            else {
                if (PREPEND_DATES_ENABLED) {
                    displayText = `${formattedDate} · ${originalDateText}`;
                }
                else {
                    displayText = `${originalDateText} · ${formattedDate}`;
                }
            }
            dateElem.firstChild.nodeValue = displayText;
        });
    }

    /**
     * Finds and processes video elements on the page based on a given configuration.
     * This function queries for video containers, extracts metadata,
     * and updates the date information.
     * @param {object} config - Configuration for a specific type of video list.
     * @param {string} config.id - Identifier for the configuration.
     * @param {RegExp} config.urlPattern - A regular expression to test against the current URL.
     * @param {string} config.videoContainerSelector - CSS selector for the video container elements.
     * @param {string} config.metaSpansSelector - CSS selector for metadata spans within the video container.
     * @param {string} config.vidLinkSelector - CSS selector for the video link element.
     * @param {boolean} config.shouldCreateDateSpan - If a new date span needs to be created.
     * @param {number} config.insertAfterIndex - Index at which to insert the new date span.
     */
    function findAndProcessVids(config) {
        // Skip when current address does not match the pattern
        if (config.urlPattern &&
            !config.urlPattern.test(window.location.href) &&
            !useAllConfigsEnabled) {
            return;
        }

        let vids = document.querySelectorAll(config.videoContainerSelector);
        if (vids.length === 0) {
            // if (debugModeEnabled) console.warn(`[YTDF] No vids found for [${config.id}]`);
            return; // No videos found for this config, just return.
        }

        // Only process some elements to avoid excessive logging in debug mode.
        if (debugModeEnabled && debugVidsArrayLength != 0 && vids.length > 1) {
            vids = Array.from(vids).slice(0, debugVidsArrayLength);
        }

        vids.forEach((vidContainer) => {
            const metaSpans = vidContainer.querySelectorAll(config.metaSpansSelector);
            if (metaSpans.length === 0) {
                if (debugModeEnabled && !findValidConfigEnable)
                    console.warn(`[YTDF] No metaSpan found for [${config.id}]`);
                return;
            }

            let dateSpan;
            // Check if a new date span needs to be created.
            if (config.shouldCreateDateSpan) {
                dateSpan = document.createElement('span');
                dateSpan.className = 'inline-metadata-item style-scope ytd-video-meta-block ytdf-date';
                dateSpan.appendChild(document.createTextNode(''));
                metaSpans[config.insertAfterIndex].insertAdjacentElement('afterend', dateSpan);
            } else {
                // Find the date span by looking for keywords.
                const spansEndWithAgo = Array.from(metaSpans).filter(span =>
                    AGO_KEYWORDS.some(ago => span.textContent.includes(ago))
                )
                dateSpan = findElementsByKeywords(spansEndWithAgo, DATE_TIME_KEYWORDS);
            }

            if (!dateSpan) {
                if (debugModeEnabled && !findValidConfigEnable)
                    console.warn(`[YTDF] dateSpan is null for [${config.id}]`);
                return;
            }

            const dateText = dateSpan.textContent;
            if (!dateText) {
                if (debugModeEnabled && !findValidConfigEnable)
                    console.warn(`[YTDF] dateText is null for [${config.id}]`);
                return;
            }

            // Skip if already processed.
            if (dateText.includes(PROCESSED_MARKER)) {
                return;
            }

            // Mark as processed by adding an invisible marker character.
            // Using textContent or innerText may cause issues.
            // For example, on a channel's video page, switching between sorting methods might not update the dates.
            dateSpan.firstChild.nodeValue = dateText + PROCESSED_MARKER;

            // Find the video link element to extract the video ID.
            const vidLinkElem = vidContainer.querySelector(config.vidLinkSelector);
            if (!vidLinkElem) {
                if (debugModeEnabled && !findValidConfigEnable)
                    console.warn(`[YTDF] No vidLinkElem found for [${config.id}]`);
                return;
            }

            const vidLink = vidLinkElem.getAttribute('href');
            if (!vidLink) {
                if (debugModeEnabled && !findValidConfigEnable)
                    console.warn(`[YTDF] vidLink is null for [${config.id}]`);
                return;
            }
            const videoId = urlToVideoId(vidLink);
            if (!videoId) {
                if (debugModeEnabled && !findValidConfigEnable)
                    console.warn(`[YTDF] videoId is null for [${config.id}]`);
                return;
            }

            if (findValidConfigEnable) {
                console.warn(`[YTDF] [${config.id}] is valid`);
            }

            fetchAndUpdateUploadDate(videoId, dateSpan, dateText);
        });
    }

    // Configuration array for different video list types.
    const configs = [
        {
            id: 'Video Page Sidebar',
            urlPattern: /watch\?v=/,
            videoContainerSelector: 'yt-lockup-view-model.lockup',
            metaSpansSelector: '.yt-core-attributed-string--link-inherit-color',
            vidLinkSelector: '.yt-lockup-view-model__content-image',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Homepage Videos',
            urlPattern: /www\.youtube\.com\/?$/,
            videoContainerSelector: 'ytd-rich-item-renderer.style-scope.ytd-rich-grid-renderer',
            metaSpansSelector: '.yt-core-attributed-string--link-inherit-color',
            vidLinkSelector: '.yt-lockup-view-model__content-image',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Homepage Shorts',
            urlPattern: /XXXwww\.youtube\.com\/?$/, // remove XXX to enable this config
            videoContainerSelector: 'dummy',
            metaSpansSelector: '#metadata-line > span',
            vidLinkSelector: '.yt-lockup-view-model__content-image',
            shouldCreateDateSpan: true,
            insertAfterIndex: 0,
        },
        {
            id: 'Search List Videos',
            urlPattern: /results\?search_query=/,
            videoContainerSelector: 'ytd-video-renderer.ytd-item-section-renderer',
            metaSpansSelector: '.inline-metadata-item',
            vidLinkSelector: '#thumbnail',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Search List Shorts',
            urlPattern: /XXXresults\?search_query=/,
            videoContainerSelector: 'dummy',
            metaSpansSelector: '#metadata-line > span',
            vidLinkSelector: '.yt-lockup-view-model__content-image',
            shouldCreateDateSpan: true,
            insertAfterIndex: 0,
        },
        {
            id: 'Subscriptions',
            urlPattern: /subscriptions/,
            videoContainerSelector: '#dismissible',
            metaSpansSelector: '#metadata-line > span',
            vidLinkSelector: 'h3 > a',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Channel Videos',
            urlPattern: /www.youtube.com\/[^\/]+?\/videos/,
            videoContainerSelector: 'ytd-rich-grid-media.ytd-rich-item-renderer',
            metaSpansSelector: '#metadata-line > span',
            vidLinkSelector: 'h3 > a',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Channel Featured Videos',
            // Some channel addresses don't include an "@" symbol.
            // Lack a suitable RegEx to match these addresses without incorrectly matching others.
            // The current RegEx only matches regular channel addresses.
            urlPattern: /www.youtube.com\/@[^\/]+?\/?(featured)?$/,
            videoContainerSelector: 'ytd-grid-video-renderer.yt-horizontal-list-renderer',
            metaSpansSelector: '#metadata-line > span',
            vidLinkSelector: 'a#thumbnail',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Channel For You Videos',
            urlPattern: /www.youtube.com\/@[^\/]+?\/?(featured)?$/,
            videoContainerSelector: 'ytd-channel-video-player-renderer.ytd-item-section-renderer',
            metaSpansSelector: '#metadata-line > span',
            vidLinkSelector: '#title a',
            shouldCreateDateSpan: false,
        },
        {
            id: 'Video Playlist',
            urlPattern: /playlist\?list=/,
            videoContainerSelector: 'ytd-playlist-video-renderer.ytd-playlist-video-list-renderer',
            metaSpansSelector: 'span.yt-formatted-string',
            vidLinkSelector: 'a#thumbnail',
            shouldCreateDateSpan: false,
        }
    ];

    // === Date Formatting Launching ===

    // Run all formatters
    function runAllFormatters() {
        try {
            if (debugModeEnabled) {
                if (!validateConfigs(configs)) {
                    return;
                }
            }
            processDescription();
            processTopicSidebar();
            // Process all video lists
            configs.forEach(findAndProcessVids);
        }
        catch (error) {
            console.error('[YTDF] Error running formatters:', error);
        }
    }

    // Get all video container selectors from the configs array
    let validConfigs = [];

    function updateSelectors() {
        const currentUrl = window.location.href;
        const newConfigs = configs.filter(config => config.urlPattern.test(currentUrl));
        if (newConfigs !== validConfigs) {
            validConfigs = newConfigs;
            if (debugModeEnabled) {
                console.log('[YTDF] Valid configs:');
                console.log(validConfigs);
            }
        }
    }

    function updateFormattingTimer() {
        if (fomattingTimer !== null) { clearInterval(fomattingTimer); }
        fomattingTimer = setInterval(() => {
            runAllFormatters();
        }, 1000)
    }

    // Dates on the subscriptions page aren't updated promptly using the observer
    // Don't know why. So add a timer to handle it
    let subscriptionsPageTimer = null;
    const subscriptionsPagePattern = /subscriptions/;
    function handleSubscriptionsPageTimer() {
        if (subscriptionsPagePattern.test(window.location.href)) {
            subscriptionsPageTimer = setInterval(() => {
                runAllFormatters();
            }, 1000);
        }
        else if (subscriptionsPageTimer !== null) {
            clearInterval(subscriptionsPageTimer);
            subscriptionsPageTimer = null;
        }
    }

    window.addEventListener('yt-navigate-finish', () => {
        updateSelectors()
        handleSubscriptionsPageTimer();
    });

    // Run once on script load for the initial page content.
    updateSelectors();
    runAllFormatters();

    // Use a MutationObserver to detect page changes
    const observer = new MutationObserver((mutationsList, observer) => {
        let observerConfigs = useAllConfigsEnabled ? configs : validConfigs;
        if (observerConfigs.length === 0) {
            if (debugModeEnabled) {
                console.warn('[YTDF] No valid configs found, skipping observer...');
            }
            return;
        }

        // handleSubscriptionsPageTimer() handles the subscriptions page, just return
        if (subscriptionsPagePattern.test(window.location.href)) { return; }

        let shouldRunFormatters = false;
        const videoContainerSelectors = observerConfigs.flatMap(config => config.videoContainerSelector);
        const videoContainerSelectorsStr = videoContainerSelectors
            .filter(selector => selector && typeof selector === 'string') // filter out invalid ones
            .join(', ');

        for (const mutation of mutationsList) {
            // Condition 1: New elements added to the DOM (e.g., infinite scroll)
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) { // Node is an element
                        // Check if the added node is a video card itself
                        if (node.matches(videoContainerSelectorsStr)) {
                            shouldRunFormatters = true;
                        } else {
                            // Check if the added node contains a video card
                            if (node.querySelector(videoContainerSelectorsStr)) {
                                shouldRunFormatters = true;
                            }
                        }
                    }
                });
            }

            // Condition 2: Attributes change on existing elements
            // Only switching sorting methods on the channel videos page meets this condition
            // This handles cases like a video card's info being replaced
            if (mutation.type === 'attributes') {
                const videoLinkSelector = configs.find(config => config.id === 'Channel Videos').vidLinkSelector;
                // Check if the mutation target is a video link
                if (mutation.target.matches(videoLinkSelector)) {
                    // If the link's href changed, we need to run the formatters
                    if (mutation.oldValue !== mutation.target.href) {
                        shouldRunFormatters = true;
                    }
                }
            }
        }

        if (shouldRunFormatters) {
            // Use a debounce or a slight delay to prevent running too frequently
            clearTimeout(window.formatterDebounce);
            window.formatterDebounce = setTimeout(() => {
                runAllFormatters();
            }, 500); // Wait 500ms before running
        }
    });

    if (!timerModeEnabled) {
        // Start observing the entire document body
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['href'] // Only observe href attribute changes
        });
    }
    else {
        // Start a timer on page load
        updateFormattingTimer();
    }

    let styleTag = document.createElement('style');
    let cssCode = "#info > span:nth-child(3) {display:none !important;}"
        + "#info > span:nth-child(4) {display:none !important;}"
        + "#info > b {font-weight:500 !important;margin-left:6px !important;}"
        + "#date-text {display:none !important;}"
        + ".ytud-description-live #info > span:nth-child(1) {display:none !important;}"
        + ".ytud-description-live #info > b {margin-left:0 !important;margin-right:6px !important;}";
    styleTag.textContent = cssCode;
    document.head.appendChild(styleTag);
})();