YouTube Multi-Player

Play multiple videos or chatroom simultaneously in new tabs or windows, and pin any video to the top. Add Bookmarklet start, resolution spoofing for specific quality, copy buttons, and a Home button.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         YouTube Multi-Player
// @name:zh-TW   YouTube 多重播放器
// @namespace    http://tampermonkey.net/
// @author       Dxzy
// @version      8.4.9
// @match        https://www.youtube.com/*
// @exclude      https://studio.youtube.com/*
// @exclude      https://accounts.youtube.com/*
// @exclude      https://www.youtube.com/live_chat*
// @exclude      https://www.youtube.com/embed/*
// @exclude      https://www.yout-ube.com/*
// @grant        GM_info
// @grant        GM_registerMenuCommand
// @license      MIT
// @description:zh-TW  以新分頁或新視窗同時播放多個影片或聊天室,並可將任意項目放大置頂。新增書籤啟動、偽造解析度指定畫質、複製按鈕、首頁鈕。
// @description  Play multiple videos or chatroom simultaneously in new tabs or windows, and pin any video to the top. Add Bookmarklet start, resolution spoofing for specific quality, copy buttons, and a Home button.
// ==/UserScript==
(function () {
    'use strict';

    // = CONFIG 區塊 - 核心參數設定 / Core Configuration =
    const CONFIG = {
        MAX_PINNED: 3,
        PORTRAIT_HEIGHT_THRESHOLD: 1.1,
        LANDSCAPE_ASPECT_RATIO_THRESHOLD: 1.7,
        LANDSCAPE_COLUMN_CONFIG: [2, 5, 10, 17, 26, 37, 50, 65],
        PORTRAIT_MAX_COLUMNS: 4,
        LIST_COUNT: 4,
        SCREEN_WIDTH_DEFAULT: 1080,
        RESOLUTION_PRESETS: [3, 4, 5, 5, 6],
        RESOLUTION_LEVELS: {
            1: [3840, 2160], 2: [2560, 1440], 3: [1920, 1080], 4: [1280, 720],
            5: [854, 480], 6: [640, 360], 7: [432, 240], 8: [256, 144]
        },
        SPOOF_RESOLUTION_ENABLED: true,
        HOME_PINNED_BY_DEFAULT: true,
        ADD_BUTTON_ENABLED_STORAGE_KEY: 'ytMulti_addButtonEnabled',
        SYNC_EVENT_KEY: 'ytMulti_syncEvent',
        ASPECT_RATIO_STANDARD: 16/9,
        CHAT_SUFFIX: '_chat',
        VIDEO_SUFFIX: '_video',
        DOMAIN_MODE_STORAGE_KEY: 'ytMulti_domainMode',
        DOMAIN_MODES: [
            { key: 'YT', domain: 'www.youtube.com', label: 'YT' },
            { key: 'YU', domain: 'www.yout-ube.com', label: 'YU' }
        ],
        POLLING_INTERVAL: 2000,
        RESIZE_DEBOUNCE: 100,
        HOVER_TIMEOUT: 5000
    };

    // = 多語言設定 / Multi-language =
    const LANG = {
        zh: {
            play: '▶ 播放', playIcon: '▶', modeCurrentTab: '分頁', modeNewTab: '分頁+', modeNewWindow: '視窗',
            list: '清單', noVideos: '當前清單無影片', addButton: '添加', addIcon: '+',
            settingsTitle: '🔧 修改設定值', settingsPrompt: '請輸入要修改的設定項目編號(輸入 0 退出):\n\n',
            settingsValuePrompt: '\n當前值: {current}\n預設值: {default}\n說明: {desc}\n\n請輸入新值(輸入 0 取消): ',
            settingsSaved: '✅ 設定已儲存', settingsCancelled: '❌ 已取消',
            settingsInvalid: '⚠️ 輸入無效', settingsExit: '👋 已退出',
            descMaxPinned: '最多可同時置頂的影片數量',
            descListCount: '影片清單總數量',
            descPortraitMaxCols: '縱向螢幕最大欄數',
            descLandscapeConfig: '橫向欄數閾值(逗號分隔)',
            descPortraitThreshold: '縱向高度倍數閾值',
            descLandscapeAR: '橫向寬高比閾值',
            descScreenWidth: '偽造解析度時使用的螢幕寬度基準值',
            descResolutionPresets: '解析度檔位 [1.2160 2.1440 3.1080 4.720 5.480 6.360 7.240 8.144],預設 3,4,5,5,6',
            descSpoofResolution: '偽造解析度開關:1=開啟 2=關閉',
            descHomePinnedDefault: '新增首頁時預設置頂:1=開啟 2=關閉',
            spoofOn: '開啟', spoofOff: '關閉'
        },
        en: {
            play: '▶ Play', playIcon: '▶', modeCurrentTab: 'Tab', modeNewTab: 'Tab+', modeNewWindow: 'Window',
            list: 'List', noVideos: 'No videos in current list', addButton: 'Add', addIcon: '+',
            settingsTitle: '🔧 Modify Settings', settingsPrompt: 'Enter setting number (0 to exit):\n\n',
            settingsValuePrompt: '\nCurrent: {current}\nDefault: {default}\nDesc: {desc}\n\nNew value (0 to cancel): ',
            settingsSaved: '✅ Settings saved', settingsCancelled: '❌ Cancelled',
            settingsInvalid: '⚠️ Invalid input', settingsExit: '👋 Exited',
            descMaxPinned: 'Max pinned videos',
            descListCount: 'Total list count',
            descPortraitMaxCols: 'Max columns in portrait',
            descLandscapeConfig: 'Landscape column thresholds (comma-separated)',
            descPortraitThreshold: 'Portrait height multiplier threshold',
            descLandscapeAR: 'Landscape AR threshold',
            descScreenWidth: 'Screen width baseline for resolution spoofing',
            descResolutionPresets: 'Resolution levels [1.2160 2.1440 3.1080 4.720 5.480 6.360 7.240 8.144], default 3,4,5,5,6',
            descSpoofResolution: 'Spoof resolution toggle: 1=Enable 2=Disable',
            descHomePinnedDefault: 'Pin new homepages by default: 1=Enable 2=Disable',
            spoofOn: 'ON', spoofOff: 'OFF'
        }
    };
    const LANG_CODE = navigator.language.startsWith('zh') ? 'zh' : 'en';
    const t = (key, params = {}) => {
        let str = LANG[LANG_CODE][key] || key;
        for (const [k, v] of Object.entries(params)) str = str.replace(`{${k}}`, v);
        return str;
    };

    // = 設定項目定義 / Settings Definitions =
    const CONFIG_ITEMS = {
        MAX_PINNED: { default: CONFIG.MAX_PINNED, type: 'int', min: 1, max: 10, descZh: LANG.zh.descMaxPinned, descEn: LANG.en.descMaxPinned },
        LIST_COUNT: { default: CONFIG.LIST_COUNT, type: 'int', min: 1, max: 10, descZh: LANG.zh.descListCount, descEn: LANG.en.descListCount },
        PORTRAIT_MAX_COLUMNS: { default: CONFIG.PORTRAIT_MAX_COLUMNS, type: 'int', min: 1, max: 6, descZh: LANG.zh.descPortraitMaxCols, descEn: LANG.en.descPortraitMaxCols },
        LANDSCAPE_COLUMN_CONFIG: { default: [...CONFIG.LANDSCAPE_COLUMN_CONFIG], type: 'array', descZh: LANG.zh.descLandscapeConfig, descEn: LANG.en.descLandscapeConfig },
        PORTRAIT_HEIGHT_THRESHOLD: { default: CONFIG.PORTRAIT_HEIGHT_THRESHOLD, type: 'float', min: 1.0, max: 2.0, step: 0.1, descZh: LANG.zh.descPortraitThreshold, descEn: LANG.en.descPortraitThreshold },
        LANDSCAPE_ASPECT_RATIO_THRESHOLD: { default: CONFIG.LANDSCAPE_ASPECT_RATIO_THRESHOLD, type: 'float', min: 1.0, max: 3.0, step: 0.1, descZh: LANG.zh.descLandscapeAR, descEn: LANG.en.descLandscapeAR },
        SCREEN_WIDTH_DEFAULT: { default: CONFIG.SCREEN_WIDTH_DEFAULT, type: 'int', min: 144, max: 3840, descZh: LANG.zh.descScreenWidth, descEn: LANG.en.descScreenWidth },
        RESOLUTION_PRESETS: { default: [...CONFIG.RESOLUTION_PRESETS], type: 'preset', descZh: LANG.zh.descResolutionPresets, descEn: LANG.en.descResolutionPresets },
        SPOOF_RESOLUTION_ENABLED: { default: CONFIG.SPOOF_RESOLUTION_ENABLED, type: 'toggle12', descZh: LANG.zh.descSpoofResolution, descEn: LANG.en.descSpoofResolution },
        HOME_PINNED_BY_DEFAULT: { default: CONFIG.HOME_PINNED_BY_DEFAULT, type: 'toggle12', descZh: LANG.zh.descHomePinnedDefault, descEn: LANG.en.descHomePinnedDefault }
    };
    const SETTINGS_PREFIX = 'ytMulti_setting_';

    // = 設定載入與儲存 / Settings Load & Save =
    const loadSettings = () => {
        for (const [key, cfg] of Object.entries(CONFIG_ITEMS)) {
            const stored = localStorage.getItem(SETTINGS_PREFIX + key);
            if (stored !== null) {
                try {
                    CONFIG[key] = cfg.type === 'array' || cfg.type === 'preset' ? JSON.parse(stored) :
                                  cfg.type === 'bool' || cfg.type === 'toggle12' ? stored === 'true' :
                                  cfg.type === 'float' ? parseFloat(stored) : parseInt(stored, 10);
                } catch (e) { CONFIG[key] = cfg.default; }
            }
        }
    };

    const generateStorageKeys = () => {
        const k = {};
        for (let i = 1; i <= CONFIG.LIST_COUNT; i++) k[`list${i}`] = `ytMulti_videoList${i}`;
        return k;
    };

    loadSettings();
    let STORAGE_LISTS = generateStorageKeys();

    const saveSetting = (key, val) => {
        const type = CONFIG_ITEMS[key].type;
        localStorage.setItem(SETTINGS_PREFIX + key, (type === 'array' || type === 'preset') ? JSON.stringify(val) : String(val));
        CONFIG[key] = val;

        if (key === 'LIST_COUNT') {
            STORAGE_LISTS = generateStorageKeys();
            if (!STORAGE_LISTS[currentList]) {
                currentList = Object.keys(STORAGE_LISTS)[0];
                localStorage.setItem(STORAGE_CURRENT, currentList);
            }
            updateListButtonCount();
        }
    };

    // = 輔助函數 / Helper Functions =
    const isChatId = id => id.endsWith(CONFIG.CHAT_SUFFIX);
    const isVideoId = id => id.endsWith(CONFIG.VIDEO_SUFFIX);
    const isHomepage = id => id.startsWith('homepage');
    const getBaseId = id => {
        if (isChatId(id)) return id.slice(0, -CONFIG.CHAT_SUFFIX.length);
        if (isVideoId(id)) return id.slice(0, -CONFIG.VIDEO_SUFFIX.length);
        return id;
    };
    const getChatId = id => {
        if (isChatId(id)) return id;
        if (isVideoId(id)) return id.slice(0, -CONFIG.VIDEO_SUFFIX.length) + CONFIG.CHAT_SUFFIX;
        return id + CONFIG.CHAT_SUFFIX;
    };
    const getVideoId = id => {
        if (isChatId(id)) return id.slice(0, -CONFIG.CHAT_SUFFIX.length);
        if (isVideoId(id)) return id.slice(0, -CONFIG.VIDEO_SUFFIX.length);
        return id;
    };
    const isFullUrl = id => id.startsWith('http');
    const getVideoIdFromUrl = url => {
        const m1 = url.match(/[?&]v=([A-Za-z0-9_-]{11})/);
        if (m1) return m1[1];
        const m2 = url.match(/youtu\.be\/([A-Za-z0-9_-]{11})/);
        return m2 ? m2[1] : null;
    };
    const getListIdFromUrl = url => {
        const m = url.match(/[?&]list=([^&]+)/);
        return m ? m[1] : null;
    };
    const isWatchPage = () => /\/watch[?\/]/.test(location.href) || /\/live[?\/]/.test(location.href);
    const getPresetLabel = level => {
        const r = CONFIG.RESOLUTION_LEVELS[level];
        return r ? `${r[0]}P` : '?';
    };
    const formatPresets = arr => arr.map((l, i) => i === 0 ? CONFIG.SCREEN_WIDTH_DEFAULT : getPresetLabel(l)).join(',');

    // = 儲存格式轉換 / Storage Format Migration =
    const migrateStorageFormat = () => {
        for (const key of Object.values(STORAGE_LISTS)) {
            const raw = localStorage.getItem(key);
            if (!raw) continue;
            try {
                const arr = JSON.parse(raw);
                let changed = false;
                const migrated = arr.map(id => {
                    if (id === 'homepage') { changed = true; return 'homepage_1'; }
                    if (isFullUrl(id)) {
                        changed = true;
                        const vid = getVideoIdFromUrl(id);
                        if (!vid) return id;
                        return id.includes('_chat') || id.includes('live_chat') ? vid + CONFIG.CHAT_SUFFIX : vid + CONFIG.VIDEO_SUFFIX;
                    }
                    if (!isVideoId(id) && !isChatId(id) && !isHomepage(id) && /^[A-Za-z0-9_-]{11}$/.test(id)) {
                        changed = true;
                        return id + CONFIG.VIDEO_SUFFIX;
                    }
                    return id;
                });
                if (changed) localStorage.setItem(key, JSON.stringify(migrated));
            } catch (e) {}
        }
    };
    migrateStorageFormat();

    // = 其他儲存鍵值 / Other Storage Keys =
    const STORAGE_POS = 'ytMulti_btnPos';
    const STORAGE_MODE = 'ytMulti_openMode';
    const STORAGE_CURRENT = 'ytMulti_currentList';
    const STORAGE_PINNED_PREFIX = 'ytMulti_pinned_';

    let currentList = localStorage.getItem(STORAGE_CURRENT) || 'list1';
    if (!STORAGE_LISTS[currentList]) {
        currentList = Object.keys(STORAGE_LISTS)[0];
        localStorage.setItem(STORAGE_CURRENT, currentList);
    }
    const getCurrentDomainMode = () => CONFIG.DOMAIN_MODES.find(m => m.key === localStorage.getItem(CONFIG.DOMAIN_MODE_STORAGE_KEY)) || CONFIG.DOMAIN_MODES[0];

    // = 設定選單 / Settings Menu =
    if (typeof GM_registerMenuCommand !== 'undefined') GM_registerMenuCommand(t('settingsTitle'), openSettingsMenu);
    function openSettingsMenu() {
        loadSettings();
        const items = Object.entries(CONFIG_ITEMS);
        while (true) {
            let menu = t('settingsPrompt');
            items.forEach(([k, c], i) => {
                const cur = CONFIG[k];
                let curStr, defStr;
                if (c.type === 'array' || c.type === 'preset') {
                    curStr = c.type === 'preset' ? formatPresets(cur) : JSON.stringify(cur);
                    defStr = c.type === 'preset' ? formatPresets(c.default) : JSON.stringify(c.default);
                } else if (c.type === 'toggle12') {
                    curStr = cur ? `1 (${t('spoofOn')})` : `2 (${t('spoofOff')})`;
                    defStr = c.default ? `1 (${t('spoofOn')})` : `2 (${t('spoofOff')})`;
                } else {
                    curStr = cur; defStr = c.default;
                }
                menu += `${i + 1}. ${k}\n   ${LANG_CODE === 'zh' ? c.descZh : c.descEn}\n   當前/Current: ${curStr}\n   預設/Default: ${defStr}\n\n`;
            });
            const choice = prompt(menu + '0 = Exit', '0');
            if (!choice || choice === '0') return;
            const idx = parseInt(choice, 10) - 1;
            if (isNaN(idx) || idx < 0 || idx >= items.length) { alert(t('settingsInvalid')); continue; }
            const [key, cfg] = items[idx];
            const cur = CONFIG[key];
            let curStr, defStr;
            if (cfg.type === 'array' || cfg.type === 'preset') {
                curStr = cfg.type === 'preset' ? formatPresets(cur) : JSON.stringify(cur);
                defStr = cfg.type === 'preset' ? formatPresets(cfg.default) : JSON.stringify(cfg.default);
            } else if (cfg.type === 'toggle12') {
                curStr = cur ? `1 (${t('spoofOn')})` : `2 (${t('spoofOff')})`;
                defStr = cfg.default ? `1 (${t('spoofOn')})` : `2 (${t('spoofOff')})`;
            } else {
                curStr = cur; defStr = cfg.default;
            }
            const desc = LANG_CODE === 'zh' ? cfg.descZh : cfg.descEn;
            const input = prompt(t('settingsValuePrompt', { current: curStr, default: defStr, desc }), '0');
            if (!input || input === '0') continue;
            try {
                let val;
                if (cfg.type === 'array') {
                    val = input.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
                    if (!val.length) throw 'E';
                } else if (cfg.type === 'preset') {
                    val = input.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n >= 1 && n <= 8);
                    if (!val.length || val.length !== 5) throw 'E';
                } else if (cfg.type === 'toggle12') {
                    val = input === '1';
                } else if (cfg.type === 'float') {
                    val = parseFloat(input); if (isNaN(val) || val < cfg.min || val > cfg.max) throw 'E';
                } else {
                    val = parseInt(input, 10); if (isNaN(val) || val < cfg.min || val > cfg.max) throw 'E';
                }
                saveSetting(key, val); alert(t('settingsSaved')); updateListButtonCount();
            } catch (e) { alert(t('settingsInvalid')); }
        }
    }

    // = 懸浮面板 / Floating Panel =
    const panel = document.createElement('div');
    panel.id = 'ytMulti_panel';
    panel.style.cssText = `position:fixed;background:rgba(0,0,0,0.8);color:#fff;padding:6px 8px;border-radius:8px;z-index:9999;display:flex;align-items:center;cursor:move;gap:6px;box-shadow:0 4px 12px rgba(0,0,0,0.2);font-family:Arial,sans-serif;backdrop-filter:blur(4px);overflow:hidden;white-space:nowrap;transition:opacity 0.2s;pointer-events:auto;`;
    document.body.appendChild(panel);
    const savedPos = JSON.parse(localStorage.getItem(STORAGE_POS) || 'null');
    if (savedPos) { panel.style.top = savedPos.top; panel.style.left = savedPos.left; panel.style.right = 'auto'; }
    let isDragging = false;
    panel.addEventListener('mousedown', e => {
        e.preventDefault(); isDragging = true;
        const startX = e.clientX, startY = e.clientY, rect = panel.getBoundingClientRect();
        let hasMoved = false;
        const onMove = ev => { panel.style.top = rect.top + ev.clientY - startY + 'px'; panel.style.left = rect.left + ev.clientX - startX + 'px'; hasMoved = true; };
        const onUp = () => { isDragging = false; if (hasMoved) localStorage.setItem(STORAGE_POS, JSON.stringify({ top: panel.style.top, left: panel.style.left })); window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
        window.addEventListener('mousemove', onMove, { passive: true }); window.addEventListener('mouseup', onUp, { passive: true });
    }, { passive: true });

    // = 按鈕樣式 / Button Styles =
    function createStyledButton(text) {
        const btn = document.createElement('button'); btn.textContent = text;
        btn.style.cssText = `padding:6px 12px;height:36px;border:none;border-radius:6px;background:#ff0000;color:white;cursor:pointer;transition:all 0.2s;font-size:13px;font-weight:500;text-shadow:0 1px 2px rgba(0,0,0,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2);display:flex;align-items:center;justify-content:center;`;
        btn.addEventListener('mouseover', () => { if (btn.style.background !== '#00aa00' && btn.style.background !== 'rgb(0, 170, 0)') btn.style.background = '#cc0000'; });
        btn.addEventListener('mouseout', () => { if (btn.style.background !== '#00aa00' && btn.style.background !== 'rgb(0, 170, 0)') btn.style.background = '#ff0000'; });
        return btn;
    }
    function createStateButton(text, isEnabled) {
        const btn = document.createElement('button'); btn.textContent = text;
        const base = isEnabled ? '#00aa00' : '#ff0000';
        btn.style.cssText = `padding:6px 12px;height:36px;border:none;border-radius:6px;background:${base};color:white;cursor:pointer;transition:all 0.2s;font-size:13px;font-weight:500;text-shadow:0 1px 2px rgba(0,0,0,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2);display:flex;align-items:center;justify-content:center;`;
        btn.addEventListener('mouseover', () => { const c = btn.style.background; btn.style.background = (c !== '#00aa00' && c !== 'rgb(0, 170, 0)') ? '#cc0000' : '#008800'; });
        btn.addEventListener('mouseout', () => { const c = btn.style.background; btn.style.background = (c === '#008800' || c === 'rgb(0, 136, 0)') ? '#00aa00' : ((c !== '#00aa00' && c !== 'rgb(0, 170, 0)') ? '#ff0000' : c); });
        return btn;
    }

    // = 面板按鈕初始化 / Panel Buttons Init =
    const playBtn = createStyledButton(t('playIcon'));
    const modeBtn = createStyledButton(t('modeCurrentTab'));
    const listBtn = createStyledButton(`${t('list')}1`);
    const domainBtn = createStyledButton(getCurrentDomainMode().label);
    let isAddButtonEnabled = localStorage.getItem(CONFIG.ADD_BUTTON_ENABLED_STORAGE_KEY) === 'true';
    const addButtonToggle = createStateButton(t('addButton'), isAddButtonEnabled);
    const otherButtons = [modeBtn, listBtn, domainBtn, addButtonToggle];
    panel.append(playBtn, modeBtn, listBtn, domainBtn, addButtonToggle);

    const toggleAddButton = () => { isAddButtonEnabled = !isAddButtonEnabled; localStorage.setItem(CONFIG.ADD_BUTTON_ENABLED_STORAGE_KEY, String(isAddButtonEnabled)); updateAddButtonState(); isAddButtonEnabled ? startObservingVideos() : stopObservingVideos(); };
    const updateAddButtonState = () => { const base = isAddButtonEnabled ? '#00aa00' : '#ff0000'; addButtonToggle.style.background = base; addButtonToggle.onmouseover = () => { addButtonToggle.style.background = isAddButtonEnabled ? '#008800' : '#cc0000'; }; addButtonToggle.onmouseout = () => { addButtonToggle.style.background = base; }; };
    addButtonToggle.addEventListener('click', e => { e.stopPropagation(); if (isCurrentWatchPage) { addToCurrentList(location.href); triggerSync(location.href); } else toggleAddButton(); });
    domainBtn.addEventListener('click', () => { const cur = getCurrentDomainMode(), idx = CONFIG.DOMAIN_MODES.findIndex(m => m.key === cur.key); const next = CONFIG.DOMAIN_MODES[(idx + 1) % CONFIG.DOMAIN_MODES.length]; localStorage.setItem(CONFIG.DOMAIN_MODE_STORAGE_KEY, next.key); domainBtn.textContent = next.label; });

    // = 面板折疊邏輯 / Panel Collapse Logic =
    let keepPanelExpanded = false, panelHovered = false;
    const collapsePanel = () => { if (isDragging || keepPanelExpanded) return; playBtn.textContent = t('playIcon'); otherButtons.forEach(b => b.style.display = 'none'); };
    const expandPanel = () => { if (isDragging) return; playBtn.textContent = t('play'); otherButtons.forEach(b => b.style.display = 'flex'); updateListButtonCount(); };
    const resetPanelExpandState = () => { keepPanelExpanded = false; if (!panelHovered && !isCurrentWatchPage) collapsePanel(); };
    collapsePanel();
    const updateListButtonCount = () => { const k = STORAGE_LISTS[currentList]; if (!k) return; const c = JSON.parse(localStorage.getItem(k) || '[]').length; listBtn.textContent = `${t('list')}${currentList.replace('list', '')} (${c})`; };

    // = 頁面狀態偵測 / Page State Detection =
    let isCurrentWatchPage = isWatchPage();
    let urlPollingId = null, wprObserver = null, wprDetected = false;
    const stopUrlPolling = () => { if (urlPollingId) { clearInterval(urlPollingId); urlPollingId = null; } };
    const setupNavigationObserver = () => {
        wprObserver = new MutationObserver(mutations => {
            for (const m of mutations) {
                if (m.type === 'attributes' && m.attributeName === 'youtube-wpr') {
                    const v = document.documentElement.getAttribute('youtube-wpr');
                    const isW = v !== null && v !== '';
                    if (isW !== isCurrentWatchPage) { isCurrentWatchPage = isW; isW ? setupWatchPageMode() : setupHomePageMode(); }
                    if (!wprDetected) { wprDetected = true; stopUrlPolling(); }
                }
            }
        });
        wprObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['youtube-wpr'] });
        let lastHref = location.href;
        urlPollingId = setInterval(() => { if (wprDetected) { stopUrlPolling(); return; } if (location.href !== lastHref) { lastHref = location.href; updatePageContext(); } }, CONFIG.POLLING_INTERVAL);
    };
    const updatePageContext = () => { const was = isCurrentWatchPage; isCurrentWatchPage = isWatchPage(); if (was !== isCurrentWatchPage) isCurrentWatchPage ? setupWatchPageMode() : setupHomePageMode(); if (isCurrentWatchPage) { panel.style.opacity = '0'; if (addButtonToggle.textContent !== t('addIcon')) { addButtonToggle.textContent = t('addIcon'); addButtonToggle.style.fontSize = '18px'; } } };
    const setupWatchPageMode = () => { panel.style.opacity = '0'; addButtonToggle.textContent = t('addIcon'); addButtonToggle.style.fontSize = '18px'; addButtonToggle.style.background = '#00aa00'; addButtonToggle.onmouseover = () => { addButtonToggle.style.background = '#008800'; }; addButtonToggle.onmouseout = () => { addButtonToggle.style.background = '#00aa00'; }; updateListButtonCount(); collapsePanel(); stopObservingVideos(); };
    const setupHomePageMode = () => { panel.style.opacity = '1'; addButtonToggle.textContent = t('addButton'); addButtonToggle.style.fontSize = '13px'; updateAddButtonState(); updateListButtonCount(); collapsePanel(); if (isAddButtonEnabled) startObservingVideos(); };

    panel.addEventListener('mouseenter', () => { panelHovered = true; panel.style.opacity = '1'; expandPanel(); }, { passive: true });
    panel.addEventListener('mouseleave', () => { panelHovered = false; if (!panel.matches(':hover')) { panel.style.opacity = isCurrentWatchPage ? '0' : '1'; isCurrentWatchPage ? collapsePanel() : resetPanelExpandState(); } }, { passive: true });
    modeBtn.addEventListener('click', () => { const cur = localStorage.getItem(STORAGE_MODE) || 'current_tab'; const nxt = cur === 'current_tab' ? 'new_tab' : cur === 'new_tab' ? 'new_window' : 'current_tab'; localStorage.setItem(STORAGE_MODE, nxt); modeBtn.textContent = nxt === 'current_tab' ? t('modeCurrentTab') : nxt === 'new_tab' ? t('modeNewTab') : t('modeNewWindow'); });
    listBtn.addEventListener('click', () => {
        const names = Object.keys(STORAGE_LISTS);
        const idx = names.indexOf(currentList);
        currentList = names[(idx + 1) % names.length];
        localStorage.setItem(STORAGE_CURRENT, currentList);
        updateListButtonCount();
    });

    // = 跨頁同步 / Cross-tab Sync =
    const setupSyncListener = () => { window.addEventListener('storage', e => { if (e.key === CONFIG.SYNC_EVENT_KEY) { try { const d = JSON.parse(e.newValue); if (d.listKey === currentList && d.type === 'videoAdded') { const k = STORAGE_LISTS[currentList], ids = JSON.parse(localStorage.getItem(k) || '[]'); if (d.videoId && !ids.includes(d.videoId)) { ids.push(d.videoId); localStorage.setItem(k, JSON.stringify(ids)); updateListButtonCount(); } } } catch (err) {} } }); };
    const triggerSync = id => { try { localStorage.setItem(CONFIG.SYNC_EVENT_KEY, JSON.stringify({ type: 'videoAdded', listKey: currentList, videoId: id, timestamp: Date.now() })); } catch (e) {} };

    // = 啟動多播放器 / Launch Multi-Player =
    const openMultiPlayer = () => {
        const k = STORAGE_LISTS[currentList];
        let ids = JSON.parse(localStorage.getItem(k) || '[]');
        const pinnedK = STORAGE_PINNED_PREFIX + currentList;
        let pinned = JSON.parse(localStorage.getItem(pinnedK) || '[]');

        if (!ids || ids.length === 0) {
            ids = ['homepage_1'];
            localStorage.setItem(k, JSON.stringify(ids));
            updateListButtonCount();
            if (CONFIG.HOME_PINNED_BY_DEFAULT && !pinned.includes('homepage_1')) {
                pinned.push('homepage_1');
            }
        }

        const url = makeBlobPage(ids, currentList, pinned, getCurrentDomainMode().domain);
        const mode = localStorage.getItem(STORAGE_MODE) || 'current_tab';
        const cleanupBlob = () => { try { URL.revokeObjectURL(url); } catch(e) {} };
        if (mode === 'current_tab') { location.href = url; setTimeout(cleanupBlob, 3000); }
        else if (mode === 'new_window') { window.open(url, '_blank', 'width=800,height=600,scrollbars=no,resizable=yes'); setTimeout(cleanupBlob, 2000); }
        else { window.open(url, '_blank'); setTimeout(cleanupBlob, 2000); }
    };
    playBtn.addEventListener('click', openMultiPlayer);

    // = 自動啟動檢查 / Auto-launch Check =
    const checkAutoLaunch = () => {
        const params = new URLSearchParams(window.location.search);
        const autoKey = params.get('ytMulti_auto');
        if (autoKey && STORAGE_LISTS[autoKey]) {
            currentList = autoKey;
            localStorage.setItem(STORAGE_CURRENT, currentList);
            updateListButtonCount();
            history.replaceState(null, '', window.location.pathname);
            setTimeout(openMultiPlayer, 400);
        }
    };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', checkAutoLaunch);
    else checkAutoLaunch();

    // = 添加到清單 / Add to List =
    const addToCurrentList = id => {
        const k = STORAGE_LISTS[currentList], ids = JSON.parse(localStorage.getItem(k) || '[]');
        let normalizedId = id;
        if (isFullUrl(id)) {
            const vid = getVideoIdFromUrl(id);
            if (vid) normalizedId = id.includes('_chat') || id.includes('live_chat') ? vid + CONFIG.CHAT_SUFFIX : vid + CONFIG.VIDEO_SUFFIX;
        } else if (!isVideoId(id) && !isChatId(id) && !isHomepage(id) && /^[A-Za-z0-9_-]{11}$/.test(id)) {
            normalizedId = id + CONFIG.VIDEO_SUFFIX;
        }
        if (normalizedId && !ids.includes(normalizedId)) {
            ids.push(normalizedId);
            localStorage.setItem(k, JSON.stringify(ids));
            updateListButtonCount();
            triggerSync(normalizedId);
            if (!isCurrentWatchPage) { keepPanelExpanded = true; expandPanel(); }
            return true;
        }
        return false;
    };

    // = 影片觀察器 / Video Observer =
    let videoObserver = null, isObserving = false, styleTag = null, processedElements = new Set();
    const getVideoContainerSelector = () => location.href.includes('/playlist?list=') ? 'ytd-playlist-video-renderer ytd-thumbnail' : 'ytd-thumbnail, ytd-playlist-thumbnail, ytd-grid-video-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-rich-item-renderer, ytd-rich-grid-media, ytd-playlist-video-renderer';
    const startObservingVideos = () => { if (isObserving || !isAddButtonEnabled || isCurrentWatchPage) return; isObserving = true; if (!styleTag) { styleTag = document.createElement('style'); styleTag.textContent = `.ytMulti-add-btn{position:absolute;top:8px;left:8px;width:42px;height:42px;background:rgba(0,0,0,0.8);color:white;border:none;border-radius:50%;cursor:pointer;font-size:31px;display:none;z-index:10000;box-shadow:0 2px 6px rgba(0,0,0,0.4);align-items:center;justify-content:center}.ytMulti-video-hover .ytMulti-add-btn{display:flex}`; document.head.appendChild(styleTag); } videoObserver = new MutationObserver(mutations => { const nodes = []; for (const m of mutations) if (m.type === 'childList') for (const n of m.addedNodes) if (n.nodeType === Node.ELEMENT_NODE) nodes.push(n); if (nodes.length) requestAnimationFrame(() => nodes.forEach(processNode)); }); videoObserver.observe(document.body, { childList: true, subtree: true }); processNode(document.body); setTimeout(() => processNode(document.body), 500); };
    const processNode = node => { if (!node || !node.querySelectorAll) return; const selector = getVideoContainerSelector(); const targets = node.matches && node.matches(selector) ? [node] : []; targets.push(...node.querySelectorAll(selector)); for (const el of targets) { const id = el.querySelector('a[href*="/watch?"]')?.href; if (id && !processedElements.has(id)) { processedElements.add(id); addButtonsToContainer(el); } } };
    const stopObservingVideos = () => { isObserving = false; document.querySelectorAll('.ytMulti-add-btn').forEach(b => b.remove()); document.querySelectorAll('[data-ytMultiProcessed]').forEach(el => { delete el.dataset.ytMultiProcessed; processedElements.delete(el.dataset.videoUrl); }); if (styleTag) { styleTag.remove(); styleTag = null; } if (videoObserver) { videoObserver.disconnect(); videoObserver = null; } };
    const addButtonsToContainer = el => { if (el.querySelector('.ytMulti-add-btn')) return; const btn = document.createElement('button'); btn.className = 'ytMulti-add-btn'; btn.textContent = '+'; btn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); let vid = el.querySelector('a[href*="/watch?"]')?.href; if (!vid) { const ep = el.querySelector('[data-endpoint]'); if (ep) try { const d = JSON.parse(ep.getAttribute('data-endpoint')); if (d?.videoId) vid = 'https://www.youtube.com/watch?v=' + d.videoId; } catch (e) {} } if (vid) addToCurrentList(vid); }, { passive: true }); el.style.position = 'relative'; el.appendChild(btn); el.addEventListener('mouseenter', () => el.classList.add('ytMulti-video-hover'), { passive: true }); el.addEventListener('mouseleave', () => el.classList.remove('ytMulti-video-hover'), { passive: true }); };

    // = 生成 Blob 頁面 / Generate Blob Page =
    const makeBlobPage = (ids, listKey, initialPinnedIds = [], domainMode = 'www.youtube.com') => {
        const nonce = document.querySelector('script[nonce]')?.nonce || '';
        const idWithOrder = ids.map((id, i) => ({ id, order: (i * 2) + 1 }));
        const ytParams = 'enablejsapi=1&autoplay=1&rel=0&fs=1&playsinline=1&iv_load_policy=3&cc_load_policy=0&controls=1&vq=hd1080';

        const blobParams = {
            MAX_PINNED: CONFIG.MAX_PINNED,
            PORTRAIT_HEIGHT_THRESHOLD: CONFIG.PORTRAIT_HEIGHT_THRESHOLD,
            LANDSCAPE_ASPECT_RATIO_THRESHOLD: CONFIG.LANDSCAPE_ASPECT_RATIO_THRESHOLD,
            LANDSCAPE_COLUMN_CONFIG: CONFIG.LANDSCAPE_COLUMN_CONFIG.join(','),
            PORTRAIT_MAX_COLUMNS: CONFIG.PORTRAIT_MAX_COLUMNS,
            ASPECT_RATIO_STANDARD: CONFIG.ASPECT_RATIO_STANDARD,
            CHAT_SUFFIX: CONFIG.CHAT_SUFFIX,
            VIDEO_SUFFIX: CONFIG.VIDEO_SUFFIX,
            DOMAIN_MODE: domainMode,
            SYNC_EVENT_KEY: CONFIG.SYNC_EVENT_KEY,
            DEBOUNCE_MS: CONFIG.RESIZE_DEBOUNCE,
            RESOLUTION_LEVELS: JSON.stringify(CONFIG.RESOLUTION_LEVELS),
            RESOLUTION_PRESETS: JSON.stringify(CONFIG.RESOLUTION_PRESETS),
            SCREEN_WIDTH: CONFIG.SCREEN_WIDTH_DEFAULT,
            SPOOF_ENABLED: CONFIG.SPOOF_RESOLUTION_ENABLED,
            LIST_COUNT: CONFIG.LIST_COUNT,
            HOME_PINNED_BY_DEFAULT: CONFIG.HOME_PINNED_BY_DEFAULT,
            YT_PARAMS: ytParams,
            HOVER_TIMEOUT: CONFIG.HOVER_TIMEOUT,
            idWithOrder: JSON.stringify(idWithOrder),
            listKey: listKey,
            STORAGE_LISTS: JSON.stringify(STORAGE_LISTS),
            INITIAL_PINNED_IDS: JSON.stringify(initialPinnedIds),
            PINNED_STORAGE_KEY: STORAGE_PINNED_PREFIX + listKey
        };

        const jsCode = `
(function(){
    'use strict';

    // = 常數宣告 / Constants =
    const MAX_PINNED = ${blobParams.MAX_PINNED};
    const PORTRAIT_HEIGHT_THRESHOLD = ${blobParams.PORTRAIT_HEIGHT_THRESHOLD};
    const LANDSCAPE_ASPECT_RATIO_THRESHOLD = ${blobParams.LANDSCAPE_ASPECT_RATIO_THRESHOLD};
    const LANDSCAPE_COLUMN_CONFIG = [${blobParams.LANDSCAPE_COLUMN_CONFIG}];
    const PORTRAIT_MAX_COLUMNS = ${blobParams.PORTRAIT_MAX_COLUMNS};
    const ASPECT_RATIO_STANDARD = ${blobParams.ASPECT_RATIO_STANDARD};
    const CHAT_SUFFIX = '${blobParams.CHAT_SUFFIX}';
    const VIDEO_SUFFIX = '${blobParams.VIDEO_SUFFIX}';
    const DOMAIN_MODE = '${blobParams.DOMAIN_MODE}';
    const SYNC_EVENT_KEY = '${blobParams.SYNC_EVENT_KEY}';
    const DEBOUNCE_MS = ${blobParams.DEBOUNCE_MS};
    const RESOLUTION_LEVELS = ${blobParams.RESOLUTION_LEVELS};
    const RESOLUTION_PRESETS = ${blobParams.RESOLUTION_PRESETS};
    const SCREEN_WIDTH = ${blobParams.SCREEN_WIDTH};
    const SPOOF_ENABLED = ${blobParams.SPOOF_ENABLED};
    const LIST_COUNT = ${blobParams.LIST_COUNT};
    const HOME_PINNED_BY_DEFAULT = ${blobParams.HOME_PINNED_BY_DEFAULT};
    const YT_PARAMS = '${blobParams.YT_PARAMS}';
    const HOVER_TIMEOUT = ${blobParams.HOVER_TIMEOUT};
    // = 首頁框架強制使用主域名,確保穩定載入 / Homepage always uses main domain =
    const HOME_DOMAIN = 'www.youtube.com';

    // = 資料結構 / Data Structures =
    let idOrderMap = new Map(${blobParams.idWithOrder}.map(i => [i.id, i.order]));
    const listKey = '${blobParams.listKey}';
    const STORAGE_LISTS = ${blobParams.STORAGE_LISTS};
    const INITIAL_PINNED_IDS = ${blobParams.INITIAL_PINNED_IDS};
    const PINNED_STORAGE_KEY = '${blobParams.PINNED_STORAGE_KEY}';
    const container = document.querySelector('.container');
    let pinnedIds = INITIAL_PINNED_IDS;
    let elementCache = new Map();
    let layoutDirty = true;
    let resizeTimer;
    const hoverTimers = new Map();

    // = 輔助函數 / Helper Functions =
    const isChatId = id => id.endsWith(CHAT_SUFFIX);
    const isVideoId = id => id.endsWith(VIDEO_SUFFIX);
    const isHomepage = id => id.startsWith('homepage');
    const getBaseId = id => {
        if (isChatId(id)) return id.slice(0, -CHAT_SUFFIX.length);
        if (isVideoId(id)) return id.slice(0, -VIDEO_SUFFIX.length);
        return id;
    };
    const getChatId = id => {
        if (isChatId(id)) return id;
        if (isVideoId(id)) return id.slice(0, -VIDEO_SUFFIX.length) + CHAT_SUFFIX;
        return id + CHAT_SUFFIX;
    };
    const getVideoId = id => {
        if (isChatId(id)) return id.slice(0, -CHAT_SUFFIX.length);
        if (isVideoId(id)) return id.slice(0, -VIDEO_SUFFIX.length);
        return id;
    };
    const isFullUrl = id => id.startsWith('http');
    const getVideoIdFromUrl = url => {
        const m1 = url.match(/[?&]v=([A-Za-z0-9_-]{11})/);
        if (m1) return m1[1];
        const m2 = url.match(/youtu\\.be\\/([A-Za-z0-9_-]{11})/);
        return m2 ? m2[1] : null;
    };
    const getListIdFromUrl = url => {
        const m = url.match(/[?&]list=([^&]+)/);
        return m ? m[1] : null;
    };

    // = 按鈕顯示控制 / Button Visibility Control =
    const showButtons = wrapper => {
        wrapper.querySelectorAll('.remove-btn,.pin-btn,.chat-toggle-btn,.add-chat-btn,.top-btn,.up-btn,.down-btn,.bottom-btn,.copy-btn,.copy-placeholder').forEach(b => b.style.display = 'flex');
    };
    const hideButtons = wrapper => {
        wrapper.querySelectorAll('.remove-btn,.pin-btn,.chat-toggle-btn,.add-chat-btn,.top-btn,.up-btn,.down-btn,.bottom-btn,.copy-btn,.copy-placeholder').forEach(b => b.style.display = 'none');
    };
    const setupHoverLogic = wrapper => {
        wrapper.addEventListener('mouseenter', e => {
            clearTimeout(hoverTimers.get(wrapper));
            showButtons(wrapper);
            hoverTimers.set(wrapper, setTimeout(() => hideButtons(wrapper), HOVER_TIMEOUT));
        });
        wrapper.addEventListener('mouseleave', () => {
            clearTimeout(hoverTimers.get(wrapper));
            hoverTimers.delete(wrapper);
            hideButtons(wrapper);
        });
    };

    // = 佈局計算 / Layout Calculation =
    const isLandscape = () => container.offsetWidth > container.offsetHeight * LANDSCAPE_ASPECT_RATIO_THRESHOLD;
    const findBestPortraitColumns = (itemCount, availableW, availableH, threshold) => {
        let best = 1, minO = Infinity;
        for (let c = 1; c <= PORTRAIT_MAX_COLUMNS; c++) {
            const r = Math.ceil(itemCount / c);
            const h = (availableW / c) / ASPECT_RATIO_STANDARD;
            const o = (r * h) / availableH;
            if (o <= threshold) return c;
            if (o < minO) { minO = o; best = c; }
        }
        return best;
    };
    const calculateLayout = () => {
        if (!layoutDirty) return { cells: [] };
        const W = container.offsetWidth, H = container.offsetHeight;
        const all = Array.from(idOrderMap.entries()).sort((a, b) => a[1] - b[1]);
        const pin = all.filter(([id]) => pinnedIds.includes(id));
        const vis = all.filter(([id]) => !pinnedIds.includes(id));
        const cells = [];
        let y = 0;
        pin.forEach(([id]) => {
            const h = W / ASPECT_RATIO_STANDARD;
            cells.push({ id, x: 0, y, w: W, h, isPinned: true });
            y += h;
        });
        if (vis.length > 0) {
            const aH = H - y;
            if (aH > 0) {
                const cc = isLandscape() ? (() => {
                    let c = 1;
                    for (let i = 0; i < LANDSCAPE_COLUMN_CONFIG.length; i++) {
                        if (vis.length >= LANDSCAPE_COLUMN_CONFIG[i]) c++;
                        else break;
                    }
                    return c;
                })() : findBestPortraitColumns(vis.length, W, aH, PORTRAIT_HEIGHT_THRESHOLD);
                const uW = W / cc, h = uW / ASPECT_RATIO_STANDARD;
                for (let i = 0; i < vis.length; i++) {
                    const r = Math.floor(i / cc), col = i % cc;
                    cells.push({ id: vis[i][0], x: col * uW, y: y + r * h, w: uW, h: h });
                }
            }
        }
        layoutDirty = false;
        return { cells };
    };
    const updateLayout = () => {
        const { cells } = calculateLayout();
        cells.forEach(c => {
            const w = elementCache.get(c.id);
            if (w) {
                w.style.transform = 'translate(' + Math.round(c.x) + 'px,' + Math.round(c.y) + 'px)';
                w.style.width = Math.round(c.w) + 'px';
                w.style.height = Math.round(c.h) + 'px';
                w.style.zIndex = c.isPinned ? '100' : '1';
                const scaler = w.querySelector('.video-scaler');
                const ifr = w.querySelector('iframe');
                if (scaler && ifr) {
                    if (isHomepage(c.id)) {
                        scaler.style.width = c.w + 'px';
                        scaler.style.height = c.h + 'px';
                        ifr.style.width = '100%';
                        ifr.style.height = '100%';
                        scaler.style.transform = 'scale(1)';
                    } else if (SPOOF_ENABLED && !isChatId(c.id)) {
                        let levelIdx = 0;
                        for (let i = 0; i < RESOLUTION_PRESETS.length; i++) {
                            if (c.w >= SCREEN_WIDTH / (i + 1)) { levelIdx = i; break; }
                        }
                        const lvl = RESOLUTION_PRESETS[levelIdx] || 5;
                        const [tw, th] = RESOLUTION_LEVELS[lvl];
                        scaler.style.width = tw + 'px';
                        scaler.style.height = th + 'px';
                        ifr.style.width = tw + 'px';
                        ifr.style.height = th + 'px';
                        const sx = c.w / tw, sy = c.h / th;
                        scaler.style.transform = 'scale(' + sx + ',' + sy + ')';
                    } else {
                        scaler.style.width = c.w + 'px';
                        scaler.style.height = c.h + 'px';
                        ifr.style.width = '100%';
                        ifr.style.height = '100%';
                        scaler.style.transform = 'scale(1)';
                    }
                    scaler.style.transformOrigin = 'top left';
                }
            }
        });
    };
    const scheduleLayout = () => {
        layoutDirty = true;
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(updateLayout, DEBOUNCE_MS);
    };

    // = 順序調整 / Order Adjustment =
    const swapOrder = (a, b) => {
        const o1 = idOrderMap.get(a), o2 = idOrderMap.get(b);
        if (o1 !== undefined && o2 !== undefined) {
            idOrderMap.set(a, o2);
            idOrderMap.set(b, o1);
            save();
            scheduleLayout();
        }
    };
    const moveTop = id => {
        const c = idOrderMap.get(id);
        if (c === undefined || c === 0) return;
        const mn = Math.min(...Array.from(idOrderMap.values()));
        for (let [i, o] of idOrderMap) if (o >= mn && o < c) idOrderMap.set(i, o + 1);
        idOrderMap.set(id, mn - 1);
        save();
        scheduleLayout();
    };
    const moveBottom = id => {
        const c = idOrderMap.get(id);
        if (c === undefined) return;
        const mx = Math.max(...Array.from(idOrderMap.values()));
        if (c === mx) return;
        for (let [i, o] of idOrderMap) if (o > c && o <= mx) idOrderMap.set(i, o - 1);
        idOrderMap.set(id, mx + 1);
        save();
        scheduleLayout();
    };
    const moveDown = id => {
        const c = idOrderMap.get(id);
        if (c === undefined) return;
        let nId = null, nO = Infinity;
        for (let [i, o] of idOrderMap) if (o > c && o < nO && !pinnedIds.includes(i)) { nO = o; nId = i; }
        if (nId) swapOrder(id, nId);
    };

    // = 按鈕建立 / Button Creation =
    const mkBtn = (cls, fn) => {
        const d = document.createElement('div');
        d.className = cls;
        d.onclick = e => { e.stopPropagation(); fn(e); };
        return d;
    };

    // = 影片/首頁框架建立 / Frame Creation =
    const createVideo = (id, order) => {
        const isHome = isHomepage(id);
        const vid = isHome ? null : (isFullUrl(id) ? getVideoIdFromUrl(id) : getVideoId(id));
        if (!isHome && (!vid || !/^[A-Za-z0-9_-]{11}$/.test(vid))) return null;

        const w = document.createElement('div');
        w.className = 'video-wrapper' + (isChatId(id) ? ' is-chat' : '');
        w.dataset.id = id;
        w.style.willChange = 'transform,opacity';
        setupHoverLogic(w);

        const isC = isChatId(id);
        // = 首頁與聊天室框架使用主域名,一般影片跟隨 DOMAIN_MODE =
        const frameDomain = (isHome || isC) ? HOME_DOMAIN : DOMAIN_MODE;
        let src = isHome ? 'https://' + frameDomain + '/' : (isC ? 'https://' + frameDomain + '/live_chat?v=' + vid : 'https://' + frameDomain + '/embed/' + vid + '?' + YT_PARAMS + (isFullUrl(id) && getListIdFromUrl(id) ? '&list=' + getListIdFromUrl(id) : ''));

        const scaler = document.createElement('div');
        scaler.className = 'video-scaler';
        scaler.style.cssText = 'will-change:transform;transform-origin:top left;background:#000;backface-visibility:hidden;-webkit-backface-visibility:hidden;image-rendering:crisp-edges;image-rendering:pixelated;';

        const ifr = document.createElement('iframe');
        ifr.style.cssText = 'display:block;border:0;outline:0;margin:0;padding:0;background:#000;transform:translateZ(0);-webkit-transform:translateZ(0);image-rendering:crisp-edges;image-rendering:pixelated;';
        ifr.src = src;
        ifr.allow = 'autoplay; encrypted-media; fullscreen';
        // = 採用現代標準 flag,僅允許使用者手勢觸發頂層導航 =
        ifr.sandbox = 'allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-pointer-lock allow-presentation allow-top-navigation-by-user-activation';

        const del = mkBtn('remove-btn', () => {
            ifr.src = 'about:blank';
            w.remove();
            elementCache.delete(id);
            idOrderMap.delete(id);
            const p = pinnedIds.indexOf(id);
            if (p !== -1) pinnedIds.splice(p, 1);
            save();
            scheduleLayout();
        });
        const pin = mkBtn('pin-btn', () => {
            const i = pinnedIds.indexOf(id);
            if (i !== -1) pinnedIds.splice(i, 1);
            else {
                if (pinnedIds.length >= MAX_PINNED) pinnedIds.shift();
                pinnedIds.push(id);
            }
            save();
            scheduleLayout();
        });
        const chatT = mkBtn('chat-toggle-btn', () => {
            if (isHome) return;
            const p = isC ? (isFullUrl(id) ? id.replace(CHAT_SUFFIX, '') : getBaseId(id)) : (isFullUrl(id) ? id + CHAT_SUFFIX : getChatId(id));
            if (idOrderMap.has(p)) return;
            idOrderMap.delete(id);
            idOrderMap.set(p, isC ? order - 1 : order + 1);
            ifr.src = 'about:blank';
            w.remove();
            elementCache.delete(id);
            const nw = createVideo(p, isC ? order - 1 : order + 1);
            if (nw) { container.appendChild(nw); elementCache.set(p, nw); }
            scheduleLayout();
        });
        const chatA = mkBtn('add-chat-btn', () => {
            if (isHome) return;
            const p = isC ? (isFullUrl(id) ? id.replace(CHAT_SUFFIX, '') : getBaseId(id)) : (isFullUrl(id) ? id + CHAT_SUFFIX : getChatId(id));
            if (idOrderMap.has(p)) return;
            idOrderMap.set(p, isC ? order - 1 : order + 1);
            save();
            const nw = createVideo(p, isC ? order - 1 : order + 1);
            if (nw) { container.appendChild(nw); elementCache.set(p, nw); }
            scheduleLayout();
        });
        const top = mkBtn('top-btn', () => { moveTop(id); });
        const up = mkBtn('up-btn', () => {
            const c = idOrderMap.get(id);
            if (c === undefined) return;
            let nId = null, nO = -Infinity;
            for (let [oid, o] of idOrderMap) if (o < c && o > nO && !pinnedIds.includes(oid)) { nO = o; nId = oid; }
            if (nId) swapOrder(id, nId);
        });
        const dn = mkBtn('down-btn', () => { moveDown(id); });
        const bot = mkBtn('bottom-btn', () => { moveBottom(id); });

        const copyCol = document.createElement('div');
        copyCol.className = 'copy-col';
        copyCol.style.cssText = 'position:absolute;top:6px;left:32px;display:flex;flex-direction:column;gap:4px;z-index:9999;';
        for (let i = 1; i <= LIST_COUNT; i++) {
            const targetKey = 'list' + i;
            if (targetKey === listKey) {
                const ph = document.createElement('div');
                ph.className = 'copy-placeholder';
                copyCol.appendChild(ph);
            } else {
                const cb = mkBtn('copy-btn', () => {
                    const tk = STORAGE_LISTS[targetKey];
                    const ti = JSON.parse(localStorage.getItem(tk) || '[]');
                    if (!ti.includes(id)) {
                        ti.push(id);
                        localStorage.setItem(tk, JSON.stringify(ti));
                        try {
                            localStorage.setItem(SYNC_EVENT_KEY, JSON.stringify({ type: 'videoAdded', listKey: targetKey, videoId: id, timestamp: Date.now() }));
                        } catch (e) {}
                    }
                });
                cb.dataset.num = i;
                cb.style.cssText = 'position:relative;width:20px;height:20px;border-radius:3px;background:#4488ff;color:white;border:none;cursor:pointer;';
                copyCol.appendChild(cb);
            }
        }
        if (copyCol.children.length > 0) w.appendChild(copyCol);

        scaler.appendChild(ifr);
        w.append(scaler, del, pin, chatT, chatA, top, up, dn, bot);
        elementCache.set(id, w);
        return w;
    };

    // = 初始渲染 / Initial Render =
    const frag = document.createDocumentFragment();
    Array.from(idOrderMap.entries()).sort((a, b) => a[1] - b[1]).forEach(([id, order]) => {
        const el = createVideo(id, order);
        if (el) frag.appendChild(el);
    });
    container.appendChild(frag);
    updateLayout();

    // = 首頁按鈕邏輯 / Home Button Logic =
    const homeBtn = document.createElement('div');
    homeBtn.className = 'home-add-btn';
    homeBtn.textContent = '+';
    homeBtn.style.cssText = 'position:fixed;bottom:20px;right:20px;width:64px;height:64px;border-radius:50%;background:rgba(20,20,20,0.85);color:#fff;border:2px solid #555;font-size:36px;cursor:pointer;z-index:99999;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,0.5);transition:opacity 0.2s, transform 0.2s;opacity:0;pointer-events:none;transform:scale(0.8);user-select:none;';
    const homeTrigger = document.createElement('div');
    homeTrigger.style.cssText = 'position:fixed;bottom:0;right:0;width:180px;height:180px;z-index:99998;cursor:default;';
    homeTrigger.appendChild(homeBtn);
    document.body.appendChild(homeTrigger);
    const toggleHomeBtn = (show) => {
        homeBtn.style.opacity = show ? '1' : '0';
        homeBtn.style.pointerEvents = show ? 'auto' : 'none';
        homeBtn.style.transform = show ? 'scale(1)' : 'scale(0.8)';
    };
    homeTrigger.addEventListener('mouseenter', () => toggleHomeBtn(true));
    homeTrigger.addEventListener('mouseleave', () => toggleHomeBtn(false));
    homeBtn.addEventListener('click', () => {
        const hId = 'homepage_' + Date.now();
        const newOrder = Math.max(...Array.from(idOrderMap.values()), 0) + 2;
        idOrderMap.set(hId, newOrder);
        if (HOME_PINNED_BY_DEFAULT) {
            if (pinnedIds.length >= MAX_PINNED) pinnedIds.shift();
            pinnedIds.push(hId);
        }
        const w = createVideo(hId, newOrder);
        if (w) {
            container.appendChild(w);
            elementCache.set(hId, w);
        }
        save();
        scheduleLayout();
    });

    // = 事件監聽 / Event Listeners =
    window.addEventListener('resize', scheduleLayout, { passive: true });
    window.addEventListener('storage', e => {
        if (e.key === SYNC_EVENT_KEY || e.key.includes(listKey)) {
            try {
                if (e.key === SYNC_EVENT_KEY) {
                    const d = JSON.parse(e.newValue);
                    if (d.listKey !== listKey || d.type !== 'videoAdded') return;
                }
                const ids = JSON.parse(localStorage.getItem(STORAGE_LISTS[listKey]) || '[]');
                if (Array.from(idOrderMap.keys()).join(',') === ids.join(',')) return;
                const ex = new Set(Array.from(container.querySelectorAll('.video-wrapper')).map(el => el.dataset.id));
                const nm = new Map();
                let od = 1;
                const f = document.createDocumentFragment();
                ids.forEach(id => {
                    if (!isChatId(id) && !isHomepage(id)) {
                        nm.set(id, od); od += 2;
                        if (!ex.has(id)) { const el = createVideo(id, nm.get(id)); if (el) f.appendChild(el); }
                        const ch = isFullUrl(id) ? id + CHAT_SUFFIX : getChatId(id);
                        if (ex.has(ch)) nm.set(ch, nm.get(id) + 1);
                    } else if (isChatId(id)) {
                        nm.set(id, od); od += 2;
                        if (!ex.has(id)) { const el = createVideo(id, nm.get(id)); if (el) f.appendChild(el); }
                    } else if (isHomepage(id)) {
                        nm.set(id, od); od += 2;
                        if (!ex.has(id)) { const el = createVideo(id, nm.get(id)); if (el) f.appendChild(el); }
                    }
                });
                Array.from(container.querySelectorAll('.video-wrapper')).forEach(el => {
                    if (!nm.has(el.dataset.id)) {
                        const tmp = elementCache.get(el.dataset.id);
                        if (tmp) { const i = tmp.querySelector('iframe'); if (i) i.src = 'about:blank'; }
                        elementCache.delete(el.dataset.id);
                        el.remove();
                    }
                });
                if (f.hasChildNodes()) container.appendChild(f);
                idOrderMap = nm;
                scheduleLayout();
            } catch (err) {}
        }
    });

    // = 儲存函數 / Save Function =
    const save = () => {
        const cleanArr = [...idOrderMap.entries()].sort((a, b) => a[1] - b[1]).map(e => e[0]);
        localStorage.setItem(STORAGE_LISTS[listKey], JSON.stringify(cleanArr));
        localStorage.setItem(PINNED_STORAGE_KEY, JSON.stringify(pinnedIds));
    };

    // = 清理函數 / Cleanup =
    window.addEventListener('beforeunload', () => {
        clearTimeout(resizeTimer);
        elementCache.forEach(w => {
            const i = w.querySelector('iframe');
            if (i) i.src = 'about:blank';
        });
        elementCache.clear();
    });
})();`;

        const css = `body{margin:0;padding:0;background:#000;overflow:hidden}.container{position:absolute;top:0;left:0;width:100vw;height:100vh;display:flex;flex-wrap:wrap;align-content:flex-start}.video-wrapper{position:absolute;overflow:hidden;background:#000;transition:transform 0.2s ease,opacity 0.2s ease;will-change:transform,opacity;backface-visibility:hidden;transform:translate3d(0,0,0);-webkit-transform:translate3d(0,0,0)}.video-scaler{will-change:transform;transform-origin:top left}.video-wrapper iframe{display:block;border:0;outline:0;margin:0;padding:0;background:#000;transform:translateZ(0);-webkit-transform:translateZ(0)}.remove-btn,.pin-btn,.chat-toggle-btn,.add-chat-btn,.top-btn,.up-btn,.down-btn,.bottom-btn{position:absolute;width:20px;height:20px;border-radius:3px;display:none;cursor:pointer;z-index:9999;box-shadow:0 0 3px rgba(0,0,0,0.3)}.remove-btn{top:6px;left:6px;background:#ff4444}.pin-btn{top:30px;left:6px;background:#44aaff}.chat-toggle-btn{top:54px;left:6px;background:#888888}.add-chat-btn{top:78px;left:6px;background:#44aa44}.top-btn{top:102px;left:6px;background:#ffaa44}.up-btn{top:126px;left:6px;background:#88cc44}.down-btn{top:150px;left:6px;background:#44cc88}.bottom-btn{top:174px;left:6px;background:#aa44ff}.remove-btn::after{content:'×';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.pin-btn::after{content:'📌';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.chat-toggle-btn::after{content:'🔄';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.add-chat-btn::after{content:'+';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.top-btn::after{content:'⤒';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.up-btn::after{content:'↑';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.down-btn::after{content:'↓';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.bottom-btn::after{content:'⤓';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.copy-col{position:absolute;top:6px;left:32px;display:flex;flex-direction:column;gap:4px;z-index:9999}.copy-btn,.copy-placeholder{width:20px;height:20px;border-radius:3px;display:none}.copy-btn{position:relative;background:#4488ff;color:white;border:none;cursor:pointer}.copy-btn::after{content:attr(data-num);color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}.copy-placeholder{background:transparent;cursor:default}`;

        const nonceAttr = nonce ? ` nonce="${nonce}"` : '';
        const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Multi-Player</title><style>${css}</style></head><body><div class="container"></div><script${nonceAttr}>${jsCode}<\/script></body></html>`;
        return URL.createObjectURL(new Blob([html], { type: 'text/html' }));
    };

    // = 初始化 / Initialization =
    updatePageContext();
    setupSyncListener();
    setupNavigationObserver();
    if (!isCurrentWatchPage && isAddButtonEnabled) startObservingVideos();
    window.addEventListener('beforeunload', () => { stopUrlPolling(); wprObserver?.disconnect(); stopObservingVideos(); });
})();