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