Files
fanqie-assistant/dist/fanqie-assistant.user.js
T
2026-08-03 16:18:42 +08:00

4675 lines
195 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name 番茄小说助手
// @namespace https://github.com/naiyQAQ/fanqie-assistant
// @version 0.0.4
// @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 api5-sinfonlinec.jxbhmy.com
// @connect api3-sinfonlinec.jxbhmy.com
// @connect reading.snssdk.com
// @connect i.snssdk.com
// @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}";
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: "",
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 = vue.reactive(normalize(read(STORE_KEY$1)));
let saveTimer;
vue.watch(
settings,
() => {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
saveTimer = void 0;
write(STORE_KEY$1, { ...settings });
}, 200);
},
{ deep: true }
);
function flushSettings() {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = void 0;
}
write(STORE_KEY$1, { ...settings });
}
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.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.decryptFont,
(on) => {
if (on) decryptPage(document);
}
);
}
const STYLE_ID$2 = "fqa-user-style";
const READER_SCOPE = "#fqa-reader-content, .muye-reader-content";
function buildCss() {
const parts = [];
const font = settings.readerFont.trim();
if (font) {
const family = /^["']|,/.test(font) ? font : `"${font}"`;
parts.push(`${READER_SCOPE}, ${READER_SCOPE} p { font-family: ${family}, inherit !important; }`);
}
if (settings.customCssEnabled && settings.customCss.trim()) {
parts.push(settings.customCss);
}
return parts.join("\n");
}
function apply() {
const css = buildCss();
let el = document.getElementById(STYLE_ID$2);
if (!css) {
el == null ? void 0 : el.remove();
return;
}
if (!el) {
el = document.createElement("style");
el.id = STYLE_ID$2;
document.head.appendChild(el);
}
el.textContent = css;
}
function initUserStyle() {
apply();
vue.watch(
() => [settings.readerFont, settings.customCssEnabled, settings.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 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 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",
...extra
});
}
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.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 decryptChapter(encrypted, 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((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, _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 = (num, text) => {
const sup = document.createElement("sup");
sup.className = FOOTNOTE_REF_CLASS;
sup.textContent = String(num);
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, text } of ordered) {
const li = document.createElement("li");
li.id = `fqa-fn-${num}`;
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 num = (_a = sup.textContent) == null ? void 0 : _a.trim();
if (!num) return;
const target = container2.querySelector(`#fqa-fn-${num}`);
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;
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) {
let scriptContainer = document.getElementById("fqa-reader-content");
if (!scriptContainer) {
scriptContainer = cloneElement(readerContainer);
scriptContainer.id = "fqa-reader-content";
scriptContainer.classList.add("fqa");
if (settings.allowCopy) scriptContainer.classList.remove("noselect");
readerContainer.insertAdjacentElement("beforebegin", scriptContainer);
}
scriptContainer.innerHTML = "";
readerContainer.classList.add("fqa-hide");
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) {
let scriptContainer = document.getElementById("fqa-comic-content");
if (!scriptContainer) {
scriptContainer = cloneElement(readerContainer);
scriptContainer.id = "fqa-reader-content";
scriptContainer.classList.add("fqa");
scriptContainer.classList.add("fqa-comic-reader");
if (settings.allowCopy) scriptContainer.classList.remove("noselect");
readerContainer.insertAdjacentElement("beforebegin", scriptContainer);
}
scriptContainer.innerHTML = "";
readerContainer.classList.add("fqa-hide");
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 response = await fetch$1(img.dataset.encryptedUrl);
const encryptedBuffer = await response.arrayBuffer();
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"
}
);
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) {
let scriptContainer = document.getElementById("fqa-comic-content");
if (!scriptContainer) {
scriptContainer = cloneElement(readerContainer);
scriptContainer.id = "fqa-reader-content";
scriptContainer.classList.add("fqa");
scriptContainer.classList.add("fqa-comic-reader");
if (settings.allowCopy) scriptContainer.classList.remove("noselect");
readerContainer.insertAdjacentElement("beforebegin", scriptContainer);
}
scriptContainer.innerHTML = "";
readerContainer.classList.add("fqa-hide");
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$3 = [
{
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.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$2 = [];
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 = '<svg xmlns="http://www.w3.org/2000/svg"\r\n width="24"\r\n height="24"\r\n viewBox="0 0 24 24"\r\n fill="none"\r\n stroke="currentColor"\r\n stroke-width="1.2"\r\n stroke-linecap="round"\r\n stroke-linejoin="round">\r\n <path d="M3.5 20h17"/>\r\n <rect x="5" y="7" width="3.5" height="13" rx="0.8"/>\r\n <rect x="8.5" y="5" width="4" height="15" rx="0.8"/>\r\n <path d="M15.1 6.2 18 5.5l3.1 13.6-2.9.7z"/>\r\n <path d="M9.8 8h1.4M6.1 10h1.3M17 8.8l1.3-.3"/>\r\n</svg>';
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;
}
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;
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.4";
const _hoisted_1$5 = {
class: "fqa-set-dialog",
role: "dialog",
"aria-modal": "true",
"aria-label": "助手设置"
};
const _hoisted_2$5 = { class: "fqa-set-side" };
const _hoisted_3$5 = ["onClick", "onKeydown"];
const _hoisted_4$4 = { class: "fqa-set-main" };
const _hoisted_5$4 = { class: "fqa-set-row" };
const _hoisted_6$4 = { class: "fqa-set-row" };
const _hoisted_7$2 = { class: "fqa-set-row" };
const _hoisted_8$2 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_9$2 = { class: "fqa-set-row" };
const _hoisted_10$2 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_11$2 = ["disabled"];
const _hoisted_12$2 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_13$1 = { class: "fqa-set-radios" };
const _hoisted_14$1 = { class: "fqa-set-radio" };
const _hoisted_15$1 = { class: "fqa-set-radio" };
const _hoisted_16 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_17 = { class: "fqa-set-field" };
const _hoisted_18 = ["placeholder"];
const _hoisted_19 = { class: "fqa-set-field" };
const _hoisted_20 = ["placeholder"];
const _hoisted_21 = { class: "fqa-set-field" };
const _hoisted_22 = ["placeholder"];
const _hoisted_23 = { class: "fqa-set-actions" };
const _hoisted_24 = ["disabled"];
const _hoisted_25 = {
key: 0,
class: "fqa-set-note"
};
const _hoisted_26 = { class: "fqa-set-note" };
const _hoisted_27 = { class: "fqa-set-links" };
const _hoisted_28 = { class: "fqa-set-link-row" };
const _hoisted_29 = ["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$5 = /* @__PURE__ */ vue.defineComponent({
__name: "SettingsView",
emits: ["close"],
setup(__props, { emit: __emit }) {
const emit = __emit;
const SECTIONS = [
{ key: "general", label: "常规" },
{ key: "ui", 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.deviceId = "";
settings.installId = "";
settings.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.deviceId || _config.currentConfig.device_id,
install_id: settings.installId || _config.currentConfig.install_id,
device_type: settings.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$5, [
vue.createElementVNode("button", {
class: "fqa-set-close",
"aria-label": "关闭",
onClick: close
}, "✕"),
vue.createElementVNode("nav", _hoisted_2$5, [
_cache[11] || (_cache[11] = 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$5);
}), 64))
]),
vue.createElementVNode("section", _hoisted_4$4, [
active.value === "general" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
_cache[15] || (_cache[15] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "常规", -1)),
vue.createElementVNode("label", _hoisted_5$4, [
_cache[12] || (_cache[12] = vue.createElementVNode("span", { class: "fqa-set-label" }, "解密网页端混淆字体", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(settings).decryptFont = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings).decryptFont]
])
]),
vue.createElementVNode("label", _hoisted_6$4, [
_cache[13] || (_cache[13] = vue.createElementVNode("span", { class: "fqa-set-label" }, "拦截网页事件上报", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.unref(settings).blockReport = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings).blockReport]
])
]),
vue.createElementVNode("label", _hoisted_7$2, [
_cache[14] || (_cache[14] = vue.createElementVNode("span", { class: "fqa-set-label" }, "允许阅读器复制文本", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => vue.unref(settings).allowCopy = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings).allowCopy]
])
])
], 64)) : active.value === "ui" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
_cache[20] || (_cache[20] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "界面", -1)),
vue.createElementVNode("div", _hoisted_8$2, [
_cache[16] || (_cache[16] = vue.createElementVNode("span", { class: "fqa-set-label" }, "阅读器字体", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => vue.unref(settings).readerFont = $event),
class: "fqa-set-input",
type: "text",
placeholder: "留空表示使用默认字体"
}, null, 512), [
[vue.vModelText, vue.unref(settings).readerFont]
]),
_cache[17] || (_cache[17] = vue.createElementVNode("p", { class: "fqa-set-note" }, "填写字体名称,例如「思源宋体」。留空则跟随网页默认。", -1))
]),
vue.createElementVNode("div", _hoisted_9$2, [
_cache[18] || (_cache[18] = vue.createElementVNode("span", { class: "fqa-set-label" }, "自定义 CSS", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => vue.unref(settings).customCssEnabled = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings).customCssEnabled]
])
]),
vue.createElementVNode("div", _hoisted_10$2, [
vue.withDirectives(vue.createElementVNode("textarea", {
"onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => vue.unref(settings).customCss = $event),
class: "fqa-set-textarea",
disabled: !vue.unref(settings).customCssEnabled,
spellcheck: "false",
placeholder: "/* 自定义 CSS */"
}, null, 8, _hoisted_11$2), [
[vue.vModelText, vue.unref(settings).customCss]
]),
_cache[19] || (_cache[19] = vue.createElementVNode("p", { class: "fqa-set-note" }, "关闭开关后内容会保留,只是不再应用。", -1))
])
], 64)) : active.value === "protocol" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
_cache[30] || (_cache[30] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "协议", -1)),
vue.createElementVNode("div", _hoisted_12$2, [
_cache[23] || (_cache[23] = vue.createElementVNode("span", { class: "fqa-set-label" }, "API 偏好", -1)),
vue.createElementVNode("div", _hoisted_13$1, [
vue.createElementVNode("label", _hoisted_14$1, [
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => vue.unref(settings).apiPreference = $event),
type: "radio",
value: "app"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings).apiPreference]
]),
_cache[21] || (_cache[21] = vue.createElementVNode("span", null, "番茄 APP", -1))
]),
vue.createElementVNode("label", _hoisted_15$1, [
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => vue.unref(settings).apiPreference = $event),
type: "radio",
value: "redcandle"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings).apiPreference]
]),
_cache[22] || (_cache[22] = vue.createElementVNode("span", null, "红烛 APP", -1))
])
]),
_cache[24] || (_cache[24] = vue.createElementVNode("p", { class: "fqa-set-note" }, "如果某协议数据不全,脚本可能会选择其他接口作为补充。", -1))
]),
vue.createElementVNode("div", _hoisted_16, [
_cache[28] || (_cache[28] = vue.createElementVNode("span", { class: "fqa-set-label" }, "设备信息", -1)),
_cache[29] || (_cache[29] = vue.createElementVNode("p", { class: "fqa-set-warn" }, " 如果不知道这是什么,请保持默认。乱填可能导致脚本功能异常。 ", -1)),
vue.createElementVNode("label", _hoisted_17, [
_cache[25] || (_cache[25] = vue.createElementVNode("span", null, "device_id", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[8] || (_cache[8] = ($event) => vue.unref(settings).deviceId = $event),
class: "fqa-set-input",
type: "text",
placeholder: currentDevice.value.device_id || "自动注册"
}, null, 8, _hoisted_18), [
[vue.vModelText, vue.unref(settings).deviceId]
])
]),
vue.createElementVNode("label", _hoisted_19, [
_cache[26] || (_cache[26] = vue.createElementVNode("span", null, "install_id (iid)", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[9] || (_cache[9] = ($event) => vue.unref(settings).installId = $event),
class: "fqa-set-input",
type: "text",
placeholder: currentDevice.value.install_id || "自动注册"
}, null, 8, _hoisted_20), [
[vue.vModelText, vue.unref(settings).installId]
])
]),
vue.createElementVNode("label", _hoisted_21, [
_cache[27] || (_cache[27] = vue.createElementVNode("span", null, "device_type", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[10] || (_cache[10] = ($event) => vue.unref(settings).deviceType = $event),
class: "fqa-set-input",
type: "text",
placeholder: currentDevice.value.device_type || "自动注册"
}, null, 8, _hoisted_22), [
[vue.vModelText, vue.unref(settings).deviceType]
])
]),
vue.createElementVNode("div", _hoisted_23, [
vue.createElementVNode("button", {
class: "fqa-set-btn",
disabled: registering.value,
onClick: reRegister
}, vue.toDisplayString(registering.value ? "注册中…" : "重新注册"), 9, _hoisted_24),
registerMsg.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_25, vue.toDisplayString(registerMsg.value), 1)) : vue.createCommentVNode("", true)
])
])
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
_cache[35] || (_cache[35] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "关于", -1)),
vue.createElementVNode("p", _hoisted_26, "番茄小说助手 v" + vue.toDisplayString(vue.unref(version)), 1),
vue.createElementVNode("div", _hoisted_27, [
vue.createElementVNode("div", { class: "fqa-set-link-row" }, [
_cache[31] || (_cache[31] = 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[32] || (_cache[32] = vue.createElementVNode("span", null, "GitHub 地址:", -1)),
vue.createElementVNode("a", {
href: GITHUB,
target: "_blank",
rel: "noreferrer noopener"
}, "跳转")
]),
vue.createElementVNode("div", _hoisted_28, [
_cache[34] || (_cache[34] = vue.createElementVNode("span", null, "问题反馈:", -1)),
vue.createElementVNode("a", {
href: FEEDBACK,
target: "_blank",
rel: "noreferrer noopener"
}, "GreasyFork"),
vue.createElementVNode("span", null, [
_cache[33] || (_cache[33] = vue.createTextVNode(" 或 ", -1)),
vue.createElementVNode("a", {
href: `${GITHUB}/issues`,
target: "_blank",
rel: "noreferrer noopener"
}, " GitHub Issues ", 8, _hoisted_29)
])
])
]),
_cache[36] || (_cache[36] = 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$1 = "fqa-settings-root";
const STYLE_ID$1 = "fqa-settings-style";
let app$1 = null;
let container$1 = null;
function injectStyle$1() {
if (document.getElementById(STYLE_ID$1)) return;
const style = document.createElement("style");
style.id = STYLE_ID$1;
style.textContent = settingscss;
document.head.appendChild(style);
}
function closeSettings() {
app$1 == null ? void 0 : app$1.unmount();
app$1 = null;
container$1 == null ? void 0 : container$1.remove();
container$1 = null;
}
function openSettings() {
if (app$1) return;
injectStyle$1();
container$1 = document.createElement("div");
container$1.id = CONTAINER_ID$1;
document.body.appendChild(container$1);
app$1 = vue.createApp({
render: () => vue.h(_sfc_main$5, { onClose: closeSettings })
});
app$1.config.errorHandler = (err, _instance, info) => {
console.error(`[fqa:settings] Vue error (${info}):`, err);
};
app$1.mount(container$1);
}
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$1(_previous) {
const userInfo = await getDetailedUserInfo();
if (!userInfo) {
return;
}
const injected = /* @__PURE__ */ new WeakSet();
const inject2 = (menuInner) => {
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("助手设置", void 0, 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$1(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$1 = [
{
id: "userHook",
event: "load",
filter: filter$1,
handler: mainHook$1
},
{
id: "userHook_guest",
event: "load",
filter: guestFilter,
handler: guestHook
}
];
const _hoisted_1$4 = ["aria-label"];
const _hoisted_2$4 = { class: "fqa-cover" };
const _hoisted_3$4 = ["src", "alt"];
const _hoisted_4$3 = {
key: 1,
class: "fqa-cover-progress"
};
const _hoisted_5$3 = ["title"];
const _hoisted_6$3 = ["title"];
const _sfc_main$4 = /* @__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 onEnter(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: onEnter,
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$4, [
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$4),
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$3, [
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$3),
vue.createElementVNode("div", {
class: "fqa-card-sub",
title: detail.value.current_chapter_title
}, vue.toDisplayString(progressText.value), 9, _hoisted_6$3)
], 64))
], 40, _hoisted_1$4);
};
}
});
const _hoisted_1$3 = ["aria-label"];
const _hoisted_2$3 = { class: "fqa-group-cover" };
const _hoisted_3$3 = { class: "fqa-group-grid" };
const _hoisted_4$2 = ["src", "alt"];
const _hoisted_5$2 = ["title"];
const _hoisted_6$2 = { class: "fqa-card-sub" };
const _sfc_main$3 = /* @__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$3, [
vue.createElementVNode("div", _hoisted_3$3, [
(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$2)
]);
}), 128))
])
]),
vue.createElementVNode("div", {
class: "fqa-card-title",
title: __props.group.name
}, vue.toDisplayString(__props.group.name), 9, _hoisted_5$2),
vue.createElementVNode("div", _hoisted_6$2, "共" + vue.toDisplayString(__props.group.books.length) + "本书", 1)
], 40, _hoisted_1$3);
};
}
});
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$2 = ["title"];
const _hoisted_2$2 = {
key: 0,
class: "fqa-hover-author"
};
const _hoisted_3$2 = { class: "fqa-hover-stats" };
const _hoisted_4$1 = ["title"];
const _hoisted_5$1 = { class: "fqa-hover-stat-k" };
const _hoisted_6$1 = { class: "fqa-hover-stat" };
const _hoisted_7$1 = { class: "fqa-hover-stat-v" };
const _hoisted_8$1 = { class: "fqa-hover-stat" };
const _hoisted_9$1 = { class: "fqa-hover-stat-v" };
const _hoisted_10$1 = { class: "fqa-hover-seg" };
const _hoisted_11$1 = { class: "fqa-hover-abstract" };
const _hoisted_12$1 = {
key: 0,
class: "fqa-hover-chapter"
};
const _sfc_main$2 = /* @__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$2),
detail.value.author ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$2, vue.toDisplayString(detail.value.author), 1)) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_3$2, [
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$1),
vue.createElementVNode("div", _hoisted_5$1, vue.toDisplayString(showUpdateTime.value ? "更新于" : "最新章"), 1)
], 32),
vue.createElementVNode("div", _hoisted_6$1, [
vue.createElementVNode("div", _hoisted_7$1, vue.toDisplayString(readAt.value), 1),
_cache[6] || (_cache[6] = vue.createElementVNode("div", { class: "fqa-hover-stat-k" }, "阅读过", -1))
]),
vue.createElementVNode("div", _hoisted_8$1, [
vue.createElementVNode("div", _hoisted_9$1, vue.toDisplayString(addedAt.value), 1),
_cache[7] || (_cache[7] = vue.createElementVNode("div", { class: "fqa-hover-stat-k" }, "已加入书架", -1))
])
]),
vue.createElementVNode("div", _hoisted_10$1, [
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$1, [
activeTab.value === "chapter" && detail.value.current_chapter_title ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_12$1, 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$1 = ["aria-disabled", "onMouseenter", "onClick"];
const _hoisted_2$1 = {
key: 0,
class: "fqa-menu-arrow"
};
const _hoisted_3$1 = ["onClick"];
const MARGIN = 8;
const _sfc_main$1 = /* @__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$1, "›")) : vue.createCommentVNode("", true)
], 42, _hoisted_1$1);
}), 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$1);
}), 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 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 = { id: "fqa-bookshelf" };
const _hoisted_2 = { class: "fqa-bs-header" };
const _hoisted_3 = { class: "fqa-bs-actions" };
const _hoisted_4 = { key: 0 };
const _hoisted_5 = ["disabled"];
const _hoisted_6 = ["aria-selected", "onClick", "onKeydown"];
const _hoisted_7 = { class: "fqa-tab-count" };
const _hoisted_8 = {
key: 0,
class: "fqa-groupbar"
};
const _hoisted_9 = { class: "fqa-groupbar-name" };
const _hoisted_10 = { class: "fqa-groupbar-count" };
const _hoisted_11 = {
key: 1,
class: "fqa-status"
};
const _hoisted_12 = {
key: 2,
class: "fqa-status"
};
const _hoisted_13 = { class: "fqa-status-title" };
const _hoisted_14 = { class: "fqa-grid" };
const _hoisted_15 = {
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 = /* @__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, [
vue.createElementVNode("div", _hoisted_2, [
_cache[1] || (_cache[1] = vue.createElementVNode("div", { class: "fqa-bs-title" }, "我的书架", -1)),
vue.createElementVNode("div", _hoisted_3, [
vue.unref(detailLoading) ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4, "正在补全详情…")) : vue.createCommentVNode("", true),
vue.createElementVNode("button", {
class: "fqa-btn",
disabled: vue.unref(loading),
onClick: refresh
}, vue.toDisplayString(vue.unref(loading) ? "刷新中…" : "刷新"), 9, _hoisted_5)
])
]),
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, vue.toDisplayString(vue.unref(counts)[tab.key]), 1)
], 42, _hoisted_6);
}), 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, [
vue.createElementVNode("button", {
class: "fqa-btn",
onClick: backToList
}, "← 返回"),
vue.createElementVNode("span", _hoisted_9, vue.toDisplayString(openedGroup.value.name), 1),
vue.createElementVNode("span", _hoisted_10, "共" + vue.toDisplayString(openedGroup.value.books.length) + "本书", 1)
])) : vue.createCommentVNode("", true),
vue.unref(error) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_11, [
_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, [
vue.createElementVNode("div", _hoisted_13, vue.toDisplayString(emptyText.value), 1)
])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
vue.createElementVNode("div", _hoisted_14, [
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$4, {
key: 0,
entry: cell.entry,
onHover: onCardHover,
onLeave: scheduleHide,
onOpen: openBook,
onVisible: onCardVisible,
onContextmenu: onCardContextMenu
}, null, 8, ["entry"])) : (vue.openBlock(), vue.createBlock(_sfc_main$3, {
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$2, {
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$1, {
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, 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#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@keyframes fqa-shimmer {\n 100% {\n transform: translateX(100%);\n }\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/* ----------------------------- 右键菜单 ----------------------------- */\n\n/*\n * 菜单 Teleport 到 body,拿不到 #fqa-bookshelf 上的变量,\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@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}\n";
const CONTAINER_ID = "fqa-bookshelf-root";
const STYLE_ID = "fqa-bookshelf-style";
const ORIGIN_SELECTOR = ".muye-bookshelf, .muye-bookshelf-home-page, .bookshelf-tabs";
let app = null;
let container = null;
let observer = null;
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = bookshelfcss;
document.head.appendChild(style);
}
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 isBookshelfPath(path) {
return path.startsWith("/bookshelf");
}
function unmount() {
var _a;
observer == null ? void 0 : observer.disconnect();
observer = null;
app == null ? void 0 : app.unmount();
app = null;
container == null ? void 0 : container.remove();
container = null;
(_a = document.getElementById("fqa-bookshelf-hover")) == null ? void 0 : _a.remove();
document.querySelectorAll(ORIGIN_SELECTOR).forEach((el) => {
el.classList.remove("fqa-hide");
});
}
async function mainHook(_previous) {
if (!isBookshelfPath(window.location.pathname)) {
unmount();
return;
}
if (app) {
hideOrigin();
return;
}
injectStyle();
const origin = await waitForElement(ORIGIN_SELECTOR);
if (!isBookshelfPath(window.location.pathname)) return;
if (app) return;
hideOrigin();
container = document.createElement("div");
container.id = CONTAINER_ID;
const anchor = origin ?? document.querySelector("#root") ?? document.body;
if (origin == null ? void 0 : origin.parentElement) {
origin.insertAdjacentElement("beforebegin", container);
} else {
anchor.appendChild(container);
}
app = vue.createApp(_sfc_main);
app.config.errorHandler = (err, _instance, info) => {
console.error(`[fqa:bookshelf] Vue error (${info}):`, err);
};
app.mount(container);
console.log("[fqa:bookshelf] 书架视图已挂载");
document.title = "我的书架 - 番茄小说";
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 filter(path, _query, _hash) {
return (isBookshelfPath(path) || !!app) && userState.isLogin;
}
const _exports = [
{
id: "bookshelfHook_onload",
event: "load",
filter,
handler: mainHook
},
{
id: "bookshelfHook_onurlchange",
event: "onUrlChange",
filter,
handler: mainHook
}
];
const hooks = [
..._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");
}
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();
initFontDecrypt();
await inject();
initUserStyle();
await ensureDevice();
await init();
void onLoad();
}
mainInit();
})(Vue, moment);