YouTube 多重播放器 YouTube Multi-Player

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

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