您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自動開啟查詢結果表格中每個詞目連結於 Modal iframe,依序播放音檔(自動偵測時長),主表格自動滾動高亮,可即時暫停/停止/點擊背景暫停/點擊表格列播放,並根據亮暗模式高亮按鈕。
当前为
// ==UserScript== // @name 教育部臺語辭典 - 自動循序播放音檔 (表格滾動與列播放) // @namespace aiuanyu // @version 4.0 // @description 自動開啟查詢結果表格中每個詞目連結於 Modal iframe,依序播放音檔(自動偵測時長),主表格自動滾動高亮,可即時暫停/停止/點擊背景暫停/點擊表格列播放,並根據亮暗模式高亮按鈕。 // @author Aiuanyu 愛灣語 + Gemini // @match http*://sutian.moe.edu.tw/und-hani/tshiau/* // @match http*://sutian.moe.edu.tw/und-hani/hunlui/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // 備用 // @connect sutian.moe.edu.tw // 允許獲取音檔 // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // --- 配置 --- const MODAL_WIDTH = '80vw'; const MODAL_HEIGHT = '70vh'; const FALLBACK_DELAY_MS = 3000; const DELAY_BUFFER_MS = 500; const DELAY_BETWEEN_CLICKS_MS = 750; const DELAY_BETWEEN_IFRAMES_MS = 500; const HIGHLIGHT_CLASS = 'userscript-audio-playing'; const OVERLAY_ID = 'userscript-modal-overlay'; const ROW_HIGHLIGHT_COLOR = 'rgba(0, 255, 0, 0.1)'; // 表格列高亮顏色 const ROW_HIGHLIGHT_DURATION = 1500; // 表格列高亮持續時間 (ms) const FONT_AWESOME_URL = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css'; const FONT_AWESOME_INTEGRITY = 'sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=='; // --- 適應亮暗模式的高亮樣式 --- const HIGHLIGHT_STYLE = ` /* 預設 (亮色模式) */ .${HIGHLIGHT_CLASS} { background-color: #FFF352 !important; color: black !important; outline: 2px solid #FFB800 !important; box-shadow: 0 0 10px #FFF352; transition: background-color 0.2s ease-in-out, outline 0.2s ease-in-out, color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; } /* 深色模式 */ @media (prefers-color-scheme: dark) { .${HIGHLIGHT_CLASS} { background-color: #66b3ff !important; color: black !important; outline: 2px solid #87CEFA !important; box-shadow: 0 0 10px #66b3ff; } } `; // --- 配置結束 --- // --- 全局狀態變數 --- let isProcessing = false; let isPaused = false; let currentLinkIndex = 0; // ** 注意:這個索引是相對於 linksToProcess 列表的 ** let totalLinks = 0; // 當前處理列表的總數 let currentSleepController = null; let currentIframe = null; let linksToProcess = []; // ** 儲存當前要處理的連結對象 {url, anchorElement, tableRow} ** let rowHighlightTimeout = null; // 用於清除表格行高亮 // --- UI 元素引用 --- let startButton; let pauseButton; let stopButton; let statusDisplay; let overlayElement = null; // --- Helper 函數 --- // 可中斷的延遲函數 function interruptibleSleep(ms) { // (程式碼與 v3.7 相同) if (currentSleepController) { currentSleepController.cancel('overridden'); } let timeoutId; let rejectFn; let resolved = false; let rejected = false; const promise = new Promise((resolve, reject) => { rejectFn = reject; timeoutId = setTimeout(() => { if (!rejected) { resolved = true; currentSleepController = null; resolve(); } }, ms); }); const controller = { promise: promise, cancel: (reason = 'cancelled') => { if (!resolved && !rejected) { rejected = true; clearTimeout(timeoutId); currentSleepController = null; const error = new Error(reason); error.isCancellation = true; error.reason = reason; rejectFn(error); } } }; currentSleepController = controller; return controller; } // 普通延遲函數 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 獲取音檔時長 (毫秒) function getAudioDuration(audioUrl) { // (程式碼與 v3.7 相同) console.log(`[自動播放] 嘗試獲取音檔時長: ${audioUrl}`); return new Promise((resolve) => { if (!audioUrl) { console.warn("[自動播放] 無效的音檔 URL,使用後備延遲。"); resolve(FALLBACK_DELAY_MS); return; } const audio = new Audio(); audio.preload = 'metadata'; const timer = setTimeout(() => { console.warn(`[自動播放] 獲取音檔 ${audioUrl} 元數據超時 (5秒),使用後備延遲。`); cleanupAudio(); resolve(FALLBACK_DELAY_MS); }, 5000); const cleanupAudio = () => { clearTimeout(timer); audio.removeEventListener('loadedmetadata', onLoadedMetadata); audio.removeEventListener('error', onError); audio.src = ''; }; const onLoadedMetadata = () => { if (audio.duration && isFinite(audio.duration)) { const durationMs = Math.ceil(audio.duration * 1000) + DELAY_BUFFER_MS; console.log(`[自動播放] 獲取到音檔時長: ${audio.duration.toFixed(2)}s, 使用延遲: ${durationMs}ms`); cleanupAudio(); resolve(durationMs); } else { console.warn(`[自動播放] 無法從元數據獲取有效時長 (${audio.duration}),使用後備延遲。`); cleanupAudio(); resolve(FALLBACK_DELAY_MS); } }; const onError = (e) => { console.error(`[自動播放] 加載音檔 ${audioUrl} 元數據時出錯:`, e); cleanupAudio(); resolve(FALLBACK_DELAY_MS); }; audio.addEventListener('loadedmetadata', onLoadedMetadata); audio.addEventListener('error', onError); try { audio.src = audioUrl; } catch (e) { console.error(`[自動播放] 設置音檔 src 時發生錯誤 (${audioUrl}):`, e); cleanupAudio(); resolve(FALLBACK_DELAY_MS); } }); } // 在 Iframe 內部添加樣式 function addStyleToIframe(iframeDoc, css) { // (程式碼與 v3.7 相同) try { const styleElement = iframeDoc.createElement('style'); styleElement.textContent = css; iframeDoc.head.appendChild(styleElement); console.log("[自動播放] 已在 iframe 中添加高亮樣式。"); } catch (e) { console.error("[自動播放] 無法在 iframe 中添加樣式:", e); } } // 背景遮罩點擊事件處理函數 function handleOverlayClick(event) { // (程式碼與 v3.7 相同) if (event.target !== overlayElement) { console.log("[自動播放][偵錯] 點擊事件目標不是遮罩本身,忽略。", event.target); return; } console.log(`[自動播放][偵錯] handleOverlayClick 觸發。isProcessing: ${isProcessing}, isPaused: ${isPaused}, currentIframe: ${currentIframe ? currentIframe.id : 'null'}`); if (isProcessing && !isPaused) { console.log("[自動播放] 點擊背景遮罩,觸發暫停並關閉 Modal。"); isPaused = true; pauseButton.textContent = '繼續'; updateStatusDisplay(); if (currentSleepController) { console.log("[自動播放][偵錯] 正在取消當前的 sleep..."); currentSleepController.cancel('paused_overlay'); } else { console.log("[自動播放][偵錯] 點擊遮罩時沒有正在進行的 sleep 可取消。"); } closeModal(); } else { console.log("[自動播放][偵錯] 點擊遮罩,但條件不滿足 (isProcessing 或 isPaused 狀態不對)。"); } } // 顯示 Modal (Iframe + Overlay) function showModal(iframe) { // (程式碼與 v3.7 相同) overlayElement = document.getElementById(OVERLAY_ID); if (!overlayElement) { overlayElement = document.createElement('div'); overlayElement.id = OVERLAY_ID; overlayElement.style.position = 'fixed'; overlayElement.style.top = '0'; overlayElement.style.left = '0'; overlayElement.style.width = '100vw'; overlayElement.style.height = '100vh'; overlayElement.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; overlayElement.style.zIndex = '9998'; overlayElement.style.cursor = 'pointer'; document.body.appendChild(overlayElement); console.log("[自動播放][偵錯] 已創建背景遮罩元素。"); } else { console.log("[自動播放][偵錯] 背景遮罩元素已存在。"); } overlayElement.removeEventListener('click', handleOverlayClick); console.log("[自動播放][偵錯] 已嘗試移除舊的遮罩點擊監聽器。"); overlayElement.addEventListener('click', handleOverlayClick); console.log("[自動播放][偵錯] 已添加新的遮罩點擊監聽器。"); iframe.style.position = 'fixed'; iframe.style.width = MODAL_WIDTH; iframe.style.height = MODAL_HEIGHT; iframe.style.top = '50%'; iframe.style.left = '50%'; iframe.style.transform = 'translate(-50%, -50%)'; iframe.style.border = '1px solid #ccc'; iframe.style.borderRadius = '8px'; iframe.style.boxShadow = '0 5px 20px rgba(0, 0, 0, 0.3)'; iframe.style.backgroundColor = 'white'; iframe.style.zIndex = '9999'; iframe.style.opacity = '1'; iframe.style.pointerEvents = 'auto'; document.body.appendChild(iframe); currentIframe = iframe; console.log(`[自動播放] 已顯示 Modal iframe, id: ${currentIframe.id}`); } // 增強關閉 Modal (Iframe + Overlay) 的健壯性 function closeModal() { // (程式碼與 v3.7 相同) console.log(`[自動播放][偵錯] closeModal 被調用。 currentIframe: ${currentIframe ? currentIframe.id : 'null'}, overlayElement: ${overlayElement ? 'exists' : 'null'}`); if (currentIframe && currentIframe.parentNode) { currentIframe.remove(); console.log("[自動播放] 已移除 iframe"); } else if (currentIframe) { console.log("[自動播放][偵錯] 嘗試移除 iframe 時,它已不在 DOM 中。"); } currentIframe = null; if (overlayElement) { overlayElement.removeEventListener('click', handleOverlayClick); if (overlayElement.parentNode) { overlayElement.remove(); console.log("[自動播放][偵錯] 已移除背景遮罩及其點擊監聽器。"); } else { console.log("[自動播放][偵錯] 嘗試移除遮罩時,它已不在 DOM 中。"); } overlayElement = null; } else { console.log("[自動播放][偵錯] 嘗試關閉 Modal 時,overlayElement 引用已為 null 或未找到元素。"); } if (currentSleepController) { console.log("[自動播放] 關閉 Modal 時取消正在進行的 sleep"); currentSleepController.cancel('modal_closed'); currentSleepController = null; } } // 處理單一連結的核心邏輯 async function processSingleLink(url, linkIndexInCurrentList) { // linkIndexInCurrentList 是相對於當前 linksToProcess 列表的索引 console.log(`[自動播放] processSingleLink 開始: 列表索引 ${linkIndexInCurrentList} (第 ${linkIndexInCurrentList + 1} / ${totalLinks} 項) - ${url}. isProcessing: ${isProcessing}, isPaused: ${isPaused}`); // ** 在處理新連結前,先關閉可能存在的舊 modal ** // ** 注意:如果是由按鈕暫停恢復,Modal 是故意留下的,不應該關閉 ** // ** 只有在開始處理一個 *新的* 連結時才需要關閉舊的 ** // ** 這個邏輯移到 processLinksSequentially 更合適 ** // closeModal(); // 從這裡移除 const iframeId = `auto-play-iframe-${Date.now()}`; const iframe = document.createElement('iframe'); iframe.id = iframeId; return new Promise(async (resolve) => { if (!isProcessing) { console.log("[自動播放][偵錯] processSingleLink 開始時 isProcessing 為 false,直接返回。"); resolve(); return; } // ** 只有在 currentIframe 不存在時才創建新的 Modal ** // ** 如果是從按鈕暫停恢復,currentIframe 應該還存在 ** if (!currentIframe) { console.log("[自動播放][偵錯] currentIframe 為 null,顯示新 Modal。"); showModal(iframe); // 創建並顯示 Modal,賦值 currentIframe } else { console.log("[自動播放][偵錯] currentIframe 已存在 (可能從按鈕暫停恢復),不重新顯示 Modal。"); // 需要確保 currentIframe 指向的是正確的 iframe if (currentIframe.contentWindow.location.href !== url) { console.warn("[自動播放][偵錯] currentIframe 存在,但 URL 不匹配!可能狀態混亂,強制關閉並重新打開。"); closeModal(); await sleep(50); // 短暫等待確保關閉完成 if (!isProcessing) { resolve(); return; } // 再次檢查狀態 showModal(iframe); // 重新打開 } else { // URL 匹配,繼續使用現有 iframe iframe = currentIframe; // 確保後續操作使用正確的 iframe 引用 } } iframe.onload = async () => { // (onload 邏輯與 v3.7 相同) console.log(`[自動播放] Iframe 載入完成: ${url}. isProcessing: ${isProcessing}, isPaused: ${isPaused}`); if (!isProcessing) { console.log("[自動播放] Iframe 載入時發現已停止,關閉 Modal"); closeModal(); resolve(); return; } if (currentIframe !== iframe) { console.warn(`[自動播放][偵錯] Iframe onload 觸發,但 currentIframe (${currentIframe ? currentIframe.id : 'null'}) 與當前 iframe (${iframe.id}) 不符!中止此 iframe 處理。`); resolve(); return; } let iframeDoc; try { await sleep(150); iframeDoc = iframe.contentWindow.document; addStyleToIframe(iframeDoc, HIGHLIGHT_STYLE); const audioButtons = iframeDoc.querySelectorAll('button.imtong-liua'); console.log(`[自動播放] 在 iframe 中找到 ${audioButtons.length} 個播放按鈕`); if (audioButtons.length > 0) { for (let i = 0; i < audioButtons.length; i++) { console.log(`[自動播放][偵錯] 進入音檔循環 ${i + 1}。 isProcessing: ${isProcessing}, isPaused: ${isPaused}`); if (!isProcessing) { console.log("[自動播放] 播放音檔前檢測到停止"); break; } while (isPaused && isProcessing) { console.log(`[自動播放] 音檔循環 ${i + 1} 偵測到暫停,等待繼續...`); updateStatusDisplay(); await sleep(500); if (!isProcessing) break; } if (!isProcessing) break; if (isPaused) { console.log(`[自動播放][偵錯] sleep(500) 後仍然是暫停狀態,繼續等待。`); i--; continue; } const button = audioButtons[i]; if (!button || !iframeDoc.body.contains(button)) { console.warn(`[自動播放] 按鈕 ${i + 1} 失效,跳過。`); continue; } console.log(`[自動播放] 準備播放 iframe 中的第 ${i + 1} 個音檔`); let actualDelayMs = FALLBACK_DELAY_MS; let audioSrc = null; let audioPath = null; const srcString = button.dataset.src; if (srcString) { try { const parsedData = JSON.parse(srcString.replace(/"/g, '"')); if (Array.isArray(parsedData) && parsedData.length > 0 && typeof parsedData[0] === 'string') { audioPath = parsedData[0]; } } catch (e) { if (typeof srcString === 'string' && srcString.trim().startsWith('/')) { audioPath = srcString.trim(); } } } if (audioPath) { try { const base = iframe.contentWindow.location.href; audioSrc = new URL(audioPath, base).href; } catch (urlError) { audioSrc = null; } } else { audioSrc = null; } actualDelayMs = await getAudioDuration(audioSrc); button.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(300); button.classList.add(HIGHLIGHT_CLASS); button.click(); console.log(`[自動播放] 已點擊按鈕 ${i + 1},等待 ${actualDelayMs}ms`); try { const sleepController = interruptibleSleep(actualDelayMs); await sleepController.promise; } catch (error) { if (error.isCancellation) { console.log(`[自動播放] 等待音檔 ${i + 1} 被 '${error.reason}' 中斷。`); if (iframeDoc.body.contains(button)) { button.classList.remove(HIGHLIGHT_CLASS); } break; } else { console.error("[自動播放] interruptibleSleep 發生意外錯誤:", error); } } finally { currentSleepController = null; } if (iframeDoc.body.contains(button) && button.classList.contains(HIGHLIGHT_CLASS)) { button.classList.remove(HIGHLIGHT_CLASS); } if (!isProcessing) break; if (i < audioButtons.length - 1) { console.log(`[自動播放] 播放下一個前等待 ${DELAY_BETWEEN_CLICKS_MS}ms`); try { const sleepController = interruptibleSleep(DELAY_BETWEEN_CLICKS_MS); await sleepController.promise; } catch (error) { if (error.isCancellation) { console.log(`[自動播放] 按鈕間等待被 '${error.reason}' 中斷。`); break; } else { throw error; } } finally { currentSleepController = null; } } if (!isProcessing) break; } } else { console.log(`[自動播放] Iframe ${url} 中未找到播放按鈕`); await sleep(1000); } } catch (error) { console.error(`[自動播放] 處理 iframe 內容時出錯 (${url}):`, error); } finally { console.log(`[自動播放][偵錯] processSingleLink finally 區塊。 isProcessing: ${isProcessing}, isPaused: ${isPaused}, currentIframe: ${currentIframe ? currentIframe.id : 'null'}`); // ** 只有在 isProcessing 為 false (停止) 或 isPaused 為 false (正常完成) 時才關閉 Modal ** if (!isProcessing || !isPaused) { console.log(`[自動播放] processSingleLink 結束,isProcessing: ${isProcessing}, isPaused: ${isPaused}。關閉 Modal`); closeModal(); } else { console.log("[自動播放] processSingleLink 結束,處於暫停狀態,保持 Modal 開啟"); if (currentSleepController) { console.warn("[自動播放][偵錯] processSingleLink 在暫停狀態結束,但 currentSleepController 仍存在,強制清除。"); currentSleepController.cancel('paused_exit'); currentSleepController = null; } } resolve(); } }; // --- iframe.onload end --- // ** 只有在需要新 iframe 時才設置 src ** if (currentIframe === iframe) { // 表示是新創建的 iframe iframe.onerror = (error) => { console.error(`[自動播放] Iframe 載入失敗 (${url}):`, error); closeModal(); resolve(); }; iframe.src = url; } else { // 如果是使用現有 iframe (從按鈕暫停恢復),不需要重新設置 src 或 onerror // onload 也不會再次觸發,所以需要手動觸發後續邏輯? // 不對,onload 應該只在第一次加載時觸發。 // 當從按鈕暫停恢復時,processSingleLink 應該直接進入 for 循環。 // 這意味著上面 showModal 後的 onload 綁定邏輯需要調整。 // ** 重新思考恢復邏輯 ** // 當按鈕暫停恢復時,processSingleLink 被再次調用,但 currentIframe 存在。 // 我們不需要執行 showModal 或綁定 onload。 // 我們需要直接進入 try...catch...finally 塊來處理 audioButtons。 // 這表示 processSingleLink 的結構需要改變。 // ** 方案 B:保持 processSingleLink 結構,但在恢復時強制重新加載 iframe ** // 這更簡單,雖然會從第一個音檔開始。 console.log("[自動播放][偵錯] 從按鈕暫停恢復,強制重新加載 iframe 以簡化流程。"); closeModal(); // 關閉舊的 await sleep(50); if (!isProcessing) { resolve(); return; } // 再次檢查 showModal(iframe); // 顯示新的 iframe.onerror = (error) => { console.error(`[自動播放] Iframe 載入失敗 (${url}):`, error); closeModal(); resolve(); }; iframe.src = url; // 設置 src } }); // --- Promise end --- } // 循序處理連結列表 - 加入滾動和高亮 async function processLinksSequentially() { console.log("[自動播放] processLinksSequentially 開始"); while (currentLinkIndex < totalLinks && isProcessing) { while (isPaused && isProcessing) { console.log(`[自動播放] 主流程已暫停 (索引 ${currentLinkIndex}),等待繼續...`); updateStatusDisplay(); await sleep(500); if (!isProcessing) break; } if (!isProcessing) break; updateStatusDisplay(); const linkInfo = linksToProcess[currentLinkIndex]; // 獲取當前連結信息 {url, anchorElement, tableRow} console.log(`[自動播放] 準備處理連結 ${currentLinkIndex + 1}/${totalLinks}`); // --- **表格滾動與高亮** --- if (linkInfo.tableRow) { console.log(`[自動播放][偵錯] 正在滾動到表格列 ${currentLinkIndex + 1}`); // 清除上一次的高亮 (如果有) if (rowHighlightTimeout) clearTimeout(rowHighlightTimeout); // 移除所有行的高亮樣式,以防萬一 document.querySelectorAll('.userscript-row-highlight').forEach(row => { row.classList.remove('userscript-row-highlight'); row.style.backgroundColor = ''; // 清除內聯樣式 row.style.transition = ''; }); linkInfo.tableRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 短暫延遲等待滾動基本完成 await sleep(300); // 添加高亮 linkInfo.tableRow.classList.add('userscript-row-highlight'); // 使用 class 控制樣式 linkInfo.tableRow.style.backgroundColor = ROW_HIGHLIGHT_COLOR; linkInfo.tableRow.style.transition = 'background-color 0.5s ease-out'; console.log(`[自動播放][偵錯] 已高亮表格列 ${currentLinkIndex + 1}`); // 設置延時移除高亮 const currentRow = linkInfo.tableRow; // 捕獲當前行引用 rowHighlightTimeout = setTimeout(() => { if (currentRow) { // 確保行仍然存在 currentRow.style.backgroundColor = ''; // 等待背景色過渡完成後移除 class setTimeout(() => { if (currentRow) currentRow.classList.remove('userscript-row-highlight'); }, 500); // 匹配過渡時間 } rowHighlightTimeout = null; }, ROW_HIGHLIGHT_DURATION); } // --- **滾動高亮結束** --- // 等待一小段時間再打開 Modal await sleep(200); if (!isProcessing || isPaused) continue; // 如果在等待時狀態改變 await processSingleLink(linkInfo.url, currentLinkIndex); // ** 注意:這裡的 currentLinkIndex 是列表索引 ** if (!isProcessing) break; if (!isPaused) { console.log(`[自動播放][偵錯] 連結 ${currentLinkIndex + 1} 處理完畢,非暫停狀態,索引增加`); currentLinkIndex++; } else { console.log(`[自動播放][偵錯] 連結 ${currentLinkIndex + 1} 處理完畢,但處於暫停狀態,索引保持不變`); } if (currentLinkIndex < totalLinks && isProcessing && !isPaused) { console.log(`[自動播放] 等待 ${DELAY_BETWEEN_IFRAMES_MS}ms 後處理下一個連結`); try { const sleepController = interruptibleSleep(DELAY_BETWEEN_IFRAMES_MS); await sleepController.promise; } catch (error) { if (error.isCancellation) { console.log(`[自動播放] 連結間等待被 '${error.reason}' 中斷。`); } else { throw error; } } finally { currentSleepController = null; } } if (!isProcessing) break; } // --- while loop end --- console.log(`[自動播放][偵錯] processLinksSequentially 循環結束。 isProcessing: ${isProcessing}, isPaused: ${isPaused}`); // 清除最後一次的高亮延時 if (rowHighlightTimeout) clearTimeout(rowHighlightTimeout); document.querySelectorAll('.userscript-row-highlight').forEach(row => { row.classList.remove('userscript-row-highlight'); row.style.backgroundColor = ''; row.style.transition = ''; }); if (!isProcessing) { console.log("[自動播放] 處理流程被停止。"); resetTriggerButton(); } else if (!isPaused) { console.log("[自動播放] 所有連結處理完畢。"); alert("所有連結攏處理完畢!"); resetTriggerButton(); } else { console.log("[自動播放] 流程結束於暫停狀態。"); /* 維持 UI */ } } // --- 控制按鈕事件處理 --- // **修改 startPlayback 以保存表格行引用** function startPlayback(startIndex = 0) { // 允許指定開始索引 console.log(`[自動播放] startPlayback 調用。 startIndex: ${startIndex}, isProcessing: ${isProcessing}, isPaused: ${isPaused}`); if (!isProcessing) { // ---- 首次開始或從停止後開始 ---- const resultTable = document.querySelector('table.table.d-none.d-md-table'); if (!resultTable) { alert("揣無結果表格!"); return; } const linkElements = resultTable.querySelectorAll('tbody tr td a[href^="/und-hani/su/"]'); if (linkElements.length === 0) { alert("表格內底揣無詞目連結!"); return; } // **創建包含元素引用的完整列表** const allLinks = Array.from(linkElements).map((a, index) => ({ url: new URL(a.getAttribute('href'), window.location.origin).href, anchorElement: a, tableRow: a.closest('tr'), originalIndex: index // 保存原始索引用於狀態顯示 })); if (startIndex >= allLinks.length) { console.error(`[自動播放] 指定的開始索引 ${startIndex} 超出範圍。`); return; } // **設置當前要處理的列表和索引** linksToProcess = allLinks.slice(startIndex); totalLinks = linksToProcess.length; // 當前列表的總數 currentLinkIndex = 0; // 相對於 linksToProcess 的索引 isProcessing = true; isPaused = false; console.log(`[自動播放] 開始新的播放流程,從全局索引 ${startIndex} 開始,共 ${totalLinks} 項。`); // 更新 UI startButton.style.display = 'none'; pauseButton.style.display = 'inline-block'; pauseButton.textContent = '暫停'; stopButton.style.display = 'inline-block'; statusDisplay.style.display = 'inline-block'; updateStatusDisplay(); // 更新狀態顯示 processLinksSequentially(); // 啟動主流程 } else if (isPaused) { // ---- 從暫停繼續 ---- isPaused = false; pauseButton.textContent = '暫停'; updateStatusDisplay(); console.log("[自動播放] 從暫停狀態繼續。"); // 簡化後的邏輯:不需要做任何事,讓等待循環解除阻塞 } else { console.warn("[自動播放][偵錯] 開始/繼續 按鈕被點擊,但 isProcessing 為 true 且 isPaused 為 false,不執行任何操作。"); } } // pausePlayback function pausePlayback() { // (程式碼與 v3.7 相同) console.log(`[自動播放] 暫停/繼續 按鈕點擊。 isProcessing: ${isProcessing}, isPaused: ${isPaused}`); if (isProcessing) { if (!isPaused) { isPaused = true; pauseButton.textContent = '繼續'; updateStatusDisplay(); console.log("[自動播放] 執行暫停 (保持 Modal 開啟)。"); if (currentSleepController) { currentSleepController.cancel('paused'); } } else { startPlayback(); } // 調用 startPlayback 處理繼續 } else { console.warn("[自動播放][偵錯] 暫停 按鈕被點擊,但 isProcessing 為 false,不執行任何操作。"); } } // stopPlayback function stopPlayback() { // (程式碼與 v3.7 相同) console.log(`[自動播放] 停止 按鈕點擊。 isProcessing: ${isProcessing}, isPaused: ${isPaused}`); if (!isProcessing && !isPaused) { console.log("[自動播放][偵錯] 停止按鈕點擊,但腳本已停止,不執行操作。"); return; } isProcessing = false; isPaused = false; if (currentSleepController) { currentSleepController.cancel('stopped'); } console.log(`[自動播放][偵錯][停止前] currentIframe: ${currentIframe ? currentIframe.id : 'null'}, overlayElement: ${overlayElement ? 'exists' : 'null'}`); closeModal(); resetTriggerButton(); updateStatusDisplay(); } // **修改 updateStatusDisplay 以顯示全局索引(可選)** function updateStatusDisplay() { if (statusDisplay) { if (isProcessing && linksToProcess.length > 0 && linksToProcess[currentLinkIndex]) { // 嘗試獲取全局索引 const globalIndex = linksToProcess[currentLinkIndex].originalIndex; const globalTotal = totalLinks + currentLinkIndex; // 估算全局總數 (可能有誤,如果不是從頭開始) // 顯示相對於當前列表的進度即可 const currentBatchProgress = `(${currentLinkIndex + 1}/${totalLinks})`; if (!isPaused) { statusDisplay.textContent = `處理中 ${currentBatchProgress}`; } else { statusDisplay.textContent = `已暫停 ${currentBatchProgress}`; } } else { statusDisplay.textContent = ''; // 不在處理中則清空 } } } // resetTriggerButton - 清除高亮 function resetTriggerButton() { console.log("[自動播放] 重置按鈕狀態。"); isProcessing = false; isPaused = false; currentLinkIndex = 0; totalLinks = 0; linksToProcess = []; if (startButton && pauseButton && stopButton && statusDisplay) { startButton.disabled = false; startButton.style.display = 'inline-block'; pauseButton.style.display = 'none'; pauseButton.textContent = '暫停'; stopButton.style.display = 'none'; statusDisplay.style.display = 'none'; statusDisplay.textContent = ''; } // 清除可能殘留的高亮 if (rowHighlightTimeout) clearTimeout(rowHighlightTimeout); document.querySelectorAll('.userscript-row-highlight').forEach(row => { row.classList.remove('userscript-row-highlight'); row.style.backgroundColor = ''; row.style.transition = ''; }); closeModal(); } // --- **新增**:表格列播放按鈕點擊處理 --- async function handleRowPlayButtonClick(event) { const button = event.currentTarget; const rowIndex = parseInt(button.dataset.rowIndex, 10); // 這是全局索引 console.log(`[自動播放] 表格列播放按鈕點擊,全局列索引: ${rowIndex}`); if (isNaN(rowIndex)) { console.error("[自動播放] 無法獲取有效的列索引。"); return; } if (isProcessing && !isPaused) { console.log("[自動播放] 目前正在播放中,請先停止或等待完成才能從指定列開始。"); alert("目前正在播放中,請先停止或等待完成才能從指定列開始。"); return; } // 如果是暫停狀態,先停止當前流程 if (isProcessing && isPaused) { console.log("[自動播放] 偵測到處於暫停狀態,先停止當前流程..."); stopPlayback(); // stopPlayback 會重置 isProcessing, isPaused 等狀態 await sleep(100); // 短暫等待確保狀態完全重置 } // 直接調用 startPlayback 並傳入起始索引 startPlayback(rowIndex); } // --- **新增**:確保 Font Awesome 加載 --- function ensureFontAwesome() { const faLinkId = 'userscript-fontawesome-css'; if (!document.getElementById(faLinkId)) { const link = document.createElement('link'); link.id = faLinkId; link.rel = 'stylesheet'; link.href = FONT_AWESOME_URL; link.integrity = FONT_AWESOME_INTEGRITY; link.crossOrigin = 'anonymous'; link.referrerPolicy = 'no-referrer'; document.head.appendChild(link); console.log('[自動播放] Font Awesome CSS 已注入。'); } } // --- **新增**:注入表格列播放按鈕 --- function injectRowPlayButtons() { const resultTable = document.querySelector('table.table.d-none.d-md-table'); if (!resultTable) return; const rows = resultTable.querySelectorAll('tbody tr'); const playButtonBaseStyle = ` background-color: #28a745; /* Green */ color: white; border: none; border-radius: 4px; padding: 2px 6px; /* Smaller padding */ margin-right: 8px; cursor: pointer; font-size: 12px; /* Smaller icon */ line-height: 1; vertical-align: middle; /* Align with text */ transition: background-color 0.2s ease; `; // 使用 class 來應用 hover 效果,避免 ID 衝突 const playButtonHoverStyle = `.userscript-row-play-button:hover { background-color: #218838 !important; }`; GM_addStyle(playButtonHoverStyle); // 添加 hover 樣式 rows.forEach((row, index) => { const firstTd = row.querySelector('td:first-child'); const numberSpan = firstTd ? firstTd.querySelector('span.fw-normal') : null; if (firstTd && numberSpan) { // 避免重複添加 if (firstTd.querySelector('.userscript-row-play-button')) return; const playButton = document.createElement('button'); playButton.className = 'userscript-row-play-button'; // 使用 class playButton.style.cssText = playButtonBaseStyle; playButton.innerHTML = '<i class="fas fa-play"></i>'; // Font Awesome icon playButton.dataset.rowIndex = index; // ** 儲存的是全局索引 ** playButton.title = `從此列開始播放 (第 ${index + 1} 項)`; playButton.addEventListener('click', handleRowPlayButtonClick); firstTd.appendChild(playButton); } }); console.log(`[自動播放] 已注入 ${rows.length} 個表格列播放按鈕。`); } // --- 添加觸發按鈕 --- function addTriggerButton() { // (程式碼與 v3.7 相同) if (document.getElementById('auto-play-controls-container')) return; const buttonContainer = document.createElement('div'); buttonContainer.id = 'auto-play-controls-container'; buttonContainer.style.position = 'fixed'; buttonContainer.style.top = '10px'; buttonContainer.style.left = '10px'; buttonContainer.style.zIndex = '10001'; buttonContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'; buttonContainer.style.padding = '5px 10px'; buttonContainer.style.borderRadius = '5px'; buttonContainer.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; const buttonStyle = `padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin-right: 5px; transition: background-color 0.2s ease;`; startButton = document.createElement('button'); startButton.id = 'auto-play-start-button'; startButton.textContent = '開始播放全部'; startButton.style.cssText = buttonStyle; startButton.style.backgroundColor = '#28a745'; startButton.style.color = 'white'; startButton.addEventListener('click', () => startPlayback(0)); buttonContainer.appendChild(startButton); // 默認從 0 開始 pauseButton = document.createElement('button'); pauseButton.id = 'auto-play-pause-button'; pauseButton.textContent = '暫停'; pauseButton.style.cssText = buttonStyle; pauseButton.style.backgroundColor = '#ffc107'; pauseButton.style.color = 'black'; pauseButton.style.display = 'none'; pauseButton.addEventListener('click', pausePlayback); buttonContainer.appendChild(pauseButton); stopButton = document.createElement('button'); stopButton.id = 'auto-play-stop-button'; stopButton.textContent = '停止'; stopButton.style.cssText = buttonStyle; stopButton.style.backgroundColor = '#dc3545'; stopButton.style.color = 'white'; stopButton.style.display = 'none'; stopButton.addEventListener('click', stopPlayback); buttonContainer.appendChild(stopButton); statusDisplay = document.createElement('span'); statusDisplay.id = 'auto-play-status'; statusDisplay.style.display = 'none'; statusDisplay.style.marginLeft = '10px'; statusDisplay.style.fontSize = '14px'; statusDisplay.style.verticalAlign = 'middle'; buttonContainer.appendChild(statusDisplay); document.body.appendChild(buttonContainer); GM_addStyle(`#auto-play-controls-container button:disabled { opacity: 0.65; cursor: not-allowed; } #auto-play-start-button:hover:not(:disabled) { background-color: #218838 !important; } #auto-play-pause-button:hover:not(:disabled) { background-color: #e0a800 !important; } #auto-play-stop-button:hover:not(:disabled) { background-color: #c82333 !important; }`); } // --- 初始化 --- function initialize() { if (window.autoPlayerInitialized) return; window.autoPlayerInitialized = true; console.log("[自動播放] 初始化腳本 v4.0 ..."); ensureFontAwesome(); // **確保 FA 已加載** addTriggerButton(); // **延遲一點執行按鈕注入,確保表格已完全渲染** setTimeout(injectRowPlayButtons, 500); } if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initialize, 0); } else { document.addEventListener('DOMContentLoaded', initialize); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址