YouTube 多重播放器 YouTube Multi-Player

以新分頁或新視窗同時播放多個影片,並可將任意影片放大置頂。 // Play multiple videos simultaneously in new tabs or windows, and pin any video to the top.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube 多重播放器 YouTube Multi-Player
// @name:zh-TW   YouTube 多重播放器
// @name:en      YouTube Multi-Player
// @namespace    http://tampermonkey.net/
// @version      7.2.1
// @match        https://www.youtube.com/
// @match        https://www.youtube.com/feed/*
// @match        https://www.youtube.com/playlist?list=*
// @match        https://www.youtube.com/@*
// @match        https://www.youtube.com/gaming
// @match        https://www.youtube.com/results?search_query=*
// @match        https://www.youtube.com/channel/*
// @exclude      https://studio.youtube.com/*
// @exclude      https://accounts.youtube.com/*
// @exclude      https://www.youtube.com/watch
// @grant        GM_info
// @license      MIT
// @description  以新分頁或新視窗同時播放多個影片,並可將任意影片放大置頂。 // Play multiple videos simultaneously in new tabs or windows, and pin any video to the top.
// @description:zh-TW  以新分頁或新視窗同時播放多個影片,並可將任意影片放大置頂。
// @description:en  Play multiple videos simultaneously in new tabs or windows, and pin any video to the top.
// ==/UserScript==
/*  版本功能說明 / Version Feature Description
    6.8: 增加影片置頂狀態的記憶功能,下次開啟時會恢復上次的置頂影片。
         Added memory for video pinning status, restoring pinned videos on next launch.
    6.9: 修復刪除影片後佈局空置的問題。新增影片置頂、上移、下移、置底按鈕。
         Fixed the issue of layout gaps after deleting videos. Added Top, Up, Down, Bottom buttons for videos.
    7.2: 採用順序編號系統管理影片,徹底解決影片移動時重新載入的問題。
         Adopted an order-number system for video management, completely resolving the issue of videos reloading during movement.
    7.2.1: 修復拖曳面板時按鈕無法固定,以及播放頁面影片顯示異常的問題。
         Fixed the issue where the panel button couldn't be fixed after dragging, and the problem where videos displayed abnormally in the play page.
*/
(function(){
    'use strict';
    // --- 腳本參數 / Script Parameters ---
    const MAX_PINNED = 2; // 最大置頂影片數量 / Maximum number of pinned videos
    const LIST_COUNT = 3; // 設定清單數量 / Number of lists
    // --- 語言檢測 / Language Detection ---
    const isChinese = navigator.language.startsWith('zh') || (typeof GM_info !== 'undefined' && GM_info.script.locale === 'zh-TW');
    // --- 文字資源 / Text Resources ---
    const TEXTS = {
        play: isChinese ? '▶ 播放' : '▶ Play',
        modeCurrentTab: isChinese ? '這分頁' : 'Current Tab',
        modeNewTab: isChinese ? '新分頁' : 'New Tab',
        modeNewWindow: isChinese ? '新視窗' : 'New Window',
        list: isChinese ? '清單' : 'List',
        noVideos: isChinese ? '當前清單無影片' : 'No videos in current list',
    };
    // --- 網址驗證 / URL Validation ---
    const validateURL = () => {
        const patterns = [
            /^https:\/\/www\.youtube\.com\/$/,
            /^https:\/\/www\.youtube\.com\/feed\/.*/,
            /^https:\/\/www\.youtube\.com\/playlist\?list=.*/,
            /^https:\/\/www\.youtube\.com\/@.*/,
            /^https:\/\/www\.youtube\.com\/gaming$/,
            /^https:\/\/www\.youtube\.com\/results\?search_query=.*/,
            /^https:\/\/www\.youtube\.com\/channel\/.*/
        ];
        return patterns.some(p => p.test(window.location.href));
    };
    // 移除面板如果網址無效 / Remove panel if URL becomes invalid
    let checkInterval = setInterval(() => {
        if(!validateURL()){
            const panel = document.getElementById('ytMulti_panel');
            if(panel) panel.remove();
            clearInterval(checkInterval);
        }
    }, 30000);
    // --- 儲存鍵名 / Storage Keys ---
    const STORAGE_POS = 'ytMulti_btnPos';
    const STORAGE_MODE = 'ytMulti_openMode';
    const STORAGE_CURRENT = 'ytMulti_currentList';
    const STORAGE_PINNED_PREFIX = 'ytMulti_pinned_';
    // --- 動態生成清單儲存鍵 / Dynamically Generate List Storage Keys ---
    const generateStorageKeys = () => {
        const keys = {};
        for (let i = 1; i <= LIST_COUNT; i++) {
            keys[`list${i}`] = `ytMulti_videoList${i}`;
        }
        return keys;
    };
    const STORAGE_LISTS = generateStorageKeys();
    let currentList = localStorage.getItem(STORAGE_CURRENT) || 'list1';
    if (!STORAGE_LISTS[currentList]) {
        currentList = Object.keys(STORAGE_LISTS)[0];
        localStorage.setItem(STORAGE_CURRENT, currentList);
    }
    // --- 創建控制面板 / Create Control 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);
    `;
    document.body.appendChild(panel);
    // --- 還原面板位置 / Restore Panel Position ---
    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';
    }
    // --- 使面板可拖曳 / Make Panel Draggable ---
    panel.addEventListener('mousedown', e => {
        e.preventDefault();
        let startX = e.clientX, startY = e.clientY;
        const rect = panel.getBoundingClientRect();
        let hasMoved = false;
        function onMove(ev){
            panel.style.top = rect.top + ev.clientY - startY + 'px';
            panel.style.left = rect.left + ev.clientX - startX + 'px';
            hasMoved = true;
        }
        function onUp(){
            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);
        window.addEventListener('mouseup', onUp); // 修正:添加事件監聽
    });
    // --- 創建樣式化按鈕 / Create Styled Button ---
    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);
        `;
        btn.addEventListener('mouseover', () => btn.style.background = '#cc0000');
        btn.addEventListener('mouseout', () => btn.style.background = '#ff0000');
        return btn;
    }
    // --- 初始化按鈕 / Initialize Buttons ---
    const playBtn = createStyledButton(TEXTS.play);
    const modeBtn = createStyledButton(getModeButtonText());
    const listBtn = createStyledButton(`${TEXTS.list}1`);
    panel.append(playBtn, modeBtn, listBtn);
    // --- 拖曳處理 / Drag and Drop Handling ---
    panel.addEventListener('dragover', e => e.preventDefault());
    panel.addEventListener('drop', e => {
        e.preventDefault();
        const data = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain');
        const vid = parseYouTubeID(data);
        if(!vid) return;
        const storageKey = STORAGE_LISTS[currentList];
        let ids = JSON.parse(localStorage.getItem(storageKey) || '[]');
        if(!ids.includes(vid)){
            ids.push(vid);
            localStorage.setItem(storageKey, JSON.stringify(ids));
            updateListButtonCount();
        }
    });
    // --- 模式切換 / Mode Toggle ---
    modeBtn.addEventListener('click', () => {
        const currentMode = localStorage.getItem(STORAGE_MODE) || 'current_tab';
        let nextMode;
        switch(currentMode) {
            case 'current_tab':
                nextMode = 'new_tab';
                break;
            case 'new_tab':
                nextMode = 'new_window';
                break;
            case 'new_window':
            default:
                nextMode = 'current_tab';
                break;
        }
        localStorage.setItem(STORAGE_MODE, nextMode);
        modeBtn.textContent = getModeButtonText();
    });
    // --- 取得模式按鈕文字 / Get Mode Button Text ---
    function getModeButtonText() {
        const mode = localStorage.getItem(STORAGE_MODE) || 'current_tab';
        switch(mode) {
            case 'current_tab': return TEXTS.modeCurrentTab;
            case 'new_tab': return TEXTS.modeNewTab;
            case 'new_window': return TEXTS.modeNewWindow;
            default: return TEXTS.modeCurrentTab;
        }
    }
    // --- 清單切換 / List Toggle ---
    listBtn.addEventListener('click', () => {
        const listNames = Object.keys(STORAGE_LISTS);
        const currentIndex = listNames.indexOf(currentList);
        const nextIndex = (currentIndex + 1) % listNames.length;
        currentList = listNames[nextIndex];
        localStorage.setItem(STORAGE_CURRENT, currentList);
        updateListButtonCount();
    });
    // --- 更新清單按鈕計數 / Update List Button Count ---
    const updateListButtonCount = () => {
        const storageKey = STORAGE_LISTS[currentList];
        const count = JSON.parse(localStorage.getItem(storageKey) || '[]').length;
        const listNum = currentList.replace('list', '');
        listBtn.textContent = `${TEXTS.list}${listNum} (${count})`;
    };
    // --- 播放功能 / Play Function ---
    playBtn.addEventListener('click', () => {
        const storageKey = STORAGE_LISTS[currentList];
        const ids = JSON.parse(localStorage.getItem(storageKey) || '[]');
        if(!ids.length) return alert(TEXTS.noVideos);
        const pinnedStorageKey = STORAGE_PINNED_PREFIX + currentList;
        const pinnedIds = JSON.parse(localStorage.getItem(pinnedStorageKey) || '[]');
        const html = makeBlobPage(ids, currentList, pinnedIds);
        const blobUrl = URL.createObjectURL(new Blob([html], {type: 'text/html'}));
        const mode = localStorage.getItem(STORAGE_MODE) || 'current_tab';
        switch(mode) {
            case 'current_tab':
                location.href = blobUrl;
                break;
            case 'new_tab':
                window.open(blobUrl, '_blank');
                break;
            case 'new_window':
                window.open(blobUrl, '_blank', 'width=800,height=600,scrollbars=no,resizable=yes');
                break;
        }
    });
    // --- 解析 YouTube ID / Parse YouTube ID ---
    function parseYouTubeID(url){
        const m = url.match(/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/);
        return m ? m[1] : null;
    }
    // --- 生成播放頁面 HTML / Generate Play Page HTML ---
    function makeBlobPage(ids, listKey, initialPinnedIds = []){
        const idWithOrder = ids.map((id, index) => ({ id, order: index }));
        const listWithOrderJson = JSON.stringify(idWithOrder);
        const storageListsJson = JSON.stringify(STORAGE_LISTS);
        const pinnedJson = JSON.stringify(initialPinnedIds);
        const pinnedStorageKey = JSON.stringify(STORAGE_PINNED_PREFIX + listKey);
        return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Multi-Player</title><style>
            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;transition:all 0.3s ease;}
            .video-wrapper iframe{width:100%;height:100%;border:none;}
            .remove-btn, .pin-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;right:6px;background:#ff4444;}
            .pin-btn{top:30px;right:6px;background:#44aaff;}
            .top-btn{top:54px;right:6px;background:#ffaa44;}
            .up-btn{top:78px;right:6px;background:#88cc44;}
            .down-btn{top:102px;right:6px;background:#44cc88;}
            .bottom-btn{top:126px;right: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;}
            .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;}
            .video-wrapper:hover .remove-btn, .video-wrapper:hover .pin-btn, .video-wrapper:hover .top-btn, .video-wrapper:hover .up-btn, .video-wrapper:hover .down-btn, .video-wrapper:hover .bottom-btn{display:block;}
        </style></head><body><div class="container"></div><script>
            const MAX_PINNED = ${MAX_PINNED};
            const ASPECT_RATIO = 16/9;
            let idOrderMap = new Map(${listWithOrderJson}.map(item => [item.id, item.order]));
            const listKey = ${JSON.stringify(listKey)};
            const STORAGE_LISTS = ${storageListsJson};
            const INITIAL_PINNED_IDS = ${pinnedJson};
            const PINNED_STORAGE_KEY = ${pinnedStorageKey};
            const container = document.querySelector('.container');
            let pinnedIds = INITIAL_PINNED_IDS.slice();
            function saveIdsToStorage() {
                const storageKey = STORAGE_LISTS[listKey];
                const sortedIds = Array.from(idOrderMap.entries()).sort((a, b) => a[1] - b[1]).map(entry => entry[0]);
                localStorage.setItem(storageKey, JSON.stringify(sortedIds));
            }
            function savePinnedState() {
                localStorage.setItem(PINNED_STORAGE_KEY, JSON.stringify(pinnedIds));
            }
            function calculateLayout(){
                const W = container.offsetWidth;
                const H = container.offsetHeight;
                const visibleIds = Array.from(idOrderMap.entries())
                    .filter(([id, order]) => !pinnedIds.includes(id))
                    .sort((a, b) => a[1] - b[1])
                    .map(([id, order]) => id);
                const n = visibleIds.length;
                if(n === 0) {
                    // 修正:處理無可視影片時的佈局
                    pinnedIds.forEach((id, index) => {
                        const pinnedVideo = document.querySelector('[data-id="'+id+'"]');
                        if(pinnedVideo){
                            pinnedVideo.style.top = (index * (W / ASPECT_RATIO)) + 'px';
                            pinnedVideo.style.left = '0px';
                            pinnedVideo.style.width = W + 'px';
                            pinnedVideo.style.height = (W / ASPECT_RATIO) + 'px';
                            pinnedVideo.style.zIndex = '100';
                        }
                    });
                    return {cols:0, rows:0, itemWidth:0, itemHeight:0, availableH:H, pinnedHeight: pinnedIds.length * (W / ASPECT_RATIO)};
                }
                let bestCols = 1;
                let bestRows = 1;
                let bestItemWidth = 0;
                let bestItemHeight = 0;
                let bestScore = 0;
                const pinnedHeight = pinnedIds.length * (W / ASPECT_RATIO);
                const availableH = H - pinnedHeight;
                for(let cols=1; cols<=Math.min(n,12); cols++){
                    const rows = Math.ceil(n/cols);
                    let itemWidth = W/cols;
                    let itemHeight = itemWidth/ASPECT_RATIO;
                    if(rows*itemHeight > availableH){
                        itemHeight = availableH/rows;
                        itemWidth = itemHeight*ASPECT_RATIO;
                    }
                    const usedWidth = cols*itemWidth;
                    const usedHeight = rows*itemHeight;
                    const areaScore = usedWidth*usedHeight;
                    const penalty = (W-usedWidth)*0.1 + (availableH-usedHeight)*0.2;
                    const totalScore = areaScore - penalty;
                    if(totalScore > bestScore){
                        bestScore = totalScore;
                        bestCols = cols;
                        bestRows = rows;
                        bestItemWidth = itemWidth;
                        bestItemHeight = itemHeight;
                    }
                }
                return {cols:bestCols, rows:bestRows, itemWidth:bestItemWidth, itemHeight:bestItemHeight, availableH, pinnedHeight};
            }
            function updateLayout(){
                const {cols, rows, itemWidth, itemHeight, availableH, pinnedHeight} = calculateLayout();
                pinnedIds.forEach((id, index) => {
                    const pinnedVideo = document.querySelector('[data-id="'+id+'"]');
                    if(pinnedVideo){
                        pinnedVideo.style.top = (index * (container.offsetWidth / ASPECT_RATIO)) + 'px';
                        pinnedVideo.style.left = '0px';
                        pinnedVideo.style.width = '100vw';
                        pinnedVideo.style.height = (container.offsetWidth / ASPECT_RATIO) + 'px';
                        pinnedVideo.style.zIndex = '100';
                    }
                });
                const visibleIds = Array.from(idOrderMap.entries())
                    .filter(([id, order]) => !pinnedIds.includes(id))
                    .sort((a, b) => a[1] - b[1])
                    .map(([id, order]) => id);
                visibleIds.forEach((id, index) => {
                    const wrap = document.querySelector('[data-id="'+id+'"]');
                    if(wrap) {
                        const col = index % cols;
                        const row = Math.floor(index / cols);
                        wrap.style.width = itemWidth + 'px';
                        wrap.style.height = itemHeight + 'px';
                        wrap.style.left = (col * itemWidth) + 'px';
                        wrap.style.top = pinnedHeight + (row * itemHeight) + 'px';
                        wrap.style.zIndex = '1';
                    }
                });
            }
            function swapOrder(id1, id2) {
                const order1 = idOrderMap.get(id1);
                const order2 = idOrderMap.get(id2);
                if (order1 !== undefined && order2 !== undefined) {
                    idOrderMap.set(id1, order2);
                    idOrderMap.set(id2, order1);
                    saveIdsToStorage();
                    updateLayout();
                }
            }
            function moveVideoToTop(movedId) {
                const currentOrder = idOrderMap.get(movedId);
                if (currentOrder === undefined || currentOrder === 0) return;
                const minOrder = Math.min(...Array.from(idOrderMap.values()));
                for (let [id, order] of idOrderMap) {
                    if (order >= minOrder && order < currentOrder) {
                        idOrderMap.set(id, order + 1);
                    }
                }
                idOrderMap.set(movedId, minOrder - 1);
                saveIdsToStorage();
                updateLayout();
            }
            function moveVideoToBottom(movedId) {
                const currentOrder = idOrderMap.get(movedId);
                if (currentOrder === undefined) return;
                const maxOrder = Math.max(...Array.from(idOrderMap.values()));
                if (currentOrder === maxOrder) return;
                for (let [id, order] of idOrderMap) {
                    if (order > currentOrder && order <= maxOrder) {
                        idOrderMap.set(id, order - 1);
                    }
                }
                idOrderMap.set(movedId, maxOrder + 1);
                saveIdsToStorage();
                updateLayout();
            }
            function moveVideoDown(movedId) {
                const currentOrder = idOrderMap.get(movedId);
                if (currentOrder === undefined) return;
                let nextHigherId = null;
                let nextHigherOrder = Infinity;
                for (let [id, order] of idOrderMap) {
                    if (order > currentOrder && order < nextHigherOrder && !pinnedIds.includes(id)) {
                        nextHigherOrder = order;
                        nextHigherId = id;
                    }
                }
                if (nextHigherId) {
                    swapOrder(movedId, nextHigherId);
                }
            }
            function createVideo(id){
                if (!/^[A-Za-z0-9_-]{11}$/.test(id)) {
                    console.error("Invalid YouTube ID:", id);
                    return null;
                }
                const wrap = document.createElement('div');
                wrap.className = 'video-wrapper';
                wrap.dataset.id = id;
                const ifr = document.createElement('iframe');
                ifr.src = 'https://www.youtube.com/embed/' + id + '?autoplay=1&playsinline=1&rel=0&modestbranding=1&origin=' + encodeURIComponent(window.location.origin);
                ifr.allow = 'autoplay; encrypted-media; fullscreen';
                ifr.onload = function() {
                    setTimeout(() => {
                        try {
                            ifr.contentWindow.postMessage({
                                event: 'command',
                                func: 'pauseVideo'
                            }, '*');
                        } catch (e) {
                        }
                    }, 1000);
                };
                const delBtn = document.createElement('div');
                delBtn.className = 'remove-btn';
                delBtn.onclick = (e) => {
                    e.stopPropagation();
                    idOrderMap.delete(id);
                    saveIdsToStorage();
                    const pinnedIndex = pinnedIds.indexOf(id);
                    if (pinnedIndex !== -1) {
                        pinnedIds.splice(pinnedIndex, 1);
                        savePinnedState();
                    }
                    wrap.remove();
                    updateLayout();
                };
                const pinBtn = document.createElement('div');
                pinBtn.className = 'pin-btn';
                pinBtn.onclick = (e) => {
                    e.stopPropagation();
                    const index = pinnedIds.indexOf(id);
                    if(index !== -1){
                        pinnedIds.splice(index, 1);
                    } else{
                        if(pinnedIds.length >= MAX_PINNED) pinnedIds.shift();
                        pinnedIds.push(id);
                    }
                    savePinnedState();
                    updateLayout();
                };
                const topBtn = document.createElement('div');
                topBtn.className = 'top-btn';
                topBtn.onclick = (e) => {
                    e.stopPropagation();
                    moveVideoToTop(id);
                };
                const upBtn = document.createElement('div');
                upBtn.className = 'up-btn';
                upBtn.onclick = (e) => {
                    e.stopPropagation();
                    const currentOrder = idOrderMap.get(id);
                    if (currentOrder === undefined) return;
                    let nextLowerId = null;
                    let nextLowerOrder = -Infinity;
                    for (let [otherId, order] of idOrderMap) {
                        if (order < currentOrder && order > nextLowerOrder && !pinnedIds.includes(otherId)) {
                            nextLowerOrder = order;
                            nextLowerId = otherId;
                        }
                    }
                    if (nextLowerId) {
                        swapOrder(id, nextLowerId);
                    }
                };
                const downBtn = document.createElement('div');
                downBtn.className = 'down-btn';
                downBtn.onclick = (e) => {
                    e.stopPropagation();
                    moveVideoDown(id);
                };
                const bottomBtn = document.createElement('div');
                bottomBtn.className = 'bottom-btn';
                bottomBtn.onclick = (e) => {
                    e.stopPropagation();
                    moveVideoToBottom(id);
                };
                wrap.append(ifr, delBtn, pinBtn, topBtn, upBtn, downBtn, bottomBtn);
                return wrap;
            }
            const sortedInitialEntries = Array.from(idOrderMap.entries()).sort((a, b) => a[1] - b[1]);
            sortedInitialEntries.forEach(([id, order]) => {
                const videoElement = createVideo(id);
                if (videoElement) {
                    container.appendChild(videoElement);
                }
            });
            // 修正:確保在DOM加載完成後執行佈局更新
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', updateLayout);
            } else {
                setTimeout(updateLayout, 100); // 添加短暫延遲確保元素已插入
            }
            window.addEventListener('resize', updateLayout);
        <\/script></body></html>`;
    }
    // --- 初始化清單計數 / Initialize List Count ---
    updateListButtonCount();
})();