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, Home button, custom home URL, and floating button layout/hide timeout settings.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube Multi-Player
// @name:zh-TW   YouTube 多重播放器
// @namespace    http://tampermonkey.net/
// @author       Dxzy
// @version      8.8.4
// @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, Home button, custom home URL, and floating button layout/hide timeout settings.
// 此版本不更動垂直螢幕,僅改進橫向螢幕的佈局。置頂影片≧2時,一般影片會平均分配在剩餘的空間。
// This version leaves the portrait orientation unchanged and only improves the landscape layout. When there are 2 or more pinned videos, regular videos will be evenly distributed across the remaining space.
// ==/UserScript==
(function () {
    'use strict';

    if (window.top !== window && /^(\/watch|\/embed|\/live_chat|\/accounts|\/studio)/.test(location.pathname)) return;
    const IS_SUB_FRAME = window.top !== window;
    let isAddButtonEnabled = IS_SUB_FRAME || localStorage.getItem('ytMulti_addButtonEnabled') === 'true';

    // = 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: 1920,
        RESOLUTION_PRESETS: [3, 4, 5, 6, 7],
        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,
        HOME_TARGET_URL: '',
        BTN_LAYOUT: 3,
        BTN_HIDE_TIMEOUT: 5000,
        LIST_NAMES: '',
        BUTTON_INJECTION_STATS: false
    };

    // = 多語言設定 / 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.3840x2160 2.2560x1440 3.1920x1080 4.1280x720 5.854x480 6.640x360 7.432x240 8.256x144]',
            descSpoofResolution: '偽造解析度開關:1=開啟 2=關閉',
            descHomePinnedDefault: '新增首頁時預設置頂:1=開啟 2=關閉',
            descHomeTargetUrl: '首頁框架目標 YouTube 連結(留空則恢復預設首頁)',
            descBtnLayout: '懸浮按鈕排列方式:1=直列左側 2=直列右側 3=橫行',
            descBtnHideTimeout: '懸浮按鈕無操作後自動隱藏的時間(毫秒)',
            descListNames: '自訂清單名稱(逗號分隔,如:主清單,音樂,遊戲)',
            descBtnStats: '統計按鈕成功注入選擇器的次數(僅記憶體,重啟腳本清零):1=開啟 2=關閉',
            spoofOn: '開啟', spoofOff: '關閉',
            layout1: '直列左', layout2: '直列右', layout3: '橫行',
            emptyVal: '(空白)'
        },
        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 (empty for blank, 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.3840x2160 2.2560x1440 3.1920x1080 4.1280x720 5.854x480 6.640x360 7.432x240 8.256x144]',
            descSpoofResolution: 'Spoof resolution toggle: 1=Enable 2=Disable',
            descHomePinnedDefault: 'Pin new homepages by default: 1=Enable 2=Disable',
            descHomeTargetUrl: 'Target YouTube URL for home frame (empty restores default)',
            descBtnLayout: 'Floating button layout: 1=Vertical-Left 2=Vertical-Right 3=Horizontal',
            descBtnHideTimeout: 'Auto-hide timeout for floating buttons after inactivity (ms)',
            descListNames: 'Custom list names (comma-separated, e.g., Main,Music,Gaming)',
            descBtnStats: 'Track successful button injections per selector (memory only): 1=Enable 2=Disable',
            spoofOn: 'ON', spoofOff: 'OFF',
            layout1: 'V-Left', layout2: 'V-Right', layout3: 'Horizontal',
            emptyVal: '(empty)'
        }
    };
    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 },
        HOME_TARGET_URL: { default: '', type: 'string', descZh: LANG.zh.descHomeTargetUrl, descEn: LANG.en.descHomeTargetUrl },
        BTN_LAYOUT: { default: 3, type: 'int', min: 1, max: 3, descZh: LANG.zh.descBtnLayout, descEn: LANG.en.descBtnLayout },
        BTN_HIDE_TIMEOUT: { default: 5000, type: 'int', min: 1000, max: 30000, step: 500, descZh: LANG.zh.descBtnHideTimeout, descEn: LANG.en.descBtnHideTimeout },
        LIST_NAMES: { default: CONFIG.LIST_NAMES, type: 'string', descZh: LANG.zh.descListNames, descEn: LANG.en.descListNames },
        BUTTON_INJECTION_STATS: { default: false, type: 'toggle12', descZh: LANG.zh.descBtnStats, descEn: LANG.en.descBtnStats }
    };
    const SETTINGS_PREFIX = 'ytMulti_setting_';

    // = 安全解析設定值 / Safe Settings Parser =
    const parseSettingValue = (stored, cfg) => {
        try {
            if (cfg.type === 'array' || cfg.type === 'preset') {
                const val = JSON.parse(stored);
                if (!Array.isArray(val)) return cfg.default;
                if (cfg.type === 'preset') return val.filter(n => Number.isInteger(n) && n >= 1 && n <= 8).slice(0, 5);
                return val.filter(n => Number.isInteger(n));
            } else if (cfg.type === 'bool' || cfg.type === 'toggle12') {
                return stored === 'true';
            } else if (cfg.type === 'float') {
                const val = parseFloat(stored);
                if (isNaN(val) || val < cfg.min || val > cfg.max) return cfg.default;
                return val;
            } else if (cfg.type === 'string') {
                return stored;
            } else {
                const val = parseInt(stored, 10);
                if (isNaN(val) || val < cfg.min || val > cfg.max) return cfg.default;
                return val;
            }
        } catch (e) { return cfg.default; }
    };

    const loadSettings = () => {
        for (const [key, cfg] of Object.entries(CONFIG_ITEMS)) {
            const stored = localStorage.getItem(SETTINGS_PREFIX + key);
            if (stored !== null) CONFIG[key] = parseSettingValue(stored, cfg);
        }
    };

    // = 安全讀取 JSON / Safe JSON Reader =
    const safeJSONParse = (str, fallback) => {
        try { return JSON.parse(str); } catch (e) { return fallback; }
    };

    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('ytMulti_currentList', currentList);
            }
            if (typeof updateListButtonCount === 'function') 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 formatPresets = arr => { const heights = arr.map(l => CONFIG.RESOLUTION_LEVELS[l]?.[1] || '?'); const levels = arr.join(','); return `${heights.join(',')} (${levels})`; };

    // = 標準化影片 ID / Normalize Video ID =
    const normalizeVideoId = (id) => {
        if (!id) return null;
        if (id.endsWith(CONFIG.CHAT_SUFFIX) || id.endsWith(CONFIG.VIDEO_SUFFIX) || id.startsWith('homepage')) return id;
        if (id.startsWith('http')) {
            const vid = getVideoIdFromUrl(id);
            if (vid && /^[A-Za-z0-9_-]{11}$/.test(vid)) return ((id.includes('_chat') || id.includes('live_chat')) ? vid + CONFIG.CHAT_SUFFIX : vid + CONFIG.VIDEO_SUFFIX);
            return null;
        }
        if (/^[A-Za-z0-9_-]{11}$/.test(id)) return id + CONFIG.VIDEO_SUFFIX;
        return null;
    };

    const formatSettingDisplay = (cfg, val, key = '') => {
        if (cfg.type === 'array' || cfg.type === 'preset') return cfg.type === 'preset' ? formatPresets(val) : JSON.stringify(val);
        if (cfg.type === 'toggle12') return val ? `1 (${t('spoofOn')})` : `2 (${t('spoofOff')})`;
        if (key === 'BTN_LAYOUT' && cfg.type === 'int') {
            const map = { 1: t('layout1'), 2: t('layout2'), 3: t('layout3') };
            return `${val} (${map[val] || val})`;
        }
        if (cfg.type === 'string' && (val === '' || val === null || val === undefined)) return t('emptyVal');
        return String(val);
    };

    // = 清單名稱對應 / List Name Mapping =
    const getListNamesArray = () => {
        const raw = CONFIG.LIST_NAMES || '';
        const names = raw.split(',').map(s => s.trim()).filter(s => s);
        const res = [];
        for (let i = 0; i < CONFIG.LIST_COUNT; i++) res.push(names[i] || `${LANG_CODE === 'zh' ? '清單' : 'List'}${i + 1}`);
        return res;
    };
    const getListDisplayName = () => {
        const idx = parseInt(currentList.replace('list', ''), 10) - 1;
        return getListNamesArray()[idx] || currentList;
    };

    const STORAGE_POS = 'ytMulti_btnPos', STORAGE_MODE = 'ytMulti_openMode', STORAGE_CURRENT = 'ytMulti_currentList', 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];

    // = 添加到清單 (共用) =
    const addToCurrentList = id => {
        const k = STORAGE_LISTS[currentList], normalizedId = normalizeVideoId(id);
        if (!normalizedId) return false;
        const ids = safeJSONParse(localStorage.getItem(k), []);
        if (ids.includes(normalizedId)) return false;
        ids.push(normalizedId); localStorage.setItem(k, JSON.stringify(ids));
        if (typeof updateListButtonCount === 'function') updateListButtonCount();
        try { localStorage.setItem(CONFIG.SYNC_EVENT_KEY, JSON.stringify({ type: 'videoAdded', listKey: currentList, videoId: normalizedId, timestamp: Date.now() })); } catch(e){}
        return true;
    };

    // = 影片觀察器 (頂層與 iframe 共用) =
    let videoObserver = null, isObserving = false, styleTag = null, pollTimer = null;
    const PROCESSED_ATTR = 'data-ytmulti-processed';

    // = 目標選擇器陣列 (含新增項目) =
    const TARGET_SELECTORS = [
        'ytd-rich-item-renderer',
        'ytd-thumbnail',
        'ytd-playlist-thumbnail',
        'ytd-playlist-video-renderer',
        'yt-lockup-view-model'
    ];
    const getVideoContainerSelector = () => location.href.includes('/playlist?list=') ? 'ytd-playlist-video-renderer ytd-thumbnail' : TARGET_SELECTORS.join(', ');

    // = 注入統計 (僅記憶體) =
    const injectionStats = {};
    TARGET_SELECTORS.forEach(s => injectionStats[s] = 0);

    const startObservingVideos = () => {
        if (!IS_SUB_FRAME && !isAddButtonEnabled) return;
        if (!IS_SUB_FRAME && 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);
        }
        if (!videoObserver) {
            videoObserver = new MutationObserver(mutations => {
                for (const m of mutations) { if (m.type === 'childList') for (const n of m.addedNodes) if (n.nodeType === Node.ELEMENT_NODE) processNode(n); }
            });
            videoObserver.observe(document.body, { childList: true, subtree: true });
        }
        if (pollTimer) clearInterval(pollTimer);
        const checkAndScan = () => {
            if (document.querySelector('ytd-browse, ytd-rich-grid-renderer, ytd-playlist-sidebar-renderer')) {
                processNode(document.body);
                clearInterval(pollTimer); pollTimer = null;
            }
        };
        pollTimer = setInterval(checkAndScan, 300); checkAndScan();
    };

    const processNode = node => {
        if (!node || !node.querySelectorAll) return;
        const selector = getVideoContainerSelector();
        if (node.matches && node.matches(selector)) tryAddButton(node);
        node.querySelectorAll(selector).forEach(tryAddButton);
    };

    const tryAddButton = el => {
        if (el.hasAttribute(PROCESSED_ATTR)) return;
        const vidLink = el.querySelector('a[href*="/watch?"]'); let hasVideo = false;
        if (vidLink?.href) hasVideo = true;
        else {
            const ep = el.querySelector('[data-endpoint]');
            if (ep) { try { const d = JSON.parse(ep.getAttribute('data-endpoint')); if (d?.videoId) hasVideo = true; } catch(e){} }
        }
        if (!hasVideo) return;
        el.setAttribute(PROCESSED_ATTR, '1'); addButtonsToContainer(el);

        if (CONFIG.BUTTON_INJECTION_STATS) {
            for (const sel of TARGET_SELECTORS) {
                if (el.matches(sel)) injectionStats[sel]++;
            }
        }

        if (!IS_SUB_FRAME && typeof window.onTargetFound === 'function') window.onTargetFound();
    };

    const stopObservingVideos = () => {
        isObserving = false;
        if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
        document.querySelectorAll('.ytMulti-add-btn').forEach(b => b.remove());
        document.querySelectorAll(`[${PROCESSED_ATTR}]`).forEach(el => el.removeAttribute(PROCESSED_ATTR));
        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);
        });
        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 });
    };

    // = 頂層專屬邏輯 (面板 / 同步 / 導航 / 設定 / 初始化) =
    let isCurrentWatchPage = isWatchPage();
    if (!IS_SUB_FRAME) {
        let hasFoundTargets = false, keepPanelExpanded = false, panelHovered = false;
        let urlPollingId = null, wprObserver = null, wprDetected = false;

        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:none;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 });

        const BTN_BASE_STYLE = 'padding:6px 12px;height:36px;border:none;border-radius:6px;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;';
        const createPanelButton = (text, options = {}) => {
            const { isActive = false, activeBg = '#00aa00', activeHover = '#008800', baseBg = '#ff0000', baseHover = '#cc0000' } = options;
            const btn = document.createElement('button'); btn.textContent = text; btn.dataset.state = isActive ? 'active' : 'inactive';
            btn.style.cssText = `${BTN_BASE_STYLE}background:${isActive ? activeBg : baseBg};`;
            const updateStyle = () => { btn.style.background = btn.dataset.state === 'active' ? activeBg : baseBg; };
            btn.addEventListener('mouseover', () => { btn.style.background = btn.dataset.state === 'active' ? activeHover : baseHover; });
            btn.addEventListener('mouseout', updateStyle); return btn;
        };

        const playBtn = createPanelButton(t('playIcon')); const modeBtn = createPanelButton(t('modeCurrentTab'));
        const listBtn = createPanelButton(`${getListDisplayName()} (0)`); const domainBtn = createPanelButton(getCurrentDomainMode().label);
        const addButtonToggle = createPanelButton(t('addButton'), { isActive: isAddButtonEnabled });
        const otherButtons = [modeBtn, listBtn, domainBtn, addButtonToggle]; panel.append(playBtn, modeBtn, listBtn, domainBtn, addButtonToggle);

        const updateListButtonCount = () => { const k = STORAGE_LISTS[currentList]; if (!k) return; const c = safeJSONParse(localStorage.getItem(k), []).length; listBtn.textContent = `${getListDisplayName()} (${c})`; };
        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 updatePanelVisibility = () => {
            if (isCurrentWatchPage) { panel.style.display = 'none'; collapsePanel(); }
            else { panel.style.display = 'flex'; if (!panelHovered && !keepPanelExpanded) collapsePanel(); }
        };

        window.onTargetFound = () => { if (!hasFoundTargets) { hasFoundTargets = true; } };

        panel.addEventListener('mouseenter', () => { panelHovered = true; panel.style.display = 'flex'; expandPanel(); }, { passive: true });
        panel.addEventListener('mouseleave', () => { panelHovered = false; updatePanelVisibility(); }, { passive: true });

        const toggleAddButton = () => {
            isAddButtonEnabled = !isAddButtonEnabled; localStorage.setItem(CONFIG.ADD_BUTTON_ENABLED_STORAGE_KEY, String(isAddButtonEnabled));
            addButtonToggle.dataset.state = isAddButtonEnabled ? 'active' : 'inactive'; addButtonToggle.dispatchEvent(new MouseEvent('mouseout'));
            isAddButtonEnabled ? startObservingVideos() : stopObservingVideos();
        };
        addButtonToggle.addEventListener('click', e => { e.stopPropagation(); if (isCurrentWatchPage) { addToCurrentList(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; });
        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(); });

        const stopUrlPolling = () => { if (urlPollingId) { clearInterval(urlPollingId); urlPollingId = null; } };
        const applyPanelMode = (isWatch) => {
            isCurrentWatchPage = isWatch;
            if (isWatch) { hasFoundTargets = false; panel.style.display = 'none'; collapsePanel(); addButtonToggle.textContent = t('addIcon'); addButtonToggle.style.fontSize = '18px'; addButtonToggle.dataset.state = 'active'; stopObservingVideos(); }
            else { addButtonToggle.textContent = t('addButton'); addButtonToggle.style.fontSize = '13px'; addButtonToggle.dataset.state = isAddButtonEnabled ? 'active' : 'inactive'; if (isAddButtonEnabled) startObservingVideos(); updatePanelVisibility(); }
            addButtonToggle.dispatchEvent(new MouseEvent('mouseout')); updateListButtonCount();
        };

        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) applyPanelMode(isW); 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; applyPanelMode(isWatchPage()); } }, CONFIG.POLLING_INTERVAL);
        };

        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 = safeJSONParse(localStorage.getItem(k), []); if (d.videoId && !ids.includes(d.videoId)) { ids.push(d.videoId); localStorage.setItem(k, JSON.stringify(ids)); updateListButtonCount(); } } } catch (err) {} } }); };

        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) => { menu += `${i + 1}. ${k}\n   ${LANG_CODE === 'zh' ? c.descZh : c.descEn}\n   當前/Current: ${formatSettingDisplay(c, CONFIG[k], k)}\n   預設/Default: ${formatSettingDisplay(c, c.default, k)}\n\n`; });

                if (CONFIG.BUTTON_INJECTION_STATS) {
                    menu += '📊 按鈕注入統計 (本會話):\n';
                    for (const [sel, count] of Object.entries(injectionStats)) {
                        const shortSel = sel.length > 45 ? sel.slice(0, 42) + '...' : sel;
                        menu += `   ${shortSel}: ${count}\n`;
                    }
                    menu += '\n';
                }

                menu += `${items.length + 1}. 📤 ${LANG_CODE === 'zh' ? '匯出清單' : 'Export Lists'}\n${items.length + 2}. 📥 ${LANG_CODE === 'zh' ? '匯入清單' : 'Import Lists'}\n\n0 = Exit`;
                const choice = prompt(menu, '0');
                if (!choice || choice === '0') return;
                const idx = parseInt(choice, 10) - 1;

                if (idx === items.length) {
                    let output = '';
                    const names = getListNamesArray();
                    for (let i = 1; i <= CONFIG.LIST_COUNT; i++) {
                        const key = STORAGE_LISTS[`list${i}`];
                        const ids = safeJSONParse(localStorage.getItem(key), []);
                        output += `${names[i - 1]}: ${ids.join(',')}\n`;
                    }
                    prompt(LANG_CODE === 'zh' ? '✅ 清單資料已生成,請複製以下內容:' : '✅ List data generated, please copy:', output.trim());
                    continue;
                }
                if (idx === items.length + 1) {
                    let listOptions = '';
                    for (let i = 1; i <= CONFIG.LIST_COUNT; i++) listOptions += `${i}. ${getListNamesArray()[i - 1]}\n`;
                    const targetChoice = prompt(`${LANG_CODE === 'zh' ? '請選擇要覆蓋的清單編號:' : 'Enter list number to overwrite:'}\n${listOptions}`, '1');
                    const tIdx = parseInt(targetChoice, 10);
                    if (isNaN(tIdx) || tIdx < 1 || tIdx > CONFIG.LIST_COUNT) { alert(t('settingsInvalid')); continue; }
                    const dataInput = prompt(LANG_CODE === 'zh' ? '請貼上清單資料(格式:清單1: ID1,ID2 或直接貼上 ID 逗號分隔):' : 'Paste list data (format: List1: ID1,ID2 or just comma-separated IDs):', '');
                    if (!dataInput || dataInput === '0') continue;
                    try {
                        let ids = dataInput.includes(':') ? dataInput.split(':')[1].trim() : dataInput.trim();
                        const parsedIds = ids.split(',').map(s => s.trim()).filter(s => s);
                        const key = STORAGE_LISTS[`list${tIdx}`];
                        localStorage.setItem(key, JSON.stringify(parsedIds));
                        if (currentList === `list${tIdx}`) updateListButtonCount();
                        alert(t('settingsSaved'));
                    } catch (e) { alert(t('settingsInvalid')); }
                    continue;
                }

                if (isNaN(idx) || idx < 0 || idx >= items.length) { alert(t('settingsInvalid')); continue; }
                const [key, cfg] = items[idx];
                const input = prompt(t('settingsValuePrompt', { current: formatSettingDisplay(cfg, CONFIG[key], key), default: formatSettingDisplay(cfg, cfg.default, key), desc: LANG_CODE === 'zh' ? cfg.descZh : cfg.descEn }), cfg.default);
                if (input === null || 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 === 'string') { val = input.trim(); }
                    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')); }
            }
        }

        // = 生成 Blob 頁面 (完整保留排版邏輯) =
        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 sharedHelpers = `const isChatId=id=>id&&id.endsWith(CHAT_SUFFIX);const isVideoId=id=>id&&id.endsWith(VIDEO_SUFFIX);const isHomepage=id=>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&&id.startsWith('http');const getVideoIdFromUrl=url=>{const m1=url&&url.match(/[?&]v=([A-Za-z0-9_-]{11})/);if(m1)return m1[1];const m2=url&&url.match(/youtu[.]be\\/([A-Za-z0-9_-]{11})/);return m2?m2[1]:null;};const getListIdFromUrl=url=>{const m=url&&url.match(/[?&]list=([^&]+)/);return m?m[1]:null;};`;
            const safeHomeUrl = CONFIG.HOME_TARGET_URL.replace(/'/g, "\\'");

            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,
                BTN_LAYOUT: CONFIG.BTN_LAYOUT,
                BTN_HIDE_TIMEOUT: CONFIG.BTN_HIDE_TIMEOUT,
                HOME_TARGET_URL: safeHomeUrl,
                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,
                SHARED_HELPERS: sharedHelpers
            };

            const jsCode = `(function(){'use strict';
const CHAT_SUFFIX='${blobParams.CHAT_SUFFIX}';const VIDEO_SUFFIX='${blobParams.VIDEO_SUFFIX}';
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 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};
const BTN_LAYOUT=${blobParams.BTN_LAYOUT};const BTN_HIDE_TIMEOUT=${blobParams.BTN_HIDE_TIMEOUT};
const HOME_TARGET_URL='${blobParams.HOME_TARGET_URL}';const HOME_DOMAIN='www.youtube.com';
${blobParams.SHARED_HELPERS}
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 hoverTimers=new Map();let layoutDirty=true;let resizeTimer;
let storageSyncTimer=null;let lastSyncTime=0;

const safeJSONParse=(str,fallback)=>{try{return JSON.parse(str);}catch(e){return fallback;}};
const clearHoverTimer=(id)=>{if(hoverTimers.has(id)){clearTimeout(hoverTimers.get(id));hoverTimers.delete(id);}};
const clearElementCache=(id)=>{const w=elementCache.get(id);if(w){const ifr=w.querySelector('iframe');if(ifr){ifr.src='about:blank';ifr.removeAttribute('src');}w.remove();}elementCache.delete(id);clearHoverTimer(id);};
const clearAllTimers=()=>{hoverTimers.forEach(clearTimeout);hoverTimers.clear();};
const clearAllElements=()=>{elementCache.forEach((w)=>{const ifr=w.querySelector('iframe');if(ifr){ifr.src='about:blank';ifr.removeAttribute('src');}w.remove();});elementCache.clear();hoverTimers.clear();};
const setupEnterShowHide=(triggerEl,targetEl,timeout)=>{let timer=null,visible=false;const show=()=>{targetEl.style.opacity='1';targetEl.style.pointerEvents='auto';targetEl.style.transform='scale(1)';};const hide=()=>{targetEl.style.opacity='0';targetEl.style.pointerEvents='none';targetEl.style.transform='scale(0.8)';};triggerEl.addEventListener('mouseenter',()=>{if(!visible){show();visible=true;clearTimeout(timer);timer=setTimeout(()=>{hide();visible=false;},timeout);}});triggerEl.addEventListener('mouseleave',()=>{clearTimeout(timer);if(visible){hide();visible=false;}});hide();};
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,id)=>{wrapper.addEventListener('mouseenter',()=>{clearHoverTimer(id);showButtons(wrapper);hoverTimers.set(id,setTimeout(()=>hideButtons(wrapper),BTN_HIDE_TIMEOUT));});wrapper.addEventListener('mouseleave',()=>{clearHoverTimer(id);hideButtons(wrapper);});};
const isLandscape=()=>container.offsetWidth>container.offsetHeight*LANDSCAPE_ASPECT_RATIO_THRESHOLD;
const findBestPortraitColumns=(itemCount,availableW,availableH,threshold)=>{if(availableH<=0)return 1;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=[];const isLand=container.offsetWidth>container.offsetHeight*LANDSCAPE_ASPECT_RATIO_THRESHOLD;
    const getCols=(cnt)=>{let c=1;for(let i=0;i<LANDSCAPE_COLUMN_CONFIG.length;i++){if(cnt>=LANDSCAPE_COLUMN_CONFIG[i])c++;else break;}return c;};
    if(isLand){
        const pCount=pin.length;
        if(pCount===0){if(vis.length>0){const cc=getCols(vis.length);const uW=W/cc;const uH=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:r*uH,w:uW,h:uH});}}}
        else if(pCount===1){const pH=W/ASPECT_RATIO_STANDARD;cells.push({id:pin[0][0],x:0,y:0,w:W,h:pH,isPinned:true});const aH=H-pH;if(aH>0&&vis.length>0){const cc=getCols(vis.length);const uW=W/cc,uH=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:pH+r*uH,w:uW,h:uH});}}}
        else if(pCount===2){
            const pW=W/2,pH=pW/ASPECT_RATIO_STANDARD;
            cells.push({id:pin[0][0],x:0,y:0,w:pW,h:pH,isPinned:true});
            cells.push({id:pin[1][0],x:pW,y:0,w:pW,h:pH,isPinned:true});
            const aH=H-pH;
            if(aH>0&&vis.length>0){
                const cc = Math.min(vis.length, 4);
                const uW = W / cc;
                const uH = 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:pH+r*uH,w:Math.min(uW,pW),h:uH});}
            }
        }
        else if(pCount>=3){
            const pW=W/2,pH=H/2;
            for(let i=0;i<Math.min(3,pCount);i++){cells.push({id:pin[i][0],x:(i%2)*pW,y:Math.floor(i/2)*pH,w:pW,h:pH,isPinned:true});}
            if(vis.length>0){
                const cc = Math.min(vis.length, 4);
                const uW = W / cc;
                const uH = 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:pW+col*uW,y:pH+r*uH,w:Math.min(uW,pW),h:uH});}
            }
        }
    }
    else{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=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'),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 res=RESOLUTION_LEVELS[lvl];const tw=res?res[0]:854,th=res?res[1]:480;scaler.style.width=tw+'px';scaler.style.height=th+'px';ifr.style.width=tw+'px';ifr.style.height=th+'px';scaler.style.transform='scale('+(c.w/tw)+','+(c.h/th)+')';}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);};
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);safeSave();scheduleLayout();}};
const moveTop=id=>{const c=idOrderMap.get(id);if(c===undefined||c===0)return;const orders=Array.from(idOrderMap.values());if(orders.length===0)return;const mn=Math.min(...orders);for(let[i,o]of idOrderMap)if(o>=mn&&o<c)idOrderMap.set(i,o+1);idOrderMap.set(id,mn-1);safeSave();scheduleLayout();};
const moveBottom=id=>{const c=idOrderMap.get(id);if(c===undefined)return;const orders=Array.from(idOrderMap.values());if(orders.length===0)return;const mx=Math.max(...orders);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);safeSave();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){nO=o;nId=i;}if(nId)swapOrder(id,nId);};
const mkBtn=(cls,fn)=>{const d=document.createElement('div');d.className=cls;d.onclick=e=>{e.stopPropagation();fn(e);};return d;};
const renumberOrders=()=>{const entries=Array.from(idOrderMap.entries()).sort((a,b)=>a[1]-b[1]);idOrderMap.clear();entries.forEach(([id],idx)=>idOrderMap.set(id,(idx+1)*2));};
const safeSave=()=>{try{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));const orders=Array.from(idOrderMap.values());if(orders.length>0&&(Math.max(...orders)>10000||Math.min(...orders)<-10000)){renumberOrders();}}catch(e){console.warn('[Blob] SafeSave error:',e);}};
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,id);const isC=isChatId(id);const frameDomain=(isHome||isC)?HOME_DOMAIN:DOMAIN_MODE;const listParam=isFullUrl(id)?getListIdFromUrl(id):null;let src=isHome?(HOME_TARGET_URL||'https://'+frameDomain+'/'):(isC?'https://'+frameDomain+'/live_chat?v='+vid:'https://'+frameDomain+'/embed/'+vid+'?'+YT_PARAMS+(listParam?'&list='+listParam:''));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';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',()=>{clearElementCache(id);idOrderMap.delete(id);const p=pinnedIds.indexOf(id);if(p!==-1)pinnedIds.splice(p,1);safeSave();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.pop();pinnedIds.push(id);}safeSave();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);clearElementCache(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);safeSave();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){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=safeJSONParse(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){console.warn('[YT-Multi-Blob] Copy sync error:',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);const btnLayout=BTN_LAYOUT;const btnPos={};if(btnLayout===1){btnPos.del={top:6,left:6};btnPos.pin={top:30,left:6};btnPos.chatT={top:54,left:6};btnPos.chatA={top:78,left:6};btnPos.top={top:102,left:6};btnPos.up={top:126,left:6};btnPos.dn={top:150,left:6};btnPos.bot={top:174,left:6};btnPos.copy={top:6,left:32};}else if(btnLayout===2){btnPos.del={top:6,right:6};btnPos.pin={top:30,right:6};btnPos.chatT={top:54,right:6};btnPos.chatA={top:78,right:6};btnPos.top={top:102,right:6};btnPos.up={top:126,right:6};btnPos.dn={top:150,right:6};btnPos.bot={top:174,right:6};btnPos.copy={top:6,right:32};copyCol.style.cssText='position:absolute;top:6px;right:32px;display:flex;flex-direction:column;gap:4px;z-index:9999;';}else{btnPos.del={top:6,left:6};btnPos.pin={top:6,left:30};btnPos.chatT={top:6,left:54};btnPos.chatA={top:6,left:78};btnPos.top={top:6,left:102};btnPos.up={top:6,left:126};btnPos.dn={top:6,left:150};btnPos.bot={top:6,left:174};btnPos.copy={top:6,left:198};copyCol.style.cssText='position:absolute;top:6px;left:198px;display:flex;flex-direction:row;gap:4px;z-index:9999;';copyCol.querySelectorAll('.copy-btn,.copy-placeholder').forEach(c=>{c.style.width='20px';c.style.height='20px';});}const applyBtnPos=(btn,key)=>{if(btnPos[key]){Object.entries(btnPos[key]).forEach(([p,v])=>btn.style[p]=v+'px');}};applyBtnPos(del,'del');applyBtnPos(pin,'pin');applyBtnPos(chatT,'chatT');applyBtnPos(chatA,'chatA');applyBtnPos(top,'top');applyBtnPos(up,'up');applyBtnPos(dn,'dn');applyBtnPos(bot,'bot');if(btnLayout===3){copyCol.style.flexDirection='row';}elementCache.set(id,w);return w;};
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();
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:80px;height:80px;z-index:99998;cursor:default;';homeTrigger.appendChild(homeBtn);document.body.appendChild(homeTrigger);setupEnterShowHide(homeTrigger,homeBtn,HOVER_TIMEOUT);homeBtn.addEventListener('click',()=>{const hId='homepage_'+Date.now();const currentOrders=Array.from(idOrderMap.values());const newOrder=currentOrders.length>0?Math.max(...currentOrders)+2:1;idOrderMap.set(hId,newOrder);if(HOME_PINNED_BY_DEFAULT){if(pinnedIds.length>=MAX_PINNED)pinnedIds.pop();pinnedIds.push(hId);}const w=createVideo(hId,newOrder);if(w){container.appendChild(w);elementCache.set(hId,w);}safeSave();scheduleLayout();});
window.addEventListener('resize',scheduleLayout,{passive:true});window.addEventListener('storage',e=>{if(e.key===STORAGE_LISTS[listKey]){const now=Date.now();if(now-lastSyncTime<200)return;lastSyncTime=now;if(storageSyncTimer)clearTimeout(storageSyncTimer);storageSyncTimer=setTimeout(()=>{storageSyncTimer=null;try{const newIds=safeJSONParse(localStorage.getItem(STORAGE_LISTS[listKey]),[]);const newPinned=safeJSONParse(localStorage.getItem(PINNED_STORAGE_KEY),[]);pinnedIds=newPinned;const newSet=new Set(newIds);for(const id of[...idOrderMap.keys()]){if(!newSet.has(id))clearElementCache(id);}let currentMax=idOrderMap.size>0?Math.max(...idOrderMap.values()):0;newIds.forEach(id=>{if(!idOrderMap.has(id)){currentMax+=2;idOrderMap.set(id,currentMax);if(!container.querySelector('.video-wrapper[data-id="'+id+'"]')){const el=createVideo(id,currentMax);if(el)container.appendChild(el);}}});scheduleLayout();}catch(err){console.warn('[YT-Multi-Blob] Sync error:',err);}},100);}});window.addEventListener('beforeunload',()=>{clearTimeout(resizeTimer);clearAllElements();clearAllTimers();});})();`;

            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{background:#ff4444}.pin-btn{background:#44aaff}.chat-toggle-btn{background:#888888}.add-chat-btn{background:#44aa44}.top-btn{background:#ffaa44}.up-btn{background:#88cc44}.down-btn{background:#44cc88}.bottom-btn{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;display:flex;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' }));
        };

        const openMultiPlayer = () => {
            const k = STORAGE_LISTS[currentList]; let ids = safeJSONParse(localStorage.getItem(k), []);
            const pinnedK = STORAGE_PINNED_PREFIX + currentList; let pinned = safeJSONParse(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); }
            window.addEventListener('pagehide', cleanupBlob, { once: true });
        };
        playBtn.addEventListener('click', openMultiPlayer);

        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();

        applyPanelMode(isWatchPage()); setupSyncListener(); setupNavigationObserver();
        window.addEventListener('beforeunload', () => { stopUrlPolling(); wprObserver?.disconnect(); stopObservingVideos(); });
    }
    // = iframe 專屬邏輯 (僅啟動觀察器,不創建面板/不觸發同步) =
    else {
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => setTimeout(startObservingVideos, 1000));
        else setTimeout(startObservingVideos, 1000);
        window.addEventListener('beforeunload', stopObservingVideos);
    }
})();