[Refactored] v2.4.0 - 支持JSON批量导入与持续添加,引入avIDs V2结构(ID/评分/时间),集成JavDB已阅高亮与同步。
// ==UserScript== // @name um-98t-list-preview // @description [Refactored] v2.4.0 - 支持JSON批量导入与持续添加,引入avIDs V2结构(ID/评分/时间),集成JavDB已阅高亮与同步。 // @version 2.5.0 // @icon https://www.google.com/s2/favicons?sz=64&domain=www.sehuatang.net // @author UnforgetMemory // @namespace https://www.sehuatang.net/ // @match https://www.sehuatang.net/forum* // @match https://www.sehuatang.org/forum* // @match https://www.sehuatang.net/forum.php?mod=forumdisplay&fid=103&page=* // @match https://www.sehuatang.org/forum.php?mod=forumdisplay&fid=103&page=* // @match https://javdb.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @require https://cdn.jsdelivr.net/npm/[email protected]/i18next.min.js // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @license GNU GPLv3 // ==/UserScript== (function () { "use strict"; // --- CONFIGURATION --- const CONFIG = { AVID_REGEX: /[a-zA-Z]{2,6}[-\s]?\d{2,5}/gi, JAVDB_HOST: "javdb.com", HTTP_HEADERS: { "User-Agent": window.navigator.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", Cookie: document.cookie, }, LOCAL_STORAGE_KEYS: { VIEWED_AVIDS: "68905cf391b2428572e6446042ab1029", VIEWED_US_TITLES: "abba24c58fc69bf0955bddc7a0eadee1", HIDE_VIEWED_MODE: "780fbed5c332f7f96ca73e19e94a9749", JIANGUOYUN: "97f6483755d45ad927caf3108b61be91", LOCALE: "ee2757153264e82a1c8f64db8ddcb3e2", }, JIANGUOYUN: { AVIDS_FILENAME: "95551967d3da7c5af36b141f630683c4", US_FILENAME: "53648e7622cfa657f1f6de856efd67c9", }, LOCALES: { enUS: "en-US", zhCN: "zh-CN", zhHK: "zh-HK", zhTW: "zh-TW" }, }; // --- GLOBAL STATE --- const STATE = { totalTasks: 0, finishedTasks: 0, processedIds: new Set(), filteredCount: 0, isJavDB: location.hostname.includes("javdb"), }; // --- I18N INITIALIZATION --- i18next.init({ lng: GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.LOCALE, CONFIG.LOCALES.enUS), fallbackLng: CONFIG.LOCALES.enUS, resources: { "en-US": { translation: { Language: "🕮 Language", "Hide Viewed": "Hide Viewed", "Manual Add": "📝 Manual Add", "Check Viewed Status": "🔍 Check Viewed Status", Jianguoyun: "☁️ Jianguoyun", "Jianguoyun Config": "☁️ Jianguoyun Config", "Upload To Jianguoyun": "↑ Upload To ☁️ Jianguoyun", "Download from Jianguoyun": "↓ Download from ☁️ Jianguoyun", "Local and Jianguoyun merge": "🔄 Local and ☁️ Jianguoyun merge", "DAV URL": "☁️ DAV URL", Account: "👤 Account", Password: "🔑 Password", Save: "Save", Close: "Close", "Save Successful!": "Save Successful!", "Upload successful!": "Upload successful!", "Sync successful!": "Sync successful!", "Download successful!": "Download successful!", "Check Config!": "Check Config!", "Update Bad": "Update Bad", "Bad Download": "Bad Download Data", "Viewed Total": "Viewed Total", click_all_magnet: "Copy All Magnet Links", copy_btn_loading: "Loading... ({{loaded}}/{{total}})", copy_btn_ready: "⚡ Copy Magnets ({{count}})", copy_done: "Copied {{count}} magnet links!", no_new_magnets: "No new magnet links found.", header_info: "Hidden: {{hidden}} | History: {{total}}", javdb_detected: "JavDB detected: {{count}} viewed items dimmed.", panel_title: "Add Record", panel_id_placeholder: "ID or JSON [{id,rating,time}...]", panel_rating_label: "Rating (0-10):", added_msg: "Added {{count}} items.", invalid_json: "Invalid JSON format", check_viewed_title: "Check Viewed Status", check_viewed_id_placeholder: "Enter AV ID", check_viewed_check_btn: "Check", check_viewed_status: "Status", check_viewed_not_viewed: "Not Viewed", check_viewed_viewed_on: "Viewed on", check_viewed_rating: "Rating", }, }, "zh-CN": { translation: { Language: "🕮 语言(简体)", "Hide Viewed": "隐藏已阅", "Manual Add": "📝 手动添加记录", "Check Viewed Status": "🔍 查询已阅状态", Jianguoyun: "☁️ 坚果云", "Jianguoyun Config": "☁️ 坚果云配置", "Upload To Jianguoyun": "↑ 上传至 ☁️ 坚果云", "Download from Jianguoyun": "从 ☁️ 坚果云 ↓ 下载", "Local and Jianguoyun merge": "🔄 双端同步 ☁️ 坚果云 ", "DAV URL": "☁️ DAV URL", Account: "👤 账号", Password: "🔑 密码", Save: "保存", Close: "关闭", "Save Successful!": "保存成功!", "Upload successful!": "上传成功!", "Sync successful!": "同步成功!", "Download successful!": "下载完成!", "Check Config!": "检查配置!", "Update Bad": "更新失败", "Bad Download": "下载数据出错", "Viewed Total": "浏览量", click_all_magnet: "复制所有磁力", copy_btn_loading: "加载中... ({{loaded}}/{{total}})", copy_btn_ready: "⚡ 一键复制磁力 ({{count}})", copy_done: "已复制 {{count}} 个磁力链接!", no_new_magnets: "未发现新的磁力链接", header_info: "本页隐藏: {{hidden}} | 历史总阅: {{total}}", javdb_detected: "JavDB检测: 已淡化 {{count}} 个已阅条目。", panel_title: "添加浏览记录", panel_id_placeholder: "输入番号 或 JSON数组", panel_rating_label: "评分 (0-10):", added_msg: "已添加 {{count}} 条记录", invalid_json: "无效的 JSON 格式", check_viewed_title: "查询已阅状态", check_viewed_id_placeholder: "输入 AV 番号", check_viewed_check_btn: "查询", check_viewed_status: "状态", check_viewed_not_viewed: "未阅", check_viewed_viewed_on: "已阅于", check_viewed_rating: "评分", }, }, }, }); // --- STYLES --- GM_addStyle(` :root { --bg-color: #121212; --card-bg-color: #1e1e1e; --text-color: #e0e0e0; --accent-color: #03dac6; --border-color: #333333; } /* Sehuatang Styles */ #filtered-info-bar { background-color: var(--card-bg-color); color: #a0a0a0; padding: 10px 25px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 1000; } .action-btn { background-color: var(--accent-color); color: #000; border: none; padding: 6px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; } .action-btn:disabled { background-color: #3a3a3a; color: #888; } #modern-preview-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 25px; padding: 25px; } .preview-card { background-color: var(--card-bg-color); border-radius: 12px; border: 1px solid var(--border-color); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.4); transition: transform 0.3s ease; } .preview-card:hover { transform: translateY(-8px); } .preview-card.viewed { opacity: 0.6; } .preview-card.viewed:hover { opacity: 1; } .card-image-container { aspect-ratio: 16 / 10; background-color: #2a2a2a; overflow: hidden; position: relative; } .card-image-container img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; } .preview-card:hover .card-image-container img { transform: scale(1.05); } .card-content { padding: 15px; display: flex; flex-direction: column; flex-grow: 1; } .card-title a { color: var(--text-color); text-decoration: none; font-size: 1.1rem; font-weight: 600; } .card-links { margin-top: auto; display: flex; justify-content: flex-end; gap: 15px; } .card-links a { font-size: 1.5rem; padding: 6px 12px; border-radius: 8px; text-decoration: none; } .magnet-link { background-color: #443b17; color: #ffc107; } .torrent-link { background-color: #1c3a1e; color: #4caf50; } /* JavDB Specific Styles */ body.javdb-enhanced .item.viewed { opacity: 0.3 !important; transition: opacity 0.3s ease-in-out; filter: grayscale(80%); } body.javdb-enhanced .item.viewed:hover { opacity: 1 !important; filter: grayscale(0%); } body.javdb-enhanced .item.viewed::after { content: "👁"; position: absolute; top: 5px; right: 5px; font-size: 1.2rem; background: rgba(0,0,0,0.5); border-radius: 50%; padding: 2px; color: #03dac6; pointer-events: none; z-index: 10; } /* Manual Add Panel Styles */ #um-manual-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; justify-content: center; align-items: center; } #um-manual-panel { background: var(--card-bg-color); padding: 25px; border-radius: 12px; border: 1px solid var(--border-color); width: 400px; display: flex; flex-direction: column; gap: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); } #um-manual-panel h3 { margin: 0; color: var(--accent-color); text-align: center; } .um-input-group { display: flex; flex-direction: column; gap: 5px; } .um-input-group label { font-size: 0.9rem; color: #aaa; } .um-input { background: #2a2a2a; border: 1px solid #444; color: white; padding: 10px; border-radius: 6px; outline: none; } .um-input:focus { border-color: var(--accent-color); } .um-btn-row { display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px; } .um-btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-weight: bold; } .um-btn-primary { background: var(--accent-color); color: #000; } .um-btn-secondary { background: #444; color: #ccc; } /* Check Viewed Panel Styles */ #um-check-viewed-panel { position: fixed; top: 50px; left: 50%; transform: translateX(-50%); background: var(--card-bg-color); padding: 20px; border-radius: 12px; border: 1px solid var(--border-color); width: 350px; display: flex; flex-direction: column; gap: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 10001; cursor: move; } #um-check-viewed-panel h3 { margin: 0; color: var(--accent-color); text-align: center; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); } #um-check-viewed-panel .um-input-group { flex-direction: row; align-items: center; } #um-check-viewed-panel .um-input { flex-grow: 1; } #um-check-viewed-panel .um-btn-primary { margin-left: 10px; } #um-check-viewed-result { margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border-color); } .um-result-row { display: flex; justify-content: space-between; padding: 5px 0; } .um-result-label { font-weight: bold; color: #aaa; } .um-result-value { color: var(--text-color); } .um-result-value.not-viewed { color: #f44336; } .um-result-value.viewed { color: #4caf50; } #um-check-viewed-close { position: absolute; top: 10px; right: 10px; background: none; border: none; color: #aaa; font-size: 20px; cursor: pointer; } `); // --- STORAGE HELPERS --- const gmFetch = (details) => new Promise((resolve, reject) => { details.onload = resolve; details.onerror = reject; details.ontimeout = reject; GM_xmlhttpRequest(details); }); const getJsonValue = (key, defaultValue = "[]") => JSON.parse(GM_getValue(key, defaultValue)); const setJsonValue = (key, value) => GM_setValue(key, JSON.stringify(value)); const showToast = (title, icon = "success") => Swal.fire({ toast: true, position: "top-end", showConfirmButton: false, timer: 3000, title, icon, }); // --- TIME UTILS --- const normalizeTime = (inputTime) => { if (!inputTime) return new Date().toISOString(); try { const d = new Date(inputTime); if (isNaN(d.getTime())) { console.warn("Invalid time format:", inputTime, "Using current time."); return new Date().toISOString(); } return d.toISOString(); } catch (e) { return new Date().toISOString(); } }; /** * Data Structure V2: * Array of Objects: { id: string, rating: number (0-10), updatedAt: string (ISO UTC) } */ const viewedAVIDs = { // 获取所有数据,包含自动迁移逻辑 getAll: () => { let data = getJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS); // Migration Logic: Convert Array<string> to Array<Object> if ( Array.isArray(data) && data.length > 0 && typeof data[0] === "string" ) { const now = new Date().toISOString(); data = data.map((id) => ({ id: id.toUpperCase(), rating: 0, updatedAt: now, })); setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS, data); console.log("Migration to avIDsV2 completed."); } return data; }, // 检查是否存在 has: (id) => { if (!id) return false; const cleanId = id.toUpperCase(); return viewedAVIDs.getAll().some((item) => item.id === cleanId); }, // 批量添加 (Batch Add) - 核心修改 batchAdd: (items) => { if (!Array.isArray(items) || items.length === 0) return 0; const currentList = viewedAVIDs.getAll(); const map = new Map(); currentList.forEach((item) => map.set(item.id, item)); let addedCount = 0; items.forEach((newItem) => { if (!newItem.id) return; const cleanId = newItem.id.toUpperCase().trim(); const cleanRating = parseInt(newItem.rating); const finalRating = isNaN(cleanRating) ? 0 : cleanRating; const finalTime = normalizeTime(newItem.time); const existing = map.get(cleanId); if (existing) { existing.updatedAt = finalTime; if (finalRating > 0) existing.rating = finalRating; } else { map.set(cleanId, { id: cleanId, rating: finalRating, updatedAt: finalTime, }); } addedCount++; }); setJsonValue( CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS, Array.from(map.values()) ); return addedCount; }, // 单个添加 (保留兼容性,底层调用batch) add: (id, rating = 0) => { viewedAVIDs.batchAdd([{ id, rating, time: new Date().toISOString() }]); }, // 重置 (通常用于覆盖) reset: (data) => setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_AVIDS, data), // 合并 (处理 V1 字符串数组和 V2 对象数组的混合情况) merge: (newData) => { const normalized = newData.map((item) => { if (typeof item === "string") { return { id: item, rating: 0 }; } return item; }); viewedAVIDs.batchAdd(normalized); }, }; const viewedUSTitles = { list: () => getJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_US_TITLES), has: (title) => viewedUSTitles.list().includes(title), add: (title) => { const c = viewedUSTitles.list(); if (!c.includes(title)) setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_US_TITLES, [...c, title]); }, reset: (titles) => setJsonValue(CONFIG.LOCAL_STORAGE_KEYS.VIEWED_US_TITLES, titles), merge: (newTitles) => viewedUSTitles.reset(_.union(viewedUSTitles.list(), newTitles)), }; // --- UI COMPONENTS --- function createManualPanel() { if (document.getElementById("um-manual-overlay")) return; const overlay = document.createElement("div"); overlay.id = "um-manual-overlay"; const panel = document.createElement("div"); panel.id = "um-manual-panel"; const title = document.createElement("h3"); title.innerText = i18next.t("panel_title"); panel.appendChild(title); const idGroup = document.createElement("div"); idGroup.className = "um-input-group"; const idInput = document.createElement("input"); idInput.type = "text"; idInput.className = "um-input"; idInput.placeholder = i18next.t("panel_id_placeholder"); idInput.onkeydown = (e) => { if (e.key === "Enter") saveBtn.click(); if (e.key === "Escape") overlay.remove(); }; idGroup.appendChild(idInput); panel.appendChild(idGroup); const rateGroup = document.createElement("div"); rateGroup.className = "um-input-group"; const rateLabel = document.createElement("label"); rateLabel.innerText = i18next.t("panel_rating_label"); rateGroup.appendChild(rateLabel); const rateSelect = document.createElement("select"); rateSelect.className = "um-input"; for (let i = 0; i <= 10; i++) { const opt = document.createElement("option"); opt.value = i; opt.innerText = i; if (i === 5) opt.selected = true; rateSelect.appendChild(opt); } rateGroup.appendChild(rateSelect); panel.appendChild(rateGroup); const btnRow = document.createElement("div"); btnRow.className = "um-btn-row"; const closeBtn = document.createElement("button"); closeBtn.className = "um-btn um-btn-secondary"; closeBtn.innerText = i18next.t("Close"); closeBtn.onclick = () => overlay.remove(); const saveBtn = document.createElement("button"); saveBtn.className = "um-btn um-btn-primary"; saveBtn.innerText = i18next.t("Save"); saveBtn.onclick = () => { const val = idInput.value.trim(); if (!val) return; let processedCount = 0; let isJson = false; if (val.startsWith("[") && val.endsWith("]")) { try { const parsed = JSON.parse(val); if (Array.isArray(parsed)) { isJson = true; processedCount = viewedAVIDs.batchAdd(parsed); } } catch (e) { showToast(i18next.t("invalid_json"), "error"); return; } } if (!isJson) { viewedAVIDs.add(val, parseInt(rateSelect.value)); processedCount = 1; } if (processedCount > 0) { showToast(i18next.t("added_msg", { count: processedCount })); idInput.value = ""; idInput.focus(); if (STATE.isJavDB) handleJavDB(); else updateSehuatangHeader(); } }; btnRow.appendChild(closeBtn); btnRow.appendChild(saveBtn); panel.appendChild(btnRow); overlay.appendChild(panel); document.body.appendChild(overlay); setTimeout(() => idInput.focus(), 100); overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); }); } function createCheckViewedPanel() { if (document.getElementById("um-check-viewed-panel")) return; const panel = document.createElement("div"); panel.id = "um-check-viewed-panel"; const title = document.createElement("h3"); title.innerText = i18next.t("check_viewed_title"); panel.appendChild(title); const closeBtn = document.createElement("button"); closeBtn.id = "um-check-viewed-close"; closeBtn.innerHTML = "×"; closeBtn.onclick = () => panel.remove(); panel.appendChild(closeBtn); const inputGroup = document.createElement("div"); inputGroup.className = "um-input-group"; const idInput = document.createElement("input"); idInput.type = "text"; idInput.className = "um-input"; idInput.placeholder = i18next.t("check_viewed_id_placeholder"); const checkBtn = document.createElement("button"); checkBtn.className = "um-btn um-btn-primary"; checkBtn.innerText = i18next.t("check_viewed_check_btn"); inputGroup.appendChild(idInput); inputGroup.appendChild(checkBtn); panel.appendChild(inputGroup); const resultDiv = document.createElement("div"); resultDiv.id = "um-check-viewed-result"; panel.appendChild(resultDiv); const checkAvid = () => { const avid = idInput.value.trim().toUpperCase(); if (!avid) return; const allViewed = viewedAVIDs.getAll(); const viewedItem = allViewed.find((item) => item.id === avid); resultDiv.innerHTML = ""; // Clear previous results const statusRow = document.createElement("div"); statusRow.className = "um-result-row"; statusRow.innerHTML = `<span class="um-result-label">${i18next.t( "check_viewed_status" )}</span>`; const statusValue = document.createElement("span"); statusValue.className = "um-result-value"; if (viewedItem) { statusValue.innerText = i18next.t("check_viewed_viewed_on"); statusValue.classList.add("viewed"); statusRow.appendChild(statusValue); resultDiv.appendChild(statusRow); const dateRow = document.createElement("div"); dateRow.className = "um-result-row"; const formattedDate = new Date( viewedItem.updatedAt ).toLocaleDateString(); dateRow.innerHTML = `<span class="um-result-label"></span><span class="um-result-value">${formattedDate}</span>`; resultDiv.appendChild(dateRow); const ratingRow = document.createElement("div"); ratingRow.className = "um-result-row"; ratingRow.innerHTML = `<span class="um-result-label">${i18next.t( "check_viewed_rating" )}</span><span class="um-result-value">${ viewedItem.rating } / 10</span>`; resultDiv.appendChild(ratingRow); } else { statusValue.innerText = i18next.t("check_viewed_not_viewed"); statusValue.classList.add("not-viewed"); statusRow.appendChild(statusValue); resultDiv.appendChild(statusRow); } }; checkBtn.onclick = checkAvid; idInput.onkeydown = (e) => { if (e.key === "Enter") checkAvid(); }; document.body.appendChild(panel); idInput.focus(); // Make the panel draggable let isDragging = false; let offsetX, offsetY; panel.addEventListener("mousedown", (e) => { isDragging = true; offsetX = e.clientX - panel.getBoundingClientRect().left; offsetY = e.clientY - panel.getBoundingClientRect().top; panel.style.userSelect = "none"; }); document.addEventListener("mousemove", (e) => { if (isDragging) { panel.style.left = `${e.clientX - offsetX}px`; panel.style.top = `${e.clientY - offsetY}px`; panel.style.transform = "none"; // Disable transform when dragging } }); document.addEventListener("mouseup", () => { isDragging = false; panel.style.userSelect = ""; }); } // --- JIANGUOYUN SYNC CLASS --- class JianguoyunClient { constructor() { const config = getJsonValue(CONFIG.LOCAL_STORAGE_KEYS.JIANGUOYUN, "{}"); this.davURL = config.url; this.auth = config.account && config.password ? `Basic ${btoa(`${config.account}:${config.password}`)}` : null; } isValid() { return !!(this.davURL && this.auth); } async request(method, fileName, data = null) { if (!this.isValid()) return Promise.reject("Bad Jianguoyun Config!"); const url = `${this.davURL}/${fileName}.json`.replace( /(?<!:)\/{2,}/g, "/" ); return await gmFetch({ method, url, data, headers: { Authorization: this.auth }, timeout: 5000, }); } async download(fileName) { return this.request("GET", fileName); } async upload(fileName, data) { return this.request("PUT", fileName, JSON.stringify(data)); } } // --- SEHUATANG LOGIC --- function updateSehuatangHeader() { const btn = document.getElementById("header-copy-btn"); const infoText = document.getElementById("header-info-text"); if (btn) { if (STATE.totalTasks > 0 && STATE.finishedTasks >= STATE.totalTasks) { const magnets = document.querySelectorAll( ".preview-card:not(.viewed) .magnet-link" ).length; btn.disabled = false; btn.textContent = i18next.t("copy_btn_ready", { count: magnets }); } else { btn.disabled = true; btn.textContent = i18next.t("copy_btn_loading", { loaded: STATE.finishedTasks, total: STATE.totalTasks, }); } } if (infoText) { infoText.textContent = i18next.t("header_info", { hidden: STATE.filteredCount, total: viewedAVIDs.getAll().length, }); } } async function createSehuatangCard(info, container) { const isViewed = info.avId ? viewedAVIDs.has(info.avId) : viewedUSTitles.has(info.fullTitle); if (GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE) && isViewed) return; STATE.totalTasks++; updateSehuatangHeader(); const card = document.createElement("div"); card.className = "preview-card"; if (info.avId) card.dataset.avid = info.avId; card.dataset.fullTitle = info.fullTitle; if (isViewed) card.classList.add("viewed"); card.innerHTML = ` <div class="card-image-container"></div> <div class="card-content"> <h3 class="card-title"><a href="${info.url}" target="_blank">${info.fullTitle}</a></h3> <p class="card-meta">${info.releaseDate}</p> <div class="card-links"></div> </div>`; container.appendChild(card); const markAsViewed = () => { if (info.avId) viewedAVIDs.add(info.avId, 0); // Default 0 for auto-add else viewedUSTitles.add(info.fullTitle); card.classList.add("viewed"); updateSehuatangHeader(); }; try { const res = await gmFetch({ method: "GET", url: info.url, headers: CONFIG.HTTP_HEADERS, }); const doc = new DOMParser().parseFromString( res.responseText, "text/html" ); const imgFile = doc .querySelector("ignore_js_op > img") ?.getAttribute("zoomfile"); const magnetLink = doc.querySelector( ".blockcode > div > ol > li" )?.innerText; const imgContainer = card.querySelector(".card-image-container"); if (imgFile) { const img = document.createElement("img"); img.src = imgFile; img.onclick = () => { window.open(info.url, "_blank"); markAsViewed(); }; imgContainer.appendChild(img); } else { imgContainer.innerText = "No Image"; imgContainer.style.color = "#666"; imgContainer.style.display = "flex"; imgContainer.style.alignItems = "center"; imgContainer.style.justifyContent = "center"; } if (magnetLink) { const a = document.createElement("a"); a.href = magnetLink; a.className = "magnet-link"; a.textContent = "⚡"; a.title = "Copy Magnet"; a.onclick = markAsViewed; card.querySelector(".card-links").appendChild(a); } } catch (e) { console.error(e); } finally { STATE.finishedTasks++; updateSehuatangHeader(); } } function handleSehuatang() { const threadListTable = document.getElementById("threadlisttableid"); if (!threadListTable) return; threadListTable.style.display = "none"; // Header Construction const infoBar = document.createElement("div"); infoBar.id = "filtered-info-bar"; const infoText = document.createElement("div"); infoText.id = "header-info-text"; infoText.className = "info-text"; infoBar.appendChild(infoText); const actionsDiv = document.createElement("div"); const copyBtn = document.createElement("button"); copyBtn.id = "header-copy-btn"; copyBtn.className = "action-btn"; copyBtn.onclick = () => { const links = Array.from( document.querySelectorAll(".preview-card:not(.viewed) .magnet-link") ); if (links.length) { GM_setClipboard(links.map((l) => l.href).join("\r\n")); links.forEach((l) => { const card = l.closest(".preview-card"); if (card.dataset.avid) viewedAVIDs.add(card.dataset.avid, 0); card.classList.add("viewed"); }); showToast(i18next.t("copy_done", { count: links.length })); updateSehuatangHeader(); } else { showToast(i18next.t("no_new_magnets"), "info"); } }; actionsDiv.appendChild(copyBtn); infoBar.appendChild(actionsDiv); const container = document.createElement("div"); container.id = "modern-preview-container"; document.body.prepend(container); document.body.prepend(infoBar); // Initial Count if (GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE)) { threadListTable .querySelectorAll('tbody[id^="normalthread_"]') .forEach((el) => { const title = el.querySelector("th a.s.xst")?.innerText.trim(); const avid = title?.match(CONFIG.AVID_REGEX)?.[0]?.toUpperCase(); if ( (avid && viewedAVIDs.has(avid)) || (title && viewedUSTitles.has(title)) ) STATE.filteredCount++; }); } updateSehuatangHeader(); // Process Loop const run = () => { threadListTable .querySelectorAll('tbody[id^="normalthread_"]:not([data-processed])') .forEach((el) => { el.dataset.processed = "true"; const linkEl = el.querySelector("th a.s.xst"); if (!linkEl) return; const title = linkEl.innerText.trim(); const avIdMatch = title.match(CONFIG.AVID_REGEX); const info = { url: linkEl.href, fullTitle: title, avId: avIdMatch ? avIdMatch[0].toUpperCase() : null, releaseDate: el.querySelector("td.by em span")?.innerText || "N/A", }; createSehuatangCard(info, container); }); }; _.debounce(run, 300)(); new MutationObserver(_.debounce(run, 500)).observe(threadListTable, { childList: true, subtree: true, }); } // --- JAVDB LOGIC --- function handleJavDB() { console.log("JavDB Enhanced Mode Activated"); document.body.classList.add("javdb-enhanced"); const processItem = (item) => { if (item.dataset.processed) return; const titleStrong = item.querySelector(".video-title strong"); if (!titleStrong) return; const avid = titleStrong.textContent.trim().toUpperCase(); if (!avid) return; item.dataset.processed = "true"; item.dataset.avid = avid; if (viewedAVIDs.has(avid)) { item.classList.add("viewed"); } item.addEventListener("click", (e) => { viewedAVIDs.add(avid, 0); item.classList.add("viewed"); }); }; const run = () => { const items = document.querySelectorAll(".item, .grid-item"); items.forEach(processItem); }; run(); const observer = new MutationObserver((mutations) => { let shouldRun = false; mutations.forEach((m) => { if (m.addedNodes.length > 0) shouldRun = true; }); if (shouldRun) run(); }); const container = document.querySelector(".movie-list") || document.body; observer.observe(container, { childList: true, subtree: true }); } // --- MENU & MAIN --- function setupMenu() { GM_registerMenuCommand(i18next.t("Manual Add"), createManualPanel); GM_registerMenuCommand( i18next.t("Check Viewed Status"), createCheckViewedPanel ); GM_registerMenuCommand(i18next.t("Language"), () => { // Toggle logic or prompt not fully implemented in snippet }); GM_registerMenuCommand( `${ GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE) ? "✅" : "❌" } ${i18next.t("Hide Viewed")}`, () => { GM_setValue( CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE, !GM_getValue(CONFIG.LOCAL_STORAGE_KEYS.HIDE_VIEWED_MODE) ); location.reload(); } ); GM_registerMenuCommand( `${i18next.t("Viewed Total")}: ${viewedAVIDs.getAll().length}`, () => { showToast( `${i18next.t("Viewed Total")}: ${viewedAVIDs.getAll().length}`, "info" ); } ); // Jianguoyun commands const jgyCmds = [ { name: "Jianguoyun Config", action: async () => { // Implementation omitted }, }, { name: "Upload To Jianguoyun", action: async () => { const jgy = new JianguoyunClient(); if (!jgy.isValid()) return showToast(i18next.t("Check Config!"), "error"); try { await jgy.upload( CONFIG.JIANGUOYUN.AVIDS_FILENAME, viewedAVIDs.getAll() ); await jgy.upload( CONFIG.JIANGUOYUN.US_FILENAME, viewedUSTitles.list() ); showToast(i18next.t("Upload successful!")); } catch (e) { showToast(i18next.t("Update Bad"), "error"); } }, }, { name: "Download from Jianguoyun", action: async () => { const jgy = new JianguoyunClient(); if (!jgy.isValid()) return showToast(i18next.t("Check Config!"), "error"); try { const res = await jgy.download(CONFIG.JIANGUOYUN.AVIDS_FILENAME); if (res.status === 200) viewedAVIDs.merge(JSON.parse(res.responseText)); showToast(i18next.t("Download successful!")); setTimeout(() => location.reload(), 1000); } catch (e) { showToast(i18next.t("Bad Download"), "error"); } }, }, { name: "Local and Jianguoyun merge", action: async () => { const jgy = new JianguoyunClient(); if (!jgy.isValid()) return showToast(i18next.t("Check Config!"), "error"); try { const r1 = await jgy.download(CONFIG.JIANGUOYUN.AVIDS_FILENAME); const c1 = r1.status === 200 ? JSON.parse(r1.responseText) : []; viewedAVIDs.merge(c1); await jgy.upload( CONFIG.JIANGUOYUN.AVIDS_FILENAME, viewedAVIDs.getAll() ); showToast(i18next.t("Sync successful!")); setTimeout(() => location.reload(), 1000); } catch (e) { showToast(i18next.t("Update Bad"), "error"); } }, }, ]; jgyCmds.forEach((c) => GM_registerMenuCommand(i18next.t(c.name), c.action)); } // --- ENTRY POINT --- setupMenu(); if (STATE.isJavDB) { handleJavDB(); } else { handleSehuatang(); } })();