Enhanced Exhentai Record (Optimized)

增強型 Exhentai 記錄腳本,優化加載進度和閱讀體驗,支持後台加載

  1. // ==UserScript==
  2. // @name Enhanced Exhentai Record (Optimized)
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.1
  5. // @description 增強型 Exhentai 記錄腳本,優化加載進度和閱讀體驗,支持後台加載
  6. // @author You
  7. // @match https://exhentai.org/watched*
  8. // @icon https://www.google.com/s2/favicons?domain=exhentai.org
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // 配置選項
  16. const CONFIG = {
  17. autoHideRecorded: true, // 自動隱藏已記錄項目
  18. loadDelay: 800, // 加載下一頁的延遲(毫秒)
  19. toastDuration: 3000, // Toast 顯示時間
  20. storageKey: 'exhentai_record',// 本地存儲鍵名
  21. continueInBackground: true // 切換頁面時繼續加載
  22. };
  23.  
  24. // DOM 元素引用
  25. let DOM = {
  26. progressBar: null,
  27. progressText: null,
  28. readingProgressBar: null,
  29. statusArea: null,
  30. totalCountElem: null,
  31. pageRecordedElem: null,
  32. pageUnrecordedElem: null,
  33. pageHiddenElem: null
  34. };
  35.  
  36. // 統計數據
  37. const STATS = {
  38. totalProcessed: 0,
  39. totalAdded: 0,
  40. totalFiltered: 0,
  41. currentPage: 1,
  42. estimatedTotalPages: 0,
  43. readingProgress: 0
  44. };
  45.  
  46. // 加載狀態
  47. const LOADING_STATE = {
  48. userPaused: false, // 用戶手動暫停
  49. backgroundPaused: false, // 因切換到後台而暫停
  50. processing: false // 正在處理
  51. };
  52.  
  53. // SVG 圖標定義
  54. const ICONS = {
  55. 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>',
  56. 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>',
  57. 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>',
  58. 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>',
  59. 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>',
  60. 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>',
  61. 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>',
  62. 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>',
  63. 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>',
  64. 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>',
  65. 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>',
  66. 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>',
  67. 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>'
  68. };
  69.  
  70. // 樣式定義
  71. const STYLES = `
  72. /* 主控制面板 */
  73. .ex-record-toolbar {
  74. position: sticky;
  75. top: 0;
  76. margin: 0 auto;
  77. padding: 15px;
  78. background-color: #333;
  79. border-radius: 5px;
  80. display: flex;
  81. align-items: center;
  82. justify-content: space-between;
  83. flex-wrap: wrap;
  84. box-shadow: 0 3px 10px rgba(0,0,0,0.3);
  85. z-index: 1000;
  86. margin-bottom: 15px;
  87. border: 1px solid #444;
  88. max-width: 95%;
  89. }
  90.  
  91. /* 按鈕樣式 */
  92. .ex-record-btn {
  93. display: inline-flex;
  94. align-items: center;
  95. justify-content: center;
  96. margin: 5px;
  97. padding: 8px 15px;
  98. background-color: #444;
  99. color: #eee;
  100. border-radius: 4px;
  101. cursor: pointer;
  102. transition: all 0.3s;
  103. border: none;
  104. font-weight: bold;
  105. font-size: 14px;
  106. min-width: 120px;
  107. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  108. }
  109.  
  110. .ex-record-btn svg {
  111. margin-right: 8px;
  112. }
  113.  
  114. .ex-record-btn:hover {
  115. background-color: #555;
  116. transform: translateY(-2px);
  117. box-shadow: 0 4px 8px rgba(0,0,0,0.3);
  118. }
  119.  
  120. .ex-record-btn:active {
  121. transform: translateY(0);
  122. box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  123. }
  124.  
  125. /* 不同類型的按鈕顏色 */
  126. .ex-record-add {
  127. background-color: #1a73e8;
  128. }
  129.  
  130. .ex-record-add:hover {
  131. background-color: #1967d2;
  132. }
  133.  
  134. .ex-record-toggle {
  135. background-color: #34a853;
  136. }
  137.  
  138. .ex-record-toggle:hover {
  139. background-color: #2d9247;
  140. }
  141.  
  142. .ex-record-export {
  143. background-color: #ea4335;
  144. }
  145.  
  146. .ex-record-export:hover {
  147. background-color: #d33426;
  148. }
  149.  
  150. .ex-record-import {
  151. background-color: #fbbc05;
  152. color: #333;
  153. }
  154.  
  155. .ex-record-import:hover {
  156. background-color: #f0b400;
  157. }
  158.  
  159. .ex-record-stop {
  160. background-color: #ea4335;
  161. }
  162.  
  163. .ex-record-stop:hover {
  164. background-color: #d33426;
  165. }
  166.  
  167. /* 信息顯示 */
  168. .ex-record-info {
  169. display: inline-flex;
  170. align-items: center;
  171. padding: 8px 12px;
  172. margin: 5px;
  173. border-radius: 4px;
  174. background-color: #444;
  175. color: #eee;
  176. font-weight: bold;
  177. border: 1px solid #555;
  178. box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  179. }
  180.  
  181. .ex-record-info svg {
  182. margin-right: 8px;
  183. color: #aaa;
  184. }
  185.  
  186. /* 標記已記錄項目 */
  187. .ex-record-highlighted {
  188. background-color: rgba(26, 115, 232, 0.15) !important;
  189. border-left: 4px solid #1a73e8 !important;
  190. }
  191.  
  192. /* 記錄時間顯示 */
  193. .ex-record-time {
  194. font-size: 12px;
  195. color: #aaa;
  196. margin-left: 8px;
  197. display: inline-block;
  198. padding: 3px 6px;
  199. background-color: rgba(0, 0, 0, 0.2);
  200. border-radius: 3px;
  201. }
  202.  
  203. /* Toast 消息 */
  204. .ex-record-toast {
  205. position: fixed;
  206. top: 20px;
  207. right: 20px;
  208. padding: 12px 20px;
  209. background-color: rgba(50, 50, 50, 0.9);
  210. color: #fff;
  211. border-radius: 4px;
  212. z-index: 10000;
  213. animation: ex-record-fadeInOut 3s ease-in-out forwards;
  214. box-shadow: 0 4px 10px rgba(0,0,0,0.3);
  215. border-left: 4px solid #1a73e8;
  216. max-width: 300px;
  217. }
  218.  
  219. @keyframes ex-record-fadeInOut {
  220. 0% { opacity: 0; transform: translateY(-20px); }
  221. 10% { opacity: 1; transform: translateY(0); }
  222. 80% { opacity: 1; transform: translateY(0); }
  223. 100% { opacity: 0; transform: translateY(-20px); }
  224. }
  225.  
  226. /* 模態對話框 */
  227. .ex-record-modal {
  228. position: fixed;
  229. top: 0;
  230. left: 0;
  231. width: 100%;
  232. height: 100%;
  233. background-color: rgba(0, 0, 0, 0.7);
  234. display: flex;
  235. justify-content: center;
  236. align-items: center;
  237. z-index: 10001;
  238. }
  239.  
  240. .ex-record-modal-content {
  241. background-color: #333;
  242. padding: 20px;
  243. border-radius: 8px;
  244. width: 80%;
  245. max-width: 600px;
  246. box-shadow: 0 5px 15px rgba(0,0,0,0.5);
  247. color: #eee;
  248. border: 1px solid #444;
  249. }
  250.  
  251. .ex-record-modal-header {
  252. display: flex;
  253. justify-content: space-between;
  254. align-items: center;
  255. margin-bottom: 15px;
  256. border-bottom: 1px solid #444;
  257. padding-bottom: 10px;
  258. }
  259.  
  260. .ex-record-modal-close {
  261. background: none;
  262. border: none;
  263. font-size: 24px;
  264. cursor: pointer;
  265. color: #aaa;
  266. }
  267.  
  268. .ex-record-modal-close:hover {
  269. color: #fff;
  270. }
  271.  
  272. .ex-record-modal-body {
  273. margin-bottom: 15px;
  274. }
  275.  
  276. .ex-record-modal textarea {
  277. width: 100%;
  278. height: 200px;
  279. background-color: #222;
  280. color: #eee;
  281. border: 1px solid #444;
  282. padding: 10px;
  283. border-radius: 4px;
  284. resize: vertical;
  285. font-family: monospace;
  286. }
  287.  
  288. .ex-record-modal-footer {
  289. display: flex;
  290. justify-content: flex-end;
  291. gap: 10px;
  292. }
  293.  
  294. .ex-record-modal-btn {
  295. padding: 8px 15px;
  296. border: none;
  297. border-radius: 4px;
  298. cursor: pointer;
  299. font-weight: bold;
  300. min-width: 80px;
  301. }
  302.  
  303. .ex-record-modal-btn-primary {
  304. background-color: #1a73e8;
  305. color: white;
  306. }
  307.  
  308. .ex-record-modal-btn-primary:hover {
  309. background-color: #1967d2;
  310. }
  311.  
  312. .ex-record-modal-btn-secondary {
  313. background-color: #444;
  314. color: #eee;
  315. }
  316.  
  317. .ex-record-modal-btn-secondary:hover {
  318. background-color: #555;
  319. }
  320.  
  321. /* 控制面板內的區域 */
  322. .ex-record-toolbar {
  323. flex-direction: column;
  324. padding: 12px 15px;
  325. }
  326.  
  327. .ex-record-controls-row {
  328. display: flex;
  329. width: 100%;
  330. justify-content: space-between;
  331. align-items: center;
  332. margin-bottom: 8px;
  333. }
  334.  
  335. .ex-record-controls-row:last-child {
  336. margin-bottom: 0;
  337. }
  338.  
  339. .ex-record-controls-left, .ex-record-controls-center, .ex-record-controls-right {
  340. display: flex;
  341. align-items: center;
  342. flex-wrap: wrap;
  343. }
  344.  
  345. .ex-record-controls-stats {
  346. flex: 1;
  347. display: flex;
  348. flex-wrap: wrap;
  349. justify-content: flex-start;
  350. }
  351.  
  352. .ex-record-controls-center {
  353. flex-grow: 1;
  354. justify-content: center;
  355. margin: 0 10px;
  356. }
  357.  
  358. .ex-record-controls-buttons {
  359. flex: 1;
  360. display: flex;
  361. justify-content: center;
  362. }
  363.  
  364. .ex-record-controls-data {
  365. display: flex;
  366. justify-content: flex-end;
  367. }
  368.  
  369. /* 進度條樣式 */
  370. .ex-record-progress-container {
  371. position: fixed;
  372. bottom: 20px;
  373. right: 20px;
  374. width: 300px;
  375. background-color: #333;
  376. border-radius: 5px;
  377. padding: 12px;
  378. box-shadow: 0 3px 10px rgba(0,0,0,0.3);
  379. border: 1px solid #444;
  380. z-index: 1000;
  381. transition: opacity 0.3s ease;
  382. }
  383.  
  384. .ex-record-progress-container.hidden {
  385. opacity: 0;
  386. pointer-events: none;
  387. }
  388.  
  389. .ex-record-progress-header {
  390. display: flex;
  391. justify-content: space-between;
  392. align-items: center;
  393. margin-bottom: 8px;
  394. }
  395.  
  396. .ex-record-progress-title {
  397. font-weight: bold;
  398. color: #eee;
  399. }
  400.  
  401. .ex-record-progress-controls {
  402. display: flex;
  403. gap: 5px;
  404. }
  405.  
  406. .ex-record-progress-btn {
  407. background: none;
  408. border: none;
  409. color: #aaa;
  410. cursor: pointer;
  411. padding: 0;
  412. display: flex;
  413. align-items: center;
  414. justify-content: center;
  415. width: 24px;
  416. height: 24px;
  417. border-radius: 50%;
  418. transition: all 0.2s;
  419. }
  420.  
  421. .ex-record-progress-btn:hover {
  422. background-color: #444;
  423. color: #fff;
  424. }
  425.  
  426. .ex-record-progress {
  427. width: 100%;
  428. height: 6px;
  429. background-color: #444;
  430. border-radius: 3px;
  431. margin: 5px 0;
  432. overflow: hidden;
  433. }
  434.  
  435. .ex-record-progress-bar {
  436. height: 100%;
  437. background-color: #1a73e8;
  438. width: 0%;
  439. transition: width 0.3s ease;
  440. }
  441.  
  442. .ex-record-reading-progress {
  443. width: 100%;
  444. height: 6px;
  445. background-color: #444;
  446. border-radius: 3px;
  447. margin: 8px 0 5px 0;
  448. overflow: hidden;
  449. }
  450.  
  451. .ex-record-reading-progress-bar {
  452. height: 100%;
  453. background-color: #34a853;
  454. width: 0%;
  455. transition: width 0.3s ease;
  456. }
  457.  
  458. .ex-record-progress-stats {
  459. display: flex;
  460. justify-content: space-between;
  461. color: #aaa;
  462. font-size: 12px;
  463. margin-top: 5px;
  464. }
  465.  
  466. .ex-record-progress-text {
  467. color: #eee;
  468. font-size: 13px;
  469. margin: 8px 0;
  470. }
  471.  
  472. /* 數據管理下拉選單 */
  473. .ex-record-controls-right {
  474. position: relative;
  475. }
  476.  
  477. .ex-record-data-buttons {
  478. position: absolute;
  479. right: 0;
  480. top: 100%;
  481. background-color: #333;
  482. border-radius: 4px;
  483. padding: 5px;
  484. display: none;
  485. flex-direction: column;
  486. z-index: 2000;
  487. box-shadow: 0 3px 8px rgba(0,0,0,0.3);
  488. border: 1px solid #444;
  489. min-width: 120px;
  490. }
  491.  
  492. .ex-record-controls-right:hover .ex-record-data-buttons {
  493. display: flex;
  494. }
  495.  
  496. .ex-record-data-toggle {
  497. display: flex;
  498. align-items: center;
  499. justify-content: center;
  500. background-color: #444;
  501. color: #eee;
  502. padding: 8px 15px;
  503. border-radius: 4px;
  504. cursor: pointer;
  505. transition: all 0.3s;
  506. font-weight: bold;
  507. font-size: 14px;
  508. border: none;
  509. }
  510.  
  511. .ex-record-data-toggle svg {
  512. margin-right: 8px;
  513. }
  514.  
  515. .ex-record-data-toggle:hover {
  516. background-color: #555;
  517. }
  518. `;
  519.  
  520. // 工具函數
  521. const Utils = {
  522. // 從 localStorage 獲取記錄
  523. getRecords() {
  524. try {
  525. const recordStr = localStorage.getItem(CONFIG.storageKey);
  526. return recordStr ? JSON.parse(recordStr) : {};
  527. } catch (e) {
  528. console.error('解析記錄失敗:', e);
  529. return {};
  530. }
  531. },
  532.  
  533. // 保存記錄到 localStorage
  534. saveRecords(records) {
  535. try {
  536. localStorage.setItem(CONFIG.storageKey, JSON.stringify(records));
  537. return true;
  538. } catch (e) {
  539. console.error('保存記錄失敗:', e);
  540. UI.showToast('保存記錄失敗: ' + e.message);
  541. return false;
  542. }
  543. },
  544.  
  545. // 格式化時間
  546. formatDate(dateString) {
  547. try {
  548. const date = new Date(dateString);
  549. const year = date.getFullYear();
  550. const month = String(date.getMonth() + 1).padStart(2, '0');
  551. const day = String(date.getDate()).padStart(2, '0');
  552. const hours = String(date.getHours()).padStart(2, '0');
  553. const minutes = String(date.getMinutes()).padStart(2, '0');
  554.  
  555. return `${year}-${month}-${day} ${hours}:${minutes}`;
  556. } catch (e) {
  557. return '未知時間';
  558. }
  559. },
  560.  
  561. // 獲取表格主體
  562. getTableBody() {
  563. const table = document.querySelector('.itg.glte');
  564. return table && table.tBodies.length > 0 ? table.tBodies[0] : null;
  565. },
  566.  
  567. // 獲取頁面中的所有項目 ID
  568. getPageItems() {
  569. const tableBody = this.getTableBody();
  570. if (!tableBody) return [];
  571.  
  572. return Array.from(tableBody.rows)
  573. .map(row => {
  574. const link = row.querySelector('a');
  575. if (!link) return null;
  576. const url = link.href.split("/").filter(i => i !== '');
  577. return url[url.length - 1] + url[url.length - 2];
  578. })
  579. .filter(id => id !== null);
  580. },
  581.  
  582. // 從URL獲取項目ID
  583. getIdFromUrl(url) {
  584. const parts = url.split("/").filter(i => i !== '');
  585. return parts[parts.length - 1] + parts[parts.length - 2];
  586. },
  587.  
  588. // 估算總頁數
  589. estimateTotalPages() {
  590. // 嘗試從分頁器中獲取頁數
  591. const pager = document.querySelector('.ptt');
  592. if (pager) {
  593. const lastPageLink = Array.from(pager.querySelectorAll('a')).pop();
  594. if (lastPageLink && lastPageLink.textContent) {
  595. const pageNum = parseInt(lastPageLink.textContent);
  596. if (!isNaN(pageNum)) {
  597. return pageNum;
  598. }
  599. }
  600. }
  601. // 如果無法從頁面獲取,返回預設值
  602. return 10;
  603. },
  604.  
  605. // 獲取當前頁碼
  606. getCurrentPage() {
  607. const pager = document.querySelector('.ptt');
  608. if (pager) {
  609. const currentPageElement = pager.querySelector('td.ptds');
  610. if (currentPageElement && currentPageElement.textContent) {
  611. const pageNum = parseInt(currentPageElement.textContent);
  612. if (!isNaN(pageNum)) {
  613. return pageNum;
  614. }
  615. }
  616. }
  617. return 1;
  618. },
  619.  
  620. // 動態調整閱讀進度
  621. updateReadingProgress() {
  622. // 計算閱讀進度百分比
  623. const tableBody = this.getTableBody();
  624. if (!tableBody) return 0;
  625.  
  626. const totalItems = tableBody.rows.length;
  627. if (totalItems === 0) return 0;
  628.  
  629. // 通過檢測可見區域來判斷閱讀進度
  630. const viewportHeight = window.innerHeight;
  631. const viewportTop = window.scrollY;
  632. const viewportBottom = viewportTop + viewportHeight;
  633.  
  634. let visibleCount = 0;
  635.  
  636. Array.from(tableBody.rows).forEach(row => {
  637. const rect = row.getBoundingClientRect();
  638. const rowTop = rect.top + viewportTop;
  639. const rowBottom = rect.bottom + viewportTop;
  640.  
  641. // 行完全可見或部分可見
  642. if ((rowTop >= viewportTop && rowTop <= viewportBottom) ||
  643. (rowBottom >= viewportTop && rowBottom <= viewportBottom) ||
  644. (rowTop <= viewportTop && rowBottom >= viewportBottom)) {
  645. visibleCount++;
  646. }
  647. // 已經滾動過的行
  648. else if (rowBottom < viewportTop) {
  649. visibleCount++;
  650. }
  651. });
  652.  
  653. const progress = Math.min(100, Math.round((visibleCount / totalItems) * 100));
  654.  
  655. if (DOM.readingProgressBar) {
  656. DOM.readingProgressBar.style.width = `${progress}%`;
  657. }
  658.  
  659. return progress;
  660. },
  661.  
  662. // 延時執行函數
  663. debounce(func, wait) {
  664. let timeout;
  665. return function(...args) {
  666. clearTimeout(timeout);
  667. timeout = setTimeout(() => func.apply(this, args), wait);
  668. };
  669. },
  670.  
  671. // 可靠的延時函數,即使在後台也能工作
  672. reliableDelay(ms) {
  673. return new Promise(resolve => {
  674. const startTime = Date.now();
  675. const checkTime = () => {
  676. const elapsedTime = Date.now() - startTime;
  677. if (elapsedTime >= ms) {
  678. resolve();
  679. } else {
  680. setTimeout(checkTime, Math.min(100, ms - elapsedTime));
  681. }
  682. };
  683. setTimeout(checkTime, Math.min(100, ms));
  684. });
  685. },
  686.  
  687. // 記錄到控制台
  688. log(message) {
  689. console.log(`[ExRecord] ${message}`);
  690. }
  691. };
  692.  
  693. // UI 操作相關
  694. const UI = {
  695. // 顯示 Toast 消息
  696. showToast(message, duration = CONFIG.toastDuration) {
  697. const toast = document.createElement('div');
  698. toast.className = 'ex-record-toast';
  699. toast.textContent = message;
  700. document.body.appendChild(toast);
  701.  
  702. setTimeout(() => {
  703. if (toast.parentNode) {
  704. document.body.removeChild(toast);
  705. }
  706. }, duration);
  707. },
  708.  
  709. // 創建進度顯示容器
  710. createProgressContainer() {
  711. const container = document.createElement('div');
  712. container.className = 'ex-record-progress-container';
  713. container.id = 'ex-record-progress-container';
  714. container.innerHTML = `
  715. <div class="ex-record-progress-header">
  716. <div class="ex-record-progress-title">加載進度</div>
  717. <div class="ex-record-progress-controls">
  718. <button class="ex-record-progress-btn" id="ex-record-pause-btn" title="暫停/繼續加載">
  719. ${ICONS.pause}
  720. </button>
  721. <button class="ex-record-progress-btn" id="ex-record-stop-btn" title="停止加載">
  722. ${ICONS.stop}
  723. </button>
  724. </div>
  725. </div>
  726. <div class="ex-record-progress-text" id="ex-record-progress-text">準備加載...</div>
  727. <div class="ex-record-progress">
  728. <div class="ex-record-progress-bar" id="ex-record-progress-bar"></div>
  729. </div>
  730. <div class="ex-record-progress-stats">
  731. <span id="ex-record-progress-page">頁面: 0/0</span>
  732. <span id="ex-record-progress-items">已加載: 0</span>
  733. </div>
  734. <div class="ex-record-progress-text">閱讀進度</div>
  735. <div class="ex-record-reading-progress">
  736. <div class="ex-record-reading-progress-bar" id="ex-record-reading-progress-bar"></div>
  737. </div>
  738. <div class="ex-record-progress-stats">
  739. <span id="ex-record-reading-percent">0%</span>
  740. <span id="ex-record-new-items">新項目: 0</span>
  741. </div>
  742. `;
  743.  
  744. document.body.appendChild(container);
  745.  
  746. // 獲取DOM引用
  747. DOM.progressBar = document.getElementById('ex-record-progress-bar');
  748. DOM.progressText = document.getElementById('ex-record-progress-text');
  749. DOM.readingProgressBar = document.getElementById('ex-record-reading-progress-bar');
  750.  
  751. // 設置暫停/停止按鈕事件
  752. document.getElementById('ex-record-pause-btn').addEventListener('click', () => {
  753. this.toggleLoadingPause();
  754. });
  755.  
  756. document.getElementById('ex-record-stop-btn').addEventListener('click', () => {
  757. this.stopLoading();
  758. });
  759.  
  760. return container;
  761. },
  762.  
  763. // 更新暫停按鈕圖標
  764. updatePauseButtonIcon(isPaused) {
  765. const pauseBtn = document.getElementById('ex-record-pause-btn');
  766. if (pauseBtn) {
  767. pauseBtn.innerHTML = isPaused ? ICONS.play : ICONS.pause;
  768. pauseBtn.title = isPaused ? "繼續加載" : "暫停加載";
  769. }
  770. },
  771.  
  772. // 更新加載進度
  773. updateProgress(percent, currentPage, totalPages, loadedItems) {
  774. if (DOM.progressBar) {
  775. DOM.progressBar.style.width = `${percent}%`;
  776. }
  777.  
  778. // 更新頁面計數
  779. const pageCountElement = document.getElementById('ex-record-progress-page');
  780. if (pageCountElement) {
  781. pageCountElement.textContent = `頁面: ${currentPage}/${totalPages || '?'}`;
  782. }
  783.  
  784. // 更新已加載項目數
  785. const itemsCountElement = document.getElementById('ex-record-progress-items');
  786. if (itemsCountElement) {
  787. itemsCountElement.textContent = `已加載: ${loadedItems}`;
  788. }
  789.  
  790. // 更新新項目數
  791. const newItemsElement = document.getElementById('ex-record-new-items');
  792. if (newItemsElement) {
  793. newItemsElement.textContent = `新項目: ${STATS.totalAdded}`;
  794. }
  795.  
  796. // 更新閱讀百分比
  797. const readingPercentElement = document.getElementById('ex-record-reading-percent');
  798. if (readingPercentElement) {
  799. const readingPercent = Utils.updateReadingProgress();
  800. STATS.readingProgress = readingPercent;
  801. readingPercentElement.textContent = `${readingPercent}%`;
  802. }
  803. },
  804.  
  805. // 更新加載狀態文本
  806. updateProgressText(text) {
  807. if (DOM.progressText) {
  808. DOM.progressText.textContent = text;
  809. }
  810. },
  811.  
  812. // 顯示/隱藏進度容器
  813. toggleProgressContainer(show = true) {
  814. const container = document.getElementById('ex-record-progress-container');
  815. if (container) {
  816. container.className = show
  817. ? 'ex-record-progress-container'
  818. : 'ex-record-progress-container hidden';
  819. }
  820. },
  821.  
  822. // 暫停/繼續加載
  823. toggleLoadingPause() {
  824. const loader = PageLoader;
  825.  
  826. if (LOADING_STATE.userPaused) {
  827. // 如果是用戶暫停,則恢復
  828. LOADING_STATE.userPaused = false;
  829. this.updatePauseButtonIcon(false);
  830.  
  831. if (!LOADING_STATE.backgroundPaused) {
  832. // 如果不是因為背景暫停,則恢復加載
  833. loader.processNextItem();
  834. this.updateProgressText('繼續加載中...');
  835. this.showToast('繼續加載');
  836. } else {
  837. this.updateProgressText('頁面處於後台,將在返回前台時繼續加載');
  838. this.showToast('已設置為繼續加載,將在返回前台時恢復');
  839. }
  840. } else {
  841. // 暫停加載
  842. LOADING_STATE.userPaused = true;
  843. this.updatePauseButtonIcon(true);
  844. this.updateProgressText('加載已暫停(用戶手動)');
  845. this.showToast('加載已暫停');
  846. }
  847. },
  848.  
  849. // 停止加載
  850. stopLoading() {
  851. PageLoader.stopLoading();
  852. LOADING_STATE.userPaused = false;
  853. LOADING_STATE.backgroundPaused = false;
  854. this.updatePauseButtonIcon(false);
  855. this.updateProgressText('加載已停止');
  856. this.showToast('加載已停止');
  857.  
  858. // 3秒後隱藏進度條
  859. setTimeout(() => {
  860. this.toggleProgressContainer(false);
  861. }, 3000);
  862. },
  863.  
  864. // 創建控制面板
  865. createControlPanel() {
  866. const controlPanel = document.createElement('div');
  867. controlPanel.className = 'ex-record-toolbar';
  868.  
  869. // 構建控制面板HTML - 分為上下兩行
  870. controlPanel.innerHTML = `
  871. <!-- 第一行:數據統計 -->
  872. <div class="ex-record-controls-row">
  873. <div class="ex-record-controls-stats">
  874. <div class="ex-record-info" id="ex-record-total-count">
  875. ${ICONS.info}總記錄: 0
  876. </div>
  877. <div class="ex-record-info" id="ex-record-page-recorded">
  878. ${ICONS.check}本頁已記錄: 0
  879. </div>
  880. <div class="ex-record-info" id="ex-record-page-unrecorded">
  881. ${ICONS.uncheck}本頁未記錄: 0
  882. </div>
  883. <div class="ex-record-info" id="ex-record-page-hidden">
  884. ${ICONS.hidden}本頁隱藏: 0
  885. </div>
  886. </div>
  887. </div>
  888.  
  889. <!-- 第二行:操作按鈕 -->
  890. <div class="ex-record-controls-row">
  891. <!-- 中間按鈕區域 -->
  892. <div class="ex-record-controls-buttons">
  893. <button class="ex-record-btn ex-record-add" id="ex-record-add-btn">
  894. ${ICONS.record}記錄此頁
  895. </button>
  896. <button class="ex-record-btn ex-record-toggle" id="ex-record-toggle-btn">
  897. ${ICONS.toggle}隱藏/顯示
  898. </button>
  899. <button class="ex-record-btn ex-record-add" id="ex-record-load-all-btn">
  900. ${ICONS.loadAll}加載所有頁面
  901. </button>
  902. </div>
  903.  
  904. <!-- 右側數據管理按鈕 -->
  905. <div class="ex-record-controls-data">
  906. <div class="ex-record-controls-right">
  907. <button class="ex-record-data-toggle" id="ex-record-data-toggle">
  908. ${ICONS.data}數據管理
  909. </button>
  910. <div class="ex-record-data-buttons">
  911. <button class="ex-record-btn ex-record-export" id="ex-record-export-btn">
  912. ${ICONS.download}匯出記錄
  913. </button>
  914. <button class="ex-record-btn ex-record-import" id="ex-record-import-btn">
  915. ${ICONS.upload}匯入記錄
  916. </button>
  917. </div>
  918. </div>
  919. </div>
  920. </div>
  921. `;
  922.  
  923. // 插入到頁面中
  924. const target = document.querySelector('.searchnav');
  925. if (target && target.parentNode) {
  926. target.parentNode.insertBefore(controlPanel, target);
  927. } else {
  928. const searchtext = document.querySelector('.searchtext');
  929. if (searchtext && searchtext.parentNode) {
  930. searchtext.parentNode.insertBefore(controlPanel, searchtext.nextSibling);
  931. } else {
  932. document.body.insertBefore(controlPanel, document.body.firstChild);
  933. }
  934. }
  935.  
  936. // 保存DOM引用
  937. DOM.totalCountElem = document.getElementById('ex-record-total-count');
  938. DOM.pageRecordedElem = document.getElementById('ex-record-page-recorded');
  939. DOM.pageUnrecordedElem = document.getElementById('ex-record-page-unrecorded');
  940. DOM.pageHiddenElem = document.getElementById('ex-record-page-hidden');
  941.  
  942. // 綁定按鈕事件
  943. document.getElementById('ex-record-add-btn').addEventListener('click', () => Record.recordCurrentPage());
  944. document.getElementById('ex-record-toggle-btn').addEventListener('click', () => Record.toggleRecordedItems());
  945. document.getElementById('ex-record-load-all-btn').addEventListener('click', () => PageLoader.loadAllPages());
  946. document.getElementById('ex-record-export-btn').addEventListener('click', () => DataManager.exportRecords());
  947. document.getElementById('ex-record-import-btn').addEventListener('click', () => DataManager.importRecords());
  948.  
  949. return controlPanel;
  950. },
  951.  
  952. // 更新統計信息顯示
  953. updateStatsDisplay() {
  954. const records = Utils.getRecords();
  955. const recordsCount = Object.keys(records).length;
  956.  
  957. // 更新記錄總數
  958. if (DOM.totalCountElem) {
  959. DOM.totalCountElem.innerHTML = `${ICONS.info}總記錄: ${recordsCount} 筆`;
  960. }
  961.  
  962. // 計算並更新當前頁面統計
  963. const pageItems = Utils.getPageItems();
  964. const pageRecorded = pageItems.filter(id => records[id]).length;
  965. const pageUnrecorded = pageItems.length - pageRecorded;
  966.  
  967. if (DOM.pageRecordedElem) {
  968. DOM.pageRecordedElem.innerHTML = `${ICONS.check}本頁已記錄: ${pageRecorded} 筆`;
  969. }
  970.  
  971. if (DOM.pageUnrecordedElem) {
  972. DOM.pageUnrecordedElem.innerHTML = `${ICONS.uncheck}本頁未記錄: ${pageUnrecorded} 筆`;
  973. }
  974.  
  975. // 統計隱藏數量
  976. let hiddenCount = 0;
  977. const tableBody = Utils.getTableBody();
  978. if (tableBody) {
  979. Array.from(tableBody.rows).forEach(row => {
  980. if (row.style.display === "none") {
  981. hiddenCount++;
  982. }
  983. });
  984. }
  985.  
  986. if (DOM.pageHiddenElem) {
  987. DOM.pageHiddenElem.innerHTML = `${ICONS.hidden}本頁隱藏: ${hiddenCount} 筆`;
  988. }
  989. },
  990.  
  991. // 添加樣式到頁面
  992. addStyles() {
  993. const styleElement = document.createElement('style');
  994. styleElement.textContent = STYLES;
  995. document.head.appendChild(styleElement);
  996. },
  997.  
  998. // 創建模態對話框
  999. createModal(title, content, buttons) {
  1000. const modal = document.createElement('div');
  1001. modal.className = 'ex-record-modal';
  1002. modal.innerHTML = `
  1003. <div class="ex-record-modal-content">
  1004. <div class="ex-record-modal-header">
  1005. <h3>${title}</h3>
  1006. <button class="ex-record-modal-close">&times;</button>
  1007. </div>
  1008. <div class="ex-record-modal-body">
  1009. ${content}
  1010. </div>
  1011. <div class="ex-record-modal-footer">
  1012. ${buttons.map(btn => `
  1013. <button class="ex-record-modal-btn ${btn.primary ? 'ex-record-modal-btn-primary' : 'ex-record-modal-btn-secondary'}"
  1014. id="${btn.id}">${btn.text}</button>
  1015. `).join('')}
  1016. </div>
  1017. </div>
  1018. `;
  1019.  
  1020. document.body.appendChild(modal);
  1021.  
  1022. // 綁定關閉按鈕
  1023. const closeBtn = modal.querySelector('.ex-record-modal-close');
  1024. if (closeBtn) {
  1025. closeBtn.addEventListener('click', () => document.body.removeChild(modal));
  1026. }
  1027.  
  1028. // 返回modal以供後續處理
  1029. return modal;
  1030. }
  1031. };
  1032.  
  1033. // 記錄操作相關
  1034. const Record = {
  1035. // 標記已記錄的項目
  1036. highlightRecorded() {
  1037. const tableBody = Utils.getTableBody();
  1038. if (!tableBody) return;
  1039.  
  1040. const records = Utils.getRecords();
  1041.  
  1042. Array.from(tableBody.rows).forEach(row => {
  1043. const link = row.querySelector('a');
  1044. if (!link) return;
  1045.  
  1046. const url = link.href.split("/").filter(i => i !== '');
  1047. const id = url[url.length - 1] + url[url.length - 2];
  1048.  
  1049. if (records[id]) {
  1050. row.classList.add('ex-record-highlighted');
  1051.  
  1052. // 添加記錄時間
  1053. const titleElement = row.querySelector('.gl4e');
  1054. if (titleElement && !titleElement.querySelector('.ex-record-time')) {
  1055. const timeSpan = document.createElement('span');
  1056. timeSpan.className = 'ex-record-time';
  1057. // 兼容新舊記錄格式
  1058. const timestamp = records[id].timestamp || records[id].t || '';
  1059. timeSpan.textContent = timestamp ? `記錄於: ${Utils.formatDate(timestamp)}` : '已記錄';
  1060. titleElement.appendChild(timeSpan);
  1061. }
  1062. } else {
  1063. row.classList.remove('ex-record-highlighted');
  1064.  
  1065. // 移除記錄時間
  1066. const timeSpan = row.querySelector('.ex-record-time');
  1067. if (timeSpan && timeSpan.parentNode) {
  1068. timeSpan.parentNode.removeChild(timeSpan);
  1069. }
  1070. }
  1071. });
  1072. },
  1073.  
  1074. // 切換顯示/隱藏已記錄的項目
  1075. toggleRecordedItems() {
  1076. const tableBody = Utils.getTableBody();
  1077. if (!tableBody) return;
  1078.  
  1079. const records = Utils.getRecords();
  1080. let hiddenCount = 0;
  1081. let shownCount = 0;
  1082.  
  1083. Array.from(tableBody.rows).forEach(row => {
  1084. const link = row.querySelector('a');
  1085. if (!link) return;
  1086.  
  1087. const url = link.href.split("/").filter(i => i !== '');
  1088. const id = url[url.length - 1] + url[url.length - 2];
  1089.  
  1090. if (records[id]) {
  1091. if (row.style.display === "none") {
  1092. row.style.display = "table-row";
  1093. shownCount++;
  1094. } else {
  1095. row.style.display = "none";
  1096. hiddenCount++;
  1097. }
  1098. }
  1099. });
  1100.  
  1101. if (hiddenCount > 0) {
  1102. UI.showToast(`已隱藏 ${hiddenCount} 筆已記錄的內容`);
  1103. } else if (shownCount > 0) {
  1104. UI.showToast(`已顯示 ${shownCount} 筆已記錄的內容`);
  1105. } else {
  1106. UI.showToast('本頁沒有已記錄的內容');
  1107. }
  1108.  
  1109. UI.updateStatsDisplay();
  1110. },
  1111.  
  1112. // 隱藏已記錄的項目
  1113. hideRecordedItems() {
  1114. const tableBody = Utils.getTableBody();
  1115. if (!tableBody) return 0;
  1116.  
  1117. const records = Utils.getRecords();
  1118. let hiddenCount = 0;
  1119.  
  1120. Array.from(tableBody.rows).forEach(row => {
  1121. const link = row.querySelector('a');
  1122. if (!link) return;
  1123.  
  1124. const url = link.href.split("/").filter(i => i !== '');
  1125. const id = url[url.length - 1] + url[url.length - 2];
  1126.  
  1127. if (records[id]) {
  1128. row.style.display = "none";
  1129. hiddenCount++;
  1130. }
  1131. });
  1132.  
  1133. UI.updateStatsDisplay();
  1134. return hiddenCount;
  1135. },
  1136.  
  1137. // 記錄當前頁面的所有項目
  1138. recordCurrentPage() {
  1139. const tableBody = Utils.getTableBody();
  1140. if (!tableBody) return;
  1141.  
  1142. const records = Utils.getRecords();
  1143. const now = new Date().toISOString();
  1144. let newCount = 0;
  1145.  
  1146. Array.from(tableBody.rows).forEach(row => {
  1147. if (row.style.display === "none") return; // 跳過已隱藏的行
  1148.  
  1149. const link = row.querySelector('a');
  1150. if (!link) return;
  1151.  
  1152. const url = link.href.split("/").filter(i => i !== '');
  1153. const id = url[url.length - 1] + url[url.length - 2];
  1154.  
  1155. if (!records[id]) {
  1156. // 使用簡化的數據結構以節省空間
  1157. records[id] = { t: now };
  1158. newCount++;
  1159. }
  1160. });
  1161.  
  1162. if (newCount > 0) {
  1163. if (Utils.saveRecords(records)) {
  1164. this.highlightRecorded();
  1165. UI.updateStatsDisplay();
  1166. UI.showToast(`已記錄 ${newCount} 筆新內容`);
  1167. } else {
  1168. UI.showToast('記錄失敗:可能超出存儲限制');
  1169. }
  1170. } else {
  1171. UI.showToast('沒有新內容可記錄');
  1172. }
  1173. }
  1174. };
  1175.  
  1176. // 頁面加載器
  1177. const PageLoader = {
  1178. loadQueue: [], // 加載隊列
  1179. isLoading: false, // 是否正在加載
  1180. isStopped: false, // 是否已停止
  1181.  
  1182. // 初始化加載器
  1183. init() {
  1184. STATS.currentPage = Utils.getCurrentPage();
  1185. STATS.estimatedTotalPages = Utils.estimateTotalPages();
  1186.  
  1187. // 設置頁面可見性變化監聽
  1188. this.setupVisibilityHandler();
  1189. },
  1190.  
  1191. // 監聽頁面可見性變化
  1192. setupVisibilityHandler() {
  1193. document.addEventListener('visibilitychange', () => {
  1194. if (document.visibilityState === 'hidden') {
  1195. // 頁面進入後台
  1196. Utils.log('頁面進入後台');
  1197. if (!CONFIG.continueInBackground && !LOADING_STATE.userPaused && this.isLoading) {
  1198. // 如果不允許在後台加載且沒有用戶手動暫停,則暫停加載
  1199. LOADING_STATE.backgroundPaused = true;
  1200. UI.updateProgressText('頁面處於後台,加載已暫停');
  1201. Utils.log('自動暫停加載');
  1202. }
  1203. } else if (document.visibilityState === 'visible') {
  1204. // 頁面回到前台
  1205. Utils.log('頁面回到前台');
  1206. if (LOADING_STATE.backgroundPaused && !LOADING_STATE.userPaused) {
  1207. // 如果因為後台而暫停且沒有用戶手動暫停,則恢復加載
  1208. LOADING_STATE.backgroundPaused = false;
  1209. UI.updateProgressText('頁面回到前台,繼續加載...');
  1210. Utils.log('自動恢復加載');
  1211. this.processNextItem();
  1212. }
  1213. }
  1214. });
  1215. },
  1216.  
  1217. // 加載所有頁面
  1218. loadAllPages() {
  1219. if (this.isLoading) {
  1220. UI.showToast('正在加載中,請等待...');
  1221. return;
  1222. }
  1223.  
  1224. // 初始化進度顯示
  1225. UI.toggleProgressContainer(true);
  1226. UI.updateProgressText('準備加載所有頁面...');
  1227. UI.updatePauseButtonIcon(false);
  1228.  
  1229. this.isLoading = true;
  1230. this.isStopped = false;
  1231. this.loadQueue = [];
  1232.  
  1233. // 重設加載狀態
  1234. LOADING_STATE.userPaused = false;
  1235. LOADING_STATE.backgroundPaused = false;
  1236. LOADING_STATE.processing = false;
  1237.  
  1238. // 重設統計
  1239. STATS.totalProcessed = 0;
  1240. STATS.totalAdded = 0;
  1241. STATS.totalFiltered = 0;
  1242.  
  1243. // 查找下一頁鏈接
  1244. const nextPageLink = document.querySelector('#unext');
  1245. if (!nextPageLink || nextPageLink.href === "javascript:void(0)") {
  1246. UI.updateProgressText('已經是最後一頁');
  1247. UI.showToast('已經是最後一頁');
  1248. this.isLoading = false;
  1249.  
  1250. // 3秒後隱藏進度條
  1251. setTimeout(() => {
  1252. UI.toggleProgressContainer(false);
  1253. }, 3000);
  1254.  
  1255. return;
  1256. }
  1257.  
  1258. // 添加第一個頁面到隊列
  1259. this.addPageToQueue(nextPageLink.href, true);
  1260.  
  1261. // 開始處理隊列
  1262. this.processNextItem();
  1263. },
  1264.  
  1265. // 添加頁面到隊列
  1266. addPageToQueue(pageUrl, recursive = false) {
  1267. this.loadQueue.push({
  1268. type: 'page',
  1269. url: pageUrl,
  1270. recursive: recursive
  1271. });
  1272. Utils.log(`頁面已添加到隊列: ${pageUrl}`);
  1273. },
  1274.  
  1275. // 添加行項目到隊列
  1276. addRowsToQueue(params) {
  1277. this.loadQueue.push({
  1278. type: 'rows',
  1279. ...params
  1280. });
  1281. Utils.log(`${params.rows.length} 行已添加到隊列`);
  1282. },
  1283.  
  1284. // 處理隊列中的下一個項目
  1285. async processNextItem() {
  1286. // 如果已停止或沒有正在加載,則退出
  1287. if (this.isStopped || !this.isLoading) {
  1288. return;
  1289. }
  1290.  
  1291. // 如果用戶暫停或後台暫停,則退出
  1292. if (LOADING_STATE.userPaused || (LOADING_STATE.backgroundPaused && !CONFIG.continueInBackground)) {
  1293. return;
  1294. }
  1295.  
  1296. // 如果正在處理項目,則退出
  1297. if (LOADING_STATE.processing) {
  1298. return;
  1299. }
  1300.  
  1301. // 如果隊列為空,則完成加載
  1302. if (this.loadQueue.length === 0) {
  1303. this.completeLoading();
  1304. return;
  1305. }
  1306.  
  1307. // 獲取隊列中的下一個項目
  1308. const nextItem = this.loadQueue.shift();
  1309.  
  1310. // 設置處理標記
  1311. LOADING_STATE.processing = true;
  1312.  
  1313. try {
  1314. if (nextItem.type === 'page') {
  1315. // 處理頁面項目
  1316. await this.processPageItem(nextItem);
  1317. } else if (nextItem.type === 'rows') {
  1318. // 處理行項目
  1319. await this.processRowsItem(nextItem);
  1320. }
  1321. } catch (error) {
  1322. console.error('處理項目失敗:', error);
  1323. UI.updateProgressText(`處理失敗: ${error.message}`);
  1324. UI.showToast(`處理失敗: ${error.message}`);
  1325. // 發生錯誤時仍然繼續處理其他項目
  1326. LOADING_STATE.processing = false;
  1327. this.processNextItem();
  1328. }
  1329. },
  1330.  
  1331. // 處理頁面項目
  1332. async processPageItem(item) {
  1333. const { url, recursive } = item;
  1334.  
  1335. STATS.currentPage++;
  1336. UI.updateProgressText(`正在加載第 ${STATS.currentPage} 頁...`);
  1337.  
  1338. try {
  1339. // 獲取頁面內容
  1340. const response = await fetch(url);
  1341. const html = await response.text();
  1342.  
  1343. const parser = new DOMParser();
  1344. const doc = parser.parseFromString(html, 'text/html');
  1345.  
  1346. // 獲取下一頁的表格
  1347. const nextPageTableBody = doc.querySelector('.itg.glte tbody');
  1348. if (!nextPageTableBody) {
  1349. throw new Error('無法解析頁面內容');
  1350. }
  1351.  
  1352. // 獲取下一頁中的行
  1353. const nextPageRows = Array.from(nextPageTableBody.rows);
  1354.  
  1355. // 獲取當前表格
  1356. const tableBody = Utils.getTableBody();
  1357. if (!tableBody) {
  1358. throw new Error('無法找到當前頁面的表格');
  1359. }
  1360.  
  1361. // 添加行項目到隊列
  1362. this.addRowsToQueue({
  1363. rows: nextPageRows,
  1364. tableBody: tableBody,
  1365. totalToProcess: nextPageRows.length,
  1366. processed: 0,
  1367. filtered: 0
  1368. });
  1369.  
  1370. // 檢查是否有下一頁
  1371. const nextPageUrl = this.getNextPageUrlFromDoc(doc);
  1372. if (nextPageUrl && recursive) {
  1373. this.addPageToQueue(nextPageUrl, true);
  1374. }
  1375.  
  1376. // 處理完成
  1377. LOADING_STATE.processing = false;
  1378. this.processNextItem();
  1379. } catch (error) {
  1380. LOADING_STATE.processing = false;
  1381. throw error;
  1382. }
  1383. },
  1384.  
  1385. // 處理行項目
  1386. async processRowsItem(item) {
  1387. const { rows, tableBody, totalToProcess } = item;
  1388. const records = Utils.getRecords();
  1389.  
  1390. let addedCount = item.processed || 0;
  1391. let filteredCount = item.filtered || 0;
  1392.  
  1393. try {
  1394. // 處理每一行
  1395. for (let i = 0; i < rows.length; i++) {
  1396. // 檢查是否已停止
  1397. if (this.isStopped) {
  1398. LOADING_STATE.processing = false;
  1399. this.isLoading = false;
  1400. return;
  1401. }
  1402.  
  1403. // 檢查是否暫停
  1404. if (LOADING_STATE.userPaused || (LOADING_STATE.backgroundPaused && !CONFIG.continueInBackground)) {
  1405. // 如果暫停,則將剩餘行重新加入隊列
  1406. const remainingRows = rows.slice(i);
  1407. this.loadQueue.unshift({
  1408. type: 'rows',
  1409. rows: remainingRows,
  1410. tableBody: tableBody,
  1411. totalToProcess: totalToProcess,
  1412. processed: addedCount,
  1413. filtered: filteredCount
  1414. });
  1415. LOADING_STATE.processing = false;
  1416. return;
  1417. }
  1418.  
  1419. const row = rows[i];
  1420.  
  1421. // 解析 ID
  1422. const link = row.querySelector('a');
  1423. if (!link) continue;
  1424.  
  1425. const id = Utils.getIdFromUrl(link.href);
  1426.  
  1427. // 檢查是否已記錄
  1428. const isAlreadyRecorded = records[id];
  1429.  
  1430. // 複製行並添加到表格
  1431. const clonedRow = row.cloneNode(true);
  1432. tableBody.appendChild(clonedRow);
  1433. addedCount++;
  1434. STATS.totalProcessed++;
  1435. STATS.totalAdded++;
  1436.  
  1437. // 如果是已記錄項目,設置高亮並可能隱藏
  1438. if (isAlreadyRecorded) {
  1439. clonedRow.classList.add('ex-record-highlighted');
  1440.  
  1441. // 添加記錄時間
  1442. const titleElement = clonedRow.querySelector('.gl4e');
  1443. if (titleElement && !titleElement.querySelector('.ex-record-time')) {
  1444. const timeSpan = document.createElement('span');
  1445. timeSpan.className = 'ex-record-time';
  1446. const timestamp = records[id].timestamp || records[id].t || '';
  1447. timeSpan.textContent = timestamp ? `記錄於: ${Utils.formatDate(timestamp)}` : '已記錄';
  1448. titleElement.appendChild(timeSpan);
  1449. }
  1450.  
  1451. // 根據當前狀態決定是否隱藏
  1452. if (CONFIG.autoHideRecorded) {
  1453. clonedRow.style.display = 'none';
  1454. filteredCount++;
  1455. STATS.totalFiltered++;
  1456. }
  1457. }
  1458.  
  1459. // 更新進度顯示
  1460. const percent = Math.round((i + 1) / totalToProcess * 100);
  1461. UI.updateProgress(
  1462. percent,
  1463. STATS.currentPage,
  1464. STATS.estimatedTotalPages,
  1465. STATS.totalProcessed
  1466. );
  1467.  
  1468. // 更新統計顯示
  1469. UI.updateStatsDisplay();
  1470.  
  1471. // 適當延遲以避免頁面凍結
  1472. if (i < rows.length - 1 && i % 10 === 0) {
  1473. await Utils.reliableDelay(10);
  1474. }
  1475. }
  1476.  
  1477. // 更新進度文本
  1478. UI.updateProgressText(`第 ${STATS.currentPage} 頁完成,已加載 ${addedCount} 項`);
  1479.  
  1480. // 添加延遲以避免請求過快
  1481. await Utils.reliableDelay(CONFIG.loadDelay);
  1482.  
  1483. // 處理完成
  1484. LOADING_STATE.processing = false;
  1485. this.processNextItem();
  1486. } catch (error) {
  1487. LOADING_STATE.processing = false;
  1488. throw error;
  1489. }
  1490. },
  1491.  
  1492. // 完成加載
  1493. completeLoading() {
  1494. this.isLoading = false;
  1495. UI.updateProgressText(`加載完成,共處理 ${STATS.totalProcessed} 項,新增 ${STATS.totalAdded} 項`);
  1496. UI.showToast(`加載完成,共處理 ${STATS.totalProcessed} 項,新增 ${STATS.totalAdded} 項`);
  1497.  
  1498. // 3秒後隱藏進度條
  1499. setTimeout(() => {
  1500. UI.toggleProgressContainer(false);
  1501. }, 3000);
  1502. },
  1503.  
  1504. // 從文檔中獲取下一頁URL
  1505. getNextPageUrlFromDoc(doc) {
  1506. const nextPageLink = doc.querySelector('#unext');
  1507. if (nextPageLink && nextPageLink.href && nextPageLink.href !== "javascript:void(0)") {
  1508. return nextPageLink.href;
  1509. }
  1510. return null;
  1511. },
  1512.  
  1513. // 停止加載
  1514. stopLoading() {
  1515. this.isStopped = true;
  1516. this.isLoading = false;
  1517. this.loadQueue = [];
  1518. LOADING_STATE.processing = false;
  1519. LOADING_STATE.userPaused = false;
  1520. LOADING_STATE.backgroundPaused = false;
  1521. }
  1522. };
  1523.  
  1524. // 數據管理
  1525. const DataManager = {
  1526. // 匯出記錄
  1527. exportRecords() {
  1528. const records = Utils.getRecords();
  1529. const exportData = JSON.stringify(records, null, 2);
  1530.  
  1531. const modalContent = `
  1532. <p>以下是您的記錄資料,請複製並保存:</p>
  1533. <textarea readonly>${exportData}</textarea>
  1534. `;
  1535.  
  1536. const buttons = [
  1537. { id: 'ex-record-copy-btn', text: '複製', primary: true },
  1538. { id: 'ex-record-modal-close-btn', text: '關閉', primary: false }
  1539. ];
  1540.  
  1541. const modal = UI.createModal('匯出記錄', modalContent, buttons);
  1542.  
  1543. document.getElementById('ex-record-copy-btn').addEventListener('click', () => {
  1544. const textarea = modal.querySelector('textarea');
  1545. if (textarea) {
  1546. textarea.select();
  1547. document.execCommand('copy');
  1548. UI.showToast('已複製到剪貼簿');
  1549. }
  1550. });
  1551.  
  1552. document.getElementById('ex-record-modal-close-btn').addEventListener('click', () => {
  1553. document.body.removeChild(modal);
  1554. });
  1555. },
  1556.  
  1557. // 匯入記錄
  1558. importRecords() {
  1559. const modalContent = `
  1560. <p>請貼上之前匯出的記錄資料:</p>
  1561. <textarea placeholder="在這裡貼上 JSON 格式的記錄資料..."></textarea>
  1562. `;
  1563.  
  1564. const buttons = [
  1565. { id: 'ex-record-import-btn', text: '匯入', primary: true },
  1566. { id: 'ex-record-modal-close-btn', text: '取消', primary: false }
  1567. ];
  1568.  
  1569. const modal = UI.createModal('匯入記錄', modalContent, buttons);
  1570.  
  1571. document.getElementById('ex-record-import-btn').addEventListener('click', () => {
  1572. const textarea = modal.querySelector('textarea');
  1573. if (!textarea) return;
  1574.  
  1575. try {
  1576. const importData = JSON.parse(textarea.value);
  1577. const currentRecords = Utils.getRecords();
  1578.  
  1579. // 合併記錄
  1580. const mergedRecords = { ...currentRecords, ...importData };
  1581.  
  1582. if (Utils.saveRecords(mergedRecords)) {
  1583. Record.highlightRecorded();
  1584. UI.updateStatsDisplay();
  1585. UI.showToast(`匯入成功,共 ${Object.keys(mergedRecords).length} 筆記錄`);
  1586. } else {
  1587. UI.showToast('匯入失敗:保存記錄時出錯');
  1588. }
  1589.  
  1590. document.body.removeChild(modal);
  1591. } catch (error) {
  1592. UI.showToast(`匯入失敗:${error.message}`);
  1593. }
  1594. });
  1595.  
  1596. document.getElementById('ex-record-modal-close-btn').addEventListener('click', () => {
  1597. document.body.removeChild(modal);
  1598. });
  1599. }
  1600. };
  1601.  
  1602. // 檢查舊數據格式並轉換
  1603. function migrateOldData() {
  1604. const oldRecordStr = localStorage.getItem("record");
  1605. if (oldRecordStr) {
  1606. try {
  1607. const oldIds = oldRecordStr.split(",").filter(id => id.trim() !== '');
  1608. const newRecords = Utils.getRecords();
  1609. const now = new Date().toISOString();
  1610.  
  1611. for (let i = 0; i < oldIds.length; i++) {
  1612. const id = oldIds[i];
  1613. if (id && !newRecords[id]) {
  1614. newRecords[id] = { t: now };
  1615. }
  1616. }
  1617.  
  1618. Utils.saveRecords(newRecords);
  1619. localStorage.removeItem("record");
  1620. UI.showToast("已轉換舊格式記錄");
  1621. } catch (e) {
  1622. console.error('轉換舊記錄失敗:', e);
  1623. }
  1624. }
  1625. }
  1626.  
  1627. // 初始化函數
  1628. function init() {
  1629. console.log('初始化 Enhanced Exhentai Record 腳本...');
  1630.  
  1631. // 添加樣式
  1632. UI.addStyles();
  1633.  
  1634. // 轉換舊數據
  1635. migrateOldData();
  1636.  
  1637. if (Utils.getTableBody()) {
  1638. // 創建控制面板
  1639. UI.createControlPanel();
  1640.  
  1641. // 創建進度容器
  1642. UI.createProgressContainer();
  1643. UI.toggleProgressContainer(false); // 默認隱藏
  1644.  
  1645. // 標記已記錄的項目
  1646. Record.highlightRecorded();
  1647.  
  1648. // 更新統計信息
  1649. UI.updateStatsDisplay();
  1650.  
  1651. // 初始化頁面加載器
  1652. PageLoader.init();
  1653.  
  1654. // 默認隱藏已記錄的項目
  1655. if (CONFIG.autoHideRecorded) {
  1656. const hiddenCount = Record.hideRecordedItems();
  1657. if (hiddenCount > 0) {
  1658. UI.showToast(`已隱藏 ${hiddenCount} 筆已記錄的內容`);
  1659. }
  1660. }
  1661.  
  1662. // 添加滾動事件來監控閱讀進度
  1663. window.addEventListener('scroll', Utils.debounce(() => {
  1664. Utils.updateReadingProgress();
  1665. }, 200));
  1666. } else {
  1667. console.log('找不到作品表格,可能不在正確的頁面');
  1668. }
  1669. }
  1670.  
  1671. // 確保頁面載入完成後執行初始化
  1672. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  1673. setTimeout(init, 1000);
  1674. } else {
  1675. document.addEventListener('DOMContentLoaded', () => {
  1676. setTimeout(init, 1000);
  1677. });
  1678. }
  1679.  
  1680. // 確保初始化執行
  1681. setTimeout(() => {
  1682. if (!document.querySelector('.ex-record-toolbar')) {
  1683. init();
  1684. }
  1685. }, 2000);
  1686. })();