您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增強型 Exhentai 記錄腳本,優化加載進度和閱讀體驗,支持後台加載
// ==UserScript== // @name Enhanced Exhentai Record (Optimized) // @namespace http://tampermonkey.net/ // @version 4.1 // @description 增強型 Exhentai 記錄腳本,優化加載進度和閱讀體驗,支持後台加載 // @author You // @match https://exhentai.org/watched* // @icon https://www.google.com/s2/favicons?domain=exhentai.org // @grant none // ==/UserScript== (function() { 'use strict'; // 配置選項 const CONFIG = { autoHideRecorded: true, // 自動隱藏已記錄項目 loadDelay: 800, // 加載下一頁的延遲(毫秒) toastDuration: 3000, // Toast 顯示時間 storageKey: 'exhentai_record',// 本地存儲鍵名 continueInBackground: true // 切換頁面時繼續加載 }; // DOM 元素引用 let DOM = { progressBar: null, progressText: null, readingProgressBar: null, statusArea: null, totalCountElem: null, pageRecordedElem: null, pageUnrecordedElem: null, pageHiddenElem: null }; // 統計數據 const STATS = { totalProcessed: 0, totalAdded: 0, totalFiltered: 0, currentPage: 1, estimatedTotalPages: 0, readingProgress: 0 }; // 加載狀態 const LOADING_STATE = { userPaused: false, // 用戶手動暫停 backgroundPaused: false, // 因切換到後台而暫停 processing: false // 正在處理 }; // SVG 圖標定義 const ICONS = { record: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>', toggle: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>', download: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>', upload: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>', loadAll: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path></svg>', info: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>', data: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>', check: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>', uncheck: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg>', hidden: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>', stop: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>', pause: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>', play: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>' }; // 樣式定義 const STYLES = ` /* 主控制面板 */ .ex-record-toolbar { position: sticky; top: 0; margin: 0 auto; padding: 15px; background-color: #333; border-radius: 5px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; box-shadow: 0 3px 10px rgba(0,0,0,0.3); z-index: 1000; margin-bottom: 15px; border: 1px solid #444; max-width: 95%; } /* 按鈕樣式 */ .ex-record-btn { display: inline-flex; align-items: center; justify-content: center; margin: 5px; padding: 8px 15px; background-color: #444; color: #eee; border-radius: 4px; cursor: pointer; transition: all 0.3s; border: none; font-weight: bold; font-size: 14px; min-width: 120px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .ex-record-btn svg { margin-right: 8px; } .ex-record-btn:hover { background-color: #555; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); } .ex-record-btn:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(0,0,0,0.2); } /* 不同類型的按鈕顏色 */ .ex-record-add { background-color: #1a73e8; } .ex-record-add:hover { background-color: #1967d2; } .ex-record-toggle { background-color: #34a853; } .ex-record-toggle:hover { background-color: #2d9247; } .ex-record-export { background-color: #ea4335; } .ex-record-export:hover { background-color: #d33426; } .ex-record-import { background-color: #fbbc05; color: #333; } .ex-record-import:hover { background-color: #f0b400; } .ex-record-stop { background-color: #ea4335; } .ex-record-stop:hover { background-color: #d33426; } /* 信息顯示 */ .ex-record-info { display: inline-flex; align-items: center; padding: 8px 12px; margin: 5px; border-radius: 4px; background-color: #444; color: #eee; font-weight: bold; border: 1px solid #555; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } .ex-record-info svg { margin-right: 8px; color: #aaa; } /* 標記已記錄項目 */ .ex-record-highlighted { background-color: rgba(26, 115, 232, 0.15) !important; border-left: 4px solid #1a73e8 !important; } /* 記錄時間顯示 */ .ex-record-time { font-size: 12px; color: #aaa; margin-left: 8px; display: inline-block; padding: 3px 6px; background-color: rgba(0, 0, 0, 0.2); border-radius: 3px; } /* Toast 消息 */ .ex-record-toast { position: fixed; top: 20px; right: 20px; padding: 12px 20px; background-color: rgba(50, 50, 50, 0.9); color: #fff; border-radius: 4px; z-index: 10000; animation: ex-record-fadeInOut 3s ease-in-out forwards; box-shadow: 0 4px 10px rgba(0,0,0,0.3); border-left: 4px solid #1a73e8; max-width: 300px; } @keyframes ex-record-fadeInOut { 0% { opacity: 0; transform: translateY(-20px); } 10% { opacity: 1; transform: translateY(0); } 80% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-20px); } } /* 模態對話框 */ .ex-record-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10001; } .ex-record-modal-content { background-color: #333; padding: 20px; border-radius: 8px; width: 80%; max-width: 600px; box-shadow: 0 5px 15px rgba(0,0,0,0.5); color: #eee; border: 1px solid #444; } .ex-record-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #444; padding-bottom: 10px; } .ex-record-modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #aaa; } .ex-record-modal-close:hover { color: #fff; } .ex-record-modal-body { margin-bottom: 15px; } .ex-record-modal textarea { width: 100%; height: 200px; background-color: #222; color: #eee; border: 1px solid #444; padding: 10px; border-radius: 4px; resize: vertical; font-family: monospace; } .ex-record-modal-footer { display: flex; justify-content: flex-end; gap: 10px; } .ex-record-modal-btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; min-width: 80px; } .ex-record-modal-btn-primary { background-color: #1a73e8; color: white; } .ex-record-modal-btn-primary:hover { background-color: #1967d2; } .ex-record-modal-btn-secondary { background-color: #444; color: #eee; } .ex-record-modal-btn-secondary:hover { background-color: #555; } /* 控制面板內的區域 */ .ex-record-toolbar { flex-direction: column; padding: 12px 15px; } .ex-record-controls-row { display: flex; width: 100%; justify-content: space-between; align-items: center; margin-bottom: 8px; } .ex-record-controls-row:last-child { margin-bottom: 0; } .ex-record-controls-left, .ex-record-controls-center, .ex-record-controls-right { display: flex; align-items: center; flex-wrap: wrap; } .ex-record-controls-stats { flex: 1; display: flex; flex-wrap: wrap; justify-content: flex-start; } .ex-record-controls-center { flex-grow: 1; justify-content: center; margin: 0 10px; } .ex-record-controls-buttons { flex: 1; display: flex; justify-content: center; } .ex-record-controls-data { display: flex; justify-content: flex-end; } /* 進度條樣式 */ .ex-record-progress-container { position: fixed; bottom: 20px; right: 20px; width: 300px; background-color: #333; border-radius: 5px; padding: 12px; box-shadow: 0 3px 10px rgba(0,0,0,0.3); border: 1px solid #444; z-index: 1000; transition: opacity 0.3s ease; } .ex-record-progress-container.hidden { opacity: 0; pointer-events: none; } .ex-record-progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .ex-record-progress-title { font-weight: bold; color: #eee; } .ex-record-progress-controls { display: flex; gap: 5px; } .ex-record-progress-btn { background: none; border: none; color: #aaa; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; transition: all 0.2s; } .ex-record-progress-btn:hover { background-color: #444; color: #fff; } .ex-record-progress { width: 100%; height: 6px; background-color: #444; border-radius: 3px; margin: 5px 0; overflow: hidden; } .ex-record-progress-bar { height: 100%; background-color: #1a73e8; width: 0%; transition: width 0.3s ease; } .ex-record-reading-progress { width: 100%; height: 6px; background-color: #444; border-radius: 3px; margin: 8px 0 5px 0; overflow: hidden; } .ex-record-reading-progress-bar { height: 100%; background-color: #34a853; width: 0%; transition: width 0.3s ease; } .ex-record-progress-stats { display: flex; justify-content: space-between; color: #aaa; font-size: 12px; margin-top: 5px; } .ex-record-progress-text { color: #eee; font-size: 13px; margin: 8px 0; } /* 數據管理下拉選單 */ .ex-record-controls-right { position: relative; } .ex-record-data-buttons { position: absolute; right: 0; top: 100%; background-color: #333; border-radius: 4px; padding: 5px; display: none; flex-direction: column; z-index: 2000; box-shadow: 0 3px 8px rgba(0,0,0,0.3); border: 1px solid #444; min-width: 120px; } .ex-record-controls-right:hover .ex-record-data-buttons { display: flex; } .ex-record-data-toggle { display: flex; align-items: center; justify-content: center; background-color: #444; color: #eee; padding: 8px 15px; border-radius: 4px; cursor: pointer; transition: all 0.3s; font-weight: bold; font-size: 14px; border: none; } .ex-record-data-toggle svg { margin-right: 8px; } .ex-record-data-toggle:hover { background-color: #555; } `; // 工具函數 const Utils = { // 從 localStorage 獲取記錄 getRecords() { try { const recordStr = localStorage.getItem(CONFIG.storageKey); return recordStr ? JSON.parse(recordStr) : {}; } catch (e) { console.error('解析記錄失敗:', e); return {}; } }, // 保存記錄到 localStorage saveRecords(records) { try { localStorage.setItem(CONFIG.storageKey, JSON.stringify(records)); return true; } catch (e) { console.error('保存記錄失敗:', e); UI.showToast('保存記錄失敗: ' + e.message); return false; } }, // 格式化時間 formatDate(dateString) { try { const date = new Date(dateString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } catch (e) { return '未知時間'; } }, // 獲取表格主體 getTableBody() { const table = document.querySelector('.itg.glte'); return table && table.tBodies.length > 0 ? table.tBodies[0] : null; }, // 獲取頁面中的所有項目 ID getPageItems() { const tableBody = this.getTableBody(); if (!tableBody) return []; return Array.from(tableBody.rows) .map(row => { const link = row.querySelector('a'); if (!link) return null; const url = link.href.split("/").filter(i => i !== ''); return url[url.length - 1] + url[url.length - 2]; }) .filter(id => id !== null); }, // 從URL獲取項目ID getIdFromUrl(url) { const parts = url.split("/").filter(i => i !== ''); return parts[parts.length - 1] + parts[parts.length - 2]; }, // 估算總頁數 estimateTotalPages() { // 嘗試從分頁器中獲取頁數 const pager = document.querySelector('.ptt'); if (pager) { const lastPageLink = Array.from(pager.querySelectorAll('a')).pop(); if (lastPageLink && lastPageLink.textContent) { const pageNum = parseInt(lastPageLink.textContent); if (!isNaN(pageNum)) { return pageNum; } } } // 如果無法從頁面獲取,返回預設值 return 10; }, // 獲取當前頁碼 getCurrentPage() { const pager = document.querySelector('.ptt'); if (pager) { const currentPageElement = pager.querySelector('td.ptds'); if (currentPageElement && currentPageElement.textContent) { const pageNum = parseInt(currentPageElement.textContent); if (!isNaN(pageNum)) { return pageNum; } } } return 1; }, // 動態調整閱讀進度 updateReadingProgress() { // 計算閱讀進度百分比 const tableBody = this.getTableBody(); if (!tableBody) return 0; const totalItems = tableBody.rows.length; if (totalItems === 0) return 0; // 通過檢測可見區域來判斷閱讀進度 const viewportHeight = window.innerHeight; const viewportTop = window.scrollY; const viewportBottom = viewportTop + viewportHeight; let visibleCount = 0; Array.from(tableBody.rows).forEach(row => { const rect = row.getBoundingClientRect(); const rowTop = rect.top + viewportTop; const rowBottom = rect.bottom + viewportTop; // 行完全可見或部分可見 if ((rowTop >= viewportTop && rowTop <= viewportBottom) || (rowBottom >= viewportTop && rowBottom <= viewportBottom) || (rowTop <= viewportTop && rowBottom >= viewportBottom)) { visibleCount++; } // 已經滾動過的行 else if (rowBottom < viewportTop) { visibleCount++; } }); const progress = Math.min(100, Math.round((visibleCount / totalItems) * 100)); if (DOM.readingProgressBar) { DOM.readingProgressBar.style.width = `${progress}%`; } return progress; }, // 延時執行函數 debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }, // 可靠的延時函數,即使在後台也能工作 reliableDelay(ms) { return new Promise(resolve => { const startTime = Date.now(); const checkTime = () => { const elapsedTime = Date.now() - startTime; if (elapsedTime >= ms) { resolve(); } else { setTimeout(checkTime, Math.min(100, ms - elapsedTime)); } }; setTimeout(checkTime, Math.min(100, ms)); }); }, // 記錄到控制台 log(message) { console.log(`[ExRecord] ${message}`); } }; // UI 操作相關 const UI = { // 顯示 Toast 消息 showToast(message, duration = CONFIG.toastDuration) { const toast = document.createElement('div'); toast.className = 'ex-record-toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { if (toast.parentNode) { document.body.removeChild(toast); } }, duration); }, // 創建進度顯示容器 createProgressContainer() { const container = document.createElement('div'); container.className = 'ex-record-progress-container'; container.id = 'ex-record-progress-container'; container.innerHTML = ` <div class="ex-record-progress-header"> <div class="ex-record-progress-title">加載進度</div> <div class="ex-record-progress-controls"> <button class="ex-record-progress-btn" id="ex-record-pause-btn" title="暫停/繼續加載"> ${ICONS.pause} </button> <button class="ex-record-progress-btn" id="ex-record-stop-btn" title="停止加載"> ${ICONS.stop} </button> </div> </div> <div class="ex-record-progress-text" id="ex-record-progress-text">準備加載...</div> <div class="ex-record-progress"> <div class="ex-record-progress-bar" id="ex-record-progress-bar"></div> </div> <div class="ex-record-progress-stats"> <span id="ex-record-progress-page">頁面: 0/0</span> <span id="ex-record-progress-items">已加載: 0</span> </div> <div class="ex-record-progress-text">閱讀進度</div> <div class="ex-record-reading-progress"> <div class="ex-record-reading-progress-bar" id="ex-record-reading-progress-bar"></div> </div> <div class="ex-record-progress-stats"> <span id="ex-record-reading-percent">0%</span> <span id="ex-record-new-items">新項目: 0</span> </div> `; document.body.appendChild(container); // 獲取DOM引用 DOM.progressBar = document.getElementById('ex-record-progress-bar'); DOM.progressText = document.getElementById('ex-record-progress-text'); DOM.readingProgressBar = document.getElementById('ex-record-reading-progress-bar'); // 設置暫停/停止按鈕事件 document.getElementById('ex-record-pause-btn').addEventListener('click', () => { this.toggleLoadingPause(); }); document.getElementById('ex-record-stop-btn').addEventListener('click', () => { this.stopLoading(); }); return container; }, // 更新暫停按鈕圖標 updatePauseButtonIcon(isPaused) { const pauseBtn = document.getElementById('ex-record-pause-btn'); if (pauseBtn) { pauseBtn.innerHTML = isPaused ? ICONS.play : ICONS.pause; pauseBtn.title = isPaused ? "繼續加載" : "暫停加載"; } }, // 更新加載進度 updateProgress(percent, currentPage, totalPages, loadedItems) { if (DOM.progressBar) { DOM.progressBar.style.width = `${percent}%`; } // 更新頁面計數 const pageCountElement = document.getElementById('ex-record-progress-page'); if (pageCountElement) { pageCountElement.textContent = `頁面: ${currentPage}/${totalPages || '?'}`; } // 更新已加載項目數 const itemsCountElement = document.getElementById('ex-record-progress-items'); if (itemsCountElement) { itemsCountElement.textContent = `已加載: ${loadedItems}`; } // 更新新項目數 const newItemsElement = document.getElementById('ex-record-new-items'); if (newItemsElement) { newItemsElement.textContent = `新項目: ${STATS.totalAdded}`; } // 更新閱讀百分比 const readingPercentElement = document.getElementById('ex-record-reading-percent'); if (readingPercentElement) { const readingPercent = Utils.updateReadingProgress(); STATS.readingProgress = readingPercent; readingPercentElement.textContent = `${readingPercent}%`; } }, // 更新加載狀態文本 updateProgressText(text) { if (DOM.progressText) { DOM.progressText.textContent = text; } }, // 顯示/隱藏進度容器 toggleProgressContainer(show = true) { const container = document.getElementById('ex-record-progress-container'); if (container) { container.className = show ? 'ex-record-progress-container' : 'ex-record-progress-container hidden'; } }, // 暫停/繼續加載 toggleLoadingPause() { const loader = PageLoader; if (LOADING_STATE.userPaused) { // 如果是用戶暫停,則恢復 LOADING_STATE.userPaused = false; this.updatePauseButtonIcon(false); if (!LOADING_STATE.backgroundPaused) { // 如果不是因為背景暫停,則恢復加載 loader.processNextItem(); this.updateProgressText('繼續加載中...'); this.showToast('繼續加載'); } else { this.updateProgressText('頁面處於後台,將在返回前台時繼續加載'); this.showToast('已設置為繼續加載,將在返回前台時恢復'); } } else { // 暫停加載 LOADING_STATE.userPaused = true; this.updatePauseButtonIcon(true); this.updateProgressText('加載已暫停(用戶手動)'); this.showToast('加載已暫停'); } }, // 停止加載 stopLoading() { PageLoader.stopLoading(); LOADING_STATE.userPaused = false; LOADING_STATE.backgroundPaused = false; this.updatePauseButtonIcon(false); this.updateProgressText('加載已停止'); this.showToast('加載已停止'); // 3秒後隱藏進度條 setTimeout(() => { this.toggleProgressContainer(false); }, 3000); }, // 創建控制面板 createControlPanel() { const controlPanel = document.createElement('div'); controlPanel.className = 'ex-record-toolbar'; // 構建控制面板HTML - 分為上下兩行 controlPanel.innerHTML = ` <!-- 第一行:數據統計 --> <div class="ex-record-controls-row"> <div class="ex-record-controls-stats"> <div class="ex-record-info" id="ex-record-total-count"> ${ICONS.info}總記錄: 0 筆 </div> <div class="ex-record-info" id="ex-record-page-recorded"> ${ICONS.check}本頁已記錄: 0 筆 </div> <div class="ex-record-info" id="ex-record-page-unrecorded"> ${ICONS.uncheck}本頁未記錄: 0 筆 </div> <div class="ex-record-info" id="ex-record-page-hidden"> ${ICONS.hidden}本頁隱藏: 0 筆 </div> </div> </div> <!-- 第二行:操作按鈕 --> <div class="ex-record-controls-row"> <!-- 中間按鈕區域 --> <div class="ex-record-controls-buttons"> <button class="ex-record-btn ex-record-add" id="ex-record-add-btn"> ${ICONS.record}記錄此頁 </button> <button class="ex-record-btn ex-record-toggle" id="ex-record-toggle-btn"> ${ICONS.toggle}隱藏/顯示 </button> <button class="ex-record-btn ex-record-add" id="ex-record-load-all-btn"> ${ICONS.loadAll}加載所有頁面 </button> </div> <!-- 右側數據管理按鈕 --> <div class="ex-record-controls-data"> <div class="ex-record-controls-right"> <button class="ex-record-data-toggle" id="ex-record-data-toggle"> ${ICONS.data}數據管理 </button> <div class="ex-record-data-buttons"> <button class="ex-record-btn ex-record-export" id="ex-record-export-btn"> ${ICONS.download}匯出記錄 </button> <button class="ex-record-btn ex-record-import" id="ex-record-import-btn"> ${ICONS.upload}匯入記錄 </button> </div> </div> </div> </div> `; // 插入到頁面中 const target = document.querySelector('.searchnav'); if (target && target.parentNode) { target.parentNode.insertBefore(controlPanel, target); } else { const searchtext = document.querySelector('.searchtext'); if (searchtext && searchtext.parentNode) { searchtext.parentNode.insertBefore(controlPanel, searchtext.nextSibling); } else { document.body.insertBefore(controlPanel, document.body.firstChild); } } // 保存DOM引用 DOM.totalCountElem = document.getElementById('ex-record-total-count'); DOM.pageRecordedElem = document.getElementById('ex-record-page-recorded'); DOM.pageUnrecordedElem = document.getElementById('ex-record-page-unrecorded'); DOM.pageHiddenElem = document.getElementById('ex-record-page-hidden'); // 綁定按鈕事件 document.getElementById('ex-record-add-btn').addEventListener('click', () => Record.recordCurrentPage()); document.getElementById('ex-record-toggle-btn').addEventListener('click', () => Record.toggleRecordedItems()); document.getElementById('ex-record-load-all-btn').addEventListener('click', () => PageLoader.loadAllPages()); document.getElementById('ex-record-export-btn').addEventListener('click', () => DataManager.exportRecords()); document.getElementById('ex-record-import-btn').addEventListener('click', () => DataManager.importRecords()); return controlPanel; }, // 更新統計信息顯示 updateStatsDisplay() { const records = Utils.getRecords(); const recordsCount = Object.keys(records).length; // 更新記錄總數 if (DOM.totalCountElem) { DOM.totalCountElem.innerHTML = `${ICONS.info}總記錄: ${recordsCount} 筆`; } // 計算並更新當前頁面統計 const pageItems = Utils.getPageItems(); const pageRecorded = pageItems.filter(id => records[id]).length; const pageUnrecorded = pageItems.length - pageRecorded; if (DOM.pageRecordedElem) { DOM.pageRecordedElem.innerHTML = `${ICONS.check}本頁已記錄: ${pageRecorded} 筆`; } if (DOM.pageUnrecordedElem) { DOM.pageUnrecordedElem.innerHTML = `${ICONS.uncheck}本頁未記錄: ${pageUnrecorded} 筆`; } // 統計隱藏數量 let hiddenCount = 0; const tableBody = Utils.getTableBody(); if (tableBody) { Array.from(tableBody.rows).forEach(row => { if (row.style.display === "none") { hiddenCount++; } }); } if (DOM.pageHiddenElem) { DOM.pageHiddenElem.innerHTML = `${ICONS.hidden}本頁隱藏: ${hiddenCount} 筆`; } }, // 添加樣式到頁面 addStyles() { const styleElement = document.createElement('style'); styleElement.textContent = STYLES; document.head.appendChild(styleElement); }, // 創建模態對話框 createModal(title, content, buttons) { const modal = document.createElement('div'); modal.className = 'ex-record-modal'; modal.innerHTML = ` <div class="ex-record-modal-content"> <div class="ex-record-modal-header"> <h3>${title}</h3> <button class="ex-record-modal-close">×</button> </div> <div class="ex-record-modal-body"> ${content} </div> <div class="ex-record-modal-footer"> ${buttons.map(btn => ` <button class="ex-record-modal-btn ${btn.primary ? 'ex-record-modal-btn-primary' : 'ex-record-modal-btn-secondary'}" id="${btn.id}">${btn.text}</button> `).join('')} </div> </div> `; document.body.appendChild(modal); // 綁定關閉按鈕 const closeBtn = modal.querySelector('.ex-record-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', () => document.body.removeChild(modal)); } // 返回modal以供後續處理 return modal; } }; // 記錄操作相關 const Record = { // 標記已記錄的項目 highlightRecorded() { const tableBody = Utils.getTableBody(); if (!tableBody) return; const records = Utils.getRecords(); Array.from(tableBody.rows).forEach(row => { const link = row.querySelector('a'); if (!link) return; const url = link.href.split("/").filter(i => i !== ''); const id = url[url.length - 1] + url[url.length - 2]; if (records[id]) { row.classList.add('ex-record-highlighted'); // 添加記錄時間 const titleElement = row.querySelector('.gl4e'); if (titleElement && !titleElement.querySelector('.ex-record-time')) { const timeSpan = document.createElement('span'); timeSpan.className = 'ex-record-time'; // 兼容新舊記錄格式 const timestamp = records[id].timestamp || records[id].t || ''; timeSpan.textContent = timestamp ? `記錄於: ${Utils.formatDate(timestamp)}` : '已記錄'; titleElement.appendChild(timeSpan); } } else { row.classList.remove('ex-record-highlighted'); // 移除記錄時間 const timeSpan = row.querySelector('.ex-record-time'); if (timeSpan && timeSpan.parentNode) { timeSpan.parentNode.removeChild(timeSpan); } } }); }, // 切換顯示/隱藏已記錄的項目 toggleRecordedItems() { const tableBody = Utils.getTableBody(); if (!tableBody) return; const records = Utils.getRecords(); let hiddenCount = 0; let shownCount = 0; Array.from(tableBody.rows).forEach(row => { const link = row.querySelector('a'); if (!link) return; const url = link.href.split("/").filter(i => i !== ''); const id = url[url.length - 1] + url[url.length - 2]; if (records[id]) { if (row.style.display === "none") { row.style.display = "table-row"; shownCount++; } else { row.style.display = "none"; hiddenCount++; } } }); if (hiddenCount > 0) { UI.showToast(`已隱藏 ${hiddenCount} 筆已記錄的內容`); } else if (shownCount > 0) { UI.showToast(`已顯示 ${shownCount} 筆已記錄的內容`); } else { UI.showToast('本頁沒有已記錄的內容'); } UI.updateStatsDisplay(); }, // 隱藏已記錄的項目 hideRecordedItems() { const tableBody = Utils.getTableBody(); if (!tableBody) return 0; const records = Utils.getRecords(); let hiddenCount = 0; Array.from(tableBody.rows).forEach(row => { const link = row.querySelector('a'); if (!link) return; const url = link.href.split("/").filter(i => i !== ''); const id = url[url.length - 1] + url[url.length - 2]; if (records[id]) { row.style.display = "none"; hiddenCount++; } }); UI.updateStatsDisplay(); return hiddenCount; }, // 記錄當前頁面的所有項目 recordCurrentPage() { const tableBody = Utils.getTableBody(); if (!tableBody) return; const records = Utils.getRecords(); const now = new Date().toISOString(); let newCount = 0; Array.from(tableBody.rows).forEach(row => { if (row.style.display === "none") return; // 跳過已隱藏的行 const link = row.querySelector('a'); if (!link) return; const url = link.href.split("/").filter(i => i !== ''); const id = url[url.length - 1] + url[url.length - 2]; if (!records[id]) { // 使用簡化的數據結構以節省空間 records[id] = { t: now }; newCount++; } }); if (newCount > 0) { if (Utils.saveRecords(records)) { this.highlightRecorded(); UI.updateStatsDisplay(); UI.showToast(`已記錄 ${newCount} 筆新內容`); } else { UI.showToast('記錄失敗:可能超出存儲限制'); } } else { UI.showToast('沒有新內容可記錄'); } } }; // 頁面加載器 const PageLoader = { loadQueue: [], // 加載隊列 isLoading: false, // 是否正在加載 isStopped: false, // 是否已停止 // 初始化加載器 init() { STATS.currentPage = Utils.getCurrentPage(); STATS.estimatedTotalPages = Utils.estimateTotalPages(); // 設置頁面可見性變化監聽 this.setupVisibilityHandler(); }, // 監聽頁面可見性變化 setupVisibilityHandler() { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { // 頁面進入後台 Utils.log('頁面進入後台'); if (!CONFIG.continueInBackground && !LOADING_STATE.userPaused && this.isLoading) { // 如果不允許在後台加載且沒有用戶手動暫停,則暫停加載 LOADING_STATE.backgroundPaused = true; UI.updateProgressText('頁面處於後台,加載已暫停'); Utils.log('自動暫停加載'); } } else if (document.visibilityState === 'visible') { // 頁面回到前台 Utils.log('頁面回到前台'); if (LOADING_STATE.backgroundPaused && !LOADING_STATE.userPaused) { // 如果因為後台而暫停且沒有用戶手動暫停,則恢復加載 LOADING_STATE.backgroundPaused = false; UI.updateProgressText('頁面回到前台,繼續加載...'); Utils.log('自動恢復加載'); this.processNextItem(); } } }); }, // 加載所有頁面 loadAllPages() { if (this.isLoading) { UI.showToast('正在加載中,請等待...'); return; } // 初始化進度顯示 UI.toggleProgressContainer(true); UI.updateProgressText('準備加載所有頁面...'); UI.updatePauseButtonIcon(false); this.isLoading = true; this.isStopped = false; this.loadQueue = []; // 重設加載狀態 LOADING_STATE.userPaused = false; LOADING_STATE.backgroundPaused = false; LOADING_STATE.processing = false; // 重設統計 STATS.totalProcessed = 0; STATS.totalAdded = 0; STATS.totalFiltered = 0; // 查找下一頁鏈接 const nextPageLink = document.querySelector('#unext'); if (!nextPageLink || nextPageLink.href === "javascript:void(0)") { UI.updateProgressText('已經是最後一頁'); UI.showToast('已經是最後一頁'); this.isLoading = false; // 3秒後隱藏進度條 setTimeout(() => { UI.toggleProgressContainer(false); }, 3000); return; } // 添加第一個頁面到隊列 this.addPageToQueue(nextPageLink.href, true); // 開始處理隊列 this.processNextItem(); }, // 添加頁面到隊列 addPageToQueue(pageUrl, recursive = false) { this.loadQueue.push({ type: 'page', url: pageUrl, recursive: recursive }); Utils.log(`頁面已添加到隊列: ${pageUrl}`); }, // 添加行項目到隊列 addRowsToQueue(params) { this.loadQueue.push({ type: 'rows', ...params }); Utils.log(`${params.rows.length} 行已添加到隊列`); }, // 處理隊列中的下一個項目 async processNextItem() { // 如果已停止或沒有正在加載,則退出 if (this.isStopped || !this.isLoading) { return; } // 如果用戶暫停或後台暫停,則退出 if (LOADING_STATE.userPaused || (LOADING_STATE.backgroundPaused && !CONFIG.continueInBackground)) { return; } // 如果正在處理項目,則退出 if (LOADING_STATE.processing) { return; } // 如果隊列為空,則完成加載 if (this.loadQueue.length === 0) { this.completeLoading(); return; } // 獲取隊列中的下一個項目 const nextItem = this.loadQueue.shift(); // 設置處理標記 LOADING_STATE.processing = true; try { if (nextItem.type === 'page') { // 處理頁面項目 await this.processPageItem(nextItem); } else if (nextItem.type === 'rows') { // 處理行項目 await this.processRowsItem(nextItem); } } catch (error) { console.error('處理項目失敗:', error); UI.updateProgressText(`處理失敗: ${error.message}`); UI.showToast(`處理失敗: ${error.message}`); // 發生錯誤時仍然繼續處理其他項目 LOADING_STATE.processing = false; this.processNextItem(); } }, // 處理頁面項目 async processPageItem(item) { const { url, recursive } = item; STATS.currentPage++; UI.updateProgressText(`正在加載第 ${STATS.currentPage} 頁...`); try { // 獲取頁面內容 const response = await fetch(url); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 獲取下一頁的表格 const nextPageTableBody = doc.querySelector('.itg.glte tbody'); if (!nextPageTableBody) { throw new Error('無法解析頁面內容'); } // 獲取下一頁中的行 const nextPageRows = Array.from(nextPageTableBody.rows); // 獲取當前表格 const tableBody = Utils.getTableBody(); if (!tableBody) { throw new Error('無法找到當前頁面的表格'); } // 添加行項目到隊列 this.addRowsToQueue({ rows: nextPageRows, tableBody: tableBody, totalToProcess: nextPageRows.length, processed: 0, filtered: 0 }); // 檢查是否有下一頁 const nextPageUrl = this.getNextPageUrlFromDoc(doc); if (nextPageUrl && recursive) { this.addPageToQueue(nextPageUrl, true); } // 處理完成 LOADING_STATE.processing = false; this.processNextItem(); } catch (error) { LOADING_STATE.processing = false; throw error; } }, // 處理行項目 async processRowsItem(item) { const { rows, tableBody, totalToProcess } = item; const records = Utils.getRecords(); let addedCount = item.processed || 0; let filteredCount = item.filtered || 0; try { // 處理每一行 for (let i = 0; i < rows.length; i++) { // 檢查是否已停止 if (this.isStopped) { LOADING_STATE.processing = false; this.isLoading = false; return; } // 檢查是否暫停 if (LOADING_STATE.userPaused || (LOADING_STATE.backgroundPaused && !CONFIG.continueInBackground)) { // 如果暫停,則將剩餘行重新加入隊列 const remainingRows = rows.slice(i); this.loadQueue.unshift({ type: 'rows', rows: remainingRows, tableBody: tableBody, totalToProcess: totalToProcess, processed: addedCount, filtered: filteredCount }); LOADING_STATE.processing = false; return; } const row = rows[i]; // 解析 ID const link = row.querySelector('a'); if (!link) continue; const id = Utils.getIdFromUrl(link.href); // 檢查是否已記錄 const isAlreadyRecorded = records[id]; // 複製行並添加到表格 const clonedRow = row.cloneNode(true); tableBody.appendChild(clonedRow); addedCount++; STATS.totalProcessed++; STATS.totalAdded++; // 如果是已記錄項目,設置高亮並可能隱藏 if (isAlreadyRecorded) { clonedRow.classList.add('ex-record-highlighted'); // 添加記錄時間 const titleElement = clonedRow.querySelector('.gl4e'); if (titleElement && !titleElement.querySelector('.ex-record-time')) { const timeSpan = document.createElement('span'); timeSpan.className = 'ex-record-time'; const timestamp = records[id].timestamp || records[id].t || ''; timeSpan.textContent = timestamp ? `記錄於: ${Utils.formatDate(timestamp)}` : '已記錄'; titleElement.appendChild(timeSpan); } // 根據當前狀態決定是否隱藏 if (CONFIG.autoHideRecorded) { clonedRow.style.display = 'none'; filteredCount++; STATS.totalFiltered++; } } // 更新進度顯示 const percent = Math.round((i + 1) / totalToProcess * 100); UI.updateProgress( percent, STATS.currentPage, STATS.estimatedTotalPages, STATS.totalProcessed ); // 更新統計顯示 UI.updateStatsDisplay(); // 適當延遲以避免頁面凍結 if (i < rows.length - 1 && i % 10 === 0) { await Utils.reliableDelay(10); } } // 更新進度文本 UI.updateProgressText(`第 ${STATS.currentPage} 頁完成,已加載 ${addedCount} 項`); // 添加延遲以避免請求過快 await Utils.reliableDelay(CONFIG.loadDelay); // 處理完成 LOADING_STATE.processing = false; this.processNextItem(); } catch (error) { LOADING_STATE.processing = false; throw error; } }, // 完成加載 completeLoading() { this.isLoading = false; UI.updateProgressText(`加載完成,共處理 ${STATS.totalProcessed} 項,新增 ${STATS.totalAdded} 項`); UI.showToast(`加載完成,共處理 ${STATS.totalProcessed} 項,新增 ${STATS.totalAdded} 項`); // 3秒後隱藏進度條 setTimeout(() => { UI.toggleProgressContainer(false); }, 3000); }, // 從文檔中獲取下一頁URL getNextPageUrlFromDoc(doc) { const nextPageLink = doc.querySelector('#unext'); if (nextPageLink && nextPageLink.href && nextPageLink.href !== "javascript:void(0)") { return nextPageLink.href; } return null; }, // 停止加載 stopLoading() { this.isStopped = true; this.isLoading = false; this.loadQueue = []; LOADING_STATE.processing = false; LOADING_STATE.userPaused = false; LOADING_STATE.backgroundPaused = false; } }; // 數據管理 const DataManager = { // 匯出記錄 exportRecords() { const records = Utils.getRecords(); const exportData = JSON.stringify(records, null, 2); const modalContent = ` <p>以下是您的記錄資料,請複製並保存:</p> <textarea readonly>${exportData}</textarea> `; const buttons = [ { id: 'ex-record-copy-btn', text: '複製', primary: true }, { id: 'ex-record-modal-close-btn', text: '關閉', primary: false } ]; const modal = UI.createModal('匯出記錄', modalContent, buttons); document.getElementById('ex-record-copy-btn').addEventListener('click', () => { const textarea = modal.querySelector('textarea'); if (textarea) { textarea.select(); document.execCommand('copy'); UI.showToast('已複製到剪貼簿'); } }); document.getElementById('ex-record-modal-close-btn').addEventListener('click', () => { document.body.removeChild(modal); }); }, // 匯入記錄 importRecords() { const modalContent = ` <p>請貼上之前匯出的記錄資料:</p> <textarea placeholder="在這裡貼上 JSON 格式的記錄資料..."></textarea> `; const buttons = [ { id: 'ex-record-import-btn', text: '匯入', primary: true }, { id: 'ex-record-modal-close-btn', text: '取消', primary: false } ]; const modal = UI.createModal('匯入記錄', modalContent, buttons); document.getElementById('ex-record-import-btn').addEventListener('click', () => { const textarea = modal.querySelector('textarea'); if (!textarea) return; try { const importData = JSON.parse(textarea.value); const currentRecords = Utils.getRecords(); // 合併記錄 const mergedRecords = { ...currentRecords, ...importData }; if (Utils.saveRecords(mergedRecords)) { Record.highlightRecorded(); UI.updateStatsDisplay(); UI.showToast(`匯入成功,共 ${Object.keys(mergedRecords).length} 筆記錄`); } else { UI.showToast('匯入失敗:保存記錄時出錯'); } document.body.removeChild(modal); } catch (error) { UI.showToast(`匯入失敗:${error.message}`); } }); document.getElementById('ex-record-modal-close-btn').addEventListener('click', () => { document.body.removeChild(modal); }); } }; // 檢查舊數據格式並轉換 function migrateOldData() { const oldRecordStr = localStorage.getItem("record"); if (oldRecordStr) { try { const oldIds = oldRecordStr.split(",").filter(id => id.trim() !== ''); const newRecords = Utils.getRecords(); const now = new Date().toISOString(); for (let i = 0; i < oldIds.length; i++) { const id = oldIds[i]; if (id && !newRecords[id]) { newRecords[id] = { t: now }; } } Utils.saveRecords(newRecords); localStorage.removeItem("record"); UI.showToast("已轉換舊格式記錄"); } catch (e) { console.error('轉換舊記錄失敗:', e); } } } // 初始化函數 function init() { console.log('初始化 Enhanced Exhentai Record 腳本...'); // 添加樣式 UI.addStyles(); // 轉換舊數據 migrateOldData(); if (Utils.getTableBody()) { // 創建控制面板 UI.createControlPanel(); // 創建進度容器 UI.createProgressContainer(); UI.toggleProgressContainer(false); // 默認隱藏 // 標記已記錄的項目 Record.highlightRecorded(); // 更新統計信息 UI.updateStatsDisplay(); // 初始化頁面加載器 PageLoader.init(); // 默認隱藏已記錄的項目 if (CONFIG.autoHideRecorded) { const hiddenCount = Record.hideRecordedItems(); if (hiddenCount > 0) { UI.showToast(`已隱藏 ${hiddenCount} 筆已記錄的內容`); } } // 添加滾動事件來監控閱讀進度 window.addEventListener('scroll', Utils.debounce(() => { Utils.updateReadingProgress(); }, 200)); } else { console.log('找不到作品表格,可能不在正確的頁面'); } } // 確保頁面載入完成後執行初始化 if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(init, 1000); } else { document.addEventListener('DOMContentLoaded', () => { setTimeout(init, 1000); }); } // 確保初始化執行 setTimeout(() => { if (!document.querySelector('.ex-record-toolbar')) { init(); } }, 2000); })();