// ==UserScript== // @name 番茄小说助手 // @namespace https://github.com/naiyQAQ/fanqie-assistant // @version 0.0.5 // @author naiyQAQ // @description 番茄小说助手,去广告、去推广、解锁章节、优化体验。 // @license GPLv3 // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgICA8cGF0aA0KICAgICAgICBkPSJNMjMuMzExNSAxSDYuNjg4NTNDMy41NDY4NCAxIDEgMy41NDY4NCAxIDYuNjg4NTNWMjMuMzExNUMxIDI2LjQ1MzIgMy41NDY4NCAyOSA2LjY4ODUzIDI5SDIzLjMxMTVDMjYuNDUzMiAyOSAyOSAyNi40NTMyIDI5IDIzLjMxMTVWNi42ODg1M0MyOSAzLjU0Njg0IDI2LjQ1MzIgMSAyMy4zMTE1IDFaIg0KICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgIDxwYXRoDQogICAgICAgIGQ9Ik0yMy4zMTE1IDAuNzVINi42ODg1M0MzLjQwODc3IDAuNzUgMC43NSAzLjQwODc3IDAuNzUgNi42ODg1M1YyMy4zMTE1QzAuNzUgMjYuNTkxMiAzLjQwODc3IDI5LjI1IDYuNjg4NTMgMjkuMjVIMjMuMzExNUMyNi41OTEyIDI5LjI1IDI5LjI1IDI2LjU5MTIgMjkuMjUgMjMuMzExNVY2LjY4ODUzQzI5LjI1IDMuNDA4NzcgMjYuNTkxMiAwLjc1IDIzLjMxMTUgMC43NVoiDQogICAgICAgIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjA4IiBzdHJva2Utd2lkdGg9IjAuNSI+PC9wYXRoPg0KICAgIDxtYXNrIGlkPSJtYXNrMF80NzBfNDgzNjQiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjEiIHk9IjEiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCI+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMjMuMzExNSAxSDYuNjg4NTNDMy41NDY4NCAxIDEgMy41NDY4NCAxIDYuNjg4NTNWMjMuMzExNUMxIDI2LjQ1MzIgMy41NDY4NCAyOSA2LjY4ODUzIDI5SDIzLjMxMTVDMjYuNDUzMiAyOSAyOSAyNi40NTMyIDI5IDIzLjMxMTVWNi42ODg1M0MyOSAzLjU0Njg0IDI2LjQ1MzIgMSAyMy4zMTE1IDFaIg0KICAgICAgICAgICAgZmlsbD0id2hpdGUiPjwvcGF0aD4NCiAgICA8L21hc2s+DQogICAgPGcgbWFzaz0idXJsKCNtYXNrMF80NzBfNDgzNjQpIj4NCiAgICAgICAgPHBhdGgNCiAgICAgICAgICAgIGQ9Ik0yMy4zMTE1IDFINi42ODg1M0MzLjU0Njg0IDEgMSAzLjU0Njg0IDEgNi42ODg1M1YyMy4zMTE1QzEgMjYuNDUzMiAzLjU0Njg0IDI5IDYuNjg4NTMgMjlIMjMuMzExNUMyNi40NTMyIDI5IDI5IDI2LjQ1MzIgMjkgMjMuMzExNVY2LjY4ODUzQzI5IDMuNTQ2ODQgMjYuNDUzMiAxIDIzLjMxMTUgMVoiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgICAgICA8cGF0aA0KICAgICAgICAgICAgZD0iTTE1LjAwMDggNDguNjY0MkMyNS40MDE3IDQ4LjY2NDIgMzMuODMzNCA0MC4yMzI2IDMzLjgzMzQgMjkuODMxNkMzMy44MzM0IDE5LjQzMDcgMjUuNDAxNyAxMC45OTkgMTUuMDAwOCAxMC45OTlDNC41OTk4NSAxMC45OTkgLTMuODMxNzkgMTkuNDMwNyAtMy44MzE3OSAyOS44MzE2Qy0zLjgzMTc5IDQwLjIzMjYgNC41OTk4NSA0OC42NjQyIDE1LjAwMDggNDguNjY0MloiDQogICAgICAgICAgICBmaWxsPSJ1cmwoI3BhaW50MF9yYWRpYWxfNDcwXzQ4MzY0KSI+PC9wYXRoPg0KICAgICAgICA8cGF0aCBkPSJNMjMuMjY4OCAxVjcuMjEyOTRMMjAuNjY2MyA1LjcxNDM3TDE4LjA2NzQgNy4yMTI5NFYxSDIzLjI2ODhaIiBmaWxsPSIjRkY1RjAwIj48L3BhdGg+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMTUuMTM0MyAxOC44ODFDMTUuMTM0MyAxOC44ODEgMTYuMTAxNCAxNy41NTEzIDE2LjEwMTQgMTYuNDA2NUMxNi4xMDE0IDE1LjI2MTcgMTUuNjY3NiAxNC43MzczIDE1LjEzNDMgMTQuNzM3M0MxNC42MDEgMTQuNzM3MyAxNC4xNjczIDE1LjI2MzUgMTQuMTY3MyAxNi40MDY1QzE0LjE2NzMgMTcuNTQ5NiAxNS4xMzQzIDE4Ljg4MSAxNS4xMzQzIDE4Ljg4MVoiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgICAgICA8cGF0aA0KICAgICAgICAgICAgZD0iTTcuNjI3MjQgMjIuNjU4NUM4Ljc3MjA1IDIyLjY1ODUgMTAuMTAxNyAyMy42MjU2IDEwLjEwMTcgMjMuNjI1NkMxMC4xMDE3IDIzLjYyNTYgOC43NzAyNyAyNC41OTI2IDcuNjI3MjQgMjQuNTkyNkM2LjQ4NDIgMjQuNTkyNiA1Ljk1ODAxIDI0LjE1ODkgNS45NTgwMSAyMy42MjU2QzUuOTU4MDEgMjMuMDkyMyA2LjQ4MjQyIDIyLjY1ODUgNy42MjcyNCAyMi42NTg1WiINCiAgICAgICAgICAgIGZpbGw9IndoaXRlIj48L3BhdGg+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMjIuNjM5NiAyNC41OTI2QzIxLjQ5NDggMjQuNTkyNiAyMC4xNjUxIDIzLjYyNTYgMjAuMTY1MSAyMy42MjU2QzIwLjE2NTEgMjMuNjI1NiAyMS40OTY2IDIyLjY1ODUgMjIuNjM5NiAyMi42NTg1QzIzLjc4MjYgMjIuNjU4NSAyNC4zMDg4IDIzLjA5MjMgMjQuMzA4OCAyMy42MjU2QzI0LjMwODggMjQuMTU4OSAyMy43ODQ0IDI0LjU5MjYgMjIuNjM5NiAyNC41OTI2WiINCiAgICAgICAgICAgIGZpbGw9IndoaXRlIj48L3BhdGg+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMTAuNDU1NSAxOC4zMTM5QzExLjI2NDMgMTkuMTIyNyAxMS41MjIxIDIwLjc0NzUgMTEuNTIyMSAyMC43NDc1QzExLjUyMjEgMjAuNzQ3NSA5Ljg5NzMyIDIwLjQ4OTcgOS4wODg0OCAxOS42ODA5QzguMjc5NjQgMTguODcyMSA4LjIxMzg3IDE4LjE5NDggOC41OTI1MSAxNy44MTYxQzguOTcxMTUgMTcuNDM3NSA5LjY0NjY2IDE3LjUwMzMgMTAuNDU3MyAxOC4zMTIxTDEwLjQ1NTUgMTguMzEzOVoiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgICAgICA8cGF0aA0KICAgICAgICAgICAgZD0iTTE4Ljc0NjUgMjAuNzQ3NkMxOC43NDY1IDIwLjc0NzYgMTkuMDA0MyAxOS4xMjI4IDE5LjgxMzEgMTguMzE0TDE5LjgxMTMgMTguMzEyMkMyMC42MjIgMTcuNTAzMyAyMS4yOTkzIDE3LjQzOTMgMjEuNjc2MSAxNy44MTYyQzIyLjA1NDggMTguMTk0OSAyMS45ODkgMTguODcyMSAyMS4xODAyIDE5LjY4MUMyMC4zNzEzIDIwLjQ4OTggMTguNzQ2NSAyMC43NDc2IDE4Ljc0NjUgMjAuNzQ3NloiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgIDwvZz4NCiAgICA8ZGVmcz4NCiAgICAgICAgPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzQ3MF80ODM2NCIgY3g9IjAiIGN5PSIwIiByPSIxIg0KICAgICAgICAgICAgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiDQogICAgICAgICAgICBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDE1LjAwMDggMjkuODMxNikgc2NhbGUoMTguODMyNikiPg0KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0NDMDUwMCI+PC9zdG9wPg0KICAgICAgICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkY1RjAwIj48L3N0b3A+DQogICAgICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPC9kZWZzPg0KPC9zdmc+ // @match *://*.fanqienovel.com/* // @require https://registry.npmmirror.com/vue/3.5.40/files/dist/vue.global.prod.js // @require https://registry.npmmirror.com/moment/2.30.1/files/min/moment.min.js // @connect fanqienovel.com // @connect jxbhmy.com // @connect snssdk.com // @connect byteimg.com // @connect fqnovelpic.com // @connect bytecdn.cn // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function (vue, moment) { 'use strict'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const shared_key = new Uint8Array([172, 37, 198, 125, 221, 143, 56, 193, 179, 122, 35, 72, 130, 142, 34, 46]).buffer; const fetch$1 = unsafeWindow.fetch; unsafeWindow.XMLHttpRequest; const defaultConfig = { install_id: "2187355326270644", device_id: "2187355326004404", device_type: "P30" }; const _config = { currentConfig: defaultConfig }; const scriptcss = "/* 移除章节锁定图标 */\n.muyeicon-lock {\n display: none;\n}\n/* 移除APP推广图标 */\n.muye-to-fanqie {\n display: none!important;\n}\n.reader-toolbar-item-download {\n display: none!important;\n}\n.download-btn {\n display: none!important;\n}\n.download-icon {\n display: none!important;\n}\n\n.fqa-hide {\n display: none!important;\n}\n/* 404 */\n.no-content {\n display: none!important;\n}\n\n.fqa-comic-img {\n width: 100%!important;\n height: 100%!important;\n max-width: 100%!important;\n max-height: 100%!important;\n padding-top: 0!important;\n padding-bottom: 0!important;\n margin-top: 0!important;\n margin-bottom: 0!important;\n}\n\n.fqa-comic-reader {\n line-height: 0!important;\n}\n\n.fqa-menu-item,\n.arco-menu-item {\n width: 100%!important;\n}\n\n#dynamic-el {\n display: none!important;\n}\n\n.fqa-footnote-ref {\n display: inline-block;\n margin: 0 0.15em;\n padding: 0 0.25em;\n font-size: 0.7em;\n line-height: 1.4;\n vertical-align: super;\n color: var(--web-brand_normal, #f14646);\n cursor: pointer;\n user-select: none;\n border-radius: 3px;\n text-indent: 0;\n}\n\n.fqa-footnote-ref:hover,\n.fqa-footnote-ref:focus-visible {\n background: var(--web-brand_light, rgba(241, 70, 70, 0.12));\n outline: none;\n}\n\n\n.fqa-footnote {\n margin-top: 2em;\n padding-top: 1em;\n border-top: 1px solid var(--web-gray_20, rgba(128, 128, 128, 0.25));\n font-size: var(--fqa-body-size, 1.6rem);\n}\n\n.muye-reader-content-16 .fqa-footnote { font-size: var(--fqa-body-size, 1.6rem); }\n.muye-reader-content-20 .fqa-footnote { font-size: var(--fqa-body-size, 2rem); }\n.muye-reader-content-24 .fqa-footnote { font-size: var(--fqa-body-size, 2.4rem); }\n.muye-reader-content-28 .fqa-footnote { font-size: var(--fqa-body-size, 2.8rem); }\n.muye-reader-content-32 .fqa-footnote { font-size: var(--fqa-body-size, 3.2rem); }\n\n.fqa-footnote-title {\n margin-bottom: 0.6em;\n font-size: 0.85em;\n font-weight: 600;\n color: var(--web-gray_40, #8a8a8a);\n text-indent: 0;\n}\n\n.fqa-footnote-list {\n margin: 0;\n padding-left: 1.6em;\n font-size: 0.85em;\n line-height: 1.7;\n color: var(--web-gray_40, #8a8a8a);\n}\n\n.fqa-footnote-list li {\n margin-bottom: 0.5em;\n text-indent: 0;\n transition: background-color 0.3s ease;\n}\n\n.fqa-footnote-list li.fqa-footnote-active {\n background: var(--web-brand_light, rgba(241, 70, 70, 0.12));\n border-radius: 4px;\n}\n\n.muye-reader-content > body {\n background-color: var(--web-bg)!important;\n}\n\n.fqa-icon-dark {\n color: #B3B3B3\n}\n\n/* ----------------------------- 右键菜单 / Toast ----------------------------- */\n\n/*\n * 书架与搜索共用。两者都把菜单 Teleport 到 body,\n * 拿不到各自根节点上的变量,所以在这里声明一份全局色板。\n */\n.fqa-menu {\n --fqa-menu-bg: #fff;\n --fqa-menu-text: #1f2329;\n --fqa-menu-sub: #8f959e;\n --fqa-menu-hover: rgba(31, 35, 41, 0.06);\n --fqa-menu-danger: #f5222d;\n\n position: fixed;\n z-index: 2147483001;\n min-width: 132px;\n max-width: 240px;\n padding: 4px;\n box-sizing: border-box;\n background: var(--fqa-menu-bg);\n border: 1px solid rgba(31, 35, 41, 0.08);\n border-radius: 8px;\n box-shadow: 0 6px 24px rgba(31, 35, 41, 0.16);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial,\n sans-serif;\n font-size: 13px;\n color: var(--fqa-menu-text);\n user-select: none;\n}\n\n/* 二级面板:分组数量多时可滚动 */\n.fqa-menu-sub {\n max-height: 320px;\n overflow-y: auto;\n}\n\n.fqa-menu-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 7px 10px;\n border-radius: 5px;\n line-height: 1.4;\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.fqa-menu-row > span:first-child {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.fqa-menu-row:hover,\n.fqa-menu-row.fqa-menu-open {\n background: var(--fqa-menu-hover);\n}\n\n.fqa-menu-arrow {\n color: var(--fqa-menu-sub);\n font-size: 15px;\n line-height: 1;\n}\n\n.fqa-menu-danger {\n color: var(--fqa-menu-danger);\n}\n\n.fqa-menu-disabled {\n color: var(--fqa-menu-sub);\n cursor: not-allowed;\n}\n\n.fqa-menu-disabled:hover {\n background: transparent;\n}\n\n/* 操作结果提示 */\n.fqa-toast {\n position: fixed;\n left: 50%;\n bottom: 48px;\n transform: translateX(-50%);\n z-index: 2147483002;\n max-width: 80vw;\n padding: 10px 18px;\n box-sizing: border-box;\n background: rgba(31, 35, 41, 0.88);\n color: #fff;\n border-radius: 8px;\n font-size: 13px;\n line-height: 1.4;\n box-shadow: 0 6px 24px rgba(31, 35, 41, 0.24);\n pointer-events: none;\n}\n\n/* 骨架屏微光。书架与搜索共用同一个动画名 */\n@keyframes fqa-shimmer {\n 100% {\n transform: translateX(100%);\n }\n}\n\n@media (prefers-color-scheme: dark) {\n .fqa-menu {\n --fqa-menu-bg: #23272e;\n --fqa-menu-text: #e5e6eb;\n --fqa-menu-sub: #8f959e;\n --fqa-menu-hover: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.1);\n }\n}"; async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function cloneElement(element) { return element.cloneNode(true); } function waitForElement(selector, timeout = 15e3) { const existing = document.querySelector(selector); if (existing) return Promise.resolve(existing); return new Promise((resolve) => { let timer; const observer2 = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { if (timer) clearTimeout(timer); observer2.disconnect(); resolve(el); } }); const start = () => { observer2.observe(document.documentElement, { childList: true, subtree: true }); timer = setTimeout(() => { observer2.disconnect(); resolve(document.querySelector(selector)); }, timeout); }; if (document.documentElement) { start(); } else { document.addEventListener("DOMContentLoaded", start, { once: true }); } }); } function chunk(list, size) { const result = []; for (let i2 = 0; i2 < list.length; i2 += size) { result.push(list.slice(i2, i2 + size)); } return result; } function concatArrayBuffers(...buffers) { const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const buf of buffers) { result.set(new Uint8Array(buf), offset); offset += buf.byteLength; } return result.buffer; } async function inject() { while (!document.body) { console.log("Waiting for body..."); await sleep(200); } GM_addStyle(scriptcss); console.log("CSS injected successfully!"); } unsafeWindow.localStorage; function write(key, value) { GM_setValue(key, JSON.stringify(value)); } function read(key) { return JSON.parse(GM_getValue(key) || "null"); } function del(key) { GM_deleteValue(key); } const STORE_KEY$1 = "settings"; const DEFAULT_SETTINGS = { decryptFont: true, blockReport: true, allowCopy: true, readerFont: "", customCssEnabled: false, customCss: "", enhanceSearch: true, // 默认关:携带登录态属于额外的隐私暴露,交给用户显式开启 searchPersonalized: false, apiPreference: "app", deviceId: "", installId: "", deviceType: "" }; function normalize(raw) { const s = { ...DEFAULT_SETTINGS }; if (!raw || typeof raw !== "object") return s; const o = raw; for (const key of Object.keys(DEFAULT_SETTINGS)) { const v = o[key]; if (v === void 0 || v === null) continue; if (typeof DEFAULT_SETTINGS[key] === typeof v) { s[key] = v; } } if (s.apiPreference !== "app" && s.apiPreference !== "redcandle") { s.apiPreference = DEFAULT_SETTINGS.apiPreference; } return s; } const settings$1 = vue.reactive(normalize(read(STORE_KEY$1))); let saveTimer; vue.watch( settings$1, () => { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { saveTimer = void 0; write(STORE_KEY$1, { ...settings$1 }); }, 200); }, { deep: true } ); function flushSettings() { if (saveTimer) { clearTimeout(saveTimer); saveTimer = void 0; } write(STORE_KEY$1, { ...settings$1 }); } const enTag = ".font-DNMrHsV173Pd4pgy"; const code_ed = 58715; const code_st = 58344; const mapping = [ "D", "在", "主", "特", "家", "军", "然", "表", "场", "4", "要", "只", "v", "和", "?", "6", "别", "还", "g", "现", "儿", "岁", "?", "?", "此", "象", "月", "3", "出", "战", "工", "相", "o", "男", "直", "失", "世", "F", "都", "平", "文", "什", "V", "O", "将", "真", "T", "那", "当", "?", "会", "立", "些", "u", "是", "十", "张", "学", "气", "大", "爱", "两", "命", "全", "后", "东", "性", "通", "被", "1", "它", "乐", "接", "而", "感", "车", "山", "公", "了", "常", "以", "何", "可", "话", "先", "p", "i", "叫", "轻", "M", "士", "w", "着", "变", "尔", "快", "l", "个", "说", "少", "色", "里", "安", "花", "远", "7", "难", "师", "放", "t", "报", "认", "面", "道", "S", "?", "克", "地", "度", "I", "好", "机", "U", "民", "写", "把", "万", "同", "水", "新", "没", "书", "电", "吃", "像", "斯", "5", "为", "y", "白", "几", "日", "教", "看", "但", "第", "加", "候", "作", "上", "拉", "住", "有", "法", "r", "事", "应", "位", "利", "你", "声", "身", "国", "问", "马", "女", "他", "Y", "比", "父", "x", "A", "H", "N", "s", "X", "边", "美", "对", "所", "金", "活", "回", "意", "到", "z", "从", "j", "知", "又", "内", "因", "点", "Q", "三", "定", "8", "R", "b", "正", "或", "夫", "向", "德", "听", "更", "?", "得", "告", "并", "本", "q", "过", "记", "L", "让", "打", "f", "人", "就", "者", "去", "原", "满", "体", "做", "经", "K", "走", "如", "孩", "c", "G", "给", "使", "物", "?", "最", "笑", "部", "?", "员", "等", "受", "k", "行", "一", "条", "果", "动", "光", "门", "头", "见", "往", "自", "解", "成", "处", "天", "能", "于", "名", "其", "发", "总", "母", "的", "死", "手", "入", "路", "进", "心", "来", "h", "时", "力", "多", "开", "已", "许", "d", "至", "由", "很", "界", "n", "小", "与", "Z", "想", "代", "么", "分", "生", "口", "再", "妈", "望", "次", "西", "风", "种", "带", "J", "?", "实", "情", "才", "这", "?", "E", "我", "神", "格", "长", "觉", "间", "年", "眼", "无", "不", "亲", "关", "结", "0", "友", "信", "下", "却", "重", "己", "老", "2", "音", "字", "m", "呢", "明", "之", "前", "高", "P", "B", "目", "太", "e", "9", "起", "稜", "她", "也", "W", "用", "方", "子", "英", "每", "理", "便", "四", "数", "期", "中", "C", "外", "样", "a", "海", "们", "任" ]; function decryptText(text) { let result = ""; let changed = false; for (const char of text) { const codePoint = char.codePointAt(0); if (typeof codePoint !== "number") { return text; } if (codePoint < code_st || codePoint > code_ed) { result += char; continue; } const mapped = mapping[codePoint - code_st]; if (mapped && mapped !== "?") { result += mapped; changed = true; } else { result += char; } } return changed ? result : text; } function decryptElement(element) { if (!settings$1.decryptFont) return; const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT ); let textNode; while (textNode = walker.nextNode()) { const oldText = textNode.nodeValue; if (!oldText) { continue; } const newText = decryptText(oldText); if (newText !== oldText) { textNode.nodeValue = newText; } } } function decryptPage(root) { if (root instanceof Element && root.matches(enTag)) { decryptElement(root); } root.querySelectorAll(enTag).forEach(decryptElement); } function initFontDecrypt() { const observer2 = new MutationObserver((mutations) => { var _a, _b; for (const mutation of mutations) { if (mutation.type === "characterData") { const encryptedElement = (_a = mutation.target.parentElement) == null ? void 0 : _a.closest( enTag ); if (encryptedElement) { decryptElement(encryptedElement); } continue; } for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { decryptPage(node); continue; } if (node.nodeType === Node.TEXT_NODE) { const encryptedElement = (_b = node.parentElement) == null ? void 0 : _b.closest( enTag ); if (encryptedElement) { decryptElement(encryptedElement); } } } } }); observer2.observe(document, { subtree: true, childList: true, characterData: true }); decryptPage(document); vue.watch( () => settings$1.decryptFont, (on) => { if (on) decryptPage(document); } ); } const STYLE_ID$3 = "fqa-user-style"; const READER_SCOPE = "#fqa-reader-content, .muye-reader-content"; function buildCss() { const parts = []; const font = settings$1.readerFont.trim(); if (font) { const family = /^["']|,/.test(font) ? font : `"${font}"`; parts.push(`${READER_SCOPE}, ${READER_SCOPE} p { font-family: ${family}, inherit !important; }`); } if (settings$1.customCssEnabled && settings$1.customCss.trim()) { parts.push(settings$1.customCss); } return parts.join("\n"); } function apply() { const css = buildCss(); let el = document.getElementById(STYLE_ID$3); if (!css) { el == null ? void 0 : el.remove(); return; } if (!el) { el = document.createElement("style"); el.id = STYLE_ID$3; document.head.appendChild(el); } el.textContent = css; } function initUserStyle() { apply(); vue.watch( () => [settings$1.readerFont, settings$1.customCssEnabled, settings$1.customCss], apply ); } const supportedMethods = /* @__PURE__ */ new Set([ "GET", "HEAD", "POST", "PUT", "DELETE" ]); function apiFetch(url, options = {}) { return new Promise((resolve, reject) => { const { signal } = options; if (signal == null ? void 0 : signal.aborted) { reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError")); return; } const headers = normalizeHeaders(options.headers); const data = normalizeBody(options.body); const method = options.method ?? (data ? "POST" : "GET"); if (!supportedMethods.has(method)) { reject(new TypeError(`Unsupported request method: ${method}`)); return; } let request; const abort = () => request == null ? void 0 : request.abort(); function cleanup() { signal == null ? void 0 : signal.removeEventListener("abort", abort); } request = GM_xmlhttpRequest({ url, method, headers, data, anonymous: options.credentials === "omit", redirect: options.redirect === "error" ? "error" : "follow", onload(response) { cleanup(); resolve(Object.assign(response, { json() { return JSON.parse(this.responseText); } })); }, onerror(response) { cleanup(); reject(createRequestError("Network request failed", response)); }, ontimeout() { cleanup(); reject(createRequestError("Network request timed out", { status: 0, statusText: "Timeout", url })); }, onabort() { cleanup(); reject( (signal == null ? void 0 : signal.reason) ?? new DOMException("The operation was aborted", "AbortError") ); } }); signal == null ? void 0 : signal.addEventListener("abort", abort, { once: true }); }); } function fetchArrayBuffer(url) { return fetch$1(url, { referrerPolicy: "no-referrer" }).then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.arrayBuffer(); }).catch((err) => { console.debug("[fqa:img] 页面 fetch 失败,改用 GM_xmlhttpRequest:", url, err); return gmArrayBuffer(url); }); } function gmArrayBuffer(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, method: "GET", responseType: "arraybuffer", onload(response) { const buf = response.response; if (response.status >= 200 && response.status < 300 && (buf == null ? void 0 : buf.byteLength)) { resolve(buf); } else { reject(new Error(`GM 请求失败(${response.status})`)); } }, onerror() { reject(new Error(`GM 请求出错,检查 @connect 是否覆盖该域名: ${url}`)); }, ontimeout: () => reject(new Error("GM 请求超时")) }); }); } function normalizeHeaders(headers) { if (!headers) { return void 0; } return Object.fromEntries(new Headers(headers).entries()); } function normalizeBody(body) { if (body == null) { return void 0; } if (body instanceof URLSearchParams) { return body.toString(); } if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || body instanceof FormData) { return body; } if (ArrayBuffer.isView(body)) { return body.buffer.slice( body.byteOffset, body.byteOffset + body.byteLength ); } throw new TypeError( "GM_xmlhttpRequest does not support ReadableStream request bodies" ); } function createRequestError(message, response) { const error = new TypeError(message); Object.defineProperty(error, "response", { configurable: true, enumerable: false, value: response }); return error; } function rotateLeft(value, shiftBits) { return value << shiftBits | value >>> 32 - shiftBits; } function addUnsigned(left, right) { return left + right >>> 0; } function f(x, y, z) { return x & y | ~x & z; } function g(x, y, z) { return x & z | y & ~z; } function h(x, y, z) { return x ^ y ^ z; } function i(x, y, z) { return y ^ (x | ~z); } function ff(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(f(b, c, d), x), ac)), s), b); } function gg(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(g(b, c, d), x), ac)), s), b); } function hh(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(h(b, c, d), x), ac)), s), b); } function ii(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(i(b, c, d), x), ac)), s), b); } function convertToWordArray(bytes) { const wordCount = Math.ceil((bytes.length + 9) / 64) * 16; const words = new Array(wordCount).fill(0); for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) { const wordIndex = Math.floor(byteIndex / 4); const bytePosition = byteIndex % 4 * 8; words[wordIndex] = words[wordIndex] | bytes[byteIndex] << bytePosition; } const paddingWordIndex = Math.floor(bytes.length / 4); const paddingBytePosition = bytes.length % 4 * 8; words[paddingWordIndex] = words[paddingWordIndex] | 128 << paddingBytePosition; words[wordCount - 2] = bytes.length << 3; words[wordCount - 1] = bytes.length >>> 29; return words; } function wordToHex(value) { let hex2 = ""; for (let byteIndex = 0; byteIndex < 4; byteIndex++) { const byte = value >>> byteIndex * 8 & 255; hex2 += byte.toString(16).padStart(2, "0"); } return hex2; } function md5(input) { const bytes = typeof input === "string" ? new TextEncoder().encode(input) : new Uint8Array(input); const words = convertToWordArray(bytes); let a = 1732584193; let b = 4023233417; let c = 2562383102; let d = 271733878; for (let k = 0; k < words.length; k += 16) { const aa = a; const bb = b; const cc = c; const dd = d; a = ff(a, b, c, d, words[k], 7, 3614090360); d = ff(d, a, b, c, words[k + 1], 12, 3905402710); c = ff(c, d, a, b, words[k + 2], 17, 606105819); b = ff(b, c, d, a, words[k + 3], 22, 3250441966); a = ff(a, b, c, d, words[k + 4], 7, 4118548399); d = ff(d, a, b, c, words[k + 5], 12, 1200080426); c = ff(c, d, a, b, words[k + 6], 17, 2821735955); b = ff(b, c, d, a, words[k + 7], 22, 4249261313); a = ff(a, b, c, d, words[k + 8], 7, 1770035416); d = ff(d, a, b, c, words[k + 9], 12, 2336552879); c = ff(c, d, a, b, words[k + 10], 17, 4294925233); b = ff(b, c, d, a, words[k + 11], 22, 2304563134); a = ff(a, b, c, d, words[k + 12], 7, 1804603682); d = ff(d, a, b, c, words[k + 13], 12, 4254626195); c = ff(c, d, a, b, words[k + 14], 17, 2792965006); b = ff(b, c, d, a, words[k + 15], 22, 1236535329); a = gg(a, b, c, d, words[k + 1], 5, 4129170786); d = gg(d, a, b, c, words[k + 6], 9, 3225465664); c = gg(c, d, a, b, words[k + 11], 14, 643717713); b = gg(b, c, d, a, words[k], 20, 3921069994); a = gg(a, b, c, d, words[k + 5], 5, 3593408605); d = gg(d, a, b, c, words[k + 10], 9, 38016083); c = gg(c, d, a, b, words[k + 15], 14, 3634488961); b = gg(b, c, d, a, words[k + 4], 20, 3889429448); a = gg(a, b, c, d, words[k + 9], 5, 568446438); d = gg(d, a, b, c, words[k + 14], 9, 3275163606); c = gg(c, d, a, b, words[k + 3], 14, 4107603335); b = gg(b, c, d, a, words[k + 8], 20, 1163531501); a = gg(a, b, c, d, words[k + 13], 5, 2850285829); d = gg(d, a, b, c, words[k + 2], 9, 4243563512); c = gg(c, d, a, b, words[k + 7], 14, 1735328473); b = gg(b, c, d, a, words[k + 12], 20, 2368359562); a = hh(a, b, c, d, words[k + 5], 4, 4294588738); d = hh(d, a, b, c, words[k + 8], 11, 2272392833); c = hh(c, d, a, b, words[k + 11], 16, 1839030562); b = hh(b, c, d, a, words[k + 14], 23, 4259657740); a = hh(a, b, c, d, words[k + 1], 4, 2763975236); d = hh(d, a, b, c, words[k + 4], 11, 1272893353); c = hh(c, d, a, b, words[k + 7], 16, 4139469664); b = hh(b, c, d, a, words[k + 10], 23, 3200236656); a = hh(a, b, c, d, words[k + 13], 4, 681279174); d = hh(d, a, b, c, words[k], 11, 3936430074); c = hh(c, d, a, b, words[k + 3], 16, 3572445317); b = hh(b, c, d, a, words[k + 6], 23, 76029189); a = hh(a, b, c, d, words[k + 9], 4, 3654602809); d = hh(d, a, b, c, words[k + 12], 11, 3873151461); c = hh(c, d, a, b, words[k + 15], 16, 530742520); b = hh(b, c, d, a, words[k + 2], 23, 3299628645); a = ii(a, b, c, d, words[k], 6, 4096336452); d = ii(d, a, b, c, words[k + 7], 10, 1126891415); c = ii(c, d, a, b, words[k + 14], 15, 2878612391); b = ii(b, c, d, a, words[k + 5], 21, 4237533241); a = ii(a, b, c, d, words[k + 12], 6, 1700485571); d = ii(d, a, b, c, words[k + 3], 10, 2399980690); c = ii(c, d, a, b, words[k + 10], 15, 4293915773); b = ii(b, c, d, a, words[k + 1], 21, 2240044497); a = ii(a, b, c, d, words[k + 8], 6, 1873313359); d = ii(d, a, b, c, words[k + 15], 10, 4264355552); c = ii(c, d, a, b, words[k + 6], 15, 2734768916); b = ii(b, c, d, a, words[k + 13], 21, 1309151649); a = ii(a, b, c, d, words[k + 4], 6, 4149444226); d = ii(d, a, b, c, words[k + 11], 10, 3174756917); c = ii(c, d, a, b, words[k + 2], 15, 718787259); b = ii(b, c, d, a, words[k + 9], 21, 3951481745); a = addUnsigned(a, aa); b = addUnsigned(b, bb); c = addUnsigned(c, cc); d = addUnsigned(d, dd); } return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase(); } function rotl(value, shift) { const normalizedShift = shift & 31; return value << normalizedShift | value >>> 32 - normalizedShift; } function p0(value) { return value ^ rotl(value, 9) ^ rotl(value, 17); } function p1(value) { return value ^ rotl(value, 15) ^ rotl(value, 23); } function sm3(input) { const inputLength = input.length; const paddedLength = Math.ceil((inputLength + 9) / 64) * 64; const message = new Uint8Array(paddedLength); message.set(input); message[inputLength] = 128; const bitLength = BigInt(inputLength) * 8n; for (let i2 = 0; i2 < 8; i2++) { message[paddedLength - 1 - i2] = Number(bitLength >> BigInt(i2 * 8) & 0xffn); } const dataView = new DataView(message.buffer); const state = new Uint32Array([ 1937774191, 1226093241, 388252375, 3666478592, 2842636476, 372324522, 3817729613, 2969243214 ]); const words = new Uint32Array(68); const expandedWords = new Uint32Array(64); for (let block = 0; block < message.length / 64; block++) { const start = block * 64; for (let i2 = 0; i2 < 16; i2++) { words[i2] = dataView.getUint32(start + i2 * 4, false); } for (let i2 = 16; i2 < 68; i2++) { words[i2] = p1(words[i2 - 16] ^ words[i2 - 9] ^ rotl(words[i2 - 3], 15)) ^ rotl(words[i2 - 13], 7) ^ words[i2 - 6]; } for (let i2 = 0; i2 < 64; i2++) { expandedWords[i2] = words[i2] ^ words[i2 + 4]; } let a = state[0]; let b = state[1]; let c = state[2]; let d = state[3]; let e = state[4]; let f2 = state[5]; let g2 = state[6]; let h2 = state[7]; for (let i2 = 0; i2 < 64; i2++) { const t = i2 <= 15 ? 2043430169 : 2055708042; const ss1 = rotl(rotl(a, 12) + e + rotl(t, i2), 7); const ss2 = ss1 ^ rotl(a, 12); const tt1 = (i2 <= 15 ? a ^ b ^ c : a & b | a & c | b & c) + d + ss2 + expandedWords[i2]; const tt2 = (i2 <= 15 ? e ^ f2 ^ g2 : e & f2 | ~e & g2) + h2 + ss1 + words[i2]; d = c; c = rotl(b, 9); b = a; a = tt1; h2 = g2; g2 = rotl(f2, 19); f2 = e; e = p0(tt2); } state[0] = state[0] ^ a; state[1] = state[1] ^ b; state[2] = state[2] ^ c; state[3] = state[3] ^ d; state[4] = state[4] ^ e; state[5] = state[5] ^ f2; state[6] = state[6] ^ g2; state[7] = state[7] ^ h2; } const result = new Uint8Array(32); for (let i2 = 0; i2 < state.length; i2++) { const word = state[i2]; result[i2 * 4] = word >>> 24; result[i2 * 4 + 1] = word >>> 16; result[i2 * 4 + 2] = word >>> 8; result[i2 * 4 + 3] = word; } return result; } function getCrypto() { const c = globalThis.crypto ?? unsafeWindow.crypto; if (!(c == null ? void 0 : c.subtle)) { throw new Error("Crypto API不可用,请检查浏览器版本是否支持该API"); } return c; } function getSubtle() { return getCrypto().subtle; } function b64decode(b64) { const binaryString = atob(b64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i2 = 0; i2 < len; i2++) { bytes[i2] = binaryString.charCodeAt(i2); } return bytes.buffer; } function b64encode(buffer) { const bytes = new Uint8Array(buffer); const chunkSize = 32768; const chunks = []; for (let i2 = 0; i2 < bytes.length; i2 += chunkSize) { chunks.push( String.fromCharCode(...bytes.subarray(i2, i2 + chunkSize)) ); } return btoa(chunks.join("")); } function unhex(hex2) { if (hex2.length % 2 !== 0) { throw new Error("Invalid hex string"); } const bytes = new Uint8Array(hex2.length / 2); for (let i2 = 0; i2 < hex2.length; i2 += 2) { const byte = parseInt(hex2.slice(i2, i2 + 2), 16); if (Number.isNaN(byte)) { throw new Error("Invalid hex string"); } bytes[i2 / 2] = byte; } return bytes.buffer; } function hex(buffer) { const bytes = new Uint8Array(buffer); let hexString = ""; for (let i2 = 0; i2 < bytes.length; i2++) { hexString += bytes[i2].toString(16).padStart(2, "0"); } return hexString; } function pkcs7Pad(data, blockSize = 16) { const padLength = blockSize - data.length % blockSize; const padded = new Uint8Array(data.length + padLength); padded.set(data); padded.fill(padLength, data.length); return padded; } function randomString(length) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; const array = new Uint8Array(length); getCrypto().getRandomValues(array); for (let i2 = 0; i2 < length; i2++) { result += chars.charAt(array[i2] % chars.length); } return result; } function toBytes(input) { if (typeof input === "string") { return new TextEncoder().encode(input); } if (input instanceof Uint8Array) { return new Uint8Array(input); } return new Uint8Array(input); } const hash = { sha256: async (input) => { const subtle = getSubtle(); const digest = await subtle.digest("SHA-256", toBytes(input)); return hex(digest); }, sha256bytes: async (input) => { const subtle = getSubtle(); return subtle.digest("SHA-256", toBytes(input)); }, sha512: async (input) => { const subtle = getSubtle(); const digest = await subtle.digest("SHA-512", toBytes(input)); return hex(digest); }, sha512bytes: async (input) => { const subtle = getSubtle(); return subtle.digest("SHA-512", toBytes(input)); }, md5: async (input) => md5( typeof input === "string" ? input : toBytes(input).buffer ), md5bytes: async (input) => unhex(md5( typeof input === "string" ? input : toBytes(input).buffer )), sm3: async (input) => hex(sm3(toBytes(input)).buffer), sm3bytes: async (input) => sm3(toBytes(input)).buffer }; const WIRE_VARINT = 0; const WIRE_BYTES = 2; const WIRE_FIXED32 = 5; class ProtobufWriter { constructor() { __publicField(this, "buf", []); } writeVarint(value) { let v = value >>> 0; while (v >= 128) { this.buf.push(v & 127 | 128); v >>>= 7; } this.buf.push(v & 127); return this; } writeKey(fieldNumber, wireType) { return this.writeVarint(fieldNumber << 3 | wireType); } varint(fieldNumber, value) { return this.writeKey(fieldNumber, WIRE_VARINT).writeVarint(value); } fixed32(fieldNumber, value) { this.writeKey(fieldNumber, WIRE_FIXED32); const v = value >>> 0; this.buf.push(v & 255, v >>> 8 & 255, v >>> 16 & 255, v >>> 24 & 255); return this; } bytes(fieldNumber, data) { this.writeKey(fieldNumber, WIRE_BYTES).writeVarint(data.length); for (let i2 = 0; i2 < data.length; i2++) { this.buf.push(data[i2] & 255); } return this; } string(fieldNumber, value) { return this.bytes(fieldNumber, new TextEncoder().encode(value)); } message(fieldNumber, build) { const sub = new ProtobufWriter(); build(sub); return this.bytes(fieldNumber, sub.toBytes()); } toBytes() { return Uint8Array.from(this.buf); } } const ROUNDS$1 = 72; const MASK64$1 = 0xffffffffffffffffn; const Z4 = 0x3dc94c3a046d678bn; function getBit(value, position) { return value >> BigInt(position) & 1n; } function rotateLeft64(v, n) { return (v << n | v >> 64n - n) & MASK64$1; } function rotateRight64(v, n) { return (v << 64n - n | v >> n) & MASK64$1; } function keyExpansion(key) { const k = [key[0] & MASK64$1, key[1] & MASK64$1, key[2] & MASK64$1, key[3] & MASK64$1]; for (let i2 = 4; i2 < ROUNDS$1; i2++) { let tmp = rotateRight64(k[i2 - 1], 3n); tmp ^= k[i2 - 3]; tmp ^= rotateRight64(tmp, 1n); k.push((~k[i2 - 4] ^ tmp ^ getBit(Z4, (i2 - 4) % 62) ^ 3n) & MASK64$1); } return k; } function simonEncrypt(plaintext, key) { const k = keyExpansion(key); let x = plaintext[0] & MASK64$1; let y = plaintext[1] & MASK64$1; for (let i2 = 0; i2 < ROUNDS$1; i2++) { const tmp = y; const f2 = rotateLeft64(y, 1n) & rotateLeft64(y, 8n); y = (x ^ f2 ^ rotateLeft64(y, 2n) ^ k[i2]) & MASK64$1; x = tmp; } return [x, y]; } const LOW_RAND = new Uint8Array([242, 129]); const HIGH_RAND = new Uint8Array([97, 111]); const XOR_PREFIX = new Uint8Array([242, 247, 252, 255, 242, 247, 252, 255]); function sm3Prefix6(data) { return sm3(data).slice(0, 6); } function decodeStub(xssStub) { const bytes = new Uint8Array(16); if (xssStub.length >= 32) { for (let i2 = 0; i2 < 16; i2++) { bytes[i2] = parseInt(xssStub.slice(i2 * 2, i2 * 2 + 2), 16) & 255; } } return bytes; } function buildProtobuf(query, xssStub, timestamp, config) { const params = new URLSearchParams(query); const deviceId = params.get("device_id") ?? ""; const versionName = params.get("version_name") ?? ""; const bodyHash = sm3Prefix6(xssStub === "" ? new Uint8Array(16) : decodeStub(xssStub)); const queryHash = sm3Prefix6( query === "" ? new Uint8Array(16) : new TextEncoder().encode(query) ); const rand = getCrypto().getRandomValues(new Uint32Array(1))[0] % 2147483647; return new ProtobufWriter().varint(1, 538970409 * 2).varint(2, 2).varint(3, rand).string(4, config.aid).string(5, deviceId).string(6, config.licenseId).string(7, versionName).string(8, config.sdkVersion).varint(9, config.sdkVersionInt).bytes(10, new Uint8Array(8)).varint(11, 0).varint(12, timestamp * 2).bytes(13, bodyHash).bytes(14, queryHash).message(15, (sub) => { sub.varint(1, 1).varint(2, 1).varint(3, 1).varint(7, 3348294860); }).string(16, "").string(20, "none").varint(21, config.callType).message(23, (sub) => { sub.string(1, "NX551J").varint(2, 8196).varint(4, 2162219008); }).varint(25, 2).toBytes(); } async function getArgus(query, xssStub, timestamp, config) { const { signKey } = config; if (signKey.length !== 32) { throw new Error(`Sign key must be 32 bytes, got ${signKey.length}`); } const protobuf = pkcs7Pad(buildProtobuf(query, xssStub, timestamp, config), 16); const sm3Input = new Uint8Array(signKey.length * 2 + LOW_RAND.length + HIGH_RAND.length); sm3Input.set(signKey, 0); sm3Input.set(LOW_RAND, signKey.length); sm3Input.set(HIGH_RAND, signKey.length + LOW_RAND.length); sm3Input.set(signKey, signKey.length + LOW_RAND.length + HIGH_RAND.length); const sm3Output = sm3(sm3Input); const keyView = new DataView(sm3Output.buffer, sm3Output.byteOffset, sm3Output.byteLength); const simonKey = [ keyView.getBigUint64(0, true), keyView.getBigUint64(8, true), keyView.getBigUint64(16, true), keyView.getBigUint64(24, true) ]; const encrypted = new Uint8Array(protobuf.length); const pbView = new DataView(protobuf.buffer, protobuf.byteOffset, protobuf.byteLength); const encView = new DataView(encrypted.buffer); for (let offset = 0; offset < protobuf.length; offset += 16) { const [low, high] = simonEncrypt( [pbView.getBigUint64(offset, true), pbView.getBigUint64(offset + 8, true)], simonKey ); encView.setBigUint64(offset, low, true); encView.setBigUint64(offset + 8, high, true); } const data = new Uint8Array(XOR_PREFIX.length + encrypted.length); data.set(XOR_PREFIX); data.set(encrypted, XOR_PREFIX.length); for (let i2 = XOR_PREFIX.length; i2 < data.length; i2++) { data[i2] ^= data[i2 % 8]; } data.reverse(); const header = new Uint8Array([166, 110, 173, 159, 119, 1, 208, 12, 24]); const plaintext = new Uint8Array(header.length + data.length + HIGH_RAND.length); plaintext.set(header); plaintext.set(data, header.length); plaintext.set(HIGH_RAND, header.length + data.length); const subtle = getSubtle(); const aesKey = await subtle.importKey( "raw", await hash.md5bytes(signKey.slice(0, 16)), { name: "AES-CBC" }, false, ["encrypt"] ); const iv = await hash.md5bytes(signKey.slice(16)); const ciphertext = new Uint8Array( await subtle.encrypt({ name: "AES-CBC", iv }, aesKey, plaintext) ); const result = new Uint8Array(LOW_RAND.length + ciphertext.length); result.set(LOW_RAND); result.set(ciphertext, LOW_RAND.length); return b64encode(result.buffer); } const ROUNDS = 34; const MASK64 = 0xffffffffffffffffn; const WORD_SIZE = 64n; const ALPHA = 8n; const BETA = 3n; function readUint64LE(view, offset) { return view.getBigUint64(offset, true); } function keySchedule(key) { const view = new DataView(key.buffer, key.byteOffset, key.byteLength); const ks = [readUint64LE(view, 0) & MASK64]; const numWords = key.length * 8 / Number(WORD_SIZE); const ls = []; for (let i2 = 1; i2 < numWords; i2++) { ls.push(readUint64LE(view, i2 * 8) & MASK64); } for (let x = 0; x < ROUNDS - 1; x++) { const rsX = (ls[x] << WORD_SIZE - ALPHA) + (ls[x] >> ALPHA) & MASK64; const addSxy = rsX + ks[x] & MASK64; const newX = BigInt(x) ^ addSxy; const lsY = (ks[x] >> WORD_SIZE - BETA) + (ks[x] << BETA) & MASK64; ls.push(newX); ks.push(newX ^ lsY); } return ks; } function encryptBlock(ks, block, out, outOffset) { const view = new DataView(block.buffer, block.byteOffset, block.byteLength); let y = readUint64LE(view, 0); let x = readUint64LE(view, 8); for (const k of ks) { const rsX = (x << WORD_SIZE - ALPHA) + (x >> ALPHA) & MASK64; const addSxy = rsX + y & MASK64; x = k ^ addSxy; const lsY = (y >> WORD_SIZE - BETA) + (y << BETA) & MASK64; y = x ^ lsY; } const outView = new DataView(out.buffer, out.byteOffset, out.byteLength); outView.setBigUint64(outOffset, y & MASK64, true); outView.setBigUint64(outOffset + 8, x & MASK64, true); } function speckEncrypt(key, plaintext) { if (key.length !== 32) { throw new Error(`Speck key must be 32 bytes, got ${key.length}`); } const padded = pkcs7Pad(plaintext, 16); const ks = keySchedule(key); const out = new Uint8Array(padded.length); for (let i2 = 0; i2 < padded.length; i2 += 16) { encryptBlock(ks, padded.subarray(i2, i2 + 16), out, i2); } return out; } async function generateLadonKey(randomBytes, aid) { const aidBytes = new TextEncoder().encode(aid); const input = new Uint8Array(randomBytes.length + aidBytes.length); input.set(randomBytes); input.set(aidBytes, randomBytes.length); const hex2 = await hash.md5(input); return new TextEncoder().encode(hex2); } async function getLadon(timestamp, config) { const randomBytes = getCrypto().getRandomValues(new Uint8Array(4)); const plaintext = new TextEncoder().encode( `${timestamp}-${config.licenseId}-${config.aid}` ); const key = await generateLadonKey(randomBytes, config.aid); const encrypted = speckEncrypt(key, plaintext); const result = new Uint8Array(randomBytes.length + encrypted.length); result.set(randomBytes); result.set(encrypted, randomBytes.length); return b64encode(result.buffer); } const defaultUnidbgConfig = { signKey: new Uint8Array( unhex("ac1adaae95a7af94a5114ab3b3a97dd80050aa0a39314c40528caec95256c28c") ), aid: "1967", licenseId: "1611921764", sdkVersion: "v04.04.05-ov-android", sdkVersionInt: 134744640, callType: 738 }; async function generateHeaders(rawQuery, xssStub = "", timestamp = Math.floor(Date.now() / 1e3), config = defaultUnidbgConfig) { const [argus, ladon] = await Promise.all([ getArgus(rawQuery, xssStub, timestamp, config), getLadon(timestamp, config) ]); return { "x-argus": argus, "x-ladon": ladon, "x-khronos": String(timestamp) }; } async function signRequest(url, body, config = defaultUnidbgConfig) { const rawQuery = new URL(url).search.replace(/^\?/, ""); const hasBody = typeof body === "string" ? body.length > 0 : ((body == null ? void 0 : body.byteLength) ?? 0) > 0; const xssStub = hasBody ? await hash.md5(body) : ""; const now = Date.now(); const headers = await generateHeaders( rawQuery, xssStub, Math.floor(now / 1e3), config ); headers["x-ss-req-ticket"] = String(now); if (hasBody) { headers["X-SS-STUB"] = xssStub; } return headers; } const appBaseUrl = "https://reading.snssdk.com/reading"; const redcandleBaseUrl = "https://api5-sinfonlinec.jxbhmy.com/reading"; const webBaseUrl = "https://fanqienovel.com/reading"; const appUserAgent = "com.dragon.read"; function buildAppQuery(extra) { const c = _config.currentConfig; return new URLSearchParams({ iid: c.install_id, device_id: c.device_id, ac: "wifi", channel: "43536163a", aid: "1967", app_name: "novelapp", version_code: "70132", version_name: "7.0.1.32", device_platform: "android", os: "android", ssmix: "a", os_version: "10", device_type: c.device_type || "P30", device_brand: c.device_brand || "realme", // 不带 update_version_code 时服务端按旧版本下发, // 搜索的筛选器、完整 tab 列表都会缺失 update_version_code: "70132", manifest_version_code: "70132", ...extra }); } async function webGet(path, query, credentials2 = "omit") { const url = `${webBaseUrl}${path}?${buildAppQuery(query).toString()}`; const signed = await signRequest(url); const res = await fetch$1(url, { headers: signed, credentials: credentials2 }); if (!res.ok) { throw new Error(`请求失败(${res.status})`); } return res.json(); } function isUsable(res) { if (!res || res.status !== 200) return false; try { const j = res.json(); return !j || j.code === void 0 || j.code === 0; } catch { return false; } } async function requestApp(path, query, headers) { const url = `${appBaseUrl}${path}?${buildAppQuery(query).toString()}`; const signed = await signRequest(url); return apiFetch(url, { method: "GET", headers: { ...signed, "User-Agent": appUserAgent, ...headers } }); } async function requestRedcandle(path, query, headers) { const url = `${redcandleBaseUrl}${path}?${buildAppQuery(query).toString()}`; return apiFetch(url, { method: "GET", headers: { "User-Agent": appUserAgent, ...headers } }); } async function appGet(path, query, headers) { if (settings$1.apiPreference === "redcandle") { try { const res = await requestRedcandle(path, query, headers); if (isUsable(res)) return res; console.warn(`[fqa:api] 红烛接口数据不全,回落到番茄 APP: ${path}`); } catch (e) { console.warn(`[fqa:api] 红烛接口请求失败,回落到番茄 APP: ${path}`, e); } } return requestApp(path, query, headers); } async function appPost(path, body, query, headers) { const url = `${appBaseUrl}${path}?${buildAppQuery(query).toString()}`; const signed = await signRequest(url, body); console.log("---start--- APP POST ", url); const res = await apiFetch(url, { method: "POST", headers: { ...signed, "User-Agent": appUserAgent, "Content-Type": "application/json; charset=utf-8", ...headers }, body }); console.log("---complete--- APP POST ", url, res); return res; } async function gzip(data) { if (typeof data === "string") { data = new TextEncoder().encode(data).buffer; } const encoder = new CompressionStream("gzip"); const stream = new Blob([data]).stream().pipeThrough(encoder); const compressed = new Response(stream).arrayBuffer(); return compressed; } async function gunzip(data) { const decoder = new DecompressionStream("gzip"); const stream = new Blob([data]).stream().pipeThrough(decoder); const decompressed = new Response(stream).arrayBuffer(); return decompressed; } async function decryptChapter(encrypted, rawData, config = defaultConfig) { var _a; if (!encrypted) { throw new Error("Invalid encrypted chapter"); } const buf = b64decode(encrypted); const iv = buf.slice(0, 16); const data = buf.slice(16); const key = (_a = config.key_info) == null ? void 0 : _a.key; if (!key) { throw new Error("Missing decrypt key"); } const subtle = getSubtle(); const cryptoKey = await subtle.importKey( "raw", key, { name: "AES-CBC" }, false, ["decrypt"] ); return subtle.decrypt( { name: "AES-CBC", iv }, cryptoKey, data ).then(async (decrypted) => { if (rawData && (rawData == null ? void 0 : rawData.compress_status) === 1) { decrypted = await gunzip(decrypted); } const decoder = new TextDecoder(); const plain = decoder.decode(decrypted); if (plain.trim().startsWith("<")) { return plain; } try { return JSON.parse(plain); } catch (e) { console.warn("Invalid chapter content: ", plain, e); return void 0; } }); } async function decryptComicImage(image, key) { const subtle = getSubtle(); const cryptoKey = await subtle.importKey( "raw", unhex(key), { name: "AES-GCM" }, false, ["decrypt"] ); const iv = image.slice(0, 12); const data = image.slice(12); return await subtle.decrypt( { name: "AES-GCM", iv }, cryptoKey, data ); } function reverseHex(value) { const be = BigInt(value).toString(16).padStart(32, "0"); let result = ""; for (let i2 = be.length; i2 > 0; i2 -= 2) result += be.slice(i2 - 2, i2); return result; } async function encryptKeyinfoBody(config) { const deviceId = config.device_id; const iv = new TextEncoder().encode(randomString(16)); const data = new Uint8Array(unhex(reverseHex(deviceId))).slice(0, 8); console.log(data); const subtle = getSubtle(); const k = await subtle.importKey( "raw", shared_key, { name: "AES-CBC" }, false, ["encrypt"] ); const encrypted = await subtle.encrypt( { name: "AES-CBC", iv }, k, data ); const final = new Uint8Array(iv.length + encrypted.byteLength); console.log(final); final.set(iv, 0); final.set(new Uint8Array(encrypted), iv.length); return JSON.stringify({ content: b64encode(final.buffer) }); } async function decryptKeyinfoResponse(encrypted) { const buf = b64decode(encrypted); const iv = buf.slice(0, 16); const data = buf.slice(16); const subtle = getSubtle(); const k = await subtle.importKey( "raw", shared_key, { name: "AES-CBC" }, false, ["decrypt"] ); return subtle.decrypt( { name: "AES-CBC", iv }, k, data ); } async function refreshKeyinfo() { var _a, _b, _c; const b = await encryptKeyinfoBody(_config.currentConfig); const res = await appPost("/crypt/registerkey", b); const j = res.json(); const ek = (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.key; if (!ek) { throw new Error(`Failed to get key info: ${res.responseText}`); } const key = await decryptKeyinfoResponse(ek); const keyinfo = { key, keyver: (_b = j == null ? void 0 : j.data) == null ? void 0 : _b.keyver }; console.log("Refreshed key info:", keyinfo); _config.currentConfig.key_info = keyinfo; write("keyinfo", { key: b64encode(key), keyver: (_c = j == null ? void 0 : j.data) == null ? void 0 : _c.keyver }); } async function ensureKeyinfo(expectedKeyVersion) { const keyinfo = _config.currentConfig.key_info; const cachedKeyInfo = read("keyinfo"); console.log("cached key info: ", cachedKeyInfo); if (cachedKeyInfo) { const cki = { key: b64decode(cachedKeyInfo.key), keyver: cachedKeyInfo.keyver }; if (typeof expectedKeyVersion === "undefined" || cki.keyver === expectedKeyVersion) { _config.currentConfig.key_info = cki; return; } } if (!keyinfo) { return await refreshKeyinfo(); } if ((keyinfo == null ? void 0 : keyinfo.keyver) !== expectedKeyVersion) { return await refreshKeyinfo(); } } async function getChapter(itemId, _retry) { var _a, _b; if (typeof _retry === "undefined") _retry = 0; if (_retry > 5) { throw new Error(`Failed to get chapter: ${itemId}`); } if (!_config.currentConfig.key_info) { await ensureKeyinfo(); } const res = await appGet("/reader/full/v", { item_id: itemId, req_type: "1" }); const j = (_a = res.json()) == null ? void 0 : _a.data; if (!j) { console.warn("Failed to get chapter: ", itemId, ", response: ", res.responseText); return await getChapter(itemId, _retry + 1); } if ((j == null ? void 0 : j.content) === "Invalid" || (j == null ? void 0 : j.key_version) !== ((_b = _config.currentConfig.key_info) == null ? void 0 : _b.keyver)) { console.warn("Key reg expired, regster again and retrying..."); await ensureKeyinfo(parseInt(j == null ? void 0 : j.key_version)); return await getChapter(itemId, _retry + 1); } j.content = await decryptChapter(j == null ? void 0 : j.content, j, _config.currentConfig); return j; } async function getCatalogRaw(bookId) { var _a; const response = await appGet("/bookapi/directory/all_items/v", { book_id: bookId }); const j = response.json(); const items = (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.item_data_list; if ((j == null ? void 0 : j.code) !== 0 || !Array.isArray(items) || items.length === 0) { throw new Error("Empty catalog"); } return [items, items.map((it) => String(it.item_id))]; } async function getCatalog(bookId) { const r = await getCatalogRaw(bookId); const catalogRaw = r[0]; const allItemIds = r[1]; const vmap = {}; const chapters = []; catalogRaw.forEach((item) => { const volumeName = item.volume_name ?? ""; const chapterItem = { item_id: String(item.item_id), title: item.title, // YYYY-MM-DD HH:mm:ss update_time: moment(item.first_pass_time * 1e3).format("YYYY-MM-DD HH:mm:ss"), char_count: item.chapter_word_number || 0, volume_title: volumeName }; chapters.push(chapterItem); if (!vmap[volumeName]) { vmap[volumeName] = { title: volumeName, book_id: bookId, chapter_list: [] }; } vmap[volumeName].chapter_list.push(chapterItem); }); return { book_id: bookId, volume_list: Object.values(vmap), chapter_list: chapters, all_item_ids: allItemIds }; } function mappingCreationStatus(status) { switch (status) { case "0": return "完结"; case "1": return "连载"; case "4": return "断更"; default: return "未知"; } } async function getBookInfoRaw(bookId) { const response = await appGet("/bookapi/multi-detail/v", { book_id: bookId }); const j = response.json(); console.log("Book Info:", j); if (typeof j === "object" && j !== null && "data" in j && Array.isArray(j.data) && j.data.length > 0) return j.data[0]; return null; } async function getBookInfo(bookId) { const bookInfo = await getBookInfoRaw(bookId); if (!bookInfo) { throw new Error("Book not found"); } return { book_id: bookInfo.book_id, title: bookInfo.book_name || bookInfo.original_book_name, author: bookInfo.author, cover_url: bookInfo.thumb_url, summary: bookInfo.abstract, // volume_list: bookInfo.volume_list, update_time: moment(bookInfo.last_chapter_first_pass_time * 1e3).format("YYYY-MM-DD HH:mm:ss"), status: mappingCreationStatus(bookInfo.creation_status) // chapter_count: bookInfo.chapter_count, }; } async function getBookInfoAndCatalog(book) { if (typeof book !== "string") { book = book.book_id; } const bookInfo = await getBookInfo(book); if (!bookInfo) { throw new Error("Book not found"); } const catalog = await getCatalog(bookInfo.book_id); console.log("Catalog:", catalog); bookInfo.volume_list = catalog.volume_list; bookInfo.chapter_list = catalog.chapter_list; return bookInfo; } const CDN_PREFIX = "https://p3-novel.byteimg.com/origin/"; const cssCache = /* @__PURE__ */ new Map(); function stripComments(css) { return css.replace(/\/\*[\s\S]*?\*\//g, ""); } function scopeSelector(selector, scope) { const s = selector.trim(); if (!s) return ""; const where = `:where(${scope})`; if (/^(body|html|:root)$/i.test(s)) return where; const m = s.match(/^(body|html)\b([\s\S]*)$/i); if (m) return `${where}${m[2]}`; return `${where} ${s}`; } function stripRootDecls(body) { return body.split(";").filter((decl) => !/^\s*(color|background|background-color|font-size)\s*:/i.test(decl)).join(";"); } function scopeCss(css, scope) { const src = stripComments(css); let out = ""; let buf = ""; let i2 = 0; while (i2 < src.length) { const ch = src[i2]; if (ch === "{") { const prelude = buf.trim(); buf = ""; i2++; if (prelude.startsWith("@")) { if (/^@(media|supports|document)\b/i.test(prelude)) { const inner2 = readBlock(src, i2); out += `${prelude}{${scopeCss(inner2.text, scope)}}`; i2 = inner2.end; } else { const inner2 = readBlock(src, i2); out += `${prelude}{${inner2.text}}`; i2 = inner2.end; } continue; } const inner = readBlock(src, i2); const selectors = prelude.split(",").map((s) => scopeSelector(s, scope)).filter(Boolean); const isRoot = selectors.length === 1 && selectors[0] === `:where(${scope})`; const declarations = isRoot ? stripRootDecls(inner.text) : inner.text; if (selectors.length && declarations.trim()) { out += `${selectors.join(",")}{${declarations}}`; } i2 = inner.end; continue; } if (ch === ";" && buf.trim().startsWith("@")) { buf = ""; i2++; continue; } buf += ch; i2++; } return out; } function readBlock(src, start) { let depth = 1; let i2 = start; while (i2 < src.length && depth > 0) { const c = src[i2]; if (c === "{") depth++; else if (c === "}") depth--; if (depth === 0) break; i2++; } return { text: src.slice(start, i2), end: Math.min(i2 + 1, src.length) }; } function parseCssMap(cssMap) { if (!cssMap || typeof cssMap !== "string") return {}; try { const parsed = JSON.parse(cssMap); return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } async function fetchCss(uri) { const cached = cssCache.get(uri); if (cached !== void 0) return cached; try { const res = await fetch(CDN_PREFIX + uri); const text = res.ok ? await res.text() : ""; cssCache.set(uri, text); return text; } catch (e) { console.warn("获取书籍样式表失败:", uri, e); cssCache.set(uri, ""); return ""; } } async function getScopedBookCss(cssMap, scope) { const map = parseCssMap(cssMap); const uris = Object.values(map).filter(Boolean); if (uris.length === 0) return ""; const sheets = await Promise.all(uris.map(fetchCss)); return sheets.filter(Boolean).map((css) => scopeCss(css, scope)).join("\n"); } async function applyBookCss(cssMap, scope, styleId = "fqa-book-style") { const css = await getScopedBookCss(cssMap, scope); let el = document.getElementById(styleId); if (!css) { el == null ? void 0 : el.remove(); return false; } if (!el) { el = document.createElement("style"); el.id = styleId; document.head.appendChild(el); } el.textContent = css; return true; } const FOOTNOTE_CLASS = "fqa-footnote"; const FOOTNOTE_REF_CLASS = "fqa-footnote-ref"; const FOOTNOTE_LIST_CLASS = "fqa-footnote-list"; function byAttr(root, tag, attr, value) { return [...root.querySelectorAll(tag)].filter((el) => { const v = el.getAttribute(attr); if (v === null) return false; return value === void 0 || v === value; }); } function processFootnotes(root) { const notes = /* @__PURE__ */ new Map(); const sections = byAttr(root, "section", "epub:type", "footnotes"); for (const section of sections) { for (const aside of section.querySelectorAll("aside")) { const id = aside.getAttribute("id"); if (id) notes.set(id, aside.innerHTML.trim()); } } if (notes.size === 0) { for (const aside of byAttr(root, "aside", "epub:type", "footnote")) { const id = aside.getAttribute("id"); if (id) notes.set(id, aside.innerHTML.trim()); } } const collectRefs = (scope) => { const set = new Set(byAttr(scope, "a", "epub:type", "noteref")); for (const img of scope.querySelectorAll("img.bdFootnote")) { const a = img.closest('a[href^="#"]'); if (a && scope.contains(a)) set.add(a); } return [...scope.querySelectorAll("a")].filter((a) => set.has(a)); }; const inSection = (el) => sections.some((s) => s.contains(el)); const refs = collectRefs(root).filter((a) => !inSection(a)); if (refs.length === 0 && notes.size === 0) return 0; const ordered = []; const numberOf = /* @__PURE__ */ new Map(); let counter = 0; const makeSup = (num2, text) => { const sup = document.createElement("sup"); sup.className = FOOTNOTE_REF_CLASS; sup.textContent = String(num2); sup.setAttribute("role", "button"); sup.setAttribute("tabindex", "0"); sup.title = stripTags(text); return sup; }; refs.forEach((ref2) => { const href = ref2.getAttribute("href") ?? ""; const id = href.startsWith("#") ? href.slice(1) : ""; const text = notes.get(id); if (text === void 0) return; counter += 1; numberOf.set(id, counter); ordered.push({ num: counter, text }); ref2.replaceWith(makeSup(counter, text)); }); for (const [id, text] of notes) { if (numberOf.has(id)) continue; counter += 1; numberOf.set(id, counter); ordered.push({ num: counter, text }); } for (const section of sections) section.remove(); if (ordered.length > 0) { const list = document.createElement("ol"); list.className = FOOTNOTE_LIST_CLASS; for (const { num: num2, text } of ordered) { const li = document.createElement("li"); li.id = `fqa-fn-${num2}`; li.innerHTML = text; for (const inner of collectRefs(li)) { const innerId = (inner.getAttribute("href") ?? "").replace(/^#/, ""); const innerNum = numberOf.get(innerId); const innerText = notes.get(innerId); if (innerNum && innerText !== void 0) { inner.replaceWith(makeSup(innerNum, innerText)); } else { inner.remove(); } } list.appendChild(li); } const wrapper = document.createElement("section"); wrapper.className = FOOTNOTE_CLASS; const heading = document.createElement("div"); heading.className = "fqa-footnote-title"; heading.textContent = "注释"; wrapper.appendChild(heading); wrapper.appendChild(list); root.appendChild(wrapper); } for (const img of root.querySelectorAll("img.bdFootnote")) img.remove(); return ordered.length; } function stripTags(html) { const el = document.createElement("div"); el.innerHTML = html; return (el.textContent ?? "").replace(/\s+/g, " ").trim(); } const KNOWN_TIERS = /muye-reader-content-(16|20|24|28|32)\b/; function syncFootnoteFontSize(container2) { const box = container2.closest('[class*="muye-reader-content-"]'); const apply2 = () => { container2.style.removeProperty("--fqa-body-size"); if (!box || KNOWN_TIERS.test(box.className)) return; const p = container2.querySelector("p"); if (!p) return; const size = getComputedStyle(p).fontSize; if (size) container2.style.setProperty("--fqa-body-size", size); }; apply2(); if (!box) return; const holder = container2; if (holder.fqaFontObserver) return; const observer2 = new MutationObserver(apply2); observer2.observe(box, { attributes: true, attributeFilter: ["class"] }); holder.fqaFontObserver = observer2; } function bindFootnoteInteraction(container2) { syncFootnoteFontSize(container2); if (container2.dataset.fqaFootnoteBound === "1") return; container2.dataset.fqaFootnoteBound = "1"; const activate = (sup) => { var _a; const num2 = (_a = sup.textContent) == null ? void 0 : _a.trim(); if (!num2) return; const target = container2.querySelector(`#fqa-fn-${num2}`); if (!target) return; target.scrollIntoView({ behavior: "smooth", block: "center" }); target.classList.add("fqa-footnote-active"); setTimeout(() => target.classList.remove("fqa-footnote-active"), 1600); }; container2.addEventListener("click", (e) => { var _a, _b; const sup = (_b = (_a = e.target) == null ? void 0 : _a.closest) == null ? void 0 : _b.call(_a, `.${FOOTNOTE_REF_CLASS}`); if (sup) { e.preventDefault(); activate(sup); } }); container2.addEventListener("keydown", (e) => { var _a, _b; const ke = e; if (ke.key !== "Enter" && ke.key !== " ") return; const sup = (_b = (_a = ke.target) == null ? void 0 : _a.closest) == null ? void 0 : _b.call(_a, `.${FOOTNOTE_REF_CLASS}`); if (sup) { ke.preventDefault(); activate(sup); } }); } let currentBook = null; let latestItemId = null; const SCRIPT_CONTAINER_ID = "fqa-reader-content"; let comicObserver = null; function ensureScriptContainer(readerContainer, comic) { let scriptContainer = document.getElementById(SCRIPT_CONTAINER_ID); if (!scriptContainer) { scriptContainer = cloneElement(readerContainer); scriptContainer.id = SCRIPT_CONTAINER_ID; scriptContainer.classList.add("fqa"); readerContainer.insertAdjacentElement("beforebegin", scriptContainer); } scriptContainer.classList.toggle("fqa-comic-reader", comic); if (settings$1.allowCopy) scriptContainer.classList.remove("noselect"); comicObserver == null ? void 0 : comicObserver.disconnect(); comicObserver = null; scriptContainer.innerHTML = ""; readerContainer.classList.add("fqa-hide"); return scriptContainer; } async function insertContent() { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j; const itemId = ((_a = window.location.pathname.split("/").pop()) == null ? void 0 : _a.substring(0, 19)) || ""; if (!itemId) { console.warn("No item_id found in URL"); return; } latestItemId = itemId; const chapter = await getChapter(itemId); if (!chapter) { console.warn("No chapter found for item_id:", itemId); return; } if (latestItemId !== itemId) { console.debug("Stale chapter response discarded:", itemId); return; } console.log("Chapter:", chapter); const pageState = unsafeWindow.__INITIAL_STATE__; const chapterTitle = ((_b = chapter.novel_data) == null ? void 0 : _b.title) || ((_d = (_c = pageState == null ? void 0 : pageState.reader) == null ? void 0 : _c.chapterData) == null ? void 0 : _d.title); if (typeof chapter.content === "string") { void applyBookCss((_e = chapter.novel_data) == null ? void 0 : _e.css_map, "#fqa-reader-content"); const dp = new DOMParser(); const doc = dp.parseFromString(chapter.content, "text/html"); const body = doc.body; body.querySelectorAll('link[rel="stylesheet"]').forEach((el) => el.remove()); let article = body.querySelector("article"); let toProcess = article || body; processFootnotes(toProcess); for (let i2 = 0; i2 < toProcess.childNodes.length; i2++) { if (i2 < 2 && ((_g = (_f = toProcess.childNodes[i2]) == null ? void 0 : _f.innerHTML) == null ? void 0 : _g.includes(chapterTitle))) { toProcess.removeChild(toProcess.childNodes[i2]); break; } } if (!article) { article = document.createElement("article"); article.innerHTML = toProcess.innerHTML; toProcess = article; } const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)"); if (readerContainer) { const scriptContainer = ensureScriptContainer(readerContainer, false); scriptContainer.appendChild(toProcess); bindFootnoteInteraction(scriptContainer); } } else if (chapter.content.picInfos) { if (chapter.content.encrypt) { const imgs = []; for (let i2 = 0; i2 < chapter.content.picInfos.length; i2++) { const picInfo = chapter.content.picInfos[i2]; const img = document.createElement("img"); img.className = "fqa-comic-img fqa-comic-encrypted"; img.alt = `第${i2 + 1}页`; img.dataset.encryptedUrl = picInfo.picUrl; img.dataset.encryptKey = chapter.content.encrypt_key; img.dataset.pageIndex = i2.toString(); img.style.minHeight = "500px"; img.style.backgroundColor = "#f0f0f0"; imgs.push(img); } const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)"); if (readerContainer) { const scriptContainer = ensureScriptContainer(readerContainer, true); imgs.forEach((img) => scriptContainer.appendChild(img)); const observer2 = new IntersectionObserver( async (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.encryptedUrl && img.dataset.encryptKey && !img.src) { observer2.unobserve(img); try { const encryptedBuffer = await fetchArrayBuffer( img.dataset.encryptedUrl ); const decryptedBuffer = await decryptComicImage( encryptedBuffer, img.dataset.encryptKey ); const blob = new Blob([decryptedBuffer], { type: "image/jpeg" }); const blobUrl = URL.createObjectURL(blob); img.src = blobUrl; img.style.minHeight = ""; img.style.backgroundColor = ""; img.onload = () => { URL.revokeObjectURL(blobUrl); }; } catch (error) { console.error(`解密图片失败 (页 ${img.dataset.pageIndex}):`, error); img.alt = `第${Number(img.dataset.pageIndex) + 1}页 - 解密失败`; img.style.backgroundColor = "#ffebee"; } } } } }, { rootMargin: "200px" } ); comicObserver = observer2; imgs.forEach((img) => observer2.observe(img)); } } else { const imgs = []; for (let i2 = 0; i2 < chapter.content.picInfos.length; i2++) { const picInfo = chapter.content.picInfos[i2]; const img = document.createElement("img"); img.className = "fqa-comic-img"; img.alt = `第${i2 + 1}页`; img.src = picInfo.picUrl; imgs.push(img); } const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)"); if (readerContainer) { const scriptContainer = ensureScriptContainer(readerContainer, true); imgs.forEach((img) => scriptContainer.appendChild(img)); } } } const muyeReaderTitle = document.querySelector("h1.muye-reader-title"); let muyeReaderSubtitle = document.querySelector("div.muye-reader-subtitle"); (_h = document.querySelector("#fqa-subtitle")) == null ? void 0 : _h.remove(); if (muyeReaderSubtitle) { let _cloned = cloneElement(muyeReaderSubtitle); muyeReaderSubtitle.classList.add("fqa-hide"); _cloned.id = "fqa-subtitle"; muyeReaderSubtitle.insertAdjacentElement("afterend", _cloned); muyeReaderSubtitle = _cloned; _cloned.classList.remove("fqa-hide"); console.log("clone subtitle: ", _cloned); } if (muyeReaderTitle) { muyeReaderTitle.textContent = chapterTitle; } console.log("Current book:", currentBook); if (!currentBook || currentBook == null || currentBook.book_id !== ((_i = chapter.novel_data) == null ? void 0 : _i.book_id)) { currentBook = await getBookInfoAndCatalog((_j = chapter.novel_data) == null ? void 0 : _j.book_id); console.log("Current book:", currentBook); } if (currentBook && currentBook.chapter_list) { const currentChapterItem = currentBook.chapter_list.find((c) => c.item_id === itemId); if (currentChapterItem) { console.log("Current chapter:", currentChapterItem); document.title = currentChapterItem.title + " - " + currentBook.title + " - 番茄小说"; if (document.getElementById("fqa-current-chapter-volume")) { const c = document.getElementById("fqa-current-chapter-volume"); if (c) { c.textContent = currentChapterItem.volume_title; } } else { const volSpan = document.createElement("span"); volSpan.className = "desc-item"; volSpan.id = "fqa-current-chapter-volume"; volSpan.textContent = currentChapterItem.volume_title; const c = muyeReaderSubtitle == null ? void 0 : muyeReaderSubtitle.firstChild; if (c) { c.insertAdjacentElement("beforebegin", volSpan); } } let updateTimeSpans = (muyeReaderSubtitle == null ? void 0 : muyeReaderSubtitle.querySelectorAll("span.desc-item")) || []; if (updateTimeSpans.length >= 2) { let updateTimeSpan = updateTimeSpans[updateTimeSpans.length - 1]; let uttspan = updateTimeSpan.firstChild; uttspan == null ? void 0 : uttspan.remove(); updateTimeSpan.innerHTML = "更新时间:" + currentChapterItem.update_time; } else { let updateTimeSpan = document.createElement("span"); updateTimeSpan.className = "desc-item"; updateTimeSpan.textContent = `更新时间:${currentChapterItem.update_time}`; } } } } async function onUrlChange$1(_previous) { await insertContent(); } async function onHashChange$1(_previous) { } async function onLoad$1() { document.querySelector("div.muye-reader-btns"); await insertContent(); } function readerFilter(path, _query, _hash) { return path.startsWith("/reader") || path.startsWith("reader"); } const _exports$4 = [ { id: "readerHook_load", event: "load", handler: onLoad$1, filter: readerFilter }, { id: "readerHook_urlChange", event: "onUrlChange", handler: onUrlChange$1, filter: readerFilter }, { id: "readerHook_hashChange", event: "onHashChange", handler: onHashChange$1, filter: readerFilter } ]; const blackList = [ "mcs.zijieapi.com", "vcs.zijieapi.com/vc/setting", "mon.zijieapi.com", "mssdk.bytedance.com/web/common", "hm.baidu.com" ]; const BLOCKED_BODY = JSON.stringify({ e: 0, sc: 10, tc: 10 }); function checkBlack(url) { if (!settings$1.blockReport) return false; return blackList.some((black) => url.includes(black)); } const originalFetch = unsafeWindow.fetch.bind(unsafeWindow); unsafeWindow.fetch = function fetch2(input, init2) { let url; if (input instanceof Request) { url = input.url; } else if (input instanceof URL) { url = input.href; } else { url = input; } if (checkBlack(url)) { console.log("blocked request: " + url); return Promise.resolve(new Response(BLOCKED_BODY, { status: 200, headers: { "Content-Type": "application/json" } })); } return originalFetch(input, init2); }; const originalXMLHttpRequest = unsafeWindow.XMLHttpRequest; unsafeWindow.XMLHttpRequest = class XMLHttpRequest extends originalXMLHttpRequest { constructor() { super(...arguments); __publicField(this, "_blockedUrl"); } open(method, url, async = true, user, password) { if (checkBlack(url)) { console.log("blocked request: " + url); this._blockedUrl = url; return; } this._blockedUrl = void 0; super.open(method, url, async, user, password); } setRequestHeader(name2, value) { if (this._blockedUrl !== void 0) return; super.setRequestHeader(name2, value); } send(body) { if (this._blockedUrl === void 0) { super.send(body); return; } const url = this._blockedUrl; const shadow = (prop, value) => Object.defineProperty(this, prop, { configurable: true, get: () => value }); setTimeout(() => { shadow("readyState", 4); shadow("status", 200); shadow("statusText", "OK"); shadow("responseURL", url); shadow("responseText", this.responseType === "" || this.responseType === "text" ? BLOCKED_BODY : ""); shadow("response", this.responseType === "json" ? { "e": 0, "sc": 10, "tc": 10 } : BLOCKED_BODY); this.dispatchEvent(new Event("readystatechange")); this.dispatchEvent(new ProgressEvent("load")); this.dispatchEvent(new ProgressEvent("loadend")); }, 0); } abort() { if (this._blockedUrl !== void 0) return; super.abort(); } getAllResponseHeaders() { if (this._blockedUrl !== void 0) return "content-type: application/json\r\n"; return super.getAllResponseHeaders(); } getResponseHeader(name2) { if (this._blockedUrl !== void 0) { return name2.toLowerCase() === "content-type" ? "application/json" : null; } return super.getResponseHeader(name2); } }; const _exports$3 = []; let userState = { isLogin: false, userInfo: null }; if (read("userState")) { userState = read("userState"); } console.log("userState:", userState); async function getDetailedUserInfo() { if (!(userState == null ? void 0 : userState.isLogin) || !(userState == null ? void 0 : userState.userInfo)) { return null; } if (userState.userInfo.gender !== void 0 && userState.userInfo.recommend_gender !== void 0 && userState.userInfo.fans_num !== void 0 && userState.userInfo.following_num !== void 0 && userState.userInfo.is_author !== void 0 && userState.userInfo.author_desc !== void 0 && userState.userInfo.read_book_num !== void 0 && userState.userInfo.read_book_time !== void 0) { return userState.userInfo; } const response = await fetch$1("https://fanqienovel.com/reading/user/basic_info/get/v?aid=1967"); const j = await response.json(); if (j == null ? void 0 : j.data) { const data = j.data; userState.userInfo.gender = data.profile_gender; userState.userInfo.recommend_gender = data.gender; userState.userInfo.fans_num = data.fans_num; userState.userInfo.following_num = data.follow_user_num; userState.userInfo.is_author = data.is_author; userState.userInfo.author_desc = data.author_desc; userState.userInfo.read_book_num = data.read_book_num; userState.userInfo.read_book_time = BigInt(data.read_book_time); return userState.userInfo; } write("userState", userState); return userState.userInfo; } async function checkLogin() { var _a, _b, _c, _d, _e, _f; const response = await fetch$1("https://fanqienovel.com/api/user/info/v2"); const j = await response.json(); const _userInfo = { id: (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.id, username: (_b = j == null ? void 0 : j.data) == null ? void 0 : _b.name, avatar: (_c = j == null ? void 0 : j.data) == null ? void 0 : _c.avatar, desc: (_d = j == null ? void 0 : j.data) == null ? void 0 : _d.desc, age: (_e = j == null ? void 0 : j.data) == null ? void 0 : _e.age }; if (((_f = j == null ? void 0 : j.data) == null ? void 0 : _f.id) > 1) { userState.isLogin = true; userState.userInfo = _userInfo; write("userState", userState); return true; } else { del("userState"); return false; } } async function init() { var _a; await checkLogin(); if (userState.isLogin) { console.log("Hello, ", (_a = userState == null ? void 0 : userState.userInfo) == null ? void 0 : _a.username); } } const bookshelf = '\r\n \r\n \r\n \r\n \r\n \r\n'; const settings = '\r\n\r\n \r\n \r\n'; const FIXED_STRING = b64decode( "TdTC5rgxYgkOUrPHpnM7pByyRiuCmrWKGWs521cXdST0m69/COjWjSanLjfBqVovHwWlGJKu8pSXMrYqOKrdWA==" ); async function encrypt(data) { const crypto = getCrypto(); const subtle = getSubtle(); const randomBytes = crypto.getRandomValues(new Uint8Array(32)); const hashValue = await hash.sha512bytes( concatArrayBuffers(await hash.sha512bytes(randomBytes), FIXED_STRING) ); const k = hashValue.slice(0, 16); const iv = hashValue.slice(16, 32); const compressedData = await gzip(data); const hashedData = concatArrayBuffers( await hash.sha512bytes(compressedData), compressedData ); const key = await subtle.importKey("raw", k, { name: "AES-CBC", length: 128 }, false, ["encrypt"]); const encryptedData = await subtle.encrypt({ name: "AES-CBC", iv }, key, hashedData); return concatArrayBuffers( new Uint8Array([116, 99, 5, 16, 0, 0]).buffer, randomBytes.buffer, encryptedData ); } const ANDROID_VERSIONS = [ { version: "9", api: 29 }, { version: "10", api: 30 }, { version: "11", api: 31 }, { version: "12", api: 32 }, { version: "13", api: 33 }, { version: "14", api: 34 } ]; const DEVICE_MODELS = [ "RMX1931", "MI8", "Honor10", "P30", "V1921A", "Redmi Note 7", "Redmi K20 Pro", "MI 9", "Mi 10 Pro", "SM-G9750", "Pixel 6", "HD1910", "M2011K2C", "LIO-AN00", "VOG-TL00", "PCLM10", "GM1900", "Pixel 7 Pro", "Pixel 6a", "SM-N9760", "POCO F1" ]; const DEVICE_BRANDS = [ "realme", "Xiaomi", "Huawei", "OPPO", "vivo", "samsung", "OnePlus", "google", "Redmi", "HONOR", "motorola", "POCO" ]; const HEX_LOW = "0123456789abcdef"; const deviceValue = { aid: "1967", appName: "novelapp", channel: "0", platform: "android", osVersion: "0", versionCode: { str: "6.3.9.32", val: "63932" } }; function randomInt(max) { return getCrypto().getRandomValues(new Uint32Array(1))[0] % max; } function randomItem(list) { return list[randomInt(list.length)]; } function randomHex(k) { if (k <= 0) return ""; const bytes = getCrypto().getRandomValues(new Uint8Array(k)); let result = ""; for (let i2 = 0; i2 < k; i2++) { result += HEX_LOW[bytes[i2] % 16]; } return result; } function uuid() { const c = getCrypto(); if (typeof c.randomUUID === "function") { return c.randomUUID(); } const b = c.getRandomValues(new Uint8Array(16)); b[6] = b[6] & 15 | 64; b[8] = b[8] & 63 | 128; const h2 = [...b].map((x) => x.toString(16).padStart(2, "0")).join(""); return `${h2.slice(0, 8)}-${h2.slice(8, 12)}-${h2.slice(12, 16)}-${h2.slice(16, 20)}-${h2.slice(20)}`; } function ipv6LinkLocal() { return "::1"; } function ipv6UniqueLocal() { const x = randomInt(1 << 8); let result = `fd${HEX_LOW[x >> 4]}${HEX_LOW[x & 15]}`; for (let i2 = 0; i2 < 7; i2++) { const v = randomInt(1 << 16); result += ":" + HEX_LOW[v >> 12 & 15] + HEX_LOW[v >> 8 & 15] + HEX_LOW[v >> 4 & 15] + HEX_LOW[v & 15]; } return result; } function generateRequestBody() { const osInfo = randomItem(ANDROID_VERSIONS); const deviceBrand = randomItem(DEVICE_BRANDS); const genTime = Date.now(); const romVersion = "coloros__" + randomHex(4).toUpperCase() + "." + String(randomInt(1e6)).padStart(6, "0") + "." + String(randomInt(1e8)).padStart(8, "0") + " release-keys"; return { magic_tag: "ss_app_log", header: { display_name: "番茄免费小说", aid: 1967, channel: "43536163a", package: "com.dragon.read", sdk_version: "3.7.0-rc.25-fanqie-xiaoshuo", sdk_target_version: 29, git_hash: "711d1a7", density_dpi: 240, display_density: "hdpi", resolution: "720x1280", language: "zh", timezone: 8, access: "wifi", not_request_sender: 0, carrier: "CHINA MOBILE", mcc_mnc: "46000", region: "CN", tz_name: "Asia/Shanghai", tz_offset: 28800, sim_region: "cn", sim_serial_number: [], oaid_may_support: false, device_platform: "android", custom: { host_bit: 32, dragon_device_type: 0 }, pre_installed_channel: "", is_system_app: 0, sdk_flavor: "china", guest_mode: 0, // 设备硬件与系统信息 os: "Android", os_version: osInfo.version, os_api: osInfo.api, device_model: randomItem(DEVICE_MODELS), device_brand: deviceBrand, device_manufacturer: deviceBrand, cpu_abi: "arm64-v8a", release_build: randomHex(7), cdid: uuid(), sig_hash: "a4a27c2633195374c15651ffc3c4a497", openudid: randomHex(20), clientudid: uuid(), req_id: uuid(), // 可选字段 rom: randomHex(14).toUpperCase(), rom_version: romVersion, apk_first_install_time: genTime - randomInt(365) * 864e5, ipv6_list: [ { type: "client_anpi", value: ipv6LinkLocal() }, { type: "client_anpi", value: ipv6UniqueLocal() }, { type: "client_anpi", value: ipv6UniqueLocal() } ] }, _gen_time: genTime }; } const REGISTER_URL = "https://i.snssdk.com/service/2/device_register/?tt_data=a"; const READING_BASE = "https://reading.snssdk.com"; const USER_AGENT = "com.dragon.read"; function buildQuery(device, versionCode, extra) { return new URLSearchParams({ device_id: device.device_id, iid: device.install_id, device_type: device.device_type, aid: deviceValue.aid, app_name: deviceValue.appName, channel: deviceValue.channel, device_platform: deviceValue.platform, os_version: deviceValue.osVersion, version_code: versionCode, ...extra }); } async function registerDevice() { const body = generateRequestBody(); const encrypted = await encrypt( new TextEncoder().encode(JSON.stringify(body)).buffer ); const res = await apiFetch(REGISTER_URL, { method: "POST", headers: { "User-Agent": "okhttp/4.10.0", "Content-Type": "application/octet-stream; tt-data=a" }, body: encrypted }); if (res.status !== 200) { throw new Error(`设备注册失败: HTTP ${res.status} ${res.statusText}`); } const json = res.json(); if (!(json == null ? void 0 : json.device_id) || !json.device_id_str || !json.install_id_str) { throw new Error(`设备注册失败: device_id 无效, 响应=${res.responseText}`); } const device = { device_id: json.device_id_str, install_id: json.install_id_str, device_type: body.header.device_model }; console.log("设备注册成功!", device); return device; } async function activatePremium(device) { var _a; const url = `${READING_BASE}/reading/user/privilege/add/v?` + buildQuery(device, deviceValue.versionCode.val, { manifest_version_code: deviceValue.versionCode.val, update_version_code: deviceValue.versionCode.val }).toString(); const body = `{"add_count_daily":0,"amount":2592000,"privilege_id":7210376203117531962,"from":8,"unique_key":"${Date.now()}"}`; try { const headers = await signRequest(url, body); const res = await apiFetch(url, { method: "POST", headers: { ...headers, "User-Agent": USER_AGENT, "Content-Type": "application/json; charset=utf-8" }, body }); const json = res.json(); if ((json == null ? void 0 : json.code) !== 0) { console.warn("设备会员激活失败:", res.responseText); return ""; } const expireTime = ((_a = json.data) == null ? void 0 : _a.expire_time) ?? ""; console.log("设备会员已成功激活!过期时间:", expireTime); return expireTime; } catch (e) { console.warn("设备会员激活失败:", e); return ""; } } async function registerKey(device) { var _a, _b; const url = `${READING_BASE}/reading/crypt/registerkey?` + buildQuery(device, deviceValue.versionCode.str).toString(); const idBytes = new Uint8Array(16); let id = BigInt(device.device_id); for (let i2 = 15; i2 >= 0; i2--) { idBytes[i2] = Number(id & 0xffn); id >>= 8n; } idBytes.reverse(); const subtle = getSubtle(); const iv = getCrypto().getRandomValues(new Uint8Array(16)); const key = await subtle.importKey("raw", shared_key, { name: "AES-CBC" }, false, ["encrypt"]); const encrypted = new Uint8Array( await subtle.encrypt({ name: "AES-CBC", iv }, key, idBytes) ); const content = new Uint8Array(iv.length + encrypted.length); content.set(iv); content.set(encrypted, iv.length); const plainBody = JSON.stringify({ content: b64encode(content.buffer) }); const gzipped = await gzip(plainBody); const headers = await signRequest(url, gzipped); const res = await apiFetch(url, { method: "POST", headers: { ...headers, "User-Agent": USER_AGENT, "Content-Type": "application/json; charset=utf-8", "Content-Encoding": "gzip" }, body: gzipped }); if (res.status !== 200) { throw new Error(`密钥注册失败: HTTP ${res.status} ${res.statusText}`); } const json = res.json(); const encryptedKey = (_a = json == null ? void 0 : json.data) == null ? void 0 : _a.key; if (!encryptedKey) { throw new Error(`密钥注册失败: 响应缺少 key, 响应=${res.responseText}`); } const buf = b64decode(encryptedKey); const decryptKey = await subtle.importKey( "raw", shared_key, { name: "AES-CBC" }, false, ["decrypt"] ); const finalKey = await subtle.decrypt( { name: "AES-CBC", iv: buf.slice(0, 16) }, decryptKey, buf.slice(16) ); const keyInfo = { key: finalKey, keyver: (_b = json.data) == null ? void 0 : _b.keyver }; console.log("密钥获取成功,版本:", keyInfo.keyver, "key:", hex(finalKey)); return keyInfo; } const STORE_KEY = "device"; function load() { const s = read(STORE_KEY); if (!(s == null ? void 0 : s.device_id) || !(s == null ? void 0 : s.install_id)) return null; return { device_id: s.device_id, install_id: s.install_id, device_type: s.device_type, device_brand: s.device_brand, key_info: s.key ? { key: b64decode(s.key), keyver: s.keyver } : void 0 }; } function save(c, vipExpireTime) { var _a; const s = { device_id: c.device_id, install_id: c.install_id, device_type: c.device_type, device_brand: c.device_brand, vip_expire_time: vipExpireTime }; if ((_a = c.key_info) == null ? void 0 : _a.key) { s.key = b64encode(c.key_info.key); s.keyver = c.key_info.keyver; } write(STORE_KEY, s); } async function provisionDevice() { const dev = await registerDevice(); const vipExpireTime = await activatePremium(dev); const keyInfo = await registerKey(dev); const c = { device_id: dev.device_id, install_id: dev.install_id, device_type: dev.device_type, key_info: keyInfo }; _config.currentConfig = c; save(c, vipExpireTime); return c; } async function ensureDevice() { const { deviceId, installId, deviceType } = settings$1; if (deviceId.trim() && installId.trim()) { const manual = { device_id: deviceId.trim(), install_id: installId.trim(), device_type: deviceType.trim() || void 0, // 手填设备没有密钥,正文接口会按需自行注册 key_info: void 0 }; _config.currentConfig = manual; console.log("使用设置里手填的设备:", manual.device_id); return manual; } const cached = load(); if (cached) { _config.currentConfig = cached; console.log("复用已缓存设备:", cached.device_id); return cached; } try { return await provisionDevice(); } catch (e) { console.warn("设备注册失败,回退到内置匿名设备:", e); _config.currentConfig = defaultConfig; return defaultConfig; } } const name = "fanqie-assistant"; const version = "0.0.5"; const _hoisted_1$8 = { class: "fqa-set-dialog", role: "dialog", "aria-modal": "true", "aria-label": "助手设置" }; const _hoisted_2$8 = { class: "fqa-set-side" }; const _hoisted_3$8 = ["onClick", "onKeydown"]; const _hoisted_4$7 = { class: "fqa-set-main" }; const _hoisted_5$7 = { class: "fqa-set-row" }; const _hoisted_6$7 = { class: "fqa-set-row" }; const _hoisted_7$5 = { class: "fqa-set-row" }; const _hoisted_8$5 = { class: "fqa-set-row fqa-set-row-col" }; const _hoisted_9$5 = { class: "fqa-set-row" }; const _hoisted_10$5 = { class: "fqa-set-row fqa-set-row-col" }; const _hoisted_11$5 = ["disabled"]; const _hoisted_12$5 = { class: "fqa-set-row" }; const _hoisted_13$4 = { class: "fqa-set-row fqa-set-row-col" }; const _hoisted_14$4 = { class: "fqa-set-row", style: { "padding-top": "0", "border-bottom": "none" } }; const _hoisted_15$2 = { class: "fqa-set-row fqa-set-row-col" }; const _hoisted_16$1 = { class: "fqa-set-radios" }; const _hoisted_17$1 = { class: "fqa-set-radio" }; const _hoisted_18$1 = { class: "fqa-set-radio" }; const _hoisted_19$1 = { class: "fqa-set-row fqa-set-row-col" }; const _hoisted_20 = { class: "fqa-set-field" }; const _hoisted_21 = ["placeholder"]; const _hoisted_22 = { class: "fqa-set-field" }; const _hoisted_23 = ["placeholder"]; const _hoisted_24 = { class: "fqa-set-field" }; const _hoisted_25 = ["placeholder"]; const _hoisted_26 = { class: "fqa-set-actions" }; const _hoisted_27 = ["disabled"]; const _hoisted_28 = { key: 0, class: "fqa-set-note" }; const _hoisted_29 = { class: "fqa-set-note" }; const _hoisted_30 = { class: "fqa-set-links" }; const _hoisted_31 = { class: "fqa-set-link-row" }; const _hoisted_32 = ["href"]; const GREASYFORK = "https://greasyfork.org/zh-CN/scripts/589115-%E7%95%AA%E8%8C%84%E5%B0%8F%E8%AF%B4%E5%8A%A9%E6%89%8B"; const GITHUB = "https://github.com/naiyQAQ/fanqie-assistant"; const _sfc_main$8 = /* @__PURE__ */ vue.defineComponent({ __name: "SettingsView", emits: ["close"], setup(__props, { emit: __emit }) { const emit = __emit; const SECTIONS = [ { key: "general", label: "常规" }, { key: "ui", label: "界面" }, { key: "search", label: "搜索" }, { key: "protocol", label: "协议" }, { key: "about", label: "关于" } ]; const active = vue.ref("general"); const registering = vue.ref(false); const registerMsg = vue.ref(null); async function reRegister() { if (registering.value) return; registering.value = true; registerMsg.value = "正在注册新设备…"; try { const c = await provisionDevice(); settings$1.deviceId = ""; settings$1.installId = ""; settings$1.deviceType = ""; flushSettings(); registerMsg.value = `注册成功:${c.device_id}`; } catch (err) { console.error("[fqa:settings] 设备注册失败:", err); registerMsg.value = err instanceof Error ? `注册失败:${err.message}` : "注册失败"; } finally { registering.value = false; } } const currentDevice = vue.computed(() => ({ device_id: settings$1.deviceId || _config.currentConfig.device_id, install_id: settings$1.installId || _config.currentConfig.install_id, device_type: settings$1.deviceType || _config.currentConfig.device_type || "" })); function close() { flushSettings(); emit("close"); } function onKey(e) { if (e.key === "Escape") close(); } vue.onMounted(() => document.addEventListener("keydown", onKey)); vue.onBeforeUnmount(() => document.removeEventListener("keydown", onKey)); const FEEDBACK = `${GREASYFORK}/feedback`; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { class: "fqa-set-mask", onClick: vue.withModifiers(close, ["self"]) }, [ vue.createElementVNode("div", _hoisted_1$8, [ vue.createElementVNode("button", { class: "fqa-set-close", "aria-label": "关闭", onClick: close }, "✕"), vue.createElementVNode("nav", _hoisted_2$8, [ _cache[13] || (_cache[13] = vue.createElementVNode("div", { class: "fqa-set-side-title" }, "助手设置", -1)), (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(SECTIONS, (s) => { return vue.createElementVNode("div", { key: s.key, class: vue.normalizeClass(["fqa-set-nav", { "fqa-set-nav-active": active.value === s.key }]), role: "button", tabindex: "0", onClick: ($event) => active.value = s.key, onKeydown: vue.withKeys(vue.withModifiers(($event) => active.value = s.key, ["prevent"]), ["enter"]) }, vue.toDisplayString(s.label), 43, _hoisted_3$8); }), 64)) ]), vue.createElementVNode("section", _hoisted_4$7, [ active.value === "general" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [ _cache[17] || (_cache[17] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "常规", -1)), vue.createElementVNode("label", _hoisted_5$7, [ _cache[14] || (_cache[14] = vue.createElementVNode("span", { class: "fqa-set-label" }, "解密网页端混淆字体", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(settings$1).decryptFont = $event), type: "checkbox", class: "fqa-set-switch" }, null, 512), [ [vue.vModelCheckbox, vue.unref(settings$1).decryptFont] ]) ]), vue.createElementVNode("label", _hoisted_6$7, [ _cache[15] || (_cache[15] = vue.createElementVNode("span", { class: "fqa-set-label" }, "拦截网页事件上报", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.unref(settings$1).blockReport = $event), type: "checkbox", class: "fqa-set-switch" }, null, 512), [ [vue.vModelCheckbox, vue.unref(settings$1).blockReport] ]) ]), vue.createElementVNode("label", _hoisted_7$5, [ _cache[16] || (_cache[16] = vue.createElementVNode("span", { class: "fqa-set-label" }, "允许阅读器复制文本", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => vue.unref(settings$1).allowCopy = $event), type: "checkbox", class: "fqa-set-switch" }, null, 512), [ [vue.vModelCheckbox, vue.unref(settings$1).allowCopy] ]) ]) ], 64)) : active.value === "ui" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [ _cache[22] || (_cache[22] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "界面", -1)), vue.createElementVNode("div", _hoisted_8$5, [ _cache[18] || (_cache[18] = vue.createElementVNode("span", { class: "fqa-set-label" }, "阅读器字体", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => vue.unref(settings$1).readerFont = $event), class: "fqa-set-input", type: "text", placeholder: "留空表示使用默认字体" }, null, 512), [ [vue.vModelText, vue.unref(settings$1).readerFont] ]), _cache[19] || (_cache[19] = vue.createElementVNode("p", { class: "fqa-set-note" }, "填写字体名称,例如「思源宋体」。留空则跟随网页默认。", -1)) ]), vue.createElementVNode("div", _hoisted_9$5, [ _cache[20] || (_cache[20] = vue.createElementVNode("span", { class: "fqa-set-label" }, "自定义 CSS", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => vue.unref(settings$1).customCssEnabled = $event), type: "checkbox", class: "fqa-set-switch" }, null, 512), [ [vue.vModelCheckbox, vue.unref(settings$1).customCssEnabled] ]) ]), vue.createElementVNode("div", _hoisted_10$5, [ vue.withDirectives(vue.createElementVNode("textarea", { "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => vue.unref(settings$1).customCss = $event), class: "fqa-set-textarea", disabled: !vue.unref(settings$1).customCssEnabled, spellcheck: "false", placeholder: "/* 自定义 CSS */" }, null, 8, _hoisted_11$5), [ [vue.vModelText, vue.unref(settings$1).customCss] ]), _cache[21] || (_cache[21] = vue.createElementVNode("p", { class: "fqa-set-note" }, "关闭开关后内容会保留,只是不再应用。", -1)) ]) ], 64)) : active.value === "search" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [ _cache[26] || (_cache[26] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "搜索", -1)), vue.createElementVNode("label", _hoisted_12$5, [ _cache[23] || (_cache[23] = vue.createElementVNode("span", { class: "fqa-set-label" }, "接管搜索界面", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => vue.unref(settings$1).enhanceSearch = $event), type: "checkbox", class: "fqa-set-switch" }, null, 512), [ [vue.vModelCheckbox, vue.unref(settings$1).enhanceSearch] ]) ]), vue.createElementVNode("div", _hoisted_13$4, [ vue.createElementVNode("label", _hoisted_14$4, [ _cache[24] || (_cache[24] = vue.createElementVNode("span", { class: "fqa-set-label" }, "个人化推荐", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => vue.unref(settings$1).searchPersonalized = $event), type: "checkbox", class: "fqa-set-switch" }, null, 512), [ [vue.vModelCheckbox, vue.unref(settings$1).searchPersonalized] ]) ]), _cache[25] || (_cache[25] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 开启后搜索走同源请求,由浏览器自动带上你的登录 Cookie,番茄据此按阅读偏好排序。 凭据不经过脚本,也不会发往番茄以外的任何地方。关闭时走匿名请求。 ", -1)) ]) ], 64)) : active.value === "protocol" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [ _cache[36] || (_cache[36] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "协议", -1)), vue.createElementVNode("div", _hoisted_15$2, [ _cache[29] || (_cache[29] = vue.createElementVNode("span", { class: "fqa-set-label" }, "API 偏好", -1)), vue.createElementVNode("div", _hoisted_16$1, [ vue.createElementVNode("label", _hoisted_17$1, [ vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[8] || (_cache[8] = ($event) => vue.unref(settings$1).apiPreference = $event), type: "radio", value: "app" }, null, 512), [ [vue.vModelRadio, vue.unref(settings$1).apiPreference] ]), _cache[27] || (_cache[27] = vue.createElementVNode("span", null, "番茄 APP", -1)) ]), vue.createElementVNode("label", _hoisted_18$1, [ vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[9] || (_cache[9] = ($event) => vue.unref(settings$1).apiPreference = $event), type: "radio", value: "redcandle" }, null, 512), [ [vue.vModelRadio, vue.unref(settings$1).apiPreference] ]), _cache[28] || (_cache[28] = vue.createElementVNode("span", null, "红烛 APP", -1)) ]) ]), _cache[30] || (_cache[30] = vue.createElementVNode("p", { class: "fqa-set-note" }, "如果某协议数据不全,脚本可能会选择其他接口作为补充。", -1)) ]), vue.createElementVNode("div", _hoisted_19$1, [ _cache[34] || (_cache[34] = vue.createElementVNode("span", { class: "fqa-set-label" }, "设备信息", -1)), _cache[35] || (_cache[35] = vue.createElementVNode("p", { class: "fqa-set-warn" }, " 如果不知道这是什么,请保持默认。乱填可能导致脚本功能异常。 ", -1)), vue.createElementVNode("label", _hoisted_20, [ _cache[31] || (_cache[31] = vue.createElementVNode("span", null, "device_id", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[10] || (_cache[10] = ($event) => vue.unref(settings$1).deviceId = $event), class: "fqa-set-input", type: "text", placeholder: currentDevice.value.device_id || "自动注册" }, null, 8, _hoisted_21), [ [vue.vModelText, vue.unref(settings$1).deviceId] ]) ]), vue.createElementVNode("label", _hoisted_22, [ _cache[32] || (_cache[32] = vue.createElementVNode("span", null, "install_id (iid)", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[11] || (_cache[11] = ($event) => vue.unref(settings$1).installId = $event), class: "fqa-set-input", type: "text", placeholder: currentDevice.value.install_id || "自动注册" }, null, 8, _hoisted_23), [ [vue.vModelText, vue.unref(settings$1).installId] ]) ]), vue.createElementVNode("label", _hoisted_24, [ _cache[33] || (_cache[33] = vue.createElementVNode("span", null, "device_type", -1)), vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[12] || (_cache[12] = ($event) => vue.unref(settings$1).deviceType = $event), class: "fqa-set-input", type: "text", placeholder: currentDevice.value.device_type || "自动注册" }, null, 8, _hoisted_25), [ [vue.vModelText, vue.unref(settings$1).deviceType] ]) ]), vue.createElementVNode("div", _hoisted_26, [ vue.createElementVNode("button", { class: "fqa-set-btn", disabled: registering.value, onClick: reRegister }, vue.toDisplayString(registering.value ? "注册中…" : "重新注册"), 9, _hoisted_27), registerMsg.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_28, vue.toDisplayString(registerMsg.value), 1)) : vue.createCommentVNode("", true) ]) ]) ], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 4 }, [ _cache[41] || (_cache[41] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "关于", -1)), vue.createElementVNode("p", _hoisted_29, "番茄小说助手 v" + vue.toDisplayString(vue.unref(version)), 1), vue.createElementVNode("div", _hoisted_30, [ vue.createElementVNode("div", { class: "fqa-set-link-row" }, [ _cache[37] || (_cache[37] = vue.createElementVNode("span", null, "GreasyFork 地址:", -1)), vue.createElementVNode("a", { href: GREASYFORK, target: "_blank", rel: "noreferrer noopener" }, "跳转") ]), vue.createElementVNode("div", { class: "fqa-set-link-row" }, [ _cache[38] || (_cache[38] = vue.createElementVNode("span", null, "GitHub 地址:", -1)), vue.createElementVNode("a", { href: GITHUB, target: "_blank", rel: "noreferrer noopener" }, "跳转") ]), vue.createElementVNode("div", _hoisted_31, [ _cache[40] || (_cache[40] = vue.createElementVNode("span", null, "问题反馈:", -1)), vue.createElementVNode("a", { href: FEEDBACK, target: "_blank", rel: "noreferrer noopener" }, "GreasyFork"), vue.createElementVNode("span", null, [ _cache[39] || (_cache[39] = vue.createTextVNode(" 或 ", -1)), vue.createElementVNode("a", { href: `${GITHUB}/issues`, target: "_blank", rel: "noreferrer noopener" }, " GitHub Issues ", 8, _hoisted_32) ]) ]) ]), _cache[42] || (_cache[42] = vue.createElementVNode("div", { class: "fqa-set-license" }, [ vue.createElementVNode("p", null, " 本脚本基于 GNU General Public License 3.0 授权,完全开源且免费,修改/二次开发请注意遵守开源协议。 "), vue.createElementVNode("p", null, "本脚本使用 TypeScript + Vue 开发,请避免直接修改编译产物。") ], -1)) ], 64)) ]) ]) ]); }; } }); const settingscss = "/* 助手设置面板 */\n\n.fqa-set-mask {\n position: fixed;\n inset: 0;\n z-index: 2147483200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.45);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial,\n sans-serif;\n font-size: 14px;\n line-height: 1.6;\n color: var(--fqa-set-text, #1f2329);\n}\n\n.fqa-set-dialog {\n --fqa-set-bg: #fff;\n --fqa-set-side-bg: #f7f8fa;\n --fqa-set-text: #1f2329;\n --fqa-set-sub: #8f959e;\n --fqa-set-border: rgba(31, 35, 41, 0.1);\n --fqa-set-accent: #ff6f3d;\n --fqa-set-hover: rgba(31, 35, 41, 0.05);\n\n position: relative;\n display: flex;\n width: min(760px, 92vw);\n height: min(520px, 84vh);\n background: var(--fqa-set-bg);\n color: var(--fqa-set-text);\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 12px 48px rgba(0, 0, 0, 0.24);\n}\n\n.fqa-set-close {\n position: absolute;\n top: 10px;\n right: 12px;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--fqa-set-sub);\n font-size: 15px;\n line-height: 1;\n cursor: pointer;\n}\n\n.fqa-set-close:hover {\n background: var(--fqa-set-hover);\n color: var(--fqa-set-text);\n}\n\n/* 左侧栏 */\n.fqa-set-side {\n flex: 0 0 148px;\n padding: 16px 8px;\n box-sizing: border-box;\n background: var(--fqa-set-side-bg);\n border-right: 1px solid var(--fqa-set-border);\n overflow-y: auto;\n}\n\n.fqa-set-side-title {\n padding: 0 10px 12px;\n font-size: 15px;\n font-weight: 600;\n}\n\n.fqa-set-nav {\n padding: 8px 10px;\n margin-bottom: 2px;\n border-radius: 6px;\n cursor: pointer;\n user-select: none;\n}\n\n.fqa-set-nav:hover {\n background: var(--fqa-set-hover);\n}\n\n.fqa-set-nav-active {\n background: var(--fqa-set-hover);\n color: var(--fqa-set-accent);\n font-weight: 600;\n}\n\n/* 右侧内容 */\n.fqa-set-main {\n flex: 1 1 auto;\n padding: 20px 24px;\n box-sizing: border-box;\n overflow-y: auto;\n}\n\n.fqa-set-h {\n margin: 0 0 14px;\n font-size: 16px;\n font-weight: 600;\n}\n\n.fqa-set-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 10px 0;\n border-bottom: 1px solid var(--fqa-set-border);\n}\n\n.fqa-set-row:last-child {\n border-bottom: none;\n}\n\n.fqa-set-row-col {\n display: block;\n}\n\n.fqa-set-label {\n font-size: 14px;\n}\n\n.fqa-set-note {\n margin: 6px 0 0;\n font-size: 12px;\n color: var(--fqa-set-sub);\n}\n\n.fqa-set-warn {\n margin: 6px 0 10px;\n padding: 8px 10px;\n font-size: 12px;\n color: #a8371f;\n background: rgba(255, 111, 61, 0.1);\n border-left: 3px solid var(--fqa-set-accent);\n border-radius: 0 4px 4px 0;\n}\n\n/* 开关:用原生 checkbox 改造,避免额外依赖 */\n.fqa-set-switch {\n appearance: none;\n flex: 0 0 auto;\n position: relative;\n width: 38px;\n height: 22px;\n margin: 0;\n border-radius: 11px;\n background: rgba(31, 35, 41, 0.18);\n cursor: pointer;\n transition: background 0.18s ease;\n}\n\n.fqa-set-switch::after {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n transition: transform 0.18s ease;\n}\n\n.fqa-set-switch:checked {\n background: var(--fqa-set-accent);\n}\n\n.fqa-set-switch:checked::after {\n transform: translateX(16px);\n}\n\n.fqa-set-input {\n width: 100%;\n margin-top: 8px;\n padding: 7px 10px;\n box-sizing: border-box;\n border: 1px solid var(--fqa-set-border);\n border-radius: 6px;\n background: var(--fqa-set-bg);\n color: var(--fqa-set-text);\n font-size: 13px;\n font-family: inherit;\n}\n\n.fqa-set-input:focus {\n outline: none;\n border-color: var(--fqa-set-accent);\n}\n\n.fqa-set-textarea {\n width: 100%;\n min-height: 150px;\n margin-top: 10px;\n padding: 10px;\n box-sizing: border-box;\n border: 1px solid var(--fqa-set-border);\n border-radius: 6px;\n background: var(--fqa-set-bg);\n color: var(--fqa-set-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 1.6;\n resize: vertical;\n}\n\n.fqa-set-textarea:disabled {\n background: var(--fqa-set-side-bg);\n color: var(--fqa-set-sub);\n cursor: not-allowed;\n}\n\n.fqa-set-textarea:focus {\n outline: none;\n border-color: var(--fqa-set-accent);\n}\n\n.fqa-set-radios {\n display: flex;\n gap: 20px;\n margin-top: 8px;\n}\n\n.fqa-set-radio {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n}\n\n.fqa-set-radio input {\n accent-color: var(--fqa-set-accent);\n}\n\n.fqa-set-field {\n display: block;\n margin-top: 10px;\n font-size: 12px;\n color: var(--fqa-set-sub);\n}\n\n.fqa-set-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-top: 14px;\n}\n\n.fqa-set-btn {\n padding: 7px 16px;\n border: 1px solid var(--fqa-set-border);\n border-radius: 6px;\n background: var(--fqa-set-bg);\n color: var(--fqa-set-text);\n font-size: 13px;\n font-family: inherit;\n cursor: pointer;\n}\n\n.fqa-set-btn:hover:not(:disabled) {\n border-color: var(--fqa-set-accent);\n color: var(--fqa-set-accent);\n}\n\n.fqa-set-btn:disabled {\n color: var(--fqa-set-sub);\n cursor: not-allowed;\n}\n\n/* 关于 */\n.fqa-set-links {\n margin-top: 12px;\n}\n\n.fqa-set-link-row {\n margin-bottom: 10px;\n font-size: 13px;\n word-break: break-all;\n}\n\n.fqa-set-link-row a {\n color: var(--fqa-set-accent);\n text-decoration: none;\n}\n\n.fqa-set-link-row a:hover {\n text-decoration: underline;\n}\n\n.fqa-set-license {\n margin-top: 20px;\n padding-top: 14px;\n border-top: 1px solid var(--fqa-set-border);\n font-size: 12px;\n color: var(--fqa-set-sub);\n}\n\n.fqa-set-license p {\n margin: 0 0 6px;\n}\n\n/* 深色 */\n@media (prefers-color-scheme: dark) {\n .fqa-set-dialog {\n --fqa-set-bg: #23272e;\n --fqa-set-side-bg: #1c2026;\n --fqa-set-text: #e5e6eb;\n --fqa-set-sub: #8f959e;\n --fqa-set-border: rgba(255, 255, 255, 0.12);\n --fqa-set-hover: rgba(255, 255, 255, 0.08);\n }\n\n .fqa-set-switch {\n background: rgba(255, 255, 255, 0.2);\n }\n\n .fqa-set-warn {\n color: #ffb59b;\n }\n}\n"; const CONTAINER_ID$2 = "fqa-settings-root"; const STYLE_ID$2 = "fqa-settings-style"; let app$2 = null; let container$2 = null; function injectStyle$2() { if (document.getElementById(STYLE_ID$2)) return; const style = document.createElement("style"); style.id = STYLE_ID$2; style.textContent = settingscss; document.head.appendChild(style); } function closeSettings() { app$2 == null ? void 0 : app$2.unmount(); app$2 = null; container$2 == null ? void 0 : container$2.remove(); container$2 = null; } function openSettings() { if (app$2) return; injectStyle$2(); container$2 = document.createElement("div"); container$2.id = CONTAINER_ID$2; document.body.appendChild(container$2); app$2 = vue.createApp({ render: () => vue.h(_sfc_main$8, { onClose: closeSettings }) }); app$2.config.errorHandler = (err, _instance, info) => { console.error(`[fqa:settings] Vue error (${info}):`, err); }; app$2.mount(container$2); } function formatReadingTime(readBookTime) { let minutes = readBookTime / 60000n; const hours = minutes / 60n; minutes = minutes % 60n; return `${hours} 时 ${minutes} 分`; } function createMenuItem(text, icon, onclick) { const item = document.createElement("div"); item.role = "menuitem"; item.classList.add("fqa-menu-item", "arco-menu-item", "serial-menu-item", "slogin-user-avatar__menu-item-wrapper"); const inner1 = document.createElement("div"); inner1.classList.add("slogin-user-avatar__menu-item"); if (icon) { const iconDiv = document.createElement("div"); iconDiv.classList.add("slogin-user-avatar__menu-item__icon"); if (document.querySelector("div.muye-reader-dark")) { console.log("isdark"); iconDiv.classList.add("fqa-icon-dark"); } else { console.log("islight"); } iconDiv.innerHTML = icon; inner1.appendChild(iconDiv); } const inner2 = document.createElement("div"); inner2.classList.add("slogin-user-avatar__menu-item__content"); inner2.textContent = text; inner1.appendChild(inner2); item.appendChild(inner1); if (onclick) { item.addEventListener("click", onclick); } return item; } async function mainHook$2(_previous) { const userInfo = await getDetailedUserInfo(); if (!userInfo) { return; } const injected = /* @__PURE__ */ new WeakSet(); const inject2 = (menuInner) => { if (menuInner.querySelectorAll("div.arco-menu-inline").length > 0) { return; } if (injected.has(menuInner)) { return; } injected.add(menuInner); console.log("user action dialog created", menuInner); const firstDiv = createMenuItem(`阅读 ${userInfo.read_book_num} 本书`); menuInner.insertAdjacentElement("afterbegin", firstDiv); const secondDiv = createMenuItem(formatReadingTime(userInfo.read_book_time ?? 0n)); firstDiv.insertAdjacentElement("afterend", secondDiv); const thirdDiv = createMenuItem("我的书架", bookshelf, () => { window.open("https://fanqienovel.com/bookshelf", "_blank"); }); secondDiv.insertAdjacentElement("afterend", thirdDiv); menuInner.appendChild(createMenuItem("助手设置", settings, openSettings)); }; const scan = (root) => { if (root.classList.contains("arco-menu-inner")) { inject2(root); } root.querySelectorAll(".arco-menu-inner").forEach(inject2); }; const observer2 = new MutationObserver((mutationsList) => { mutationsList.forEach((mutation) => { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node instanceof HTMLElement) { scan(node); } }); } }); }); observer2.observe(document.body, { childList: true, subtree: true }); scan(document.body); } async function guestHook(_previous) { const injected = /* @__PURE__ */ new WeakSet(); const inject2 = (registerBtn) => { const parent = registerBtn.parentElement; if (!parent || injected.has(parent)) return; if (parent.querySelector(".fqa-settings-entry")) return; injected.add(parent); const entry = cloneElement(registerBtn); entry.classList.add("fqa-settings-entry"); entry.textContent = "助手设置"; entry.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); openSettings(); }); registerBtn.insertAdjacentElement("afterend", entry); }; const scan = (root) => { root.querySelectorAll(".slogin-user-avatar__buttons__item").forEach((el) => { var _a; if (el.classList.contains("fqa-settings-entry")) return; if (((_a = el.textContent) == null ? void 0 : _a.trim()) === "注册") inject2(el); }); }; const observer2 = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node instanceof HTMLElement) scan(node); } } }); observer2.observe(document.body, { childList: true, subtree: true }); scan(document); } function filter$2(path, _query, _hash) { return userState.isLogin && !path.startsWith("/writer") && !path.startsWith("/welfare"); } function guestFilter(path, _query, _hash) { return !userState.isLogin && !path.startsWith("/writer") && !path.startsWith("/welfare"); } const _exports$2 = [ { id: "userHook", event: "load", filter: filter$2, handler: mainHook$2 }, { id: "userHook_guest", event: "load", filter: guestFilter, handler: guestHook } ]; const _hoisted_1$7 = ["aria-label"]; const _hoisted_2$7 = { class: "fqa-cover" }; const _hoisted_3$7 = ["src", "alt"]; const _hoisted_4$6 = { key: 1, class: "fqa-cover-progress" }; const _hoisted_5$6 = ["title"]; const _hoisted_6$6 = ["title"]; const _sfc_main$7 = /* @__PURE__ */ vue.defineComponent({ __name: "BookCard", props: { entry: {} }, emits: ["hover", "leave", "open", "visible", "contextmenu"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const detail = vue.computed(() => props.entry.detail); const title = vue.computed(() => { var _a; return ((_a = detail.value) == null ? void 0 : _a.title) ?? ""; }); const tag = vue.computed(() => { const d = detail.value; if (!d) return null; if (d.update_status === "1") return { text: "更新", cls: "" }; if (d.status === "4") return { text: "断更", cls: "fqa-cover-tag-gray" }; if (d.status === "0") return { text: "完结", cls: "fqa-cover-tag-gray" }; if (d.status === "1") return { text: "连载", cls: "fqa-cover-tag-gray" }; return null; }); const progressText = vue.computed(() => { const d = detail.value; if (!d) return ""; const total = d.total_chapter_count || 0; const order = Math.max(0, Math.min(d.current_chapter_order || 0, total)); if (!order) return total ? `未读 · 共${total}章` : "未读"; return `${order}章/${total}章`; }); const progressPercent = vue.computed(() => { const d = detail.value; if (!d || !d.total_chapter_count) return 0; const ratio = (d.current_chapter_order || 0) / d.total_chapter_count; return Math.max(0, Math.min(1, ratio)) * 100; }); function onEnter2(event) { emit("hover", { entry: props.entry, el: event.currentTarget }); } function onContextMenu(event) { event.preventDefault(); emit("contextmenu", { entry: props.entry, x: event.clientX, y: event.clientY }); } const imgLoaded = vue.ref(false); vue.watch( () => { var _a; return (_a = detail.value) == null ? void 0 : _a.cover_url; }, () => { imgLoaded.value = false; } ); const cardRef = vue.ref(null); let io = null; vue.onMounted(() => { if (typeof IntersectionObserver === "undefined") { emit("visible", props.entry); return; } io = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) { emit("visible", props.entry); } }, { rootMargin: "200px" } ); if (cardRef.value) io.observe(cardRef.value); }); vue.onBeforeUnmount(() => { io == null ? void 0 : io.disconnect(); io = null; }); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { ref_key: "cardRef", ref: cardRef, class: "fqa-card", role: "link", tabindex: "0", "aria-label": title.value, onMouseenter: onEnter2, onMouseleave: _cache[2] || (_cache[2] = ($event) => emit("leave")), onContextmenu: onContextMenu, onClick: _cache[3] || (_cache[3] = ($event) => emit("open", __props.entry)), onKeydown: [ _cache[4] || (_cache[4] = vue.withKeys(vue.withModifiers(($event) => emit("open", __props.entry), ["prevent"]), ["enter"])), _cache[5] || (_cache[5] = vue.withKeys(vue.withModifiers(($event) => emit("open", __props.entry), ["prevent"]), ["space"])) ] }, [ !detail.value ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [ _cache[6] || (_cache[6] = vue.createElementVNode("div", { class: "fqa-sk-cover fqa-sk-anim" }, null, -1)), _cache[7] || (_cache[7] = vue.createElementVNode("div", { class: "fqa-sk-line fqa-sk-anim", style: { "width": "90%" } }, null, -1)), _cache[8] || (_cache[8] = vue.createElementVNode("div", { class: "fqa-sk-line fqa-sk-anim", style: { "width": "55%" } }, null, -1)) ], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [ vue.createElementVNode("div", _hoisted_2$7, [ vue.createElementVNode("img", { class: vue.normalizeClass(["fqa-cover-img", { "fqa-cover-img-loading": !imgLoaded.value }]), crossorigin: "anonymous", loading: "lazy", referrerpolicy: "no-referrer", src: detail.value.cover_url, alt: title.value, onLoad: _cache[0] || (_cache[0] = ($event) => imgLoaded.value = true), onError: _cache[1] || (_cache[1] = ($event) => imgLoaded.value = true) }, null, 42, _hoisted_3$7), tag.value ? (vue.openBlock(), vue.createElementBlock("span", { key: 0, class: vue.normalizeClass(["fqa-cover-tag", tag.value.cls]) }, vue.toDisplayString(tag.value.text), 3)) : vue.createCommentVNode("", true), progressPercent.value > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_4$6, [ vue.createElementVNode("span", { class: "fqa-cover-progress-bar", style: vue.normalizeStyle({ width: progressPercent.value + "%" }) }, null, 4) ])) : vue.createCommentVNode("", true) ]), vue.createElementVNode("div", { class: "fqa-card-title", title: title.value }, vue.toDisplayString(title.value), 9, _hoisted_5$6), vue.createElementVNode("div", { class: "fqa-card-sub", title: detail.value.current_chapter_title }, vue.toDisplayString(progressText.value), 9, _hoisted_6$6) ], 64)) ], 40, _hoisted_1$7); }; } }); const _hoisted_1$6 = ["aria-label"]; const _hoisted_2$6 = { class: "fqa-group-cover" }; const _hoisted_3$6 = { class: "fqa-group-grid" }; const _hoisted_4$5 = ["src", "alt"]; const _hoisted_5$5 = ["title"]; const _hoisted_6$5 = { class: "fqa-card-sub" }; const _sfc_main$6 = /* @__PURE__ */ vue.defineComponent({ __name: "BookGroupCard", props: { group: {} }, emits: ["open", "visible"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const previewBooks = vue.computed(() => props.group.books.slice(0, 4)); const covers = vue.computed( () => previewBooks.value.map((entry) => entry.detail).filter((detail) => !!detail) ); const cardRef = vue.ref(null); let io = null; function request() { const pending = previewBooks.value.filter((entry) => !entry.detail); if (pending.length) emit("visible", pending); } vue.onMounted(() => { if (typeof IntersectionObserver === "undefined") { request(); return; } io = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) request(); }, { rootMargin: "200px" } ); if (cardRef.value) io.observe(cardRef.value); }); vue.onBeforeUnmount(() => { io == null ? void 0 : io.disconnect(); io = null; }); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { ref_key: "cardRef", ref: cardRef, class: "fqa-card", role: "button", tabindex: "0", "aria-label": `分组 ${__props.group.name}`, onClick: _cache[0] || (_cache[0] = ($event) => emit("open", __props.group)), onKeydown: [ _cache[1] || (_cache[1] = vue.withKeys(vue.withModifiers(($event) => emit("open", __props.group), ["prevent"]), ["enter"])), _cache[2] || (_cache[2] = vue.withKeys(vue.withModifiers(($event) => emit("open", __props.group), ["prevent"]), ["space"])) ] }, [ vue.createElementVNode("div", _hoisted_2$6, [ vue.createElementVNode("div", _hoisted_3$6, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(covers.value, (detail) => { return vue.openBlock(), vue.createElementBlock("div", { key: detail.book_id, class: "fqa-group-cell" }, [ vue.createElementVNode("img", { crossorigin: "anonymous", loading: "lazy", referrerpolicy: "no-referrer", src: detail.cover_url, alt: detail.title }, null, 8, _hoisted_4$5) ]); }), 128)) ]) ]), vue.createElementVNode("div", { class: "fqa-card-title", title: __props.group.name }, vue.toDisplayString(__props.group.name), 9, _hoisted_5$5), vue.createElementVNode("div", _hoisted_6$5, "共" + vue.toDisplayString(__props.group.books.length) + "本书", 1) ], 40, _hoisted_1$6); }; } }); const SECOND = 1e3; const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; const MONTH = 30 * DAY; const YEAR = 365 * DAY; function normalizeTimestamp(ts) { if (!ts || ts <= 0) return 0; return ts < 1e12 ? ts * 1e3 : ts; } function fromNow(ts) { const time = normalizeTimestamp(ts); if (!time) return "从未"; const diff = Date.now() - time; if (diff < MINUTE) return "刚刚"; if (diff < HOUR) return `${Math.floor(diff / MINUTE)}分钟前`; if (diff < DAY) return `${Math.floor(diff / HOUR)}小时前`; if (diff < MONTH) return `${Math.floor(diff / DAY)}天前`; if (diff < YEAR) return `${Math.floor(diff / MONTH)}个月前`; return `${Math.floor(diff / YEAR)}年前`; } function formatDateTime(ts) { const time = normalizeTimestamp(ts); if (!time) return "未知"; const d = new Date(time); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } const _hoisted_1$5 = ["title"]; const _hoisted_2$5 = { key: 0, class: "fqa-hover-author" }; const _hoisted_3$5 = { class: "fqa-hover-stats" }; const _hoisted_4$4 = ["title"]; const _hoisted_5$4 = { class: "fqa-hover-stat-k" }; const _hoisted_6$4 = { class: "fqa-hover-stat" }; const _hoisted_7$4 = { class: "fqa-hover-stat-v" }; const _hoisted_8$4 = { class: "fqa-hover-stat" }; const _hoisted_9$4 = { class: "fqa-hover-stat-v" }; const _hoisted_10$4 = { class: "fqa-hover-seg" }; const _hoisted_11$4 = { class: "fqa-hover-abstract" }; const _hoisted_12$4 = { key: 0, class: "fqa-hover-chapter" }; const _sfc_main$5 = /* @__PURE__ */ vue.defineComponent({ __name: "BookHoverCard", props: { entry: {}, x: {}, y: {}, height: {}, visible: { type: Boolean } }, emits: ["panel-enter", "panel-leave"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const detail = vue.computed(() => { var _a; return ((_a = props.entry) == null ? void 0 : _a.detail) ?? null; }); const item = vue.computed(() => { var _a; return ((_a = props.entry) == null ? void 0 : _a.item) ?? null; }); const readAt = vue.computed(() => { var _a; return fromNow((_a = item.value) == null ? void 0 : _a.last_read_timestamp); }); const addedAt = vue.computed(() => { var _a; return fromNow((_a = item.value) == null ? void 0 : _a.add_shelf_time); }); const updatedAt = vue.computed(() => { var _a; return fromNow((_a = detail.value) == null ? void 0 : _a.last_chapter_update_time); }); const updatedAtFull = vue.computed(() => { var _a; return formatDateTime((_a = detail.value) == null ? void 0 : _a.last_chapter_update_time); }); const showUpdateTime = vue.ref(false); const latestChapter = vue.computed(() => { const d = detail.value; if (!d) return "—"; return d.total_chapter_count ? `${d.total_chapter_count}章` : "—"; }); const pickedTab = vue.ref(null); const chapterText = vue.computed(() => { var _a, _b; return ((_b = (_a = detail.value) == null ? void 0 : _a.current_chapter_summary) == null ? void 0 : _b.trim()) ?? ""; }); const bookText = vue.computed(() => { var _a, _b; return ((_b = (_a = detail.value) == null ? void 0 : _a.summary) == null ? void 0 : _b.trim()) ?? ""; }); const activeTab = vue.computed(() => { if (pickedTab.value) return pickedTab.value; return chapterText.value ? "chapter" : "book"; }); const abstractText = vue.computed(() => { const text = activeTab.value === "chapter" ? chapterText.value : bookText.value; return text || "暂无数据"; }); vue.watch( () => { var _a; return (_a = props.entry) == null ? void 0 : _a.item.book_id; }, () => { pickedTab.value = null; showUpdateTime.value = false; } ); const root = vue.ref(null); const supportsPopover = typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function"; vue.watch( () => props.visible && !!detail.value, (show) => { const el = root.value; if (!el || !supportsPopover) return; try { if (show) el.showPopover(); else el.hidePopover(); } catch { } } ); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { id: "fqa-bookshelf-hover", ref_key: "root", ref: root, popover: "manual", class: vue.normalizeClass({ "fqa-visible": __props.visible && !!detail.value }), style: vue.normalizeStyle({ left: __props.x + "px", top: __props.y + "px" }), onMouseenter: _cache[4] || (_cache[4] = ($event) => emit("panel-enter")), onMouseleave: _cache[5] || (_cache[5] = ($event) => emit("panel-leave")) }, [ detail.value ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, class: "fqa-hover-inner", style: vue.normalizeStyle(__props.height ? { height: __props.height + "px" } : void 0) }, [ vue.createElementVNode("div", { class: "fqa-hover-title", title: detail.value.title }, vue.toDisplayString(detail.value.title), 9, _hoisted_1$5), detail.value.author ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$5, vue.toDisplayString(detail.value.author), 1)) : vue.createCommentVNode("", true), vue.createElementVNode("div", _hoisted_3$5, [ vue.createElementVNode("div", { class: "fqa-hover-stat", onMouseenter: _cache[0] || (_cache[0] = ($event) => showUpdateTime.value = true), onMouseleave: _cache[1] || (_cache[1] = ($event) => showUpdateTime.value = false) }, [ vue.createElementVNode("div", { class: "fqa-hover-stat-v", title: updatedAtFull.value }, vue.toDisplayString(showUpdateTime.value ? updatedAt.value : latestChapter.value), 9, _hoisted_4$4), vue.createElementVNode("div", _hoisted_5$4, vue.toDisplayString(showUpdateTime.value ? "更新于" : "最新章"), 1) ], 32), vue.createElementVNode("div", _hoisted_6$4, [ vue.createElementVNode("div", _hoisted_7$4, vue.toDisplayString(readAt.value), 1), _cache[6] || (_cache[6] = vue.createElementVNode("div", { class: "fqa-hover-stat-k" }, "阅读过", -1)) ]), vue.createElementVNode("div", _hoisted_8$4, [ vue.createElementVNode("div", _hoisted_9$4, vue.toDisplayString(addedAt.value), 1), _cache[7] || (_cache[7] = vue.createElementVNode("div", { class: "fqa-hover-stat-k" }, "已加入书架", -1)) ]) ]), vue.createElementVNode("div", _hoisted_10$4, [ vue.createElementVNode("button", { class: vue.normalizeClass(["fqa-hover-seg-btn", { "fqa-hover-seg-active": activeTab.value === "chapter" }]), onClick: _cache[2] || (_cache[2] = ($event) => pickedTab.value = "chapter") }, " 本章梗概 ", 2), vue.createElementVNode("button", { class: vue.normalizeClass(["fqa-hover-seg-btn", { "fqa-hover-seg-active": activeTab.value === "book" }]), onClick: _cache[3] || (_cache[3] = ($event) => pickedTab.value = "book") }, " 全书简介 ", 2) ]), vue.createElementVNode("div", _hoisted_11$4, [ activeTab.value === "chapter" && detail.value.current_chapter_title ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_12$4, vue.toDisplayString(detail.value.current_chapter_title), 1)) : vue.createCommentVNode("", true), vue.createTextVNode(" " + vue.toDisplayString(abstractText.value), 1) ]) ], 4)) : vue.createCommentVNode("", true) ], 38); }; } }); const _hoisted_1$4 = ["aria-disabled", "onMouseenter", "onClick"]; const _hoisted_2$4 = { key: 0, class: "fqa-menu-arrow" }; const _hoisted_3$4 = ["onClick"]; const MARGIN = 8; const _sfc_main$4 = /* @__PURE__ */ vue.defineComponent({ __name: "ContextMenu", props: { visible: { type: Boolean }, x: {}, y: {}, items: {} }, emits: ["select", "close"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const rootRef = vue.ref(null); const subRef = vue.ref(null); const openKey = vue.ref(null); const pos = vue.ref({ x: 0, y: 0 }); const subPos = vue.ref({ x: 0, y: 0 }); async function place() { pos.value = { x: props.x, y: props.y }; await vue.nextTick(); const el = rootRef.value; if (!el) return; const { width, height } = el.getBoundingClientRect(); let x = props.x; let y = props.y; if (x + width > window.innerWidth - MARGIN) x = props.x - width; if (y + height > window.innerHeight - MARGIN) y = window.innerHeight - height - MARGIN; pos.value = { x: Math.max(MARGIN, x), y: Math.max(MARGIN, y) }; } vue.watch( () => [props.visible, props.x, props.y], () => { openKey.value = null; if (props.visible) void place(); } ); async function openSub(item, event) { var _a; if (!((_a = item.children) == null ? void 0 : _a.length)) { openKey.value = null; return; } openKey.value = item.key; const row = event.currentTarget; const rect = row.getBoundingClientRect(); subPos.value = { x: rect.right - 2, y: rect.top }; await vue.nextTick(); const el = subRef.value; if (!el) return; const { width, height } = el.getBoundingClientRect(); let x = rect.right - 2; let y = rect.top; if (x + width > window.innerWidth - MARGIN) x = rect.left - width + 2; if (y + height > window.innerHeight - MARGIN) y = window.innerHeight - height - MARGIN; subPos.value = { x: Math.max(MARGIN, x), y: Math.max(MARGIN, y) }; } function choose(item) { var _a; if (item.disabled || ((_a = item.children) == null ? void 0 : _a.length)) return; emit("select", item.key); emit("close"); } function onDocPointer(e) { var _a, _b; const t = e.target; if (((_a = rootRef.value) == null ? void 0 : _a.contains(t)) || ((_b = subRef.value) == null ? void 0 : _b.contains(t))) return; emit("close"); } function onKey(e) { if (e.key === "Escape") emit("close"); } vue.onMounted(() => { document.addEventListener("pointerdown", onDocPointer, true); document.addEventListener("keydown", onKey); window.addEventListener("scroll", () => emit("close"), true); }); vue.onBeforeUnmount(() => { document.removeEventListener("pointerdown", onDocPointer, true); document.removeEventListener("keydown", onKey); }); const style = vue.computed(() => ({ left: `${pos.value.x}px`, top: `${pos.value.y}px` })); const subStyle = vue.computed(() => ({ left: `${subPos.value.x}px`, top: `${subPos.value.y}px` })); const activeChildren = vue.computed( () => { var _a; return ((_a = props.items.find((i2) => i2.key === openKey.value)) == null ? void 0 : _a.children) ?? []; } ); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [ __props.visible ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, ref_key: "rootRef", ref: rootRef, class: "fqa-menu", style: vue.normalizeStyle(style.value), role: "menu" }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(__props.items, (item) => { var _a; return vue.openBlock(), vue.createElementBlock("div", { key: item.key, class: vue.normalizeClass(["fqa-menu-row", { "fqa-menu-danger": item.danger, "fqa-menu-disabled": item.disabled, "fqa-menu-open": openKey.value === item.key }]), role: "menuitem", "aria-disabled": item.disabled, onMouseenter: ($event) => openSub(item, $event), onClick: ($event) => choose(item) }, [ vue.createElementVNode("span", null, vue.toDisplayString(item.label), 1), ((_a = item.children) == null ? void 0 : _a.length) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_2$4, "›")) : vue.createCommentVNode("", true) ], 42, _hoisted_1$4); }), 128)) ], 4)) : vue.createCommentVNode("", true), __props.visible && activeChildren.value.length ? (vue.openBlock(), vue.createElementBlock("div", { key: 1, ref_key: "subRef", ref: subRef, class: "fqa-menu fqa-menu-sub", style: vue.normalizeStyle(subStyle.value), role: "menu" }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(activeChildren.value, (child) => { return vue.openBlock(), vue.createElementBlock("div", { key: child.key, class: vue.normalizeClass(["fqa-menu-row", { "fqa-menu-disabled": child.disabled }]), role: "menuitem", onClick: ($event) => choose(child) }, [ vue.createElementVNode("span", null, vue.toDisplayString(child.label), 1) ], 10, _hoisted_3$4); }), 128)) ], 4)) : vue.createCommentVNode("", true) ], 64); }; } }); const SHELF_BASE = "https://fanqienovel.com/reading/bookapi/bookshelf"; async function getBookshelf() { const responses = await Promise.all([ fetch$1(SHELF_BASE + "/info/v:version/?aid=1967&iid=0&version_code=57700&update_version_code=57700"), fetch$1("https://fanqienovel.com/api/reader/book/progress") ]); const response = responses[0]; const response2 = responses[1]; const data = await response.json(); const info = data.data.book_shelf_info; const data_ = await response2.json(); const data2 = data_.data; const all_items = info.map((item) => item.book_id); const response3 = await fetch$1("https://fanqienovel.com/api/book/simple/info", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ book_ids: all_items }) }); const data__ = await response3.json(); const data3 = data__.data.bookList; const results = []; for (let i2 of data.data.book_shelf_info) { const progress = data2.find((it) => it.book_id === i2.book_id); const simple = data3.find((it) => it.book_id === i2.book_id); const item = { book_id: i2.book_id, last_operate_time: progress ? progress.read_timestamp : i2.last_operate_time, add_shelf_time: i2.add_shelf_time, group_name: i2.group_name, last_read_timestamp: progress ? progress.read_timestamp : 0, last_read_chapter_id: progress ? progress.item_id : "0", is_publish: (simple == null ? void 0 : simple.genre) === "6" }; results.push(item); } return results; } async function multidetail(books) { const response = await fetch$1("https://fanqienovel.com/api/bookshelf/multidetail", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ books: books.map((book) => { return { book_id: book.book_id, item_id: book.last_read_chapter_id }; }) }) }); const data = await response.json(); const list = data.data.detail_list; const results = []; for (let i2 of list) { const item = { book_id: i2.book_id, summary: i2.abstract, title: i2.book_name, author: i2.author_name, cover_url: i2.thumb_url, current_chapter_title: i2.item_show_title, current_chapter_id: i2.item_id, current_chapter_order: i2.real_chapter_order, total_chapter_count: i2.serial_count, last_chapter_update_time: i2.last_chapter_update_time * 1e3, last_chapter_id: i2.last_chapter_item_id, current_chapter_summary: i2.item_abstract, // last_chapter_title: i.last_chapter_show_title, status: i2.update_stop === "1" ? 4 : i2.creation_status, update_status: i2.update_status }; results.push(item); } return results; } function identify(bookId, modifyTime = 0) { return { asterisked: false, book_id: bookId, book_type: 0, modify_time: modifyTime }; } async function shelfPost(path, body) { const res = await fetch$1(`${SHELF_BASE}${path}/v?aid=1967`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(body) }); const json = await res.json().catch(() => null); if (!res.ok || json && json.code !== 0 && json.code !== void 0) { throw new Error(`书架操作失败(${path}): ${(json == null ? void 0 : json.message) ?? res.status}`); } return json; } async function addToBookshelf(bookId) { await shelfPost("/add", { add_book_source: 0, identify_data: [identify(bookId)] }); } async function removeFromBookshelf(bookId) { await shelfPost("/delete", { identify_data: [identify(bookId, Date.now())] }); } async function moveToGroup(bookId, groupName) { await shelfPost("/update", { book_data: [ { asterisked: false, book_id: bookId, book_type: 0, group_name: groupName, has_shown: false, is_pin: false, modify_time: Date.now() } ] }); } const DETAIL_BATCH_SIZE = 20; const PAGE_SIZE = 24; const TABS = [ { key: "all", label: "全部" }, { key: "group", label: "分组" }, { key: "publish", label: "出版" } ]; function useBookshelf() { const entries = vue.ref([]); const loading = vue.ref(false); const detailLoading = vue.ref(false); const error = vue.ref(null); let loadToken = 0; let detailCache = /* @__PURE__ */ new Map(); let inflight = /* @__PURE__ */ new Set(); function applyDetails(details) { if (!details.length) return; const byId = new Map(details.map((d) => [d.book_id, d])); entries.value = entries.value.map((entry) => { const detail = byId.get(entry.item.book_id); return detail ? { ...entry, detail } : entry; }); } async function ensureDetails(items) { const token = loadToken; const pending = items.filter( (item) => !detailCache.has(item.book_id) && !inflight.has(item.book_id) ); if (!pending.length) return; pending.forEach((item) => inflight.add(item.book_id)); detailLoading.value = true; try { for (const batch of chunk(pending, DETAIL_BATCH_SIZE)) { if (token !== loadToken) return; try { const details = await multidetail(batch); if (token !== loadToken) return; details.forEach((d) => detailCache.set(d.book_id, d)); applyDetails(details); } catch (err) { console.error("[fqa:bookshelf] 加载书籍详情失败:", err); } } } finally { pending.forEach((item) => inflight.delete(item.book_id)); if (token === loadToken && inflight.size === 0) detailLoading.value = false; } } async function load2(force = false) { const token = ++loadToken; inflight.clear(); if (force) detailCache = /* @__PURE__ */ new Map(); loading.value = true; error.value = null; try { const items = await getBookshelf(); if (token !== loadToken) return; items.sort((a, b) => b.last_operate_time - a.last_operate_time); entries.value = items.map((item) => ({ item, detail: detailCache.get(item.book_id) ?? null })); loading.value = false; } catch (err) { if (token !== loadToken) return; console.error("[fqa:bookshelf] 加载书架失败:", err); error.value = err instanceof Error ? err.message : String(err); loading.value = false; } } const groups = vue.computed(() => { const map = /* @__PURE__ */ new Map(); for (const entry of entries.value) { const name2 = entry.item.group_name; if (!name2) continue; let group = map.get(name2); if (!group) { group = { name: name2, books: [], last_operate_time: 0 }; map.set(name2, group); } group.books.push(entry); group.last_operate_time = Math.max(group.last_operate_time, entry.item.last_operate_time); } return [...map.values()].sort((a, b) => b.last_operate_time - a.last_operate_time); }); const allCells = vue.computed(() => { const sortable = []; for (const entry of entries.value) { if (entry.item.group_name) continue; sortable.push({ time: entry.item.last_operate_time, cell: { kind: "book", key: `book:${entry.item.book_id}`, entry } }); } for (const group of groups.value) { sortable.push({ time: group.last_operate_time, cell: { kind: "group", key: `group:${group.name}`, group } }); } return sortable.sort((a, b) => b.time - a.time).map((s) => s.cell); }); const groupCells = vue.computed( () => groups.value.map((group) => ({ kind: "group", key: `group:${group.name}`, group })) ); const publishCells = vue.computed( () => entries.value.filter((entry) => entry.item.is_publish).map((entry) => ({ kind: "book", key: `book:${entry.item.book_id}`, entry })) ); const counts = vue.computed(() => ({ all: entries.value.length, group: groups.value.length, publish: entries.value.filter((entry) => entry.item.is_publish).length })); function cellsOf(tab) { if (tab === "group") return groupCells.value; if (tab === "publish") return publishCells.value; return allCells.value; } function findGroup(name2) { return groups.value.find((group) => group.name === name2) ?? null; } return { entries, loading, detailLoading, error, groups, counts, load: load2, ensureDetails, cellsOf, findGroup, PAGE_SIZE }; } const _hoisted_1$3 = { id: "fqa-bookshelf" }; const _hoisted_2$3 = { class: "fqa-bs-header" }; const _hoisted_3$3 = { class: "fqa-bs-actions" }; const _hoisted_4$3 = { key: 0 }; const _hoisted_5$3 = ["disabled"]; const _hoisted_6$3 = ["aria-selected", "onClick", "onKeydown"]; const _hoisted_7$3 = { class: "fqa-tab-count" }; const _hoisted_8$3 = { key: 0, class: "fqa-groupbar" }; const _hoisted_9$3 = { class: "fqa-groupbar-name" }; const _hoisted_10$3 = { class: "fqa-groupbar-count" }; const _hoisted_11$3 = { key: 1, class: "fqa-status" }; const _hoisted_12$3 = { key: 2, class: "fqa-status" }; const _hoisted_13$3 = { class: "fqa-status-title" }; const _hoisted_14$3 = { class: "fqa-grid" }; const _hoisted_15$1 = { key: 0, class: "fqa-toast" }; const HOVER_DELAY = 300; const HIDE_DELAY = 160; const HOVER_WIDTH = 280; const HOVER_GAP = 12; const VIEWPORT_MARGIN = 8; const MOVE_PREFIX = "move:"; const _sfc_main$3 = /* @__PURE__ */ vue.defineComponent({ __name: "BookshelfView", setup(__props) { const { loading, detailLoading, error, counts, groups, load: load2, ensureDetails, cellsOf, findGroup, PAGE_SIZE: PAGE_SIZE2 } = useBookshelf(); const activeTab = vue.ref("all"); const openedGroupName = vue.ref(null); const tabsRef = vue.ref(null); const inkStyle = vue.ref({ left: "0px", width: "0px" }); const hoverEntry = vue.ref(null); const hoverVisible = vue.ref(false); const hoverPos = vue.ref({ x: 0, y: 0 }); const hoverHeight = vue.ref(0); let hoverTimer; let hideTimer; let hoverAnchor = null; const pointerInPanel = vue.ref(false); const openedGroup = vue.computed( () => openedGroupName.value ? findGroup(openedGroupName.value) : null ); const allCells = vue.computed(() => { const group = openedGroup.value; if (group) { return group.books.map((entry) => ({ kind: "book", key: `book:${entry.item.book_id}`, entry })); } return cellsOf(activeTab.value); }); const visibleCount = vue.ref(PAGE_SIZE2); const cells = vue.computed(() => allCells.value.slice(0, visibleCount.value)); const hasMore = vue.computed(() => visibleCount.value < allCells.value.length); const sentinel = vue.ref(null); let pageObserver = null; function resetPaging() { visibleCount.value = PAGE_SIZE2; } async function refresh() { resetPaging(); await load2(true); } vue.watch([activeTab, openedGroupName], resetPaging); const isEmpty = vue.computed(() => !loading.value && !error.value && allCells.value.length === 0); const emptyText = vue.computed(() => { if (openedGroup.value) return "这个分组还没有书"; if (activeTab.value === "group") return "还没有创建任何分组"; if (activeTab.value === "publish") return "书架里还没有出版物"; return "书架空空如也,去首页找几本书看看吧"; }); function updateInk() { const wrap = tabsRef.value; if (!wrap) return; const index = TABS.findIndex((tab) => tab.key === activeTab.value); const el = wrap.querySelectorAll(".fqa-tab")[index]; if (!el) return; inkStyle.value = { left: `${el.offsetLeft}px`, width: `${el.offsetWidth}px` }; } function selectTab(key) { openedGroupName.value = null; activeTab.value = key; hideHover(true); } vue.watch(activeTab, () => vue.nextTick(updateInk)); vue.watch(counts, () => vue.nextTick(updateInk), { deep: true }); function computePosition(el) { const cover = el.querySelector(".fqa-cover") ?? el; const rect = cover.getBoundingClientRect(); let x = rect.right + HOVER_GAP; if (x + HOVER_WIDTH > window.innerWidth - VIEWPORT_MARGIN) { x = rect.left - HOVER_WIDTH - HOVER_GAP; } x = Math.max(VIEWPORT_MARGIN, Math.min(x, window.innerWidth - HOVER_WIDTH - VIEWPORT_MARGIN)); const topLimit = VIEWPORT_MARGIN; const bottomLimit = window.innerHeight - VIEWPORT_MARGIN; const height = Math.min(rect.height, bottomLimit - topLimit); const y = Math.max(topLimit, Math.min(rect.top, bottomLimit - height)); hoverHeight.value = height; hoverPos.value = { x, y }; } function onCardHover({ entry, el }) { if (hoverTimer) clearTimeout(hoverTimer); if (hideTimer) { clearTimeout(hideTimer); hideTimer = void 0; } if (!entry.detail) return; hoverAnchor = el; if (hoverVisible.value && hoverEntry.value !== entry) { computePosition(el); hoverEntry.value = entry; return; } hoverTimer = setTimeout(() => { hoverTimer = void 0; computePosition(el); hoverEntry.value = entry; hoverVisible.value = true; }, HOVER_DELAY); } function scheduleHide() { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = void 0; } if (hideTimer) clearTimeout(hideTimer); hideTimer = setTimeout(() => { hideTimer = void 0; if (!pointerInPanel.value) hideHover(true); }, HIDE_DELAY); } function hideHover(immediate = false) { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = void 0; } if (hideTimer) { clearTimeout(hideTimer); hideTimer = void 0; } pointerInPanel.value = false; hoverAnchor = null; hoverVisible.value = false; if (immediate) hoverEntry.value = null; } function onPanelEnter() { if (hideTimer) { clearTimeout(hideTimer); hideTimer = void 0; } pointerInPanel.value = true; } function onPanelLeave() { pointerInPanel.value = false; scheduleHide(); } let pendingItems = []; let flushTimer; function queueDetails(entries) { const pending = entries.filter((entry) => !entry.detail); if (!pending.length) return; pendingItems.push(...pending.map((entry) => entry.item)); if (flushTimer) return; flushTimer = setTimeout(() => { flushTimer = void 0; const batch = pendingItems; pendingItems = []; void ensureDetails(batch); }, 50); } function onCardVisible(entry) { queueDetails([entry]); } function onGroupVisible(entries) { queueDetails(entries); } function openBook(entry) { var _a; hideHover(true); const chapterId = ((_a = entry.detail) == null ? void 0 : _a.current_chapter_id) || entry.item.last_read_chapter_id; const url = chapterId && chapterId !== "0" ? `https://fanqienovel.com/reader/${chapterId}` : `https://fanqienovel.com/page/${entry.item.book_id}`; unsafeWindow.location.href = url; } function openGroup(group) { hideHover(true); openedGroupName.value = group.name; } const menuVisible = vue.ref(false); const menuPos = vue.ref({ x: 0, y: 0 }); const menuEntry = vue.ref(null); const toast = vue.ref(null); let toastTimer; function showToast(msg) { toast.value = msg; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.value = null; toastTimer = void 0; }, 2600); } const NO_GROUP_KEY = `${MOVE_PREFIX}`; const menuItems = vue.computed(() => { const entry = menuEntry.value; if (!entry) return []; const current = entry.item.group_name ?? ""; const targets = groups.value.filter((g2) => g2.name !== current).map((g2) => ({ key: `${MOVE_PREFIX}${g2.name}`, label: g2.name })); if (current) targets.push({ key: NO_GROUP_KEY, label: "无分组" }); return [ { key: "open", label: "打开" }, { key: "detail", label: "查看详情" }, { key: "move", label: "移动到分组", disabled: targets.length === 0, children: targets }, { key: "remove", label: "从书架删除", danger: true } ]; }); function onCardContextMenu({ entry, x, y }) { hideHover(true); menuEntry.value = entry; menuPos.value = { x, y }; menuVisible.value = true; } async function onMenuSelect(key) { var _a; const entry = menuEntry.value; if (!entry) return; const bookId = entry.item.book_id; if (key === "open") { openBook(entry); return; } if (key === "detail") { unsafeWindow.location.href = `https://fanqienovel.com/page/${bookId}`; return; } if (key === "remove") { if (!unsafeWindow.confirm(`确定要把《${((_a = entry.detail) == null ? void 0 : _a.title) ?? bookId}》从书架删除吗?`)) return; try { await removeFromBookshelf(bookId); showToast("已从书架删除"); await refresh(); } catch (err) { console.error("[fqa:bookshelf] 删除失败:", err); showToast(err instanceof Error ? err.message : "删除失败"); } return; } if (key.startsWith(MOVE_PREFIX)) { const groupName = key.slice(MOVE_PREFIX.length); try { await moveToGroup(bookId, groupName); showToast(groupName ? `已移动到「${groupName}」` : "已移出分组"); await refresh(); } catch (err) { console.error("[fqa:bookshelf] 移动分组失败:", err); showToast(err instanceof Error ? err.message : "移动分组失败"); } } } function backToList() { openedGroupName.value = null; hideHover(true); } function onScrollOrResize() { if (hoverAnchor && (hoverVisible.value || hoverTimer)) { if (hoverAnchor.isConnected) { computePosition(hoverAnchor); } else { hideHover(true); } } updateInk(); } vue.onMounted(async () => { await load2(); await vue.nextTick(); updateInk(); window.addEventListener("scroll", onScrollOrResize, true); window.addEventListener("resize", onScrollOrResize); if (typeof IntersectionObserver !== "undefined") { pageObserver = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting) && hasMore.value) { visibleCount.value += PAGE_SIZE2; } }, { rootMargin: "400px" } ); vue.watch( sentinel, (el) => { pageObserver == null ? void 0 : pageObserver.disconnect(); if (el) pageObserver == null ? void 0 : pageObserver.observe(el); }, { immediate: true } ); } }); vue.onBeforeUnmount(() => { if (hoverTimer) clearTimeout(hoverTimer); if (hideTimer) clearTimeout(hideTimer); if (flushTimer) clearTimeout(flushTimer); pageObserver == null ? void 0 : pageObserver.disconnect(); pageObserver = null; window.removeEventListener("scroll", onScrollOrResize, true); window.removeEventListener("resize", onScrollOrResize); }); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$3, [ vue.createElementVNode("div", _hoisted_2$3, [ _cache[1] || (_cache[1] = vue.createElementVNode("div", { class: "fqa-bs-title" }, "我的书架", -1)), vue.createElementVNode("div", _hoisted_3$3, [ vue.unref(detailLoading) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4$3, "正在补全详情…")) : vue.createCommentVNode("", true), vue.createElementVNode("button", { class: "fqa-btn", disabled: vue.unref(loading), onClick: refresh }, vue.toDisplayString(vue.unref(loading) ? "刷新中…" : "刷新"), 9, _hoisted_5$3) ]) ]), vue.createElementVNode("div", { ref_key: "tabsRef", ref: tabsRef, class: "fqa-tabs", role: "tablist" }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(TABS), (tab) => { return vue.openBlock(), vue.createElementBlock("div", { key: tab.key, class: vue.normalizeClass(["fqa-tab", { "fqa-tab-active": activeTab.value === tab.key }]), role: "tab", tabindex: "0", "aria-selected": activeTab.value === tab.key, onClick: ($event) => selectTab(tab.key), onKeydown: vue.withKeys(vue.withModifiers(($event) => selectTab(tab.key), ["prevent"]), ["enter"]) }, [ vue.createTextVNode(vue.toDisplayString(tab.label), 1), vue.createElementVNode("span", _hoisted_7$3, vue.toDisplayString(vue.unref(counts)[tab.key]), 1) ], 42, _hoisted_6$3); }), 128)), vue.createElementVNode("span", { class: "fqa-tab-ink", style: vue.normalizeStyle(inkStyle.value) }, null, 4) ], 512), openedGroup.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_8$3, [ vue.createElementVNode("button", { class: "fqa-btn", onClick: backToList }, "← 返回"), vue.createElementVNode("span", _hoisted_9$3, vue.toDisplayString(openedGroup.value.name), 1), vue.createElementVNode("span", _hoisted_10$3, "共" + vue.toDisplayString(openedGroup.value.books.length) + "本书", 1) ])) : vue.createCommentVNode("", true), vue.unref(error) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_11$3, [ _cache[2] || (_cache[2] = vue.createElementVNode("div", { class: "fqa-status-title" }, "书架加载失败", -1)), vue.createElementVNode("div", null, vue.toDisplayString(vue.unref(error)), 1), vue.createElementVNode("button", { class: "fqa-btn", onClick: refresh }, "重试") ])) : isEmpty.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_12$3, [ vue.createElementVNode("div", _hoisted_13$3, vue.toDisplayString(emptyText.value), 1) ])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [ vue.createElementVNode("div", _hoisted_14$3, [ vue.unref(loading) ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, vue.renderList(8, (n) => { return vue.createElementVNode("div", { key: `sk-${n}`, class: "fqa-card" }, [..._cache[3] || (_cache[3] = [ vue.createElementVNode("div", { class: "fqa-sk-cover fqa-sk-anim" }, null, -1), vue.createElementVNode("div", { class: "fqa-sk-line fqa-sk-anim", style: { "width": "90%" } }, null, -1), vue.createElementVNode("div", { class: "fqa-sk-line fqa-sk-anim", style: { "width": "55%" } }, null, -1) ])]); }), 64)) : (vue.openBlock(true), vue.createElementBlock(vue.Fragment, { key: 1 }, vue.renderList(cells.value, (cell) => { return vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: cell.key }, [ cell.kind === "book" ? (vue.openBlock(), vue.createBlock(_sfc_main$7, { key: 0, entry: cell.entry, onHover: onCardHover, onLeave: scheduleHide, onOpen: openBook, onVisible: onCardVisible, onContextmenu: onCardContextMenu }, null, 8, ["entry"])) : (vue.openBlock(), vue.createBlock(_sfc_main$6, { key: 1, group: cell.group, onOpen: openGroup, onVisible: onGroupVisible }, null, 8, ["group"])) ], 64); }), 128)) ]), !vue.unref(loading) && hasMore.value ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, ref_key: "sentinel", ref: sentinel, class: "fqa-loadmore" }, "加载中…", 512)) : vue.createCommentVNode("", true) ], 64)), (vue.openBlock(), vue.createBlock(vue.Teleport, { to: "body" }, [ vue.createVNode(_sfc_main$5, { entry: hoverEntry.value, x: hoverPos.value.x, y: hoverPos.value.y, height: hoverHeight.value, visible: hoverVisible.value, onPanelEnter, onPanelLeave }, null, 8, ["entry", "x", "y", "height", "visible"]), vue.createVNode(_sfc_main$4, { visible: menuVisible.value, x: menuPos.value.x, y: menuPos.value.y, items: menuItems.value, onSelect: onMenuSelect, onClose: _cache[0] || (_cache[0] = ($event) => menuVisible.value = false) }, null, 8, ["visible", "x", "y", "items"]), toast.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_15$1, vue.toDisplayString(toast.value), 1)) : vue.createCommentVNode("", true) ])) ]); }; } }); const bookshelfcss = "#fqa-bookshelf {\n --fqa-text: #1f2329;\n --fqa-text-sub: #646a73;\n --fqa-text-weak: #8f959e;\n --fqa-border: rgba(31, 35, 41, 0.08);\n --fqa-hover: rgba(31, 35, 41, 0.04);\n --fqa-accent: #ff6f3d;\n --fqa-skeleton: rgba(31, 35, 41, 0.06);\n --fqa-skeleton-hl: rgba(31, 35, 41, 0.12);\n --fqa-shadow: 0 4px 16px rgba(31, 35, 41, 0.08);\n\n display: block;\n box-sizing: border-box;\n width: 100%;\n max-width: 1100px;\n margin: 0 auto;\n /* 顶部留出原站 fixed 顶栏(80px)的高度,否则标题和 tab 会被压在下面 */\n padding: calc(80px + 24px) 16px 64px;\n color: var(--fqa-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial,\n sans-serif;\n}\n\n#fqa-bookshelf *,\n#fqa-bookshelf *::before,\n#fqa-bookshelf *::after {\n box-sizing: border-box;\n}\n\n#fqa-bookshelf div,\n#fqa-bookshelf span,\n#fqa-bookshelf h1,\n#fqa-bookshelf ul,\n#fqa-bookshelf li {\n margin: 0;\n padding: 0;\n border: 0;\n list-style: none;\n float: none;\n position: static;\n}\n\n/* ------------------------------ 顶部 / Tabs ------------------------------ */\n\n#fqa-bookshelf .fqa-bs-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n}\n\n#fqa-bookshelf .fqa-bs-title {\n font-size: 24px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n#fqa-bookshelf .fqa-bs-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 13px;\n color: var(--fqa-text-weak);\n}\n\n#fqa-bookshelf .fqa-btn {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 6px 14px;\n border: 1px solid var(--fqa-border);\n border-radius: 999px;\n background: transparent;\n color: var(--fqa-text-sub);\n font-size: 13px;\n font-family: inherit;\n line-height: 1.4;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;\n}\n\n#fqa-bookshelf .fqa-btn:hover:not(:disabled) {\n border-color: var(--fqa-accent);\n color: var(--fqa-accent);\n background: rgba(255, 111, 61, 0.06);\n}\n\n#fqa-bookshelf .fqa-btn:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n#fqa-bookshelf .fqa-tabs {\n position: relative;\n display: flex;\n align-items: center;\n gap: 4px;\n margin-bottom: 24px;\n border-bottom: 1px solid var(--fqa-border);\n}\n\n#fqa-bookshelf .fqa-tab {\n padding: 10px 16px;\n color: var(--fqa-text-sub);\n font-size: 15px;\n line-height: 22px;\n cursor: pointer;\n user-select: none;\n transition: color 0.15s ease;\n}\n\n#fqa-bookshelf .fqa-tab:hover {\n color: var(--fqa-text);\n}\n\n#fqa-bookshelf .fqa-tab-active {\n color: var(--fqa-accent);\n font-weight: 600;\n}\n\n#fqa-bookshelf .fqa-tab-count {\n margin-left: 4px;\n font-size: 12px;\n font-weight: 400;\n color: var(--fqa-text-weak);\n}\n\n#fqa-bookshelf .fqa-tab-ink {\n position: absolute;\n bottom: -1px;\n left: 0;\n width: 0;\n height: 2px;\n border-radius: 2px;\n background: var(--fqa-accent);\n transition: left 0.25s ease, width 0.25s ease;\n}\n\n/* ------------------------------- 书架网格 ------------------------------- */\n\n/* 原站一排最多 4 本;窄屏逐级降到 3 / 2 */\n#fqa-bookshelf .fqa-grid {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 28px 24px;\n align-items: start;\n}\n\n@media (max-width: 900px) {\n #fqa-bookshelf .fqa-grid {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n@media (max-width: 600px) {\n #fqa-bookshelf .fqa-grid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n}\n\n#fqa-bookshelf .fqa-card {\n display: block;\n border-radius: 8px;\n cursor: pointer;\n outline: none;\n}\n\n#fqa-bookshelf .fqa-card:focus-visible {\n box-shadow: 0 0 0 2px var(--fqa-accent);\n}\n\n/* 封面:3:4,靠 aspect-ratio 定高,内部元素绝对定位 */\n#fqa-bookshelf .fqa-cover {\n position: relative;\n display: block;\n width: 100%;\n aspect-ratio: 3 / 4;\n border-radius: 6px;\n overflow: hidden;\n background: var(--fqa-skeleton);\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n#fqa-bookshelf .fqa-card:hover .fqa-cover {\n transform: translateY(-4px);\n box-shadow: var(--fqa-shadow);\n}\n\n#fqa-bookshelf .fqa-cover-img {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: opacity 0.25s ease;\n}\n\n#fqa-bookshelf .fqa-cover-img-loading {\n opacity: 0;\n}\n\n#fqa-bookshelf .fqa-cover-tag {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 2px 6px;\n border-radius: 0 6px 0 6px;\n background: var(--fqa-accent);\n color: #fff;\n font-size: 11px;\n line-height: 16px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* 连载 / 完结 / 断更共用:灰底,弱化于“更新”角标 */\n#fqa-bookshelf .fqa-cover-tag-gray {\n background: rgba(31, 35, 41, 0.55);\n}\n\n@media (prefers-color-scheme: dark) {\n #fqa-bookshelf .fqa-cover-tag-gray {\n background: rgba(0, 0, 0, 0.6);\n }\n}\n\n#fqa-bookshelf .fqa-cover-progress {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 2;\n height: 3px;\n background: rgba(255, 255, 255, 0.35);\n}\n\n#fqa-bookshelf .fqa-cover-progress-bar {\n display: block;\n height: 100%;\n background: var(--fqa-accent);\n transition: width 0.3s ease;\n}\n\n/* 文字区:与封面同为普通流元素,不会重叠 */\n#fqa-bookshelf .fqa-card-title {\n display: -webkit-box;\n margin-top: 8px;\n color: var(--fqa-text);\n font-size: 14px;\n line-height: 20px;\n font-weight: 500;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n word-break: break-all;\n}\n\n#fqa-bookshelf .fqa-card-sub {\n margin-top: 4px;\n color: var(--fqa-text-weak);\n font-size: 12px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ------------------------------- 分组卡片 ------------------------------- */\n\n#fqa-bookshelf .fqa-group-cover {\n position: relative;\n display: block;\n width: 100%;\n aspect-ratio: 3 / 4;\n border-radius: 6px;\n overflow: hidden;\n background: linear-gradient(135deg, rgba(255, 111, 61, 0.12), rgba(78, 131, 253, 0.12));\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n#fqa-bookshelf .fqa-card:hover .fqa-group-cover {\n transform: translateY(-4px);\n box-shadow: var(--fqa-shadow);\n}\n\n#fqa-bookshelf .fqa-group-grid {\n position: absolute;\n inset: 0;\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-template-rows: 1fr 1fr;\n gap: 4px;\n padding: 6px;\n}\n\n#fqa-bookshelf .fqa-group-cell {\n position: relative;\n border-radius: 3px;\n overflow: hidden;\n background: rgba(31, 35, 41, 0.06);\n}\n\n#fqa-bookshelf .fqa-group-cell img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n/* 分组详情返回条 */\n\n#fqa-bookshelf .fqa-groupbar {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 16px;\n}\n\n#fqa-bookshelf .fqa-groupbar-name {\n font-size: 16px;\n font-weight: 600;\n}\n\n#fqa-bookshelf .fqa-groupbar-count {\n color: var(--fqa-text-weak);\n font-size: 13px;\n}\n\n/* --------------------------- hover 详情浮层 --------------------------- */\n\n/*\n * 用 popover 进入浏览器顶层,不参与页面 z-index 竞争,\n * 因此不会被相邻卡片或原站的层叠上下文盖住。z-index 仅作降级保险。\n */\n#fqa-bookshelf-hover {\n position: fixed;\n z-index: 2147483000;\n /* 容器本身透传,只有内部卡片接收事件,避免空白区挡住下层 */\n pointer-events: none;\n opacity: 0;\n transform: translateY(4px);\n /* allow-discrete:顶层元素从 display:none 切入时也能播放淡入 */\n transition: opacity 0.16s ease, transform 0.16s ease, display 0.16s allow-discrete;\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial,\n sans-serif;\n}\n\n/* popover 默认带边框/内边距/居中定位,全部清掉,改由 left/top 控制 */\n#fqa-bookshelf-hover:popover-open,\n#fqa-bookshelf-hover[popover] {\n margin: 0;\n padding: 0;\n border: 0;\n background: transparent;\n overflow: visible;\n inset: auto;\n width: auto;\n height: auto;\n max-width: none;\n max-height: none;\n color: inherit;\n}\n\n#fqa-bookshelf-hover::backdrop {\n background: transparent;\n}\n\n#fqa-bookshelf-hover.fqa-visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n@starting-style {\n #fqa-bookshelf-hover.fqa-visible {\n opacity: 0;\n transform: translateY(4px);\n }\n}\n\n/* 高度由 JS 按封面尺寸设定;纵向 flex 让简介吃掉剩余空间 */\n#fqa-bookshelf-hover .fqa-hover-inner {\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n width: 280px;\n padding: 12px 14px;\n border-radius: 10px;\n background: #fff;\n box-shadow: 0 8px 32px rgba(31, 35, 41, 0.16);\n color: #1f2329;\n overflow: hidden;\n /* 卡片可交互:鼠标可以移进来而不触发收起 */\n pointer-events: auto;\n}\n\n#fqa-bookshelf-hover .fqa-hover-title {\n flex: none;\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n#fqa-bookshelf-hover .fqa-hover-author {\n flex: none;\n margin-top: 2px;\n color: #8f959e;\n font-size: 12px;\n line-height: 17px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stats {\n display: flex;\n flex: none;\n margin-top: 10px;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat {\n flex: 1 1 0;\n min-width: 0;\n padding: 0 6px;\n text-align: center;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat:first-child {\n padding-left: 0;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat:last-child {\n padding-right: 0;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat + .fqa-hover-stat {\n border-left: 1px solid rgba(31, 35, 41, 0.08);\n}\n\n/* 第一栏可悬停切换为更新时间,给个可交互提示 */\n#fqa-bookshelf-hover .fqa-hover-stat:first-child {\n border-radius: 4px;\n cursor: default;\n transition: background 0.15s ease;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat:first-child:hover {\n background: rgba(31, 35, 41, 0.05);\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat-v {\n font-size: 13px;\n line-height: 18px;\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n#fqa-bookshelf-hover .fqa-hover-stat-k {\n margin-top: 1px;\n color: #8f959e;\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* 梗概 / 简介 双栏切换 */\n#fqa-bookshelf-hover .fqa-hover-seg {\n display: flex;\n flex: none;\n gap: 4px;\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px solid rgba(31, 35, 41, 0.08);\n}\n\n#fqa-bookshelf-hover .fqa-hover-seg-btn {\n flex: 1 1 0;\n padding: 4px 0;\n border: 0;\n border-radius: 5px;\n background: rgba(31, 35, 41, 0.05);\n color: #646a73;\n font-family: inherit;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n}\n\n#fqa-bookshelf-hover .fqa-hover-seg-btn:hover {\n color: #1f2329;\n}\n\n#fqa-bookshelf-hover .fqa-hover-seg-active {\n background: rgba(255, 111, 61, 0.12);\n color: #ff6f3d;\n font-weight: 500;\n}\n\n#fqa-bookshelf-hover .fqa-hover-seg-active:hover {\n color: #ff6f3d;\n}\n\n/*\n * 撑满剩余高度。行数不再写死,由容器高度自然裁切;\n * min-height:0 让 flex 子项允许被压缩,否则 overflow 不生效。\n */\n#fqa-bookshelf-hover .fqa-hover-abstract {\n flex: 1 1 auto;\n min-height: 0;\n margin-top: 8px;\n color: #646a73;\n font-size: 12px;\n line-height: 18px;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n#fqa-bookshelf-hover .fqa-hover-abstract::-webkit-scrollbar {\n width: 4px;\n}\n\n#fqa-bookshelf-hover .fqa-hover-abstract::-webkit-scrollbar-thumb {\n border-radius: 2px;\n background: rgba(31, 35, 41, 0.18);\n}\n\n\n#fqa-bookshelf-hover .fqa-hover-chapter {\n display: block;\n margin-bottom: 1px;\n color: #1f2329;\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ------------------------------- 骨架屏 ------------------------------- */\n\n#fqa-bookshelf .fqa-sk-cover {\n display: block;\n width: 100%;\n aspect-ratio: 3 / 4;\n border-radius: 6px;\n background: var(--fqa-skeleton);\n}\n\n#fqa-bookshelf .fqa-sk-line {\n height: 12px;\n margin-top: 8px;\n border-radius: 4px;\n background: var(--fqa-skeleton);\n}\n\n#fqa-bookshelf .fqa-sk-anim {\n position: relative;\n overflow: hidden;\n}\n\n/* keyframes fqa-shimmer 在 script.css 里全局声明 */\n#fqa-bookshelf .fqa-sk-anim::after {\n content: '';\n position: absolute;\n inset: 0;\n transform: translateX(-100%);\n background: linear-gradient(90deg, transparent, var(--fqa-skeleton-hl), transparent);\n animation: fqa-shimmer 1.4s infinite;\n}\n\n/* --------------------------- 空态 / 错误态 --------------------------- */\n\n#fqa-bookshelf .fqa-loadmore {\n padding: 24px 0;\n text-align: center;\n color: var(--fqa-text-weak);\n font-size: 13px;\n}\n\n#fqa-bookshelf .fqa-status {\n padding: 80px 16px;\n text-align: center;\n color: var(--fqa-text-weak);\n font-size: 14px;\n line-height: 22px;\n}\n\n#fqa-bookshelf .fqa-status-title {\n margin-bottom: 8px;\n color: var(--fqa-text);\n font-size: 16px;\n font-weight: 500;\n}\n\n#fqa-bookshelf .fqa-status .fqa-btn {\n margin-top: 16px;\n}\n\n/* ------------------------------- 深色模式 ------------------------------- */\n\n@media (prefers-color-scheme: dark) {\n #fqa-bookshelf {\n --fqa-text: #e6e6e6;\n --fqa-text-sub: #a6a6a6;\n --fqa-text-weak: #7a7a7a;\n --fqa-border: rgba(255, 255, 255, 0.1);\n --fqa-hover: rgba(255, 255, 255, 0.06);\n --fqa-skeleton: rgba(255, 255, 255, 0.08);\n --fqa-skeleton-hl: rgba(255, 255, 255, 0.14);\n --fqa-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n }\n\n #fqa-bookshelf-hover .fqa-hover-inner {\n background: #212125;\n color: #e6e6e6;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n }\n\n #fqa-bookshelf-hover .fqa-hover-chapter {\n color: #e6e6e6;\n }\n\n #fqa-bookshelf-hover .fqa-hover-abstract {\n color: #a6a6a6;\n border-top-color: rgba(255, 255, 255, 0.1);\n }\n\n #fqa-bookshelf-hover .fqa-hover-stat + .fqa-hover-stat {\n border-left-color: rgba(255, 255, 255, 0.1);\n }\n}\n\n/* 右键菜单与 toast 样式已移到 script.css,书架和搜索共用 */\n"; const CONTAINER_ID$1 = "fqa-bookshelf-root"; const STYLE_ID$1 = "fqa-bookshelf-style"; const ORIGIN_SELECTOR$1 = ".muye-bookshelf, .muye-bookshelf-home-page, .bookshelf-tabs"; let app$1 = null; let container$1 = null; let observer$1 = null; function injectStyle$1() { if (document.getElementById(STYLE_ID$1)) return; const style = document.createElement("style"); style.id = STYLE_ID$1; style.textContent = bookshelfcss; document.head.appendChild(style); } function hideOrigin$1(root = document) { root.querySelectorAll(ORIGIN_SELECTOR$1).forEach((el) => { if (el.id === CONTAINER_ID$1 || el.closest(`#${CONTAINER_ID$1}`)) return; el.classList.add("fqa-hide"); }); } function isBookshelfPath(path) { return path.startsWith("/bookshelf"); } function unmount$1() { var _a; observer$1 == null ? void 0 : observer$1.disconnect(); observer$1 = null; app$1 == null ? void 0 : app$1.unmount(); app$1 = null; container$1 == null ? void 0 : container$1.remove(); container$1 = null; (_a = document.getElementById("fqa-bookshelf-hover")) == null ? void 0 : _a.remove(); document.querySelectorAll(ORIGIN_SELECTOR$1).forEach((el) => { el.classList.remove("fqa-hide"); }); } async function mainHook$1(_previous) { if (!isBookshelfPath(window.location.pathname)) { unmount$1(); return; } if (app$1) { hideOrigin$1(); return; } injectStyle$1(); const origin = await waitForElement(ORIGIN_SELECTOR$1); if (!isBookshelfPath(window.location.pathname)) return; if (app$1) return; hideOrigin$1(); container$1 = document.createElement("div"); container$1.id = CONTAINER_ID$1; const anchor = origin ?? document.querySelector("#root") ?? document.body; if (origin == null ? void 0 : origin.parentElement) { origin.insertAdjacentElement("beforebegin", container$1); } else { anchor.appendChild(container$1); } app$1 = vue.createApp(_sfc_main$3); app$1.config.errorHandler = (err, _instance, info) => { console.error(`[fqa:bookshelf] Vue error (${info}):`, err); }; app$1.mount(container$1); console.log("[fqa:bookshelf] 书架视图已挂载"); document.title = "我的书架 - 番茄小说"; observer$1 = new MutationObserver((mutations) => { var _a; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.id === CONTAINER_ID$1 || node.closest(`#${CONTAINER_ID$1}`)) continue; if ((_a = node.matches) == null ? void 0 : _a.call(node, ORIGIN_SELECTOR$1)) { node.classList.add("fqa-hide"); } else { hideOrigin$1(node); } } } }); observer$1.observe(document.body, { childList: true, subtree: true }); } function filter$1(path, _query, _hash) { return (isBookshelfPath(path) || !!app$1) && userState.isLogin; } const _exports$1 = [ { id: "bookshelfHook_onload", event: "load", filter: filter$1, handler: mainHook$1 }, { id: "bookshelfHook_onurlchange", event: "onUrlChange", filter: filter$1, handler: mainHook$1 } ]; const _hoisted_1$2 = ["aria-label"]; const _hoisted_2$2 = { class: "fqa-sr-cover" }; const _hoisted_3$2 = ["src", "alt"]; const _hoisted_4$2 = { key: 0, class: "fqa-sr-badge" }; const _hoisted_5$2 = { class: "fqa-sr-body" }; const _hoisted_6$2 = ["innerHTML"]; const _hoisted_7$2 = { key: 1, class: "fqa-sr-title" }; const _hoisted_8$2 = { class: "fqa-sr-author" }; const _hoisted_9$2 = { key: 2, class: "fqa-sr-summary" }; const _hoisted_10$2 = { class: "fqa-sr-meta" }; const _hoisted_11$2 = { key: 0, class: "fqa-sr-read" }; const _hoisted_12$2 = { key: 3, class: "fqa-sr-update" }; const _hoisted_13$2 = { key: 0, class: "fqa-sr-chapter" }; const _hoisted_14$2 = { key: 1, class: "fqa-sr-time" }; const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({ __name: "SearchBookCard", props: { book: {} }, emits: ["open", "contextmenu"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const titleHtml = vue.computed(() => props.book.highlight_title ?? null); const statusText = vue.computed(() => mappingCreationStatus(props.book.status)); const wordText = vue.computed(() => { const n = props.book.word_count; if (!n) return ""; if (n >= 1e4) return `${(n / 1e4).toFixed(1).replace(/\.0$/, "")}万字`; return `${n}字`; }); const metaParts = vue.computed( () => [statusText.value, wordText.value, props.book.category].filter(Boolean) ); const readText = vue.computed(() => { if (props.book.sub_info) return props.book.sub_info; const n = props.book.read_count; if (!n) return ""; return n >= 1e4 ? `${(n / 1e4).toFixed(1).replace(/\.0$/, "")}万人在读` : `${n}人在读`; }); const updateText = vue.computed(() => { const t = props.book.last_publish_time; return t ? `${fromNow(t)}更新` : ""; }); const imgLoaded = vue.ref(false); vue.watch( () => props.book.cover_url, () => { imgLoaded.value = false; } ); function onContextMenu(event) { event.preventDefault(); emit("contextmenu", { book: props.book, x: event.clientX, y: event.clientY }); } return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { class: "fqa-sr-card", role: "link", tabindex: "0", "aria-label": __props.book.title, onClick: _cache[2] || (_cache[2] = ($event) => emit("open", __props.book)), onKeydown: _cache[3] || (_cache[3] = vue.withKeys(vue.withModifiers(($event) => emit("open", __props.book), ["prevent"]), ["enter"])), onContextmenu: onContextMenu }, [ vue.createElementVNode("div", _hoisted_2$2, [ vue.createElementVNode("img", { class: vue.normalizeClass(["fqa-sr-cover-img", { "fqa-sr-cover-loading": !imgLoaded.value }]), crossorigin: "anonymous", loading: "lazy", referrerpolicy: "no-referrer", src: __props.book.cover_url, alt: __props.book.title, onLoad: _cache[0] || (_cache[0] = ($event) => imgLoaded.value = true), onError: _cache[1] || (_cache[1] = ($event) => imgLoaded.value = true) }, null, 42, _hoisted_3$2), __props.book.in_bookshelf ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4$2, "在书架")) : vue.createCommentVNode("", true) ]), vue.createElementVNode("div", _hoisted_5$2, [ titleHtml.value ? (vue.openBlock(), vue.createElementBlock("h3", { key: 0, class: "fqa-sr-title", innerHTML: titleHtml.value }, null, 8, _hoisted_6$2)) : (vue.openBlock(), vue.createElementBlock("h3", _hoisted_7$2, vue.toDisplayString(__props.book.title), 1)), vue.createElementVNode("div", _hoisted_8$2, [ vue.createElementVNode("span", null, vue.toDisplayString(__props.book.author), 1), vue.createElementVNode("span", { class: vue.normalizeClass(["fqa-sr-score", { "fqa-sr-score-none": !__props.book.score }]) }, vue.toDisplayString(__props.book.score ? `${__props.book.score}分` : "暂无评分"), 3) ]), __props.book.summary ? (vue.openBlock(), vue.createElementBlock("p", _hoisted_9$2, vue.toDisplayString(__props.book.summary), 1)) : vue.createCommentVNode("", true), vue.createElementVNode("div", _hoisted_10$2, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(metaParts.value, (part) => { return vue.openBlock(), vue.createElementBlock("span", { key: part, class: "fqa-sr-tag" }, vue.toDisplayString(part), 1); }), 128)), readText.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_11$2, vue.toDisplayString(readText.value), 1)) : vue.createCommentVNode("", true) ]), updateText.value || __props.book.last_chapter_title ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_12$2, [ __props.book.last_chapter_title ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_13$2, vue.toDisplayString(__props.book.last_chapter_title), 1)) : vue.createCommentVNode("", true), updateText.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_14$2, vue.toDisplayString(updateText.value), 1)) : vue.createCommentVNode("", true) ])) : vue.createCommentVNode("", true) ]) ], 40, _hoisted_1$2); }; } }); const BOOK_TAB_TYPE = 1; const RESULT_SHOW_TYPE = 110; const RENDERABLE_TABS = /* @__PURE__ */ new Set([1, 2, 3, 5, 8]); function credentials() { return settings$1.searchPersonalized ? "include" : "omit"; } function num(v) { const n = Number(v); return Number.isFinite(n) ? n : 0; } function bool(v) { if (typeof v === "string") return v !== "" && v !== "0" && v !== "false"; return Boolean(v); } function sanitizeHighlight(html) { if (typeof html !== "string" || !html) return void 0; return html.replace(/<(?!\/?em\s*\/?>)[^>]*>/gi, ""); } function normalizeBook(raw, cell) { var _a; const bookId = String((raw == null ? void 0 : raw.book_id) ?? ""); if (!bookId || bookId === "0") return null; const hl = cell == null ? void 0 : cell.search_high_light; return { book_id: bookId, title: raw.book_name || raw.original_book_name || "", author: raw.author || "", cover_url: raw.thumb_url || raw.audio_thumb_url_hd || "", summary: raw.abstract || "", status: String(raw.creation_status ?? ""), word_count: num(raw.word_number), // sub_info 是接口给的展示文案,如「404章」「10.5万人在读」 sub_info: String(raw.sub_info ?? ""), read_count: num(raw.read_count), // 未评分的书下发 "0",视为没有评分 score: num(raw.score) > 0 ? String(raw.score) : "", category: raw.category || "", chapter_count: num(raw.serial_count), last_chapter_title: raw.last_chapter_title || "", last_publish_time: num(raw.last_publish_time) * 1e3, highlight_title: sanitizeHighlight((_a = hl == null ? void 0 : hl.title) == null ? void 0 : _a.rich_text), genre: raw.genre != null ? String(raw.genre) : void 0, in_bookshelf: bool(raw.in_bookshelf) }; } function collectBooks(cells) { const books = []; const seen = /* @__PURE__ */ new Set(); const take = (cell) => { for (const raw of (cell == null ? void 0 : cell.book_data) ?? []) { const book = normalizeBook(raw, cell); if (!book || seen.has(book.book_id)) continue; seen.add(book.book_id); books.push(book); } }; const results = cells.filter((c) => Number(c == null ? void 0 : c.show_type) === RESULT_SHOW_TYPE); if (results.length) { results.forEach(take); return books; } cells.filter((c) => Array.isArray(c == null ? void 0 : c.book_data) && c.book_data.length).forEach(take); return books; } function collectTabs(tabs) { return tabs.map((t) => { var _a; return { tab_type: num(t == null ? void 0 : t.tab_type), // 字段名是 title,不是 tab_name tab_name: String((t == null ? void 0 : t.title) ?? ""), has_selector: Array.isArray((_a = t == null ? void 0 : t.selector) == null ? void 0 : _a.rows) && t.selector.rows.length > 0 }; }).filter((t) => t.tab_type > 0 && t.tab_name && RENDERABLE_TABS.has(t.tab_type)); } function collectSelectorRows(tabs) { var _a, _b; const rows = (_b = (_a = tabs.find((t) => { var _a2; return Array.isArray((_a2 = t == null ? void 0 : t.selector) == null ? void 0 : _a2.rows); })) == null ? void 0 : _a.selector) == null ? void 0 : _b.rows; if (!Array.isArray(rows)) return []; return rows.map((row) => ({ name: String((row == null ? void 0 : row.row_name) ?? ""), items: (Array.isArray(row == null ? void 0 : row.items) ? row.items : []).map((item) => ({ id: String((item == null ? void 0 : item.selector_item_id) ?? ""), name: String((item == null ? void 0 : item.show_name) ?? ""), default: Boolean(item == null ? void 0 : item.is_default_selected) })).filter((item) => item.id && item.name) })).filter((row) => row.name && row.items.length > 0); } async function search({ query, passback = 0, selectedItems = [], tabType = BOOK_TAB_TYPE }) { const selected = tabType === BOOK_TAB_TYPE ? selectedItems.filter(Boolean).join(",") : ""; const j = await webGet( "/bookapi/search/tab/v", { query, passback: String(passback), selected_items: selected, tab_type: String(tabType) }, credentials() ); if ((j == null ? void 0 : j.code) !== void 0 && j.code !== 0) { throw new Error(j.message || `搜索失败(code=${j.code})`); } const tabs = Array.isArray(j == null ? void 0 : j.search_tabs) ? j.search_tabs : []; const current = tabs.find((t) => num(t == null ? void 0 : t.tab_type) === tabType); const cells = Array.isArray(current == null ? void 0 : current.data) ? current.data : []; const books = collectBooks(cells); return { books, // 回填服务端游标;没给就按已取回的书数往前推,至少不会原地打转 next_passback: num(current == null ? void 0 : current.passback) || passback + Math.max(books.length, 1), has_more: Boolean(current == null ? void 0 : current.has_more) && books.length > 0, tabs: collectTabs(tabs), selector_rows: collectSelectorRows(tabs) }; } function collectWords(list) { if (!Array.isArray(list)) return []; return list.map((t) => ({ word: String((t == null ? void 0 : t.tag_title) ?? ""), // 「877405热搜值」之类的附加说明 tag: (t == null ? void 0 : t.tag_attached) ? String(t.tag_attached) : void 0, label: (t == null ? void 0 : t.label) ? String(t.label) : void 0 })).filter((w) => w.word); } function collectSugBooks(list) { if (!Array.isArray(list)) return []; return list.map((b) => ({ book_id: String((b == null ? void 0 : b.book_id) ?? ""), title: String((b == null ? void 0 : b.book_name) ?? ""), cover_url: String((b == null ? void 0 : b.thumb_url) ?? ""), author: (b == null ? void 0 : b.author) ? String(b.author) : void 0, desc: (b == null ? void 0 : b.sub_info) ? String(b.sub_info) : void 0 })).filter((b) => b.book_id && b.title); } function pushSection(out, title, words, books) { if (!words.length && !books.length) return; out.push({ title: title || (words.length ? "热搜" : "推荐"), kind: words.length ? "hotword" : "book", words, books }); } async function getSearchLanding() { const j = await webGet( "/bookapi/plan/v", { search_source: "1", scene: "10", new_search_middle_page: "true", search_middle_page_version: "2", from: "search_input_page", tab_name: "store", bookstore_tab: "2", bookstore_tab_type: "2", hot_word_exchange: "false", query_history_removed: "false", user_is_login: settings$1.searchPersonalized ? "1" : "0" }, credentials() ); if ((j == null ? void 0 : j.code) !== void 0 && j.code !== 0) { throw new Error(j.message || `加载推荐失败(code=${j.code})`); } const sections = []; for (const cell of Array.isArray(j == null ? void 0 : j.data) ? j.data : []) { if (Array.isArray(cell == null ? void 0 : cell.cell_data)) { for (const sub of cell.cell_data) { pushSection( sections, String((sub == null ? void 0 : sub.cell_name) ?? ""), collectWords(sub == null ? void 0 : sub.search_tag_data), collectSugBooks(sub == null ? void 0 : sub.book_data) ); } continue; } pushSection( sections, String((cell == null ? void 0 : cell.cell_name) ?? "") || "猜你想搜", collectWords(cell == null ? void 0 : cell.search_tag_data), collectSugBooks(cell == null ? void 0 : cell.book_data) ); } return sections; } const _hoisted_1$1 = { class: "fqa-s-landing" }; const _hoisted_2$1 = { key: 0, class: "fqa-s-landing-sk" }; const _hoisted_3$1 = { key: 1, class: "fqa-s-status" }; const _hoisted_4$1 = { key: 2, class: "fqa-s-status" }; const _hoisted_5$1 = { class: "fqa-s-sec-title" }; const _hoisted_6$1 = { key: 0, class: "fqa-s-words" }; const _hoisted_7$1 = ["onClick"]; const _hoisted_8$1 = { key: 0, class: "fqa-s-word-label" }; const _hoisted_9$1 = { key: 1, class: "fqa-s-word-tag" }; const _hoisted_10$1 = { key: 1, class: "fqa-s-sugs" }; const _hoisted_11$1 = ["onClick", "onKeydown"]; const _hoisted_12$1 = ["src", "alt"]; const _hoisted_13$1 = ["title"]; const _hoisted_14$1 = { key: 0, class: "fqa-s-sug-sub" }; const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({ __name: "SearchLanding", emits: ["word"], setup(__props, { emit: __emit }) { const emit = __emit; const sections = vue.ref([]); const loading = vue.ref(true); const error = vue.ref(null); async function load2() { loading.value = true; error.value = null; try { sections.value = await getSearchLanding(); } catch (err) { console.error("[fqa:search] 推荐加载失败:", err); error.value = err instanceof Error ? err.message : String(err); } finally { loading.value = false; } } function openBook(bookId) { unsafeWindow.location.href = `https://fanqienovel.com/page/${bookId}`; } vue.onMounted(load2); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [ loading.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$1, [ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(12, (n) => { return vue.createElementVNode("div", { key: n, class: "fqa-sk-chip fqa-sk-anim" }); }), 64)) ])) : error.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_3$1, [ _cache[0] || (_cache[0] = vue.createElementVNode("div", { class: "fqa-s-status-title" }, "推荐内容加载失败", -1)), _cache[1] || (_cache[1] = vue.createElementVNode("div", null, "直接在上方输入关键词也可以搜索", -1)), vue.createElementVNode("button", { class: "fqa-s-submit", onClick: load2 }, "重试") ])) : !sections.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_4$1, [..._cache[2] || (_cache[2] = [ vue.createElementVNode("div", { class: "fqa-s-status-title" }, "输入关键词开始搜索", -1) ])])) : (vue.openBlock(true), vue.createElementBlock(vue.Fragment, { key: 3 }, vue.renderList(sections.value, (section, i2) => { return vue.openBlock(), vue.createElementBlock("section", { key: `${section.title}-${i2}`, class: "fqa-s-sec" }, [ vue.createElementVNode("h3", _hoisted_5$1, vue.toDisplayString(section.title), 1), section.words.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_6$1, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(section.words, (w, wi) => { return vue.openBlock(), vue.createElementBlock("button", { key: `${w.word}-${wi}`, class: "fqa-s-word", onClick: ($event) => emit("word", w.word) }, [ w.label ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_8$1, vue.toDisplayString(w.label), 1)) : vue.createCommentVNode("", true), vue.createTextVNode(" " + vue.toDisplayString(w.word) + " ", 1), w.tag ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_9$1, vue.toDisplayString(w.tag), 1)) : vue.createCommentVNode("", true) ], 8, _hoisted_7$1); }), 128)) ])) : vue.createCommentVNode("", true), section.books.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10$1, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(section.books, (b) => { return vue.openBlock(), vue.createElementBlock("div", { key: b.book_id, class: "fqa-s-sug", role: "link", tabindex: "0", onClick: ($event) => openBook(b.book_id), onKeydown: vue.withKeys(vue.withModifiers(($event) => openBook(b.book_id), ["prevent"]), ["enter"]) }, [ vue.createElementVNode("img", { class: "fqa-s-sug-cover", crossorigin: "anonymous", loading: "lazy", referrerpolicy: "no-referrer", src: b.cover_url, alt: b.title }, null, 8, _hoisted_12$1), vue.createElementVNode("div", { class: "fqa-s-sug-title", title: b.title }, vue.toDisplayString(b.title), 9, _hoisted_13$1), b.desc || b.author ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_14$1, vue.toDisplayString(b.desc || b.author), 1)) : vue.createCommentVNode("", true) ], 40, _hoisted_11$1); }), 128)) ])) : vue.createCommentVNode("", true) ]); }), 128)) ]); }; } }); const FALLBACK_TABS = [{ tab_type: BOOK_TAB_TYPE, tab_name: "综合", has_selector: true }]; function useSearch() { const query = vue.ref(""); const tabType = vue.ref(BOOK_TAB_TYPE); const tabs = vue.ref([]); const selectorRows = vue.ref([]); const selected = vue.ref({}); const books = vue.ref([]); const passback = vue.ref(0); const hasMore = vue.ref(false); const loading = vue.ref(false); const loadingMore = vue.ref(false); const error = vue.ref(null); const searched = vue.ref(false); let token = 0; const selectedItems = vue.computed( () => Object.values(selected.value).filter(Boolean) ); const availableTabs = vue.computed(() => tabs.value.length ? tabs.value : FALLBACK_TABS); const canFilter = vue.computed(() => tabType.value === BOOK_TAB_TYPE && selectorRows.value.length > 0); const filterCount = vue.computed(() => selectedItems.value.length); async function run(reset2) { const q = query.value.trim(); if (!q) return; const current = ++token; if (reset2) { passback.value = 0; loading.value = true; error.value = null; } else { if (loadingMore.value || !hasMore.value) return; loadingMore.value = true; } try { const result = await search({ query: q, passback: passback.value, selectedItems: selectedItems.value, tabType: tabType.value }); if (current !== token) return; if (reset2) { books.value = result.books; } else { const seen = new Set(books.value.map((b) => b.book_id)); books.value = [...books.value, ...result.books.filter((b) => !seen.has(b.book_id))]; } passback.value = result.next_passback; hasMore.value = result.has_more; if (result.tabs.length) tabs.value = result.tabs; if (result.selector_rows.length) selectorRows.value = result.selector_rows; searched.value = true; } catch (err) { if (current !== token) return; console.error("[fqa:search] 搜索失败:", err); error.value = err instanceof Error ? err.message : String(err); if (reset2) books.value = []; searched.value = true; } finally { if (current === token) { loading.value = false; loadingMore.value = false; } } } async function submit(q) { if (q !== void 0) query.value = q; await run(true); } function reset() { token++; query.value = ""; books.value = []; passback.value = 0; hasMore.value = false; searched.value = false; error.value = null; loading.value = false; loadingMore.value = false; selected.value = {}; tabType.value = BOOK_TAB_TYPE; } async function loadMore() { if (!hasMore.value || loading.value || loadingMore.value) return; await run(false); } function selectTab(next) { if (tabType.value === next) return; tabType.value = next; if (next !== BOOK_TAB_TYPE) selected.value = {}; void run(true); } function toggleFilter(rowName, itemId) { const cur = selected.value[rowName]; if (cur === itemId) { const next = { ...selected.value }; delete next[rowName]; selected.value = next; } else { selected.value = { ...selected.value, [rowName]: itemId }; } void run(true); } function clearFilters() { if (!filterCount.value) return; selected.value = {}; void run(true); } const isEmpty = vue.computed( () => searched.value && !loading.value && !error.value && books.value.length === 0 ); return { query, tabType, availableTabs, selectorRows, selected, canFilter, filterCount, books, hasMore, loading, loadingMore, error, searched, isEmpty, submit, reset, loadMore, selectTab, toggleFilter, clearFilters }; } const routeQuery = vue.ref(""); function parseQuery(path) { const rest = path.replace(/^\/search\/?/, ""); if (!rest) return ""; try { return decodeURIComponent(rest); } catch { return rest; } } function syncFromUrl(path) { const q = parseQuery(path); if (routeQuery.value !== q) routeQuery.value = q; } function pushQuery(q) { routeQuery.value = q; const next = q ? `/search/${encodeURIComponent(q)}` : "/search"; if (unsafeWindow.location.pathname === next) return; unsafeWindow.history.pushState(null, "", next); } const _hoisted_1 = { id: "fqa-search" }; const _hoisted_2 = { class: "fqa-s-bar" }; const _hoisted_3 = { class: "fqa-s-inputwrap" }; const _hoisted_4 = ["disabled"]; const _hoisted_5 = ["aria-selected", "onClick", "onKeydown"]; const _hoisted_6 = { key: 0, class: "fqa-s-filterbar" }; const _hoisted_7 = { key: 0, class: "fqa-s-fcount" }; const _hoisted_8 = { class: "fqa-s-hint" }; const _hoisted_9 = { key: 1, class: "fqa-s-filters" }; const _hoisted_10 = { class: "fqa-s-frow-name" }; const _hoisted_11 = { class: "fqa-s-fitems" }; const _hoisted_12 = ["onClick"]; const _hoisted_13 = { key: 2, class: "fqa-s-status" }; const _hoisted_14 = { key: 3, class: "fqa-s-status" }; const _hoisted_15 = { class: "fqa-s-list" }; const _hoisted_16 = { key: 1, class: "fqa-s-loadmore" }; const _hoisted_17 = { key: 2, class: "fqa-s-inline-error" }; const _hoisted_18 = { key: 2, class: "fqa-s-privacy" }; const _hoisted_19 = { key: 0, class: "fqa-toast" }; const _sfc_main = /* @__PURE__ */ vue.defineComponent({ __name: "SearchView", setup(__props) { const { query, tabType, availableTabs, selectorRows, selected, canFilter, filterCount, books, hasMore, loading, loadingMore, error, isEmpty, submit, reset, loadMore, selectTab, toggleFilter, clearFilters } = useSearch(); const input = vue.ref(routeQuery.value); const filterOpen = vue.ref(false); const showLanding = vue.computed(() => !query.value.trim()); async function doSearch(q = input.value) { const trimmed = q.trim(); if (!trimmed) return; input.value = trimmed; filterOpen.value = false; pushQuery(trimmed); await submit(trimmed); } function onWord(word) { void doSearch(word); } const sentinel = vue.ref(null); let io = null; vue.onMounted(async () => { if (routeQuery.value) await doSearch(routeQuery.value); if (typeof IntersectionObserver !== "undefined") { io = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) void loadMore(); }, { rootMargin: "400px" } ); vue.watch( sentinel, (el) => { io == null ? void 0 : io.disconnect(); if (el) io == null ? void 0 : io.observe(el); }, { immediate: true, flush: "post" } ); } }); vue.onBeforeUnmount(() => { io == null ? void 0 : io.disconnect(); io = null; }); vue.watch( routeQuery, (q) => { input.value = q; if (q === query.value.trim()) return; if (q) { void submit(q); } else { filterOpen.value = false; reset(); } } ); const menuVisible = vue.ref(false); const menuPos = vue.ref({ x: 0, y: 0 }); const menuBook = vue.ref(null); const toast = vue.ref(null); let toastTimer; function showToast(msg) { toast.value = msg; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.value = null; toastTimer = void 0; }, 2600); } const menuItems = vue.computed(() => { var _a, _b, _c; return [ { key: "detail", label: "查看详情" }, { key: "shelf", label: ((_a = menuBook.value) == null ? void 0 : _a.in_bookshelf) ? "已在书架" : "加入书架", disabled: !userState.isLogin || Boolean((_b = menuBook.value) == null ? void 0 : _b.in_bookshelf) }, { key: "author", label: "搜索该作者", disabled: !((_c = menuBook.value) == null ? void 0 : _c.author) } ]; }); function onCardContextMenu({ book, x, y }) { menuBook.value = book; menuPos.value = { x, y }; menuVisible.value = true; } function openBook(book) { unsafeWindow.location.href = `https://fanqienovel.com/page/${book.book_id}`; } async function onMenuSelect(key) { const book = menuBook.value; if (!book) return; if (key === "detail") { openBook(book); return; } if (key === "author") { void doSearch(book.author); return; } if (key === "shelf") { try { await addToBookshelf(book.book_id); book.in_bookshelf = true; showToast(`已把《${book.title}》加入书架`); } catch (err) { console.error("[fqa:search] 加入书架失败:", err); showToast(err instanceof Error ? err.message : "加入书架失败"); } } } const tabsRef = vue.ref(null); const inkStyle = vue.ref({ left: "0px", width: "0px" }); function updateInk() { const wrap = tabsRef.value; if (!wrap) return; const index = availableTabs.value.findIndex((t) => t.tab_type === tabType.value); const el = wrap.querySelectorAll(".fqa-s-tab")[index]; if (!el) return; inkStyle.value = { left: `${el.offsetLeft}px`, width: `${el.offsetWidth}px` }; } vue.watch([tabType, availableTabs], () => vue.nextTick(updateInk), { deep: true }); vue.watch(showLanding, () => vue.nextTick(updateInk)); vue.onMounted(() => { window.addEventListener("resize", updateInk); }); vue.onBeforeUnmount(() => { window.removeEventListener("resize", updateInk); if (toastTimer) clearTimeout(toastTimer); }); const resultHint = vue.computed(() => { if (loading.value) return "搜索中…"; if (books.value.length) return `已加载 ${books.value.length} 条`; return ""; }); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ vue.createElementVNode("div", _hoisted_2, [ vue.createElementVNode("div", _hoisted_3, [ vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => input.value = $event), class: "fqa-s-input", type: "search", placeholder: "搜索书名、作者", "aria-label": "搜索", onKeydown: _cache[1] || (_cache[1] = vue.withKeys(vue.withModifiers(($event) => doSearch(), ["prevent"]), ["enter"])) }, null, 544), [ [vue.vModelText, input.value] ]), input.value ? (vue.openBlock(), vue.createElementBlock("button", { key: 0, class: "fqa-s-clear", "aria-label": "清空", onClick: _cache[2] || (_cache[2] = ($event) => input.value = "") }, " ✕ ")) : vue.createCommentVNode("", true) ]), vue.createElementVNode("button", { class: "fqa-s-submit", disabled: !input.value.trim() || vue.unref(loading), onClick: _cache[3] || (_cache[3] = ($event) => doSearch()) }, " 搜索 ", 8, _hoisted_4) ]), showLanding.value ? (vue.openBlock(), vue.createBlock(_sfc_main$1, { key: 0, onWord })) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [ vue.createElementVNode("div", { ref_key: "tabsRef", ref: tabsRef, class: "fqa-s-tabs", role: "tablist" }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(availableTabs), (tab) => { return vue.openBlock(), vue.createElementBlock("div", { key: tab.tab_type, class: vue.normalizeClass(["fqa-s-tab", { "fqa-s-tab-active": vue.unref(tabType) === tab.tab_type }]), role: "tab", tabindex: "0", "aria-selected": vue.unref(tabType) === tab.tab_type, onClick: ($event) => vue.unref(selectTab)(tab.tab_type), onKeydown: vue.withKeys(vue.withModifiers(($event) => vue.unref(selectTab)(tab.tab_type), ["prevent"]), ["enter"]) }, vue.toDisplayString(tab.tab_name), 43, _hoisted_5); }), 128)), vue.createElementVNode("span", { class: "fqa-s-tab-ink", style: vue.normalizeStyle(inkStyle.value) }, null, 4) ], 512), vue.unref(canFilter) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_6, [ vue.createElementVNode("button", { class: vue.normalizeClass(["fqa-s-fbtn", { "fqa-s-fbtn-on": filterOpen.value || vue.unref(filterCount) > 0 }]), onClick: _cache[4] || (_cache[4] = ($event) => filterOpen.value = !filterOpen.value) }, [ _cache[8] || (_cache[8] = vue.createTextVNode(" 筛选", -1)), vue.unref(filterCount) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_7, vue.toDisplayString(vue.unref(filterCount)), 1)) : vue.createCommentVNode("", true) ], 2), vue.unref(filterCount) ? (vue.openBlock(), vue.createElementBlock("button", { key: 0, class: "fqa-s-fclear", onClick: _cache[5] || (_cache[5] = //@ts-ignore (...args) => vue.unref(clearFilters) && vue.unref(clearFilters)(...args)) }, "清除")) : vue.createCommentVNode("", true), vue.createElementVNode("span", _hoisted_8, vue.toDisplayString(resultHint.value), 1) ])) : vue.createCommentVNode("", true), vue.unref(canFilter) && filterOpen.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(selectorRows), (row) => { return vue.openBlock(), vue.createElementBlock("div", { key: row.name, class: "fqa-s-frow" }, [ vue.createElementVNode("span", _hoisted_10, vue.toDisplayString(row.name), 1), vue.createElementVNode("div", _hoisted_11, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(row.items, (item) => { return vue.openBlock(), vue.createElementBlock("button", { key: item.id, class: vue.normalizeClass(["fqa-s-fitem", { "fqa-s-fitem-on": vue.unref(selected)[row.name] === item.id }]), onClick: ($event) => vue.unref(toggleFilter)(row.name, item.id) }, vue.toDisplayString(item.name), 11, _hoisted_12); }), 128)) ]) ]); }), 128)) ])) : vue.createCommentVNode("", true), vue.unref(error) && !vue.unref(books).length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_13, [ _cache[9] || (_cache[9] = vue.createElementVNode("div", { class: "fqa-s-status-title" }, "搜索失败", -1)), vue.createElementVNode("div", null, vue.toDisplayString(vue.unref(error)), 1), vue.createElementVNode("button", { class: "fqa-s-submit", onClick: _cache[6] || (_cache[6] = ($event) => doSearch()) }, "重试") ])) : vue.unref(isEmpty) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_14, [..._cache[10] || (_cache[10] = [ vue.createElementVNode("div", { class: "fqa-s-status-title" }, "没有找到相关内容", -1), vue.createElementVNode("div", null, "换个关键词或者放宽筛选条件试试", -1) ])])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 4 }, [ vue.createElementVNode("div", _hoisted_15, [ vue.unref(loading) ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, vue.renderList(5, (n) => { return vue.createElementVNode("div", { key: `sk-${n}`, class: "fqa-sr-card fqa-sr-skeleton" }, [..._cache[11] || (_cache[11] = [ vue.createStaticVNode('
', 2) ])]); }), 64)) : (vue.openBlock(true), vue.createElementBlock(vue.Fragment, { key: 1 }, vue.renderList(vue.unref(books), (book) => { return vue.openBlock(), vue.createBlock(_sfc_main$2, { key: book.book_id, book, onOpen: openBook, onContextmenu: onCardContextMenu }, null, 8, ["book"]); }), 128)) ]), !vue.unref(loading) && vue.unref(hasMore) ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, ref_key: "sentinel", ref: sentinel, class: "fqa-s-loadmore" }, vue.toDisplayString(vue.unref(loadingMore) ? "加载中…" : "滚动加载更多"), 513)) : !vue.unref(loading) && vue.unref(books).length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_16, "没有更多了")) : vue.createCommentVNode("", true), vue.unref(error) && vue.unref(books).length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_17, vue.toDisplayString(vue.unref(error)), 1)) : vue.createCommentVNode("", true) ], 64)) ], 64)), !vue.unref(settings$1).searchPersonalized ? (vue.openBlock(), vue.createElementBlock("p", _hoisted_18, " 当前使用匿名搜索。如需按你的阅读偏好排序,可在助手设置里开启个人化推荐。 ")) : vue.createCommentVNode("", true), (vue.openBlock(), vue.createBlock(vue.Teleport, { to: "body" }, [ vue.createVNode(_sfc_main$4, { visible: menuVisible.value, x: menuPos.value.x, y: menuPos.value.y, items: menuItems.value, onSelect: onMenuSelect, onClose: _cache[7] || (_cache[7] = ($event) => menuVisible.value = false) }, null, 8, ["visible", "x", "y", "items"]), toast.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_19, vue.toDisplayString(toast.value), 1)) : vue.createCommentVNode("", true) ])) ]); }; } }); const searchcss = "#fqa-search {\n --fqa-text: #1f2329;\n --fqa-text-sub: #646a73;\n --fqa-text-weak: #8f959e;\n --fqa-border: rgba(31, 35, 41, 0.08);\n --fqa-hover: rgba(31, 35, 41, 0.04);\n --fqa-accent: #ff6f3d;\n --fqa-skeleton: rgba(31, 35, 41, 0.06);\n --fqa-skeleton-hl: rgba(31, 35, 41, 0.12);\n --fqa-shadow: 0 4px 16px rgba(31, 35, 41, 0.08);\n --fqa-card-bg: #fff;\n\n display: block;\n box-sizing: border-box;\n width: 100%;\n max-width: 1000px;\n margin: 0 auto;\n /* 顶部让开原站 fixed 顶栏 */\n padding: calc(80px + 24px) 16px 64px;\n color: var(--fqa-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial,\n sans-serif;\n}\n\n#fqa-search *,\n#fqa-search *::before,\n#fqa-search *::after {\n box-sizing: border-box;\n}\n\n#fqa-search div,\n#fqa-search span,\n#fqa-search h3,\n#fqa-search p {\n margin: 0;\n padding: 0;\n border: 0;\n float: none;\n position: static;\n}\n\n/* -------------------------------- 搜索栏 -------------------------------- */\n\n#fqa-search .fqa-s-bar {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 20px;\n}\n\n#fqa-search .fqa-s-inputwrap {\n position: relative;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n#fqa-search .fqa-s-input {\n width: 100%;\n padding: 11px 36px 11px 16px;\n box-sizing: border-box;\n border: 1px solid var(--fqa-border);\n border-radius: 999px;\n background: var(--fqa-card-bg);\n color: var(--fqa-text);\n font-size: 15px;\n font-family: inherit;\n line-height: 22px;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n}\n\n#fqa-search .fqa-s-input::placeholder {\n color: var(--fqa-text-weak);\n}\n\n#fqa-search .fqa-s-input:focus {\n outline: none;\n border-color: var(--fqa-accent);\n box-shadow: 0 0 0 3px rgba(255, 111, 61, 0.12);\n}\n\n/* 隐藏浏览器自带的 search 清除按钮,用自己的 */\n#fqa-search .fqa-s-input::-webkit-search-cancel-button {\n appearance: none;\n}\n\n#fqa-search .fqa-s-clear {\n position: absolute;\n top: 50%;\n right: 10px;\n transform: translateY(-50%);\n width: 22px;\n height: 22px;\n padding: 0;\n border: 0;\n border-radius: 50%;\n background: var(--fqa-hover);\n color: var(--fqa-text-weak);\n font-size: 11px;\n line-height: 1;\n cursor: pointer;\n}\n\n#fqa-search .fqa-s-clear:hover {\n color: var(--fqa-text);\n}\n\n#fqa-search .fqa-s-submit {\n flex: 0 0 auto;\n padding: 11px 24px;\n border: 0;\n border-radius: 999px;\n background: var(--fqa-accent);\n color: #fff;\n font-size: 15px;\n font-family: inherit;\n line-height: 22px;\n cursor: pointer;\n transition: opacity 0.15s ease;\n}\n\n#fqa-search .fqa-s-submit:hover:not(:disabled) {\n opacity: 0.88;\n}\n\n#fqa-search .fqa-s-submit:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n/* --------------------------------- tab --------------------------------- */\n\n#fqa-search .fqa-s-tabs {\n position: relative;\n display: flex;\n align-items: center;\n gap: 4px;\n margin-bottom: 16px;\n border-bottom: 1px solid var(--fqa-border);\n overflow-x: auto;\n scrollbar-width: none;\n}\n\n#fqa-search .fqa-s-tabs::-webkit-scrollbar {\n display: none;\n}\n\n#fqa-search .fqa-s-tab {\n flex: 0 0 auto;\n padding: 10px 16px;\n color: var(--fqa-text-sub);\n font-size: 15px;\n line-height: 22px;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n transition: color 0.15s ease;\n}\n\n#fqa-search .fqa-s-tab:hover {\n color: var(--fqa-text);\n}\n\n#fqa-search .fqa-s-tab-active {\n color: var(--fqa-accent);\n font-weight: 600;\n}\n\n#fqa-search .fqa-s-tab-ink {\n position: absolute;\n bottom: -1px;\n left: 0;\n width: 0;\n height: 2px;\n border-radius: 2px;\n background: var(--fqa-accent);\n transition: left 0.25s ease, width 0.25s ease;\n}\n\n/* -------------------------------- 筛选器 -------------------------------- */\n\n#fqa-search .fqa-s-filterbar {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 12px;\n}\n\n#fqa-search .fqa-s-fbtn,\n#fqa-search .fqa-s-fclear {\n padding: 5px 14px;\n border: 1px solid var(--fqa-border);\n border-radius: 999px;\n background: transparent;\n color: var(--fqa-text-sub);\n font-size: 13px;\n font-family: inherit;\n line-height: 20px;\n cursor: pointer;\n transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;\n}\n\n#fqa-search .fqa-s-fbtn:hover,\n#fqa-search .fqa-s-fclear:hover {\n border-color: var(--fqa-accent);\n color: var(--fqa-accent);\n}\n\n#fqa-search .fqa-s-fbtn-on {\n border-color: var(--fqa-accent);\n color: var(--fqa-accent);\n background: rgba(255, 111, 61, 0.08);\n}\n\n#fqa-search .fqa-s-fcount {\n display: inline-block;\n min-width: 16px;\n margin-left: 5px;\n padding: 0 4px;\n border-radius: 8px;\n background: var(--fqa-accent);\n color: #fff;\n font-size: 11px;\n line-height: 16px;\n text-align: center;\n}\n\n#fqa-search .fqa-s-hint {\n margin-left: auto;\n color: var(--fqa-text-weak);\n font-size: 13px;\n}\n\n#fqa-search .fqa-s-filters {\n margin-bottom: 16px;\n padding: 14px 16px;\n border: 1px solid var(--fqa-border);\n border-radius: 10px;\n background: var(--fqa-card-bg);\n}\n\n#fqa-search .fqa-s-frow {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 7px 0;\n}\n\n#fqa-search .fqa-s-frow-name {\n flex: 0 0 58px;\n padding-top: 5px;\n color: var(--fqa-text-weak);\n font-size: 13px;\n line-height: 20px;\n}\n\n#fqa-search .fqa-s-fitems {\n display: flex;\n flex: 1 1 auto;\n flex-wrap: wrap;\n gap: 8px;\n min-width: 0;\n}\n\n#fqa-search .fqa-s-fitem {\n padding: 4px 12px;\n border: 1px solid transparent;\n border-radius: 6px;\n background: var(--fqa-hover);\n color: var(--fqa-text-sub);\n font-size: 13px;\n font-family: inherit;\n line-height: 20px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n}\n\n#fqa-search .fqa-s-fitem:hover {\n color: var(--fqa-text);\n}\n\n#fqa-search .fqa-s-fitem-on {\n background: rgba(255, 111, 61, 0.12);\n border-color: rgba(255, 111, 61, 0.4);\n color: var(--fqa-accent);\n font-weight: 500;\n}\n\n/* ------------------------------- 结果列表 ------------------------------- */\n\n#fqa-search .fqa-s-list {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n#fqa-search .fqa-sr-card {\n display: flex;\n gap: 16px;\n padding: 16px 12px;\n border-radius: 10px;\n cursor: pointer;\n outline: none;\n transition: background 0.15s ease;\n}\n\n#fqa-search .fqa-sr-card:hover {\n background: var(--fqa-hover);\n}\n\n#fqa-search .fqa-sr-card:focus-visible {\n box-shadow: 0 0 0 2px var(--fqa-accent);\n}\n\n#fqa-search .fqa-sr-skeleton {\n cursor: default;\n}\n\n#fqa-search .fqa-sr-skeleton:hover {\n background: transparent;\n}\n\n#fqa-search .fqa-sr-cover {\n position: relative;\n flex: 0 0 96px;\n width: 96px;\n aspect-ratio: 3 / 4;\n border-radius: 6px;\n overflow: hidden;\n background: var(--fqa-skeleton);\n}\n\n#fqa-search .fqa-sr-cover-img {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: opacity 0.25s ease;\n}\n\n#fqa-search .fqa-sr-cover-loading {\n opacity: 0;\n}\n\n#fqa-search .fqa-sr-badge {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n padding: 2px 4px;\n background: rgba(31, 35, 41, 0.7);\n color: #fff;\n font-size: 10px;\n line-height: 14px;\n text-align: center;\n}\n\n#fqa-search .fqa-sr-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-width: 0;\n}\n\n#fqa-search .fqa-sr-title {\n color: var(--fqa-text);\n font-size: 16px;\n font-weight: 600;\n line-height: 24px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* 接口下发的高亮片段 */\n#fqa-search .fqa-sr-title em {\n color: var(--fqa-accent);\n font-style: normal;\n}\n\n#fqa-search .fqa-sr-author {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 3px;\n color: var(--fqa-text-sub);\n font-size: 13px;\n line-height: 20px;\n}\n\n#fqa-search .fqa-sr-author > span:first-child {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n#fqa-search .fqa-sr-score {\n flex: 0 0 auto;\n color: var(--fqa-accent);\n font-weight: 500;\n}\n\n/* 无评分时弱化,不跟真实分数抢注意力 */\n#fqa-search .fqa-sr-score-none {\n color: var(--fqa-text-weak);\n font-weight: 400;\n}\n\n#fqa-search .fqa-sr-summary {\n display: -webkit-box;\n margin-top: 6px;\n color: var(--fqa-text-sub);\n font-size: 13px;\n line-height: 20px;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n#fqa-search .fqa-sr-meta {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 8px;\n}\n\n#fqa-search .fqa-sr-tag {\n padding: 1px 7px;\n border-radius: 4px;\n background: var(--fqa-hover);\n color: var(--fqa-text-weak);\n font-size: 12px;\n line-height: 18px;\n}\n\n#fqa-search .fqa-sr-read {\n color: var(--fqa-text-weak);\n font-size: 12px;\n line-height: 18px;\n}\n\n#fqa-search .fqa-sr-update {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n color: var(--fqa-text-weak);\n font-size: 12px;\n line-height: 18px;\n}\n\n#fqa-search .fqa-sr-chapter {\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n#fqa-search .fqa-sr-time {\n flex: 0 0 auto;\n}\n\n@media (max-width: 600px) {\n #fqa-search .fqa-sr-cover {\n flex-basis: 72px;\n width: 72px;\n }\n\n #fqa-search .fqa-sr-summary {\n -webkit-line-clamp: 3;\n line-clamp: 3;\n }\n}\n\n/* -------------------------------- 落地页 -------------------------------- */\n\n#fqa-search .fqa-s-sec {\n margin-bottom: 28px;\n}\n\n#fqa-search .fqa-s-sec-title {\n margin-bottom: 12px;\n font-size: 16px;\n font-weight: 600;\n line-height: 24px;\n}\n\n#fqa-search .fqa-s-words {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n#fqa-search .fqa-s-word {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 6px 14px;\n border: 0;\n border-radius: 999px;\n background: var(--fqa-hover);\n color: var(--fqa-text-sub);\n font-size: 13px;\n font-family: inherit;\n line-height: 20px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n}\n\n#fqa-search .fqa-s-word:hover {\n background: rgba(255, 111, 61, 0.1);\n color: var(--fqa-accent);\n}\n\n#fqa-search .fqa-s-word-tag {\n color: var(--fqa-text-weak);\n font-size: 11px;\n}\n\n/* 「荐」「热」这类角标 */\n#fqa-search .fqa-s-word-label {\n padding: 0 4px;\n border-radius: 3px;\n background: var(--fqa-accent);\n color: #fff;\n font-size: 10px;\n line-height: 15px;\n}\n\n#fqa-search .fqa-s-sugs {\n display: grid;\n grid-template-columns: repeat(6, minmax(0, 1fr));\n gap: 20px 16px;\n}\n\n@media (max-width: 900px) {\n #fqa-search .fqa-s-sugs {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n }\n}\n\n@media (max-width: 600px) {\n #fqa-search .fqa-s-sugs {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n#fqa-search .fqa-s-sug {\n cursor: pointer;\n outline: none;\n}\n\n#fqa-search .fqa-s-sug-cover {\n display: block;\n width: 100%;\n aspect-ratio: 3 / 4;\n border-radius: 6px;\n object-fit: cover;\n background: var(--fqa-skeleton);\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n#fqa-search .fqa-s-sug:hover .fqa-s-sug-cover {\n transform: translateY(-4px);\n box-shadow: var(--fqa-shadow);\n}\n\n#fqa-search .fqa-s-sug-title {\n margin-top: 7px;\n font-size: 13px;\n line-height: 19px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n#fqa-search .fqa-s-sug-sub {\n margin-top: 2px;\n color: var(--fqa-text-weak);\n font-size: 12px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n#fqa-search .fqa-s-landing-sk {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n#fqa-search .fqa-sk-chip {\n width: 84px;\n height: 32px;\n border-radius: 999px;\n background: var(--fqa-skeleton);\n}\n\n/* ------------------------------ 骨架 / 状态 ------------------------------ */\n\n#fqa-search .fqa-sk-cover {\n flex: 0 0 96px;\n width: 96px;\n aspect-ratio: 3 / 4;\n border-radius: 6px;\n background: var(--fqa-skeleton);\n}\n\n#fqa-search .fqa-sk-line {\n height: 12px;\n margin-bottom: 9px;\n border-radius: 4px;\n background: var(--fqa-skeleton);\n}\n\n#fqa-search .fqa-sk-anim {\n position: relative;\n overflow: hidden;\n}\n\n#fqa-search .fqa-sk-anim::after {\n content: '';\n position: absolute;\n inset: 0;\n transform: translateX(-100%);\n background: linear-gradient(90deg, transparent, var(--fqa-skeleton-hl), transparent);\n animation: fqa-shimmer 1.4s infinite;\n}\n\n#fqa-search .fqa-s-status {\n padding: 72px 16px;\n text-align: center;\n color: var(--fqa-text-weak);\n font-size: 14px;\n line-height: 22px;\n}\n\n#fqa-search .fqa-s-status-title {\n margin-bottom: 8px;\n color: var(--fqa-text);\n font-size: 16px;\n font-weight: 500;\n}\n\n#fqa-search .fqa-s-status .fqa-s-submit {\n margin-top: 16px;\n}\n\n#fqa-search .fqa-s-loadmore {\n padding: 24px 0;\n text-align: center;\n color: var(--fqa-text-weak);\n font-size: 13px;\n}\n\n#fqa-search .fqa-s-inline-error {\n padding: 12px 0;\n text-align: center;\n color: #f5222d;\n font-size: 13px;\n}\n\n#fqa-search .fqa-s-privacy {\n margin-top: 24px;\n padding-top: 16px;\n border-top: 1px solid var(--fqa-border);\n color: var(--fqa-text-weak);\n font-size: 12px;\n line-height: 18px;\n text-align: center;\n}\n\n/* ------------------------------- 深色模式 ------------------------------- */\n\n@media (prefers-color-scheme: dark) {\n #fqa-search {\n --fqa-text: #e6e6e6;\n --fqa-text-sub: #a6a6a6;\n --fqa-text-weak: #7a7a7a;\n --fqa-border: rgba(255, 255, 255, 0.1);\n --fqa-hover: rgba(255, 255, 255, 0.06);\n --fqa-skeleton: rgba(255, 255, 255, 0.08);\n --fqa-skeleton-hl: rgba(255, 255, 255, 0.14);\n --fqa-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n --fqa-card-bg: #212125;\n }\n}\n"; const CONTAINER_ID = "fqa-search-root"; const STYLE_ID = "fqa-search-style"; const RESULT_SELECTOR = ".muye-search"; const NOTFOUND_SELECTOR = ".muye-undefined"; const ORIGIN_SELECTOR = `${RESULT_SELECTOR}, ${NOTFOUND_SELECTOR}`; let app = null; let container = null; let observer = null; let stopTitleWatch = null; function injectStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = searchcss; document.head.appendChild(style); } function isSearchPath(path) { return path === "/search" || path === "/search/" || path.startsWith("/search/"); } function hideOrigin(root = document) { root.querySelectorAll(ORIGIN_SELECTOR).forEach((el) => { if (el.id === CONTAINER_ID || el.closest(`#${CONTAINER_ID}`)) return; el.classList.add("fqa-hide"); }); } function unmount() { observer == null ? void 0 : observer.disconnect(); observer = null; stopTitleWatch == null ? void 0 : stopTitleWatch(); stopTitleWatch = null; app == null ? void 0 : app.unmount(); app = null; container == null ? void 0 : container.remove(); container = null; document.querySelectorAll(ORIGIN_SELECTOR).forEach((el) => { el.classList.remove("fqa-hide"); }); } async function mainHook(_previous) { const path = window.location.pathname; if (!isSearchPath(path) || !settings$1.enhanceSearch) { unmount(); return; } syncFromUrl(path); if (app) { hideOrigin(); return; } injectStyle(); const origin = await waitForElement(ORIGIN_SELECTOR, 8e3); if (!isSearchPath(window.location.pathname)) return; if (app) return; hideOrigin(); mount(origin); observer = new MutationObserver((mutations) => { var _a; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.id === CONTAINER_ID || node.closest(`#${CONTAINER_ID}`)) continue; if ((_a = node.matches) == null ? void 0 : _a.call(node, ORIGIN_SELECTOR)) { node.classList.add("fqa-hide"); } else { hideOrigin(node); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } function mount(origin) { container = document.createElement("div"); container.id = CONTAINER_ID; if (origin == null ? void 0 : origin.parentElement) { origin.insertAdjacentElement("beforebegin", container); } else { const anchor = document.querySelector("#root") ?? document.body; anchor.appendChild(container); } app = vue.createApp(_sfc_main); app.config.errorHandler = (err, _instance, info) => { console.error(`[fqa:search] Vue error (${info}):`, err); }; app.mount(container); stopTitleWatch = vue.watch( routeQuery, (q) => { document.title = q ? `${q} - 搜索 - 番茄小说` : "搜索 - 番茄小说"; }, { immediate: true } ); console.log("[fqa:search] 搜索视图已挂载:", routeQuery.value || "(落地页)"); } function filter(path, _query, _hash) { return isSearchPath(path) || !!app; } async function overloadTitle(_previous) { const searchKey = routeQuery.value || "搜索"; document.title = `${searchKey} - 番茄小说`; } const _exports = [ { id: "searchHook_onload", event: "load", filter, handler: mainHook }, { id: "searchHook_onurlchange", event: "onUrlChange", filter, handler: mainHook }, { id: "searchHook_e", event: "enter", filter, handler: overloadTitle } ]; const hooks = [ ..._exports$4, ..._exports$3, ..._exports$2, ..._exports$1, ..._exports ]; async function onEvent(event, previous) { console.log(`onEvent, event: ${event}, previous: ${previous}`); const path = window.location.pathname; const hash2 = window.location.hash; const params = new URLSearchParams(window.location.search); const tasks = []; for (const hook of hooks) { if (hook.event === event && hook.filter(path, params, hash2)) { tasks.push(async () => { try { await hook.handler(previous); } catch (err) { console.error(`[hook:${hook.id}] handler failed:`, err); } }); } } console.log(`tasks: `, tasks); if (tasks.length > 0) { await Promise.allSettled(tasks.map((task) => task())); } } async function onUrlChange(previous) { return await onEvent("onUrlChange", previous); } async function onHashChange(previous) { return await onEvent("onHashChange", previous); } async function onLoad() { return await onEvent("load"); } async function onEnter() { return await onEvent("enter"); } const win = unsafeWindow; let previousUrl = win.location.href; let previousHash = win.location.hash; function installNavigationHooks() { for (const method of ["pushState", "replaceState"]) { const original = win.history[method]; win.history[method] = function(...args) { const result = original.apply(this, args); console.debug(`history.${method} called with args:`, args); void onUrlChange(previousUrl); previousUrl = win.location.href; return result; }; } win.addEventListener("popstate", () => { void onUrlChange(previousUrl); previousUrl = win.location.href; }); win.addEventListener("hashchange", () => { void onHashChange(previousHash); previousHash = win.location.hash; }); } async function mainInit() { console.log(`================================================`); console.log(`== ${name} - ${version} ==`); console.log(`================================================`); installNavigationHooks(); void onEnter(); initFontDecrypt(); await inject(); initUserStyle(); await ensureDevice(); await init(); void onLoad(); } mainInit(); })(Vue, moment);