diff --git a/dist/fanqie-assistant.user.js b/dist/fanqie-assistant.user.js
index 94d50c5..9b53e63 100644
--- a/dist/fanqie-assistant.user.js
+++ b/dist/fanqie-assistant.user.js
@@ -133,6 +133,8 @@
decryptFont: true,
blockReport: true,
allowCopy: true,
+ shelfRemoveConfirm: true,
+ logoutConfirm: true,
readerFont: "",
customCssEnabled: false,
customCss: "",
@@ -1379,6 +1381,7 @@
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";
+ const APP_REQUEST = { credentials: "omit" };
function buildQuery(device, versionCode, extra) {
return new URLSearchParams({
device_id: device.device_id,
@@ -1399,6 +1402,7 @@
new TextEncoder().encode(JSON.stringify(body)).buffer
);
const res = await apiFetch(REGISTER_URL, {
+ ...APP_REQUEST,
method: "POST",
headers: {
"User-Agent": "okhttp/4.10.0",
@@ -1431,6 +1435,7 @@
try {
const headers = await signRequest(url, body);
const res = await apiFetch(url, {
+ ...APP_REQUEST,
method: "POST",
headers: {
...headers,
@@ -1475,6 +1480,7 @@
const gzipped = await gzip(plainBody);
const headers = await signRequest(url, gzipped);
const res = await apiFetch(url, {
+ ...APP_REQUEST,
method: "POST",
headers: {
...headers,
@@ -1973,7 +1979,7 @@
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 [null, null];
}
return [items, items.map((it) => String(it.item_id))];
}
@@ -1983,24 +1989,26 @@
const rj = response.json();
const d = rj.data;
const allItems = d.allItemIds;
- const volmap = {};
+ const cs = [];
const vname = d.volumeNameList;
for (let i2 = 0; i2 < vname.length; i2++) {
const volumeName = vname[i2];
if (volumeName !== void 0) {
- volmap[volumeName] = d.chapterListWithVolume[i2];
+ cs.push(...d.chapterListWithVolume[i2]);
}
}
- return [volmap, allItems];
+ return [cs, allItems];
}
async function getCatalog(bookId2) {
const r = await getCatalogRaw(bookId2);
let catalogRaw = r[0];
let allItemIds = r[1];
+ console.log("catalogRaw", catalogRaw, "allItemIds", allItemIds);
if (!catalogRaw || !allItemIds) {
const rw = await webCatalog(bookId2);
catalogRaw = rw[0];
allItemIds = rw[1];
+ console.log("webCatalog", catalogRaw, "allItemIds", allItemIds);
}
const vmap = {};
const chapters = [];
@@ -4557,9 +4565,12 @@
color: var(--fqa-audio-text);
}
-/* 页面切到夜间模式时整体反色 */
-.muye-reader-dark .fqa-audio-root,
-.fqa-audio-root.fqa-audio-dark {
+/*
+ * 暗色反色。悬浮栏挂在 body 下,够不到 .muye-reader-dark 的后代选择器,
+ * 所以 audioPanel 在 #fqa-audio-root 容器上打 .fqa-audio-dark 类;
+ * 真正的 .fqa-audio-root(Vue 根)是容器的子节点,用后代选择器作用到它。
+ */
+.fqa-audio-dark .fqa-audio-root {
--fqa-audio-bg: #2b2b2b;
--fqa-audio-text: #b3b3b3;
--fqa-audio-sub: #7a7a7a;
@@ -4939,10 +4950,547 @@
};
app$4.mount(container$4);
}
+ 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(bookId2, modifyTime = 0) {
+ return {
+ asterisked: false,
+ book_id: bookId2,
+ book_type: 0,
+ modify_time: modifyTime
+ };
+ }
+ async function shelfPost(path, body) {
+ const res = await fetch$1(`${SHELF_BASE}${path}/v?aid=1967`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(body)
+ });
+ const json = await res.json().catch(() => null);
+ if (!res.ok || json && json.code !== 0 && json.code !== void 0) {
+ throw new Error(`书架操作失败(${path}): ${(json == null ? void 0 : json.message) ?? res.status}`);
+ }
+ return json;
+ }
+ async function addToBookshelf(bookId2) {
+ await shelfPost("/add", {
+ add_book_source: 0,
+ identify_data: [identify(bookId2)]
+ });
+ }
+ async function removeFromBookshelf(bookId2) {
+ await shelfPost("/delete", {
+ identify_data: [identify(bookId2, Date.now())]
+ });
+ }
+ async function moveToGroup(bookId2, groupName) {
+ await shelfPost("/update", {
+ book_data: [
+ {
+ asterisked: false,
+ book_id: bookId2,
+ book_type: 0,
+ group_name: groupName,
+ has_shown: false,
+ is_pin: false,
+ modify_time: Date.now()
+ }
+ ]
+ });
+ }
+ async function isInBookshelf(book_id) {
+ const response = await fetch$1(`https://fanqienovel.com/reading/bookapi/bookshelf/check/v:version/?aid=1967&iid=0&version_code=57700&update_version_code=57700&book_id=${book_id}`);
+ const data = await response.json();
+ return Boolean(data.data);
+ }
+ function addResponseModifier(modifier) {
+ modifiers.push(modifier);
+ return () => {
+ const i2 = modifiers.indexOf(modifier);
+ if (i2 >= 0) modifiers.splice(i2, 1);
+ };
+ }
+ const modifiers = [];
+ const blackList = [
+ "mcs.zijieapi.com",
+ "vcs.zijieapi.com/vc/setting",
+ "mon.zijieapi.com",
+ "mssdk.bytedance.com/web/common",
+ "hm.baidu.com"
+ ];
+ const BLOCKED_BODY = JSON.stringify({
+ e: 0,
+ sc: 10,
+ tc: 10
+ });
+ function checkBlack(url) {
+ if (!settings$1.blockReport) return false;
+ return blackList.some((black) => url.includes(black));
+ }
+ function toLocalArrayBuffer(buf) {
+ const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
+ const out = new ArrayBuffer(bytes.byteLength);
+ new Uint8Array(out).set(bytes);
+ return out;
+ }
+ function textToBody(text) {
+ return toLocalArrayBuffer(new TextEncoder().encode(text));
+ }
+ function bodyToText(body) {
+ if (typeof body === "string") return body;
+ return new TextDecoder().decode(body);
+ }
+ function bodyToBuffer(body) {
+ if (typeof body === "string") return textToBody(body);
+ return body;
+ }
+ function resolveSimple(simple) {
+ return { ...simple, responseBody: bodyToBuffer(simple.responseBody) };
+ }
+ function matchingModifiers(url) {
+ return modifiers.filter((m) => m.matcher(url));
+ }
+ function applyModifiers(url, matched, simple) {
+ let current = simple;
+ for (const m of matched) {
+ if (m.modify_response) current = m.modify_response(url, current);
+ }
+ return current;
+ }
+ function responseAllowsBody(status) {
+ return status !== 204 && status !== 205 && status !== 304;
+ }
+ function simpleToResponse(simple) {
+ return new Response(responseAllowsBody(simple.statusCode) ? simple.responseBody : null, {
+ status: simple.statusCode,
+ headers: simple.responseHeaders
+ });
+ }
+ async function responseToSimple(res) {
+ const body = toLocalArrayBuffer(await res.arrayBuffer());
+ const headers = {};
+ res.headers.forEach((value, key) => {
+ headers[key] = value;
+ });
+ return { responseBody: body, statusCode: res.status, responseHeaders: headers };
+ }
+ const originalFetch = unsafeWindow.fetch.bind(unsafeWindow);
+ unsafeWindow.fetch = async 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 new Response(BLOCKED_BODY, {
+ status: 200,
+ headers: { "Content-Type": "application/json" }
+ });
+ }
+ const matched = matchingModifiers(url);
+ if (matched.length === 0) return originalFetch(input, init2);
+ const breaker = matched.find((m) => m.should_break(url));
+ if (breaker == null ? void 0 : breaker.make_response) {
+ return simpleToResponse(breaker.make_response(url));
+ }
+ if (!matched.some((m) => m.modify_response)) return originalFetch(input, init2);
+ const res = await originalFetch(input, init2);
+ try {
+ const simple = await responseToSimple(res.clone());
+ return simpleToResponse(applyModifiers(url, matched, simple));
+ } catch (error) {
+ console.error("[fqa:fetch] 修改响应失败,回退原始响应:", error);
+ return res;
+ }
+ };
+ const originalXMLHttpRequest = unsafeWindow.XMLHttpRequest;
+ const XHR_PROTO = originalXMLHttpRequest.prototype;
+ const XHR_DESC = {
+ status: Object.getOwnPropertyDescriptor(XHR_PROTO, "status"),
+ responseText: Object.getOwnPropertyDescriptor(XHR_PROTO, "responseText"),
+ response: Object.getOwnPropertyDescriptor(XHR_PROTO, "response")
+ };
+ const XHR_GETALL = XHR_PROTO.getAllResponseHeaders;
+ const XHR_GETONE = XHR_PROTO.getResponseHeader;
+ function readXhrHeaders(self) {
+ const headers = {};
+ const all = XHR_GETALL.call(self);
+ if (!all) return headers;
+ for (const line of all.trim().split(/[\r\n]+/)) {
+ const idx = line.indexOf(":");
+ if (idx > 0) {
+ headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
+ }
+ }
+ return headers;
+ }
+ function readXhrBody(self) {
+ const type = self.responseType;
+ if (type === "arraybuffer") {
+ return toLocalArrayBuffer(XHR_DESC.response.get.call(self));
+ }
+ if (type === "json") {
+ const parsed = XHR_DESC.response.get.call(self);
+ return textToBody(JSON.stringify(parsed));
+ }
+ return textToBody(XHR_DESC.responseText.get.call(self));
+ }
+ unsafeWindow.XMLHttpRequest = class XMLHttpRequest extends originalXMLHttpRequest {
+ constructor() {
+ super(...arguments);
+ __publicField(this, "_blockedUrl");
+ __publicField(this, "_fxaUrl", "");
+ __publicField(this, "_fxaMatched", null);
+ __publicField(this, "_fxaBreaker", null);
+ __publicField(this, "_fxaApplied", false);
+ __publicField(this, "_fxaSimple", null);
+ }
+ open(method, url, async = true, user, password) {
+ this._fxaUrl = url;
+ this._fxaMatched = null;
+ this._fxaBreaker = null;
+ this._fxaApplied = false;
+ this._fxaSimple = null;
+ if (checkBlack(url)) {
+ console.log("blocked request: " + url);
+ this._blockedUrl = url;
+ return;
+ }
+ this._blockedUrl = void 0;
+ const matched = matchingModifiers(url);
+ const breaker = matched.find((m) => m.should_break(url));
+ if (breaker == null ? void 0 : breaker.make_response) {
+ this._fxaBreaker = breaker;
+ this._fxaMatched = matched;
+ } else if (matched.some((m) => m.modify_response)) {
+ this._fxaMatched = matched;
+ }
+ super.open(method, url, async, user, password);
+ }
+ setRequestHeader(name2, value) {
+ if (this._blockedUrl !== void 0) return;
+ super.setRequestHeader(name2, value);
+ }
+ send(body) {
+ var _a, _b;
+ if (this._blockedUrl !== void 0) {
+ this._synthesizeFromSimple({
+ responseBody: textToBody(BLOCKED_BODY),
+ statusCode: 200,
+ responseHeaders: { "content-type": "application/json" }
+ });
+ return;
+ }
+ if ((_a = this._fxaBreaker) == null ? void 0 : _a.make_response) {
+ this._synthesizeFromSimple(this._fxaBreaker.make_response(this._fxaUrl));
+ return;
+ }
+ if ((_b = this._fxaMatched) == null ? void 0 : _b.length) this._armTransform();
+ super.send(body);
+ }
+ 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);
+ }
+ /**
+ * 中断请求后合成一个假响应并手动派发完成事件。
+ * 与 fetch 的 make_response 对齐,也复用了黑名单那条原本的 setTimeout 派发逻辑。
+ */
+ _synthesizeFromSimple(raw) {
+ const url = this._blockedUrl ?? this._fxaUrl;
+ const simple = resolveSimple(raw);
+ const headers = simple.responseHeaders;
+ const shadow = (prop, value) => Object.defineProperty(this, prop, { configurable: true, get: () => value });
+ setTimeout(() => {
+ const text = bodyToText(simple.responseBody);
+ shadow("readyState", 4);
+ shadow("status", simple.statusCode);
+ shadow("statusText", "OK");
+ shadow("responseURL", url);
+ const type = this.responseType;
+ shadow("responseText", type === "" || type === "text" ? text : "");
+ let value;
+ if (type === "json") {
+ try {
+ value = JSON.parse(text);
+ } catch {
+ value = text;
+ }
+ } else if (type === "arraybuffer") {
+ value = simple.responseBody;
+ } else if (type === "blob") {
+ value = new Blob([simple.responseBody]);
+ } else {
+ value = text;
+ }
+ shadow("response", value);
+ shadow(
+ "getAllResponseHeaders",
+ () => Object.entries(headers).map(([k, v]) => `${k}: ${v}\r
+`).join("")
+ );
+ shadow("getResponseHeader", (name2) => {
+ const lower = name2.toLowerCase();
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === lower);
+ return key ? headers[key] : null;
+ });
+ this.dispatchEvent(new Event("readystatechange"));
+ this.dispatchEvent(new ProgressEvent("load"));
+ this.dispatchEvent(new ProgressEvent("loadend"));
+ }, 0);
+ }
+ /**
+ * 在实例上挂影子属性:等真实响应就绪(readyState=4),
+ * 页面第一次读取时才同步套用 modify_response 并缓存结果。
+ * 这样不用跟页面的 onload / onreadystatechange 争先后顺序。
+ */
+ _armTransform() {
+ const type = this.responseType;
+ if (type === "blob" || type === "document") return;
+ const self = this;
+ const matched = this._fxaMatched;
+ const url = this._fxaUrl;
+ const ensure = () => {
+ if (self._fxaApplied || self.readyState !== 4) return;
+ self._fxaSimple = resolveSimple(applyModifiers(url, matched, {
+ responseBody: readXhrBody(self),
+ statusCode: XHR_DESC.status.get.call(self),
+ responseHeaders: readXhrHeaders(self)
+ }));
+ self._fxaApplied = true;
+ };
+ Object.defineProperty(this, "status", {
+ configurable: true,
+ get() {
+ ensure();
+ if (self._fxaApplied) return self._fxaSimple.statusCode;
+ return XHR_DESC.status.get.call(self);
+ }
+ });
+ Object.defineProperty(this, "responseText", {
+ configurable: true,
+ get() {
+ ensure();
+ const t = self.responseType;
+ if (!self._fxaApplied || t !== "" && t !== "text") {
+ return XHR_DESC.responseText.get.call(self);
+ }
+ return bodyToText(self._fxaSimple.responseBody);
+ }
+ });
+ Object.defineProperty(this, "response", {
+ configurable: true,
+ get() {
+ ensure();
+ if (!self._fxaApplied) return XHR_DESC.response.get.call(self);
+ const body = self._fxaSimple.responseBody;
+ switch (self.responseType) {
+ case "":
+ case "text":
+ return bodyToText(body);
+ case "json":
+ return JSON.parse(bodyToText(body));
+ case "arraybuffer":
+ return body;
+ default:
+ return XHR_DESC.response.get.call(self);
+ }
+ }
+ });
+ this.getAllResponseHeaders = function getAllResponseHeaders() {
+ ensure();
+ if (!self._fxaApplied) return XHR_GETALL.call(self);
+ return Object.entries(self._fxaSimple.responseHeaders).map(([k, v]) => `${k}: ${v}\r
+`).join("");
+ };
+ this.getResponseHeader = function getResponseHeader(name2) {
+ ensure();
+ if (!self._fxaApplied) return XHR_GETONE.call(self, name2);
+ const lower = name2.toLowerCase();
+ const key = Object.keys(self._fxaSimple.responseHeaders).find((k) => k.toLowerCase() === lower);
+ return key ? self._fxaSimple.responseHeaders[key] : null;
+ };
+ }
+ };
+ const _exports$5 = [];
+ 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;
+ 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 (j.code !== -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);
+ }
+ }
let currentBook = null;
let latestItemId = null;
+ let currentChapterWithContent = null;
const SCRIPT_CONTAINER_ID = "fqa-reader-content";
let comicObserver = null;
+ addResponseModifier({
+ matcher: (url) => url.indexOf("/reading/bookapi/bookshelf/check/") !== -1 && location.pathname.startsWith("/reader"),
+ should_break: () => true,
+ make_response: () => {
+ const body = JSON.stringify({
+ code: 0,
+ message: "SUCCESS",
+ data: 0
+ });
+ return {
+ responseBody: body,
+ statusCode: 200,
+ responseHeaders: {
+ "content-type": "application/json; charset=utf-8",
+ "content-length": String(new TextEncoder().encode(body).length)
+ }
+ };
+ }
+ });
function ensureScriptContainer(readerContainer, comic) {
let scriptContainer = document.getElementById(SCRIPT_CONTAINER_ID);
if (!scriptContainer) {
@@ -4960,7 +5508,7 @@
return scriptContainer;
}
async function insertContent() {
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
const itemId2 = ((_a = window.location.pathname.split("/").pop()) == null ? void 0 : _a.substring(0, 19)) || "";
if (!itemId2) {
console.warn("No item_id found in URL");
@@ -4977,6 +5525,7 @@
return;
}
console.log("Chapter:", chapter);
+ currentChapterWithContent = 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") {
@@ -5121,15 +5670,36 @@
}
}
let updateTimeSpans = (muyeReaderSubtitle == null ? void 0 : muyeReaderSubtitle.querySelectorAll("span.desc-item")) || [];
- if (updateTimeSpans.length >= 2) {
+ console.log("spans len", updateTimeSpans.length, "assertIsOffshelf", updateTimeSpans.length < 3);
+ if (updateTimeSpans.length >= 3) {
+ console.log("if");
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");
+ console.log("else");
+ (_k = updateTimeSpans[updateTimeSpans.length - 1]) == null ? void 0 : _k.remove();
+ const updateTimeSpan = document.createElement("span");
updateTimeSpan.className = "desc-item";
updateTimeSpan.textContent = `更新时间:${currentChapterItem.update_time}`;
+ console.log("assert equal", currentChapterItem.item_id === ((_l = currentChapterWithContent == null ? void 0 : currentChapterWithContent.novel_data) == null ? void 0 : _l.item_id));
+ console.log("wordcnt", (_m = currentChapterWithContent == null ? void 0 : currentChapterWithContent.novel_data) == null ? void 0 : _m.chapter_word_number);
+ const c = document.getElementById("fqa-current-chapter-volume");
+ let b = null;
+ if (currentChapterItem.item_id === ((_n = currentChapterWithContent == null ? void 0 : currentChapterWithContent.novel_data) == null ? void 0 : _n.item_id)) {
+ const wordCntSpan = document.createElement("span");
+ wordCntSpan.className = "desc-item";
+ wordCntSpan.textContent = `本章字数:${(_o = currentChapterWithContent == null ? void 0 : currentChapterWithContent.novel_data) == null ? void 0 : _o.chapter_word_number}字`;
+ console.log(wordCntSpan);
+ if (c) {
+ console.log("insert wordcnt");
+ c.insertAdjacentElement("afterend", wordCntSpan);
+ }
+ b = wordCntSpan;
+ }
+ console.log("insert update time");
+ (_p = b || c) == null ? void 0 : _p.insertAdjacentElement("afterend", updateTimeSpan);
}
}
}
@@ -5163,6 +5733,13 @@
}
async function onLoad$1() {
var _a;
+ async function fetchBookInfo() {
+ var _a2, _b;
+ const pageState = unsafeWindow.__INITIAL_STATE__;
+ const bid = (_b = (_a2 = pageState == null ? void 0 : pageState.reader) == null ? void 0 : _a2.chapterData) == null ? void 0 : _b.bookId;
+ currentBook = await getBookInfoAndCatalog(bid);
+ }
+ void fetchBookInfo();
const toolbar = document.querySelector("div.reader-toolbar > div");
const toolbarButton = document.querySelector("div.reader-toolbar > div > div:nth-child(3)");
if (toolbarButton && toolbar) {
@@ -5184,13 +5761,53 @@
c.addEventListener("click", () => void startAudioPlay());
toolbar.appendChild(c);
}
+ const bookshelfButton = document.querySelector("div.reader-toolbar-item");
+ if (userState.isLogin && bookshelfButton && bookshelfButton.innerHTML.includes("书架")) {
+ const shelf = cloneElement(bookshelfButton);
+ bookshelfButton.replaceWith(shelf);
+ let inShelf = false;
+ let ready = false;
+ let busy = false;
+ async function ensureBookshelfState() {
+ while (!currentBook) {
+ await sleep(50);
+ }
+ inShelf = await isInBookshelf(currentBook.book_id);
+ shelf.classList.toggle("reader-toolbar-item-disabled", inShelf);
+ const label = shelf.lastChild;
+ if (label) label.textContent = inShelf ? "已在书架" : "加入书架";
+ ready = true;
+ }
+ void ensureBookshelfState();
+ shelf.addEventListener("click", async () => {
+ const book = currentBook;
+ if (!ready || busy || !book) return;
+ busy = true;
+ try {
+ if (inShelf) {
+ if (settings$1.shelfRemoveConfirm) {
+ if (!unsafeWindow.confirm(`确定要把《${book.title ?? "这本书"}》从书架移出吗?`)) return;
+ }
+ await removeFromBookshelf(book.book_id);
+ } else {
+ await addToBookshelf(book.book_id);
+ }
+ await ensureBookshelfState();
+ } catch (error) {
+ console.error("[fqa:reader] 书架操作失败:", error);
+ unsafeWindow.alert(error instanceof Error ? error.message : "书架操作失败");
+ } finally {
+ busy = false;
+ }
+ });
+ }
document.querySelector("div.muye-reader-btns");
await insertContent();
}
function readerFilter(path, _query, _hash) {
return path.startsWith("/reader") || path.startsWith("reader");
}
- const _exports$5 = [
+ const _exports$4 = [
{
id: "readerHook_load",
event: "load",
@@ -5210,161 +5827,8 @@
filter: readerFilter
}
];
- const blackList = [
- "mcs.zijieapi.com",
- "vcs.zijieapi.com/vc/setting",
- "mon.zijieapi.com",
- "mssdk.bytedance.com/web/common",
- "hm.baidu.com"
- ];
- const BLOCKED_BODY = JSON.stringify({
- e: 0,
- sc: 10,
- tc: 10
- });
- function checkBlack(url) {
- if (!settings$1.blockReport) return false;
- return blackList.some((black) => url.includes(black));
- }
- const originalFetch = unsafeWindow.fetch.bind(unsafeWindow);
- unsafeWindow.fetch = function fetch2(input, init2) {
- let url;
- if (input instanceof Request) {
- url = input.url;
- } else if (input instanceof URL) {
- url = input.href;
- } else {
- url = input;
- }
- if (checkBlack(url)) {
- console.log("blocked request: " + url);
- return Promise.resolve(new Response(BLOCKED_BODY, {
- status: 200,
- headers: { "Content-Type": "application/json" }
- }));
- }
- return originalFetch(input, init2);
- };
- const originalXMLHttpRequest = unsafeWindow.XMLHttpRequest;
- unsafeWindow.XMLHttpRequest = class XMLHttpRequest extends originalXMLHttpRequest {
- constructor() {
- super(...arguments);
- __publicField(this, "_blockedUrl");
- }
- open(method, url, async = true, user, password) {
- if (checkBlack(url)) {
- console.log("blocked request: " + url);
- this._blockedUrl = url;
- return;
- }
- this._blockedUrl = void 0;
- super.open(method, url, async, user, password);
- }
- setRequestHeader(name2, value) {
- if (this._blockedUrl !== void 0) return;
- super.setRequestHeader(name2, value);
- }
- send(body) {
- if (this._blockedUrl === void 0) {
- super.send(body);
- return;
- }
- const url = this._blockedUrl;
- const shadow = (prop, value) => Object.defineProperty(this, prop, { configurable: true, get: () => value });
- setTimeout(() => {
- shadow("readyState", 4);
- shadow("status", 200);
- shadow("statusText", "OK");
- shadow("responseURL", url);
- shadow("responseText", this.responseType === "" || this.responseType === "text" ? BLOCKED_BODY : "");
- shadow("response", this.responseType === "json" ? {
- "e": 0,
- "sc": 10,
- "tc": 10
- } : BLOCKED_BODY);
- this.dispatchEvent(new Event("readystatechange"));
- this.dispatchEvent(new ProgressEvent("load"));
- this.dispatchEvent(new ProgressEvent("loadend"));
- }, 0);
- }
- abort() {
- if (this._blockedUrl !== void 0) return;
- super.abort();
- }
- getAllResponseHeaders() {
- if (this._blockedUrl !== void 0) return "content-type: application/json\r\n";
- return super.getAllResponseHeaders();
- }
- getResponseHeader(name2) {
- if (this._blockedUrl !== void 0) {
- return name2.toLowerCase() === "content-type" ? "application/json" : null;
- }
- return super.getResponseHeader(name2);
- }
- };
- const _exports$4 = [];
- 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 = '';
- const settings = '\r\n';
+ const settings = '\r\n';
const name = "fanqie-assistant";
const version = "0.0.6";
const _hoisted_1$9 = {
@@ -5379,72 +5843,74 @@
const _hoisted_5$8 = { class: "fqa-set-row" };
const _hoisted_6$8 = { class: "fqa-set-row" };
const _hoisted_7$6 = { class: "fqa-set-row" };
- const _hoisted_8$6 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_8$6 = { class: "fqa-set-row" };
const _hoisted_9$6 = { class: "fqa-set-row" };
const _hoisted_10$6 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_11$5 = ["disabled"];
+ const _hoisted_11$5 = { class: "fqa-set-row" };
const _hoisted_12$5 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_13$4 = { class: "fqa-set-radios" };
- const _hoisted_14$4 = { class: "fqa-set-radio" };
- const _hoisted_15$2 = { class: "fqa-set-radio" };
- const _hoisted_16$1 = { class: "fqa-set-row" };
- const _hoisted_17$1 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_18$1 = {
- class: "fqa-set-row",
- style: { "padding-top": "0", "border-bottom": "none" }
- };
- const _hoisted_19$1 = { class: "fqa-set-row" };
- const _hoisted_20 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_21 = { class: "fqa-set-radios" };
- const _hoisted_22 = { class: "fqa-set-radio" };
- const _hoisted_23 = { class: "fqa-set-radio" };
- const _hoisted_24 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_25 = { class: "fqa-set-radios" };
- const _hoisted_26 = { class: "fqa-set-radio" };
- const _hoisted_27 = { class: "fqa-set-radio" };
- const _hoisted_28 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_29 = {
- class: "fqa-set-row",
- style: { "padding-top": "0", "border-bottom": "none" }
- };
- const _hoisted_30 = {
+ const _hoisted_13$4 = ["disabled"];
+ const _hoisted_14$4 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_15$2 = { class: "fqa-set-radios" };
+ const _hoisted_16$1 = { class: "fqa-set-radio" };
+ const _hoisted_17$1 = { class: "fqa-set-radio" };
+ const _hoisted_18$1 = { class: "fqa-set-row" };
+ const _hoisted_19$1 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_20 = {
class: "fqa-set-row",
style: { "padding-top": "0", "border-bottom": "none" }
};
+ const _hoisted_21 = { class: "fqa-set-row" };
+ const _hoisted_22 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_23 = { class: "fqa-set-radios" };
+ const _hoisted_24 = { class: "fqa-set-radio" };
+ const _hoisted_25 = { class: "fqa-set-radio" };
+ const _hoisted_26 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_27 = { class: "fqa-set-radios" };
+ const _hoisted_28 = { class: "fqa-set-radio" };
+ const _hoisted_29 = { class: "fqa-set-radio" };
+ const _hoisted_30 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_31 = {
class: "fqa-set-row",
style: { "padding-top": "0", "border-bottom": "none" }
};
- const _hoisted_32 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_33 = { class: "fqa-set-field" };
- const _hoisted_34 = { class: "fqa-set-field" };
+ const _hoisted_32 = {
+ class: "fqa-set-row",
+ style: { "padding-top": "0", "border-bottom": "none" }
+ };
+ const _hoisted_33 = {
+ class: "fqa-set-row",
+ style: { "padding-top": "0", "border-bottom": "none" }
+ };
+ const _hoisted_34 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_35 = { class: "fqa-set-field" };
- const _hoisted_36 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_37 = { class: "fqa-set-radios" };
- const _hoisted_38 = { class: "fqa-set-radio" };
- const _hoisted_39 = { class: "fqa-set-radio" };
- const _hoisted_40 = { class: "fqa-set-row" };
- const _hoisted_41 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_42 = { class: "fqa-set-radios" };
- const _hoisted_43 = { class: "fqa-set-radio" };
- const _hoisted_44 = { class: "fqa-set-radio" };
- const _hoisted_45 = { class: "fqa-set-row fqa-set-row-col" };
- const _hoisted_46 = { class: "fqa-set-field" };
- const _hoisted_47 = ["placeholder"];
+ const _hoisted_36 = { class: "fqa-set-field" };
+ const _hoisted_37 = { class: "fqa-set-field" };
+ const _hoisted_38 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_39 = { class: "fqa-set-radios" };
+ const _hoisted_40 = { class: "fqa-set-radio" };
+ const _hoisted_41 = { class: "fqa-set-radio" };
+ const _hoisted_42 = { class: "fqa-set-row" };
+ const _hoisted_43 = { class: "fqa-set-row fqa-set-row-col" };
+ const _hoisted_44 = { class: "fqa-set-radios" };
+ const _hoisted_45 = { class: "fqa-set-radio" };
+ const _hoisted_46 = { class: "fqa-set-radio" };
+ const _hoisted_47 = { class: "fqa-set-row fqa-set-row-col" };
const _hoisted_48 = { class: "fqa-set-field" };
const _hoisted_49 = ["placeholder"];
const _hoisted_50 = { class: "fqa-set-field" };
const _hoisted_51 = ["placeholder"];
- const _hoisted_52 = { class: "fqa-set-actions" };
- const _hoisted_53 = ["disabled"];
- const _hoisted_54 = {
+ const _hoisted_52 = { class: "fqa-set-field" };
+ const _hoisted_53 = ["placeholder"];
+ const _hoisted_54 = { class: "fqa-set-actions" };
+ const _hoisted_55 = ["disabled"];
+ const _hoisted_56 = {
key: 0,
class: "fqa-set-note"
};
- const _hoisted_55 = { class: "fqa-set-note" };
- const _hoisted_56 = { class: "fqa-set-links" };
- const _hoisted_57 = { class: "fqa-set-link-row" };
- const _hoisted_58 = ["href"];
+ const _hoisted_57 = { class: "fqa-set-note" };
+ const _hoisted_58 = { class: "fqa-set-links" };
+ const _hoisted_59 = { class: "fqa-set-link-row" };
+ const _hoisted_60 = ["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$9 = /* @__PURE__ */ vue.defineComponent({
@@ -5515,7 +5981,7 @@
onClick: close2
}, "✕"),
vue.createElementVNode("nav", _hoisted_2$9, [
- _cache[29] || (_cache[29] = vue.createElementVNode("div", { class: "fqa-set-side-title" }, "助手设置", -1)),
+ _cache[31] || (_cache[31] = 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,
@@ -5529,9 +5995,9 @@
]),
vue.createElementVNode("section", _hoisted_4$8, [
active.value === "general" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
- _cache[33] || (_cache[33] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "常规", -1)),
+ _cache[37] || (_cache[37] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "常规", -1)),
vue.createElementVNode("label", _hoisted_5$8, [
- _cache[30] || (_cache[30] = vue.createElementVNode("span", { class: "fqa-set-label" }, "解密网页端混淆字体", -1)),
+ _cache[32] || (_cache[32] = vue.createElementVNode("span", { class: "fqa-set-label" }, "解密网页端混淆字体", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(settings$1).decryptFont = $event),
type: "checkbox",
@@ -5541,7 +6007,7 @@
])
]),
vue.createElementVNode("label", _hoisted_6$8, [
- _cache[31] || (_cache[31] = vue.createElementVNode("span", { class: "fqa-set-label" }, "拦截网页事件上报", -1)),
+ _cache[33] || (_cache[33] = vue.createElementVNode("span", { class: "fqa-set-label" }, "拦截网页事件上报", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.unref(settings$1).blockReport = $event),
type: "checkbox",
@@ -5551,7 +6017,7 @@
])
]),
vue.createElementVNode("label", _hoisted_7$6, [
- _cache[32] || (_cache[32] = vue.createElementVNode("span", { class: "fqa-set-label" }, "允许阅读器复制文本", -1)),
+ _cache[34] || (_cache[34] = vue.createElementVNode("span", { class: "fqa-set-label" }, "允许阅读器复制文本", -1)),
vue.withDirectives(vue.createElementVNode("input", {
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => vue.unref(settings$1).allowCopy = $event),
type: "checkbox",
@@ -5559,199 +6025,219 @@
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).allowCopy]
])
+ ]),
+ vue.createElementVNode("label", _hoisted_8$6, [
+ _cache[35] || (_cache[35] = vue.createElementVNode("span", { class: "fqa-set-label" }, "移出书架时二次确认", -1)),
+ vue.withDirectives(vue.createElementVNode("input", {
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => vue.unref(settings$1).shelfRemoveConfirm = $event),
+ type: "checkbox",
+ class: "fqa-set-switch"
+ }, null, 512), [
+ [vue.vModelCheckbox, vue.unref(settings$1).shelfRemoveConfirm]
+ ])
+ ]),
+ vue.createElementVNode("label", _hoisted_9$6, [
+ _cache[36] || (_cache[36] = vue.createElementVNode("span", { class: "fqa-set-label" }, "退出登录时二次确认", -1)),
+ vue.withDirectives(vue.createElementVNode("input", {
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => vue.unref(settings$1).logoutConfirm = $event),
+ type: "checkbox",
+ class: "fqa-set-switch"
+ }, null, 512), [
+ [vue.vModelCheckbox, vue.unref(settings$1).logoutConfirm]
+ ])
])
], 64)) : active.value === "ui" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
- _cache[42] || (_cache[42] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "界面", -1)),
- vue.createElementVNode("div", _hoisted_8$6, [
- _cache[34] || (_cache[34] = vue.createElementVNode("span", { class: "fqa-set-label" }, "阅读器字体", -1)),
+ _cache[46] || (_cache[46] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "界面", -1)),
+ vue.createElementVNode("div", _hoisted_10$6, [
+ _cache[38] || (_cache[38] = vue.createElementVNode("span", { class: "fqa-set-label" }, "阅读器字体", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => vue.unref(settings$1).readerFont = $event),
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => vue.unref(settings$1).readerFont = $event),
class: "fqa-set-input",
type: "text",
placeholder: "留空表示使用默认字体"
}, null, 512), [
[vue.vModelText, vue.unref(settings$1).readerFont]
]),
- _cache[35] || (_cache[35] = vue.createElementVNode("p", { class: "fqa-set-note" }, "填写字体名称,例如「思源宋体」。留空则跟随网页默认。", -1))
+ _cache[39] || (_cache[39] = vue.createElementVNode("p", { class: "fqa-set-note" }, "填写字体名称,例如「思源宋体」。留空则跟随网页默认。", -1))
]),
- vue.createElementVNode("div", _hoisted_9$6, [
- _cache[36] || (_cache[36] = vue.createElementVNode("span", { class: "fqa-set-label" }, "自定义 CSS", -1)),
+ vue.createElementVNode("div", _hoisted_11$5, [
+ _cache[40] || (_cache[40] = vue.createElementVNode("span", { class: "fqa-set-label" }, "自定义 CSS", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => vue.unref(settings$1).customCssEnabled = $event),
+ "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => vue.unref(settings$1).customCssEnabled = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).customCssEnabled]
])
]),
- vue.createElementVNode("div", _hoisted_10$6, [
+ vue.createElementVNode("div", _hoisted_12$5, [
vue.withDirectives(vue.createElementVNode("textarea", {
- "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => vue.unref(settings$1).customCss = $event),
+ "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => vue.unref(settings$1).customCss = $event),
class: "fqa-set-textarea",
disabled: !vue.unref(settings$1).customCssEnabled,
spellcheck: "false",
placeholder: "/* 自定义 CSS */"
- }, null, 8, _hoisted_11$5), [
+ }, null, 8, _hoisted_13$4), [
[vue.vModelText, vue.unref(settings$1).customCss]
]),
- _cache[37] || (_cache[37] = vue.createElementVNode("p", { class: "fqa-set-note" }, "关闭开关后内容会保留,只是不再应用。", -1))
+ _cache[41] || (_cache[41] = vue.createElementVNode("p", { class: "fqa-set-note" }, "关闭开关后内容会保留,只是不再应用。", -1))
]),
- vue.createElementVNode("div", _hoisted_12$5, [
- _cache[40] || (_cache[40] = vue.createElementVNode("span", { class: "fqa-set-label" }, "书架单击书本", -1)),
- vue.createElementVNode("div", _hoisted_13$4, [
- vue.createElementVNode("label", _hoisted_14$4, [
+ vue.createElementVNode("div", _hoisted_14$4, [
+ _cache[44] || (_cache[44] = vue.createElementVNode("span", { class: "fqa-set-label" }, "书架单击书本", -1)),
+ vue.createElementVNode("div", _hoisted_15$2, [
+ vue.createElementVNode("label", _hoisted_16$1, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => vue.unref(settings$1).bookshelfClickAction = $event),
+ "onUpdate:modelValue": _cache[8] || (_cache[8] = ($event) => vue.unref(settings$1).bookshelfClickAction = $event),
type: "radio",
value: "read"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).bookshelfClickAction]
]),
- _cache[38] || (_cache[38] = vue.createElementVNode("span", null, "继续阅读", -1))
+ _cache[42] || (_cache[42] = vue.createElementVNode("span", null, "继续阅读", -1))
]),
- vue.createElementVNode("label", _hoisted_15$2, [
+ vue.createElementVNode("label", _hoisted_17$1, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => vue.unref(settings$1).bookshelfClickAction = $event),
+ "onUpdate:modelValue": _cache[9] || (_cache[9] = ($event) => vue.unref(settings$1).bookshelfClickAction = $event),
type: "radio",
value: "detail"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).bookshelfClickAction]
]),
- _cache[39] || (_cache[39] = vue.createElementVNode("span", null, "查看详情", -1))
+ _cache[43] || (_cache[43] = vue.createElementVNode("span", null, "查看详情", -1))
])
]),
- _cache[41] || (_cache[41] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 按住 Ctrl 单击去往另一项;中键在新标签页打开,Ctrl+中键同样反向。 选「继续阅读」但这本书还没读过时,会退回详情页。 ", -1))
+ _cache[45] || (_cache[45] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 按住 Ctrl 单击去往另一项;中键在新标签页打开,Ctrl+中键同样反向。 选「继续阅读」但这本书还没读过时,会退回详情页。 ", -1))
])
], 64)) : active.value === "search" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
- _cache[46] || (_cache[46] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "搜索", -1)),
- vue.createElementVNode("label", _hoisted_16$1, [
- _cache[43] || (_cache[43] = vue.createElementVNode("span", { class: "fqa-set-label" }, "接管搜索界面", -1)),
+ _cache[50] || (_cache[50] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "搜索", -1)),
+ vue.createElementVNode("label", _hoisted_18$1, [
+ _cache[47] || (_cache[47] = vue.createElementVNode("span", { class: "fqa-set-label" }, "接管搜索界面", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[8] || (_cache[8] = ($event) => vue.unref(settings$1).enhanceSearch = $event),
+ "onUpdate:modelValue": _cache[10] || (_cache[10] = ($event) => vue.unref(settings$1).enhanceSearch = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).enhanceSearch]
])
]),
- vue.createElementVNode("div", _hoisted_17$1, [
- vue.createElementVNode("label", _hoisted_18$1, [
- _cache[44] || (_cache[44] = vue.createElementVNode("span", { class: "fqa-set-label" }, "个人化推荐", -1)),
+ vue.createElementVNode("div", _hoisted_19$1, [
+ vue.createElementVNode("label", _hoisted_20, [
+ _cache[48] || (_cache[48] = vue.createElementVNode("span", { class: "fqa-set-label" }, "个人化推荐", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[9] || (_cache[9] = ($event) => vue.unref(settings$1).searchPersonalized = $event),
+ "onUpdate:modelValue": _cache[11] || (_cache[11] = ($event) => vue.unref(settings$1).searchPersonalized = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).searchPersonalized]
])
]),
- _cache[45] || (_cache[45] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 开启后搜索走同源请求,由浏览器自动带上你的登录 Cookie,番茄据此按阅读偏好排序。 凭据不经过脚本,也不会发往番茄以外的任何地方。关闭时走匿名请求。 ", -1))
+ _cache[49] || (_cache[49] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 开启后搜索走同源请求,由浏览器自动带上你的登录 Cookie,番茄据此按阅读偏好排序。 凭据不经过脚本,也不会发往番茄以外的任何地方。关闭时走匿名请求。 ", -1))
])
], 64)) : active.value === "download" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
- _cache[67] || (_cache[67] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "下载", -1)),
- vue.createElementVNode("label", _hoisted_19$1, [
- _cache[47] || (_cache[47] = vue.createElementVNode("span", { class: "fqa-set-label" }, "显示下载入口", -1)),
+ _cache[71] || (_cache[71] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "下载", -1)),
+ vue.createElementVNode("label", _hoisted_21, [
+ _cache[51] || (_cache[51] = vue.createElementVNode("span", { class: "fqa-set-label" }, "显示下载入口", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[10] || (_cache[10] = ($event) => vue.unref(settings$1).enableDownload = $event),
+ "onUpdate:modelValue": _cache[12] || (_cache[12] = ($event) => vue.unref(settings$1).enableDownload = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).enableDownload]
])
]),
- vue.createElementVNode("div", _hoisted_20, [
- _cache[50] || (_cache[50] = vue.createElementVNode("span", { class: "fqa-set-label" }, "默认格式", -1)),
- vue.createElementVNode("div", _hoisted_21, [
- vue.createElementVNode("label", _hoisted_22, [
+ vue.createElementVNode("div", _hoisted_22, [
+ _cache[54] || (_cache[54] = vue.createElementVNode("span", { class: "fqa-set-label" }, "默认格式", -1)),
+ vue.createElementVNode("div", _hoisted_23, [
+ vue.createElementVNode("label", _hoisted_24, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[11] || (_cache[11] = ($event) => vue.unref(settings$1).downloadFormat = $event),
+ "onUpdate:modelValue": _cache[13] || (_cache[13] = ($event) => vue.unref(settings$1).downloadFormat = $event),
type: "radio",
value: "epub"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).downloadFormat]
]),
- _cache[48] || (_cache[48] = vue.createElementVNode("span", null, "EPUB", -1))
+ _cache[52] || (_cache[52] = vue.createElementVNode("span", null, "EPUB", -1))
]),
- vue.createElementVNode("label", _hoisted_23, [
+ vue.createElementVNode("label", _hoisted_25, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[12] || (_cache[12] = ($event) => vue.unref(settings$1).downloadFormat = $event),
+ "onUpdate:modelValue": _cache[14] || (_cache[14] = ($event) => vue.unref(settings$1).downloadFormat = $event),
type: "radio",
value: "txt"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).downloadFormat]
]),
- _cache[49] || (_cache[49] = vue.createElementVNode("span", null, "TXT", -1))
+ _cache[53] || (_cache[53] = vue.createElementVNode("span", null, "TXT", -1))
])
]),
- _cache[51] || (_cache[51] = vue.createElementVNode("p", { class: "fqa-set-note" }, " EPUB 保留原始排版、插图与分卷目录;TXT 是纯文本。右键菜单里可以单次指定格式。 ", -1))
+ _cache[55] || (_cache[55] = vue.createElementVNode("p", { class: "fqa-set-note" }, " EPUB 保留原始排版、插图与分卷目录;TXT 是纯文本。右键菜单里可以单次指定格式。 ", -1))
]),
- vue.createElementVNode("div", _hoisted_24, [
- _cache[54] || (_cache[54] = vue.createElementVNode("span", { class: "fqa-set-label" }, "TXT 编码", -1)),
- vue.createElementVNode("div", _hoisted_25, [
- vue.createElementVNode("label", _hoisted_26, [
+ vue.createElementVNode("div", _hoisted_26, [
+ _cache[58] || (_cache[58] = vue.createElementVNode("span", { class: "fqa-set-label" }, "TXT 编码", -1)),
+ vue.createElementVNode("div", _hoisted_27, [
+ vue.createElementVNode("label", _hoisted_28, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[13] || (_cache[13] = ($event) => vue.unref(settings$1).downloadCharset = $event),
+ "onUpdate:modelValue": _cache[15] || (_cache[15] = ($event) => vue.unref(settings$1).downloadCharset = $event),
type: "radio",
value: "utf-8"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).downloadCharset]
]),
- _cache[52] || (_cache[52] = vue.createElementVNode("span", null, "UTF-8", -1))
+ _cache[56] || (_cache[56] = vue.createElementVNode("span", null, "UTF-8", -1))
]),
- vue.createElementVNode("label", _hoisted_27, [
+ vue.createElementVNode("label", _hoisted_29, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[14] || (_cache[14] = ($event) => vue.unref(settings$1).downloadCharset = $event),
+ "onUpdate:modelValue": _cache[16] || (_cache[16] = ($event) => vue.unref(settings$1).downloadCharset = $event),
type: "radio",
value: "gbk"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).downloadCharset]
]),
- _cache[53] || (_cache[53] = vue.createElementVNode("span", null, "GBK", -1))
+ _cache[57] || (_cache[57] = vue.createElementVNode("span", null, "GBK", -1))
])
]),
- _cache[55] || (_cache[55] = vue.createElementVNode("p", { class: "fqa-set-note" }, "EPUB 固定使用 UTF-8。老设备或部分阅读器可能需要 GBK。", -1))
+ _cache[59] || (_cache[59] = vue.createElementVNode("p", { class: "fqa-set-note" }, "EPUB 固定使用 UTF-8。老设备或部分阅读器可能需要 GBK。", -1))
]),
- vue.createElementVNode("div", _hoisted_28, [
- _cache[59] || (_cache[59] = vue.createElementVNode("span", { class: "fqa-set-label" }, "EPUB 选项", -1)),
- vue.createElementVNode("label", _hoisted_29, [
- _cache[56] || (_cache[56] = vue.createElementVNode("span", { class: "fqa-set-label" }, "下载正文插图", -1)),
+ vue.createElementVNode("div", _hoisted_30, [
+ _cache[63] || (_cache[63] = vue.createElementVNode("span", { class: "fqa-set-label" }, "EPUB 选项", -1)),
+ vue.createElementVNode("label", _hoisted_31, [
+ _cache[60] || (_cache[60] = vue.createElementVNode("span", { class: "fqa-set-label" }, "下载正文插图", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[15] || (_cache[15] = ($event) => vue.unref(settings$1).downloadImages = $event),
+ "onUpdate:modelValue": _cache[17] || (_cache[17] = ($event) => vue.unref(settings$1).downloadImages = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).downloadImages]
])
]),
- vue.createElementVNode("label", _hoisted_30, [
- _cache[57] || (_cache[57] = vue.createElementVNode("span", { class: "fqa-set-label" }, "保留书籍排版样式", -1)),
+ vue.createElementVNode("label", _hoisted_32, [
+ _cache[61] || (_cache[61] = vue.createElementVNode("span", { class: "fqa-set-label" }, "保留书籍排版样式", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[16] || (_cache[16] = ($event) => vue.unref(settings$1).downloadBookCss = $event),
+ "onUpdate:modelValue": _cache[18] || (_cache[18] = ($event) => vue.unref(settings$1).downloadBookCss = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).downloadBookCss]
])
]),
- vue.createElementVNode("label", _hoisted_31, [
- _cache[58] || (_cache[58] = vue.createElementVNode("span", { class: "fqa-set-label" }, "为每卷生成卷页", -1)),
+ vue.createElementVNode("label", _hoisted_33, [
+ _cache[62] || (_cache[62] = vue.createElementVNode("span", { class: "fqa-set-label" }, "为每卷生成卷页", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[17] || (_cache[17] = ($event) => vue.unref(settings$1).downloadVolumePage = $event),
+ "onUpdate:modelValue": _cache[19] || (_cache[19] = ($event) => vue.unref(settings$1).downloadVolumePage = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).downloadVolumePage]
])
]),
- _cache[60] || (_cache[60] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 插图逐张下载,图多的书会明显变慢、文件也更大;关闭后正文里仍保留图片地址,联网可看。 ", -1))
+ _cache[64] || (_cache[64] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 插图逐张下载,图多的书会明显变慢、文件也更大;关闭后正文里仍保留图片地址,联网可看。 ", -1))
]),
- vue.createElementVNode("div", _hoisted_32, [
- _cache[64] || (_cache[64] = vue.createElementVNode("span", { class: "fqa-set-label" }, "请求节奏", -1)),
- _cache[65] || (_cache[65] = vue.createElementVNode("p", { class: "fqa-set-warn" }, " 接口对批量正文有限制,调得太激进会导致大量章节抓不到甚至触发风控。不清楚就别改。 ", -1)),
- vue.createElementVNode("label", _hoisted_33, [
- _cache[61] || (_cache[61] = vue.createElementVNode("span", null, "每批章节数(1-30)", -1)),
+ vue.createElementVNode("div", _hoisted_34, [
+ _cache[68] || (_cache[68] = vue.createElementVNode("span", { class: "fqa-set-label" }, "请求节奏", -1)),
+ _cache[69] || (_cache[69] = vue.createElementVNode("p", { class: "fqa-set-warn" }, " 接口对批量正文有限制,调得太激进会导致大量章节抓不到甚至触发风控。不清楚就别改。 ", -1)),
+ vue.createElementVNode("label", _hoisted_35, [
+ _cache[65] || (_cache[65] = vue.createElementVNode("span", null, "每批章节数(1-30)", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[18] || (_cache[18] = ($event) => vue.unref(settings$1).downloadBatchSize = $event),
+ "onUpdate:modelValue": _cache[20] || (_cache[20] = ($event) => vue.unref(settings$1).downloadBatchSize = $event),
class: "fqa-set-input",
type: "number",
min: "1",
@@ -5765,10 +6251,10 @@
]
])
]),
- vue.createElementVNode("label", _hoisted_34, [
- _cache[62] || (_cache[62] = vue.createElementVNode("span", null, "批次间隔 (ms)", -1)),
+ vue.createElementVNode("label", _hoisted_36, [
+ _cache[66] || (_cache[66] = vue.createElementVNode("span", null, "批次间隔 (ms)", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[19] || (_cache[19] = ($event) => vue.unref(settings$1).downloadInterval = $event),
+ "onUpdate:modelValue": _cache[21] || (_cache[21] = ($event) => vue.unref(settings$1).downloadInterval = $event),
class: "fqa-set-input",
type: "number",
min: "0",
@@ -5783,10 +6269,10 @@
]
])
]),
- vue.createElementVNode("label", _hoisted_35, [
- _cache[63] || (_cache[63] = vue.createElementVNode("span", null, "重试轮数", -1)),
+ vue.createElementVNode("label", _hoisted_37, [
+ _cache[67] || (_cache[67] = vue.createElementVNode("span", null, "重试轮数", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[20] || (_cache[20] = ($event) => vue.unref(settings$1).downloadRetries = $event),
+ "onUpdate:modelValue": _cache[22] || (_cache[22] = ($event) => vue.unref(settings$1).downloadRetries = $event),
class: "fqa-set-input",
type: "number",
min: "0",
@@ -5800,7 +6286,7 @@
]
])
]),
- _cache[66] || (_cache[66] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 实测单次请求最多返回 30 章正文,间隔小于约 750ms 会被限流成每次 1 章。 ", -1)),
+ _cache[70] || (_cache[70] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 实测单次请求最多返回 30 章正文,间隔小于约 750ms 会被限流成每次 1 章。 ", -1)),
vue.createElementVNode("div", { class: "fqa-set-actions" }, [
vue.createElementVNode("button", {
class: "fqa-set-btn",
@@ -5809,123 +6295,123 @@
])
])
], 64)) : active.value === "audiobook" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 4 }, [
- _cache[73] || (_cache[73] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "听书", -1)),
- vue.createElementVNode("div", _hoisted_36, [
- _cache[70] || (_cache[70] = vue.createElementVNode("span", { class: "fqa-set-label" }, "读完一章后", -1)),
- vue.createElementVNode("div", _hoisted_37, [
- vue.createElementVNode("label", _hoisted_38, [
+ _cache[77] || (_cache[77] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "听书", -1)),
+ vue.createElementVNode("div", _hoisted_38, [
+ _cache[74] || (_cache[74] = vue.createElementVNode("span", { class: "fqa-set-label" }, "读完一章后", -1)),
+ vue.createElementVNode("div", _hoisted_39, [
+ vue.createElementVNode("label", _hoisted_40, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[21] || (_cache[21] = ($event) => vue.unref(settings$1).audiobookChapterEnd = $event),
+ "onUpdate:modelValue": _cache[23] || (_cache[23] = ($event) => vue.unref(settings$1).audiobookChapterEnd = $event),
type: "radio",
value: "next"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).audiobookChapterEnd]
]),
- _cache[68] || (_cache[68] = vue.createElementVNode("span", null, "自动下一章", -1))
+ _cache[72] || (_cache[72] = vue.createElementVNode("span", null, "自动下一章", -1))
]),
- vue.createElementVNode("label", _hoisted_39, [
+ vue.createElementVNode("label", _hoisted_41, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[22] || (_cache[22] = ($event) => vue.unref(settings$1).audiobookChapterEnd = $event),
+ "onUpdate:modelValue": _cache[24] || (_cache[24] = ($event) => vue.unref(settings$1).audiobookChapterEnd = $event),
type: "radio",
value: "stop"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).audiobookChapterEnd]
]),
- _cache[69] || (_cache[69] = vue.createElementVNode("span", null, "停止播放", -1))
+ _cache[73] || (_cache[73] = vue.createElementVNode("span", null, "停止播放", -1))
])
]),
- _cache[71] || (_cache[71] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 自动下一章会点击页面上的「下一章」,读到全书最后一章时自动停下。 ", -1))
+ _cache[75] || (_cache[75] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 自动下一章会点击页面上的「下一章」,读到全书最后一章时自动停下。 ", -1))
]),
- vue.createElementVNode("label", _hoisted_40, [
- _cache[72] || (_cache[72] = vue.createElementVNode("span", { class: "fqa-set-label" }, "高亮并跟随朗读段落", -1)),
+ vue.createElementVNode("label", _hoisted_42, [
+ _cache[76] || (_cache[76] = vue.createElementVNode("span", { class: "fqa-set-label" }, "高亮并跟随朗读段落", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[23] || (_cache[23] = ($event) => vue.unref(settings$1).audiobookFollow = $event),
+ "onUpdate:modelValue": _cache[25] || (_cache[25] = ($event) => vue.unref(settings$1).audiobookFollow = $event),
type: "checkbox",
class: "fqa-set-switch"
}, null, 512), [
[vue.vModelCheckbox, vue.unref(settings$1).audiobookFollow]
])
]),
- _cache[74] || (_cache[74] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 关闭后仍会高亮当前段落,但不再自动滚动页面。点击任意段落可跳到该处朗读。 ", -1))
+ _cache[78] || (_cache[78] = vue.createElementVNode("p", { class: "fqa-set-note" }, " 关闭后仍会高亮当前段落,但不再自动滚动页面。点击任意段落可跳到该处朗读。 ", -1))
], 64)) : active.value === "protocol" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 5 }, [
- _cache[84] || (_cache[84] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "协议", -1)),
- vue.createElementVNode("div", _hoisted_41, [
- _cache[77] || (_cache[77] = vue.createElementVNode("span", { class: "fqa-set-label" }, "API 偏好", -1)),
- vue.createElementVNode("div", _hoisted_42, [
- vue.createElementVNode("label", _hoisted_43, [
+ _cache[88] || (_cache[88] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "协议", -1)),
+ vue.createElementVNode("div", _hoisted_43, [
+ _cache[81] || (_cache[81] = vue.createElementVNode("span", { class: "fqa-set-label" }, "API 偏好", -1)),
+ vue.createElementVNode("div", _hoisted_44, [
+ vue.createElementVNode("label", _hoisted_45, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[24] || (_cache[24] = ($event) => vue.unref(settings$1).apiPreference = $event),
+ "onUpdate:modelValue": _cache[26] || (_cache[26] = ($event) => vue.unref(settings$1).apiPreference = $event),
type: "radio",
value: "app"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).apiPreference]
]),
- _cache[75] || (_cache[75] = vue.createElementVNode("span", null, "番茄 APP", -1))
+ _cache[79] || (_cache[79] = vue.createElementVNode("span", null, "番茄 APP", -1))
]),
- vue.createElementVNode("label", _hoisted_44, [
+ vue.createElementVNode("label", _hoisted_46, [
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[25] || (_cache[25] = ($event) => vue.unref(settings$1).apiPreference = $event),
+ "onUpdate:modelValue": _cache[27] || (_cache[27] = ($event) => vue.unref(settings$1).apiPreference = $event),
type: "radio",
value: "redcandle"
}, null, 512), [
[vue.vModelRadio, vue.unref(settings$1).apiPreference]
]),
- _cache[76] || (_cache[76] = vue.createElementVNode("span", null, "红烛 APP", -1))
+ _cache[80] || (_cache[80] = vue.createElementVNode("span", null, "红烛 APP", -1))
])
]),
- _cache[78] || (_cache[78] = vue.createElementVNode("p", { class: "fqa-set-note" }, "如果某协议数据不全,脚本可能会选择其他接口作为补充。", -1))
+ _cache[82] || (_cache[82] = vue.createElementVNode("p", { class: "fqa-set-note" }, "如果某协议数据不全,脚本可能会选择其他接口作为补充。", -1))
]),
- vue.createElementVNode("div", _hoisted_45, [
- _cache[82] || (_cache[82] = vue.createElementVNode("span", { class: "fqa-set-label" }, "设备信息", -1)),
- _cache[83] || (_cache[83] = vue.createElementVNode("p", { class: "fqa-set-warn" }, " 如果不知道这是什么,请保持默认。乱填可能导致脚本功能异常。 ", -1)),
- vue.createElementVNode("label", _hoisted_46, [
- _cache[79] || (_cache[79] = vue.createElementVNode("span", null, "device_id", -1)),
+ vue.createElementVNode("div", _hoisted_47, [
+ _cache[86] || (_cache[86] = vue.createElementVNode("span", { class: "fqa-set-label" }, "设备信息", -1)),
+ _cache[87] || (_cache[87] = vue.createElementVNode("p", { class: "fqa-set-warn" }, " 如果不知道这是什么,请保持默认。乱填可能导致脚本功能异常。 ", -1)),
+ vue.createElementVNode("label", _hoisted_48, [
+ _cache[83] || (_cache[83] = vue.createElementVNode("span", null, "device_id", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[26] || (_cache[26] = ($event) => vue.unref(settings$1).deviceId = $event),
+ "onUpdate:modelValue": _cache[28] || (_cache[28] = ($event) => vue.unref(settings$1).deviceId = $event),
class: "fqa-set-input",
type: "text",
placeholder: currentDevice.value.device_id || "自动注册"
- }, null, 8, _hoisted_47), [
+ }, null, 8, _hoisted_49), [
[vue.vModelText, vue.unref(settings$1).deviceId]
])
]),
- vue.createElementVNode("label", _hoisted_48, [
- _cache[80] || (_cache[80] = vue.createElementVNode("span", null, "install_id (iid)", -1)),
+ vue.createElementVNode("label", _hoisted_50, [
+ _cache[84] || (_cache[84] = vue.createElementVNode("span", null, "install_id (iid)", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[27] || (_cache[27] = ($event) => vue.unref(settings$1).installId = $event),
+ "onUpdate:modelValue": _cache[29] || (_cache[29] = ($event) => vue.unref(settings$1).installId = $event),
class: "fqa-set-input",
type: "text",
placeholder: currentDevice.value.install_id || "自动注册"
- }, null, 8, _hoisted_49), [
+ }, null, 8, _hoisted_51), [
[vue.vModelText, vue.unref(settings$1).installId]
])
]),
- vue.createElementVNode("label", _hoisted_50, [
- _cache[81] || (_cache[81] = vue.createElementVNode("span", null, "device_type", -1)),
+ vue.createElementVNode("label", _hoisted_52, [
+ _cache[85] || (_cache[85] = vue.createElementVNode("span", null, "device_type", -1)),
vue.withDirectives(vue.createElementVNode("input", {
- "onUpdate:modelValue": _cache[28] || (_cache[28] = ($event) => vue.unref(settings$1).deviceType = $event),
+ "onUpdate:modelValue": _cache[30] || (_cache[30] = ($event) => vue.unref(settings$1).deviceType = $event),
class: "fqa-set-input",
type: "text",
placeholder: currentDevice.value.device_type || "自动注册"
- }, null, 8, _hoisted_51), [
+ }, null, 8, _hoisted_53), [
[vue.vModelText, vue.unref(settings$1).deviceType]
])
]),
- vue.createElementVNode("div", _hoisted_52, [
+ vue.createElementVNode("div", _hoisted_54, [
vue.createElementVNode("button", {
class: "fqa-set-btn",
disabled: registering.value,
onClick: reRegister
- }, vue.toDisplayString(registering.value ? "注册中…" : "重新注册"), 9, _hoisted_53),
- registerMsg.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_54, vue.toDisplayString(registerMsg.value), 1)) : vue.createCommentVNode("", true)
+ }, vue.toDisplayString(registering.value ? "注册中…" : "重新注册"), 9, _hoisted_55),
+ registerMsg.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_56, vue.toDisplayString(registerMsg.value), 1)) : vue.createCommentVNode("", true)
])
])
], 64)) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 6 }, [
- _cache[89] || (_cache[89] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "关于", -1)),
- vue.createElementVNode("p", _hoisted_55, "番茄小说助手 v" + vue.toDisplayString(vue.unref(version)), 1),
- vue.createElementVNode("div", _hoisted_56, [
+ _cache[93] || (_cache[93] = vue.createElementVNode("h3", { class: "fqa-set-h" }, "关于", -1)),
+ vue.createElementVNode("p", _hoisted_57, "番茄小说助手 v" + vue.toDisplayString(vue.unref(version)), 1),
+ vue.createElementVNode("div", _hoisted_58, [
vue.createElementVNode("div", { class: "fqa-set-link-row" }, [
- _cache[85] || (_cache[85] = vue.createElementVNode("span", null, "GreasyFork 地址:", -1)),
+ _cache[89] || (_cache[89] = vue.createElementVNode("span", null, "GreasyFork 地址:", -1)),
vue.createElementVNode("a", {
href: GREASYFORK,
target: "_blank",
@@ -5933,31 +6419,31 @@
}, "跳转")
]),
vue.createElementVNode("div", { class: "fqa-set-link-row" }, [
- _cache[86] || (_cache[86] = vue.createElementVNode("span", null, "GitHub 地址:", -1)),
+ _cache[90] || (_cache[90] = vue.createElementVNode("span", null, "GitHub 地址:", -1)),
vue.createElementVNode("a", {
href: GITHUB,
target: "_blank",
rel: "noreferrer noopener"
}, "跳转")
]),
- vue.createElementVNode("div", _hoisted_57, [
- _cache[88] || (_cache[88] = vue.createElementVNode("span", null, "问题反馈:", -1)),
+ vue.createElementVNode("div", _hoisted_59, [
+ _cache[92] || (_cache[92] = vue.createElementVNode("span", null, "问题反馈:", -1)),
vue.createElementVNode("a", {
href: FEEDBACK,
target: "_blank",
rel: "noreferrer noopener"
}, "GreasyFork"),
vue.createElementVNode("span", null, [
- _cache[87] || (_cache[87] = vue.createTextVNode(" 或 ", -1)),
+ _cache[91] || (_cache[91] = vue.createTextVNode(" 或 ", -1)),
vue.createElementVNode("a", {
href: `${GITHUB}/issues`,
target: "_blank",
rel: "noreferrer noopener"
- }, " GitHub Issues ", 8, _hoisted_58)
+ }, " GitHub Issues ", 8, _hoisted_60)
])
])
]),
- _cache[90] || (_cache[90] = vue.createElementVNode("div", { class: "fqa-set-license" }, [
+ _cache[94] || (_cache[94] = vue.createElementVNode("div", { class: "fqa-set-license" }, [
vue.createElementVNode("p", null, " 本脚本基于 GNU General Public License 3.0 授权,完全开源且免费,修改/二次开发请注意遵守开源协议。 "),
vue.createElementVNode("p", null, "本脚本使用 TypeScript + Vue 开发,请避免直接修改编译产物。")
], -1))
@@ -6058,6 +6544,20 @@
});
secondDiv.insertAdjacentElement("afterend", thirdDiv);
menuInner.appendChild(createMenuItem("助手设置", settings, openSettings));
+ menuInner.childNodes.forEach((node) => {
+ var _a, _b;
+ if (((_b = (_a = node.lastChild) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) === "退出登录") {
+ const original = node;
+ const replaced = cloneElement(node);
+ original.classList.add("fqa-hide");
+ original.insertAdjacentElement("afterend", replaced);
+ replaced.addEventListener("click", (_) => {
+ if (!settings$1.logoutConfirm || unsafeWindow.confirm("确认退出登录吗?")) {
+ original.click();
+ }
+ });
+ }
+ });
};
const scan = (root) => {
if (root.classList.contains("arco-menu-inner")) {
@@ -6742,134 +7242,6 @@
};
}
});
- 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(bookId2, modifyTime = 0) {
- return {
- asterisked: false,
- book_id: bookId2,
- book_type: 0,
- modify_time: modifyTime
- };
- }
- async function shelfPost(path, body) {
- const res = await fetch$1(`${SHELF_BASE}${path}/v?aid=1967`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- credentials: "include",
- body: JSON.stringify(body)
- });
- const json = await res.json().catch(() => null);
- if (!res.ok || json && json.code !== 0 && json.code !== void 0) {
- throw new Error(`书架操作失败(${path}): ${(json == null ? void 0 : json.message) ?? res.status}`);
- }
- return json;
- }
- async function addToBookshelf(bookId2) {
- await shelfPost("/add", {
- add_book_source: 0,
- identify_data: [identify(bookId2)]
- });
- }
- async function removeFromBookshelf(bookId2) {
- await shelfPost("/delete", {
- identify_data: [identify(bookId2, Date.now())]
- });
- }
- async function moveToGroup(bookId2, groupName) {
- await shelfPost("/update", {
- book_data: [
- {
- asterisked: false,
- book_id: bookId2,
- 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 = [
@@ -10191,8 +10563,8 @@ ${body}
}
];
const hooks = [
- ..._exports$5,
..._exports$4,
+ ..._exports$5,
..._exports$3,
..._exports$2,
..._exports$1,
diff --git a/src/api/bookshelf.ts b/src/api/bookshelf.ts
index e08543e..0c4d7a5 100644
--- a/src/api/bookshelf.ts
+++ b/src/api/bookshelf.ts
@@ -148,3 +148,9 @@ export async function moveToGroup(bookId: string, groupName: string): Promise {
+ const response = await fetch(`https://fanqienovel.com/reading/bookapi/bookshelf/check/v:version/?aid=1967&iid=0&version_code=57700&update_version_code=57700&book_id=${book_id}`)
+ const data = await response.json()
+ return Boolean(data.data)
+}
\ No newline at end of file
diff --git a/src/api/catalog.ts b/src/api/catalog.ts
index f7ef1ff..faca8c4 100644
--- a/src/api/catalog.ts
+++ b/src/api/catalog.ts
@@ -14,7 +14,7 @@ export async function getCatalogRaw(bookId: string): Promise> {
const j: any = response.json()
const items = j?.data?.item_data_list
if (j?.code !== 0 || !Array.isArray(items) || items.length === 0) {
- throw new Error('Empty catalog')
+ return [null, null]
}
return [items, items.map((it: any) => String(it.item_id))]
}
@@ -39,25 +39,29 @@ export async function webCatalog(bookId: string): Promise> {
"firstPassTime": "1749281928"
},
*/
- const volmap: Record> = {}
+ const cs: Array = []
const vname: string[] = d.volumeNameList
for (let i = 0; i < vname.length; i++) {
const volumeName = vname[i]
if (volumeName !== undefined) {
- volmap[volumeName] = d.chapterListWithVolume[i]
+ // volmap[volumeName] = d.chapterListWithVolume[i]
+ cs.push(...d.chapterListWithVolume[i])
}
}
- return [volmap, allItems]
+ return [cs, allItems]
}
export async function getCatalog(bookId: string): Promise {
const r = await getCatalogRaw(bookId)
let catalogRaw = r[0] as any[] // item_data_list
let allItemIds = r[1] as string[] // all item ids
+ console.log('catalogRaw', catalogRaw, 'allItemIds', allItemIds)
if (!catalogRaw || !allItemIds) {
const rw = await webCatalog(bookId)
catalogRaw = rw[0]
allItemIds = rw[1]
+ console.log('webCatalog', catalogRaw, 'allItemIds', allItemIds)
+ // console.log('catalogRaw', catalogRaw, 'allItemIds', allItemIds)
}
const vmap: Record = {} // title -> VolumeItem
const chapters: Array = []
diff --git a/src/api/device.ts b/src/api/device.ts
index 757b35b..70c62b1 100644
--- a/src/api/device.ts
+++ b/src/api/device.ts
@@ -13,6 +13,20 @@ 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'
+/**
+ * 设备协议请求一律不带 Cookie。
+ *
+ * GM_xmlhttpRequest 默认会带上扩展上下文里属于这些域名的 Cookie
+ * (store-region、install_id、ttreq 之类),那是浏览器的痕迹,APP 的 okhttp 不带。
+ * 而且「注册一台新设备」还带着已有的 install_id cookie 本身自相矛盾。
+ * anonymous 让 GM 不发 Cookie —— 这一点在页面里用 fetch/XHR 反而做不到。
+ *
+ * 去不掉的另一些:Sec-Fetch-*、Priority、Accept-Encoding、Accept-Language
+ * 是 Chromium 按请求上下文自己加的禁止头,JS(含扩展)都改不了,
+ * 要完全消除只能让请求离开浏览器(原生客户端或服务端代理)。
+ */
+const APP_REQUEST = { credentials: 'omit' } as const
+
/** 设备注册结果 */
export interface RegisteredDevice {
device_id: string;
@@ -56,6 +70,7 @@ export async function registerDevice(): Promise {
)
const res = await apiFetch(REGISTER_URL, {
+ ...APP_REQUEST,
method: 'POST',
headers: {
'User-Agent': 'okhttp/4.10.0',
@@ -107,6 +122,7 @@ export async function activatePremium(device: RegisteredDevice): Promise
try {
const headers = await signRequest(url, body)
const res = await apiFetch(url, {
+ ...APP_REQUEST,
method: 'POST',
headers: {
...headers,
@@ -166,6 +182,7 @@ export async function registerKey(device: RegisteredDevice): Promise {
// 签名针对 gzip 之后的字节,与 Go 里 SignedRequest 收到压缩流的行为一致
const headers = await signRequest(url, gzipped)
const res = await apiFetch(url, {
+ ...APP_REQUEST,
method: 'POST',
headers: {
...headers,
diff --git a/src/api/user.ts b/src/api/user.ts
index e017814..fabcbd5 100644
--- a/src/api/user.ts
+++ b/src/api/user.ts
@@ -77,7 +77,7 @@ export async function checkLogin() {
desc: j?.data?.desc,
age: j?.data?.age,
}
- if (j?.data?.id > 1) {
+ if (j.code !== -1) {
userState.isLogin = true
userState.userInfo = _userInfo
write('userState', userState)
diff --git a/src/assets/audiobook.css b/src/assets/audiobook.css
index 49298b3..a041468 100644
--- a/src/assets/audiobook.css
+++ b/src/assets/audiobook.css
@@ -17,9 +17,12 @@
color: var(--fqa-audio-text);
}
-/* 页面切到夜间模式时整体反色 */
-.muye-reader-dark .fqa-audio-root,
-.fqa-audio-root.fqa-audio-dark {
+/*
+ * 暗色反色。悬浮栏挂在 body 下,够不到 .muye-reader-dark 的后代选择器,
+ * 所以 audioPanel 在 #fqa-audio-root 容器上打 .fqa-audio-dark 类;
+ * 真正的 .fqa-audio-root(Vue 根)是容器的子节点,用后代选择器作用到它。
+ */
+.fqa-audio-dark .fqa-audio-root {
--fqa-audio-bg: #2b2b2b;
--fqa-audio-text: #b3b3b3;
--fqa-audio-sub: #7a7a7a;
diff --git a/src/assets/settings.svg b/src/assets/settings.svg
index 927674c..2477f76 100644
--- a/src/assets/settings.svg
+++ b/src/assets/settings.svg
@@ -2,8 +2,8 @@
\ No newline at end of file
diff --git a/src/hooks/fetchHook.ts b/src/hooks/fetchHook.ts
index 8ba21a2..c1fa337 100644
--- a/src/hooks/fetchHook.ts
+++ b/src/hooks/fetchHook.ts
@@ -1,6 +1,51 @@
import type { HookConfig } from "../config"
import { settings } from "../settings"
+// ---------------------------------------------------------------------------
+// 功能性的请求拦截 / 响应改写层。
+//
+// 与下面的 blockReport 黑名单不同:这里是「有目的、主动」的拦截,
+// 由模块自己往 modifiers 里注册规则,不随设置开关而变化。
+// 每一条规则可以:
+// - should_break:不把请求发给服务器,直接用 make_response 造一个本地响应;
+// - modify_response:照常发请求,等真实响应回来后按需改写(body/status/headers)。
+// 两者按 modifiers 声明顺序叠加,前一条的输出是后一条的输入。
+// ---------------------------------------------------------------------------
+
+export interface SimpleResponse {
+ /**
+ * 响应体。给字符串就按 UTF-8 编码,省得每条规则都自己 TextEncoder 一遍。
+ * 读进来的(modify_response 的入参)一律是 ArrayBuffer,
+ * 要按文本处理用下面的 bodyToText。
+ */
+ responseBody: ArrayBuffer | string
+ statusCode: number
+ responseHeaders: Record
+}
+
+/** 内部流转用:responseBody 已经归一成 ArrayBuffer */
+interface ResolvedResponse extends SimpleResponse {
+ responseBody: ArrayBuffer
+}
+
+export interface ResponseModifier {
+ matcher: (url: string) => boolean // 匹配规则
+ should_break: (url: string) => boolean // 是否中断请求(不发给服务器)
+ make_response?: (url: string) => SimpleResponse // 中断请求后该返回什么响应(should_break为true时)
+ modify_response?: (url: string, response: SimpleResponse) => SimpleResponse // 服务器响应后应该怎么改(should_break为false时)
+}
+
+/** 注册一条响应修改规则。返回一个取消函数,方便需要临时挂载的模块 */
+export function addResponseModifier(modifier: ResponseModifier): () => void {
+ modifiers.push(modifier)
+ return () => {
+ const i = modifiers.indexOf(modifier)
+ if (i >= 0) modifiers.splice(i, 1)
+ }
+}
+
+export const modifiers: ResponseModifier[] = []
+
// disable report
const blackList = [
'mcs.zijieapi.com',
@@ -22,9 +67,79 @@ function checkBlack(url: string): boolean {
return blackList.some(black => url.includes(black))
}
+// ----------------------------- 工具 -------------------------------------
+
+/** 拷贝到一个本地 realm 的精确尺寸 ArrayBuffer,避免跨 realm 共享底块 */
+function toLocalArrayBuffer(buf: ArrayBuffer | ArrayBufferView): ArrayBuffer {
+ const bytes =
+ buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
+ const out = new ArrayBuffer(bytes.byteLength)
+ new Uint8Array(out).set(bytes)
+ return out
+}
+
+function textToBody(text: string): ArrayBuffer {
+ return toLocalArrayBuffer(new TextEncoder().encode(text))
+}
+
+/** 响应体转文本。给 modifier 作者用,免得自己判断联合类型 */
+export function bodyToText(body: ArrayBuffer | string): string {
+ if (typeof body === 'string') return body
+ return new TextDecoder().decode(body)
+}
+
+/** 响应体转 ArrayBuffer。字符串按 UTF-8 编码 */
+export function bodyToBuffer(body: ArrayBuffer | string): ArrayBuffer {
+ if (typeof body === 'string') return textToBody(body)
+ return body
+}
+
+/** 把规则产出的响应归一成内部形态,只在这里做一次编码 */
+function resolveSimple(simple: SimpleResponse): ResolvedResponse {
+ return { ...simple, responseBody: bodyToBuffer(simple.responseBody) }
+}
+
+/** 命中当前 URL 的全部修饰器 */
+function matchingModifiers(url: string): ResponseModifier[] {
+ return modifiers.filter(m => m.matcher(url))
+}
+
+/** 依次套用 modify_response(只对定义了它的修饰器) */
+function applyModifiers(url: string, matched: ResponseModifier[], simple: SimpleResponse): SimpleResponse {
+ let current = simple
+ for (const m of matched) {
+ if (m.modify_response) current = m.modify_response(url, current)
+ }
+ return current
+}
+
+/** 状态码不允许带 body 的响应,构造 Response 时给 null 避免抛错 */
+function responseAllowsBody(status: number): boolean {
+ return status !== 204 && status !== 205 && status !== 304
+}
+
+function simpleToResponse(simple: SimpleResponse): Response {
+ return new Response(responseAllowsBody(simple.statusCode) ? simple.responseBody : null, {
+ status: simple.statusCode,
+ headers: simple.responseHeaders,
+ })
+}
+
+/** 真实 Response -> SimpleResponse(不消费原 body,调用方自行决定是否要克隆) */
+async function responseToSimple(res: Response): Promise {
+ const body = toLocalArrayBuffer(await res.arrayBuffer())
+ const headers: Record = {}
+ res.headers.forEach((value, key) => {
+ headers[key] = value
+ })
+ return { responseBody: body, statusCode: res.status, responseHeaders: headers }
+}
+
+// --------------------------------- fetch --------------------------------
+
const originalFetch = unsafeWindow.fetch.bind(unsafeWindow)
-unsafeWindow.fetch = function fetch(input: RequestInfo | URL, init?: RequestInit): Promise {
+unsafeWindow.fetch = async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise {
let url: string
if (input instanceof Request) {
@@ -37,27 +152,109 @@ unsafeWindow.fetch = function fetch(input: RequestInfo | URL, init?: RequestInit
if (checkBlack(url)) {
console.log('blocked request: ' + url)
- return Promise.resolve(new Response(BLOCKED_BODY, {
+ return new Response(BLOCKED_BODY, {
status: 200,
headers: { 'Content-Type': 'application/json' },
- }))
+ })
}
- return originalFetch(input, init)
+ const matched = matchingModifiers(url)
+ if (matched.length === 0) return originalFetch(input, init)
+
+ // 中断请求:不发给服务器,就地造一个响应
+ const breaker = matched.find(m => m.should_break(url))
+ if (breaker?.make_response) {
+ return simpleToResponse(breaker.make_response(url))
+ }
+
+ // 没有命中任何改写规则时就原样透传,避免为无关请求多读一遍 body
+ if (!matched.some(m => m.modify_response)) return originalFetch(input, init)
+
+ const res = await originalFetch(input, init)
+ try {
+ // 克隆后再读,原响应保留给页面(或改写失败时回退用)
+ const simple = await responseToSimple(res.clone())
+ return simpleToResponse(applyModifiers(url, matched, simple))
+ } catch (error) {
+ console.error('[fqa:fetch] 修改响应失败,回退原始响应:', error)
+ return res
+ }
}
+// ----------------------------- XMLHttpRequest -----------------------------
+
const originalXMLHttpRequest = unsafeWindow.XMLHttpRequest
+const XHR_PROTO = originalXMLHttpRequest.prototype
+
+// 把原型上的访问器/方法先抄一份,实例上做影子属性时用它绕开自身递归
+const XHR_DESC = {
+ status: Object.getOwnPropertyDescriptor(XHR_PROTO, 'status')!,
+ responseText: Object.getOwnPropertyDescriptor(XHR_PROTO, 'responseText')!,
+ response: Object.getOwnPropertyDescriptor(XHR_PROTO, 'response')!,
+}
+const XHR_GETALL = XHR_PROTO.getAllResponseHeaders as () => string
+const XHR_GETONE = XHR_PROTO.getResponseHeader as (name: string) => string | null
+
+/** 用原型方法读原始响应头,避开实例上的影子方法 */
+function readXhrHeaders(self: XMLHttpRequest): Record {
+ const headers: Record = {}
+ const all = XHR_GETALL.call(self)
+ if (!all) return headers
+ for (const line of all.trim().split(/[\r\n]+/)) {
+ const idx = line.indexOf(':')
+ if (idx > 0) {
+ headers[line.slice(0, idx).trim()] = line.slice(idx + 1).trim()
+ }
+ }
+ return headers
+}
+
+/** 用原型方法读原始响应体并按 responseType 归一成 ArrayBuffer */
+function readXhrBody(self: XMLHttpRequest): ArrayBuffer {
+ const type = self.responseType
+ if (type === 'arraybuffer') {
+ return toLocalArrayBuffer(XHR_DESC.response.get!.call(self) as ArrayBuffer)
+ }
+ if (type === 'json') {
+ // responseType=json 时 responseText 会抛错,只能拿解析后的对象再序列化
+ const parsed = XHR_DESC.response.get!.call(self)
+ return textToBody(JSON.stringify(parsed))
+ }
+ return textToBody(XHR_DESC.responseText.get!.call(self) as string)
+}
unsafeWindow.XMLHttpRequest = class XMLHttpRequest extends originalXMLHttpRequest {
private _blockedUrl?: string
+ private _fxaUrl = ''
+ private _fxaMatched: ResponseModifier[] | null = null
+ private _fxaBreaker: ResponseModifier | null = null
+ private _fxaApplied = false
+ private _fxaSimple: ResolvedResponse | null = null
open(method: string, url: string, async: boolean = true, user?: string | null, password?: string | null): void {
+ this._fxaUrl = url
+ this._fxaMatched = null
+ this._fxaBreaker = null
+ this._fxaApplied = false
+ this._fxaSimple = null
+
if (checkBlack(url)) {
console.log('blocked request: ' + url)
this._blockedUrl = url
return
}
this._blockedUrl = undefined
+
+ // 功能性的拦截/改写与 blockReport 无关,独立判定
+ const matched = matchingModifiers(url)
+ const breaker = matched.find(m => m.should_break(url))
+ if (breaker?.make_response) {
+ this._fxaBreaker = breaker
+ this._fxaMatched = matched
+ } else if (matched.some(m => m.modify_response)) {
+ this._fxaMatched = matched
+ }
+
super.open(method, url, async, user, password)
}
@@ -67,29 +264,20 @@ unsafeWindow.XMLHttpRequest = class XMLHttpRequest extends originalXMLHttpReques
}
send(body?: Document | XMLHttpRequestBodyInit | null): void {
- if (this._blockedUrl === undefined) {
- super.send(body)
+ if (this._blockedUrl !== undefined) {
+ this._synthesizeFromSimple({
+ responseBody: textToBody(BLOCKED_BODY),
+ statusCode: 200,
+ responseHeaders: { 'content-type': 'application/json' },
+ })
return
}
- const url = this._blockedUrl
- const shadow = (prop: string, value: unknown) =>
- 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)
+ if (this._fxaBreaker?.make_response) {
+ this._synthesizeFromSimple(this._fxaBreaker.make_response(this._fxaUrl))
+ return
+ }
+ if (this._fxaMatched?.length) this._armTransform()
+ super.send(body)
}
abort(): void {
@@ -108,6 +296,149 @@ unsafeWindow.XMLHttpRequest = class XMLHttpRequest extends originalXMLHttpReques
}
return super.getResponseHeader(name)
}
+
+ /**
+ * 中断请求后合成一个假响应并手动派发完成事件。
+ * 与 fetch 的 make_response 对齐,也复用了黑名单那条原本的 setTimeout 派发逻辑。
+ */
+ private _synthesizeFromSimple(raw: SimpleResponse): void {
+ const url = this._blockedUrl ?? this._fxaUrl
+ // 提前归一,下面 arraybuffer / blob 分支要的是真正的字节
+ const simple = resolveSimple(raw)
+ const headers = simple.responseHeaders
+ const shadow = (prop: string, value: unknown) =>
+ Object.defineProperty(this, prop, { configurable: true, get: () => value })
+
+ setTimeout(() => {
+ const text = bodyToText(simple.responseBody)
+ shadow('readyState', 4)
+ shadow('status', simple.statusCode)
+ shadow('statusText', 'OK')
+ shadow('responseURL', url)
+ const type = this.responseType
+ shadow('responseText', type === '' || type === 'text' ? text : '')
+
+ let value: unknown
+ if (type === 'json') {
+ try {
+ value = JSON.parse(text)
+ } catch {
+ value = text
+ }
+ } else if (type === 'arraybuffer') {
+ value = simple.responseBody
+ } else if (type === 'blob') {
+ value = new Blob([simple.responseBody])
+ } else {
+ value = text
+ }
+ shadow('response', value)
+
+ shadow('getAllResponseHeaders', () =>
+ Object.entries(headers)
+ .map(([k, v]) => `${k}: ${v}\r\n`)
+ .join('')
+ )
+ shadow('getResponseHeader', (name: string) => {
+ const lower = name.toLowerCase()
+ const key = Object.keys(headers).find(k => k.toLowerCase() === lower)
+ return key ? headers[key]! : null
+ })
+
+ this.dispatchEvent(new Event('readystatechange'))
+ this.dispatchEvent(new ProgressEvent('load'))
+ this.dispatchEvent(new ProgressEvent('loadend'))
+ }, 0)
+ }
+
+ /**
+ * 在实例上挂影子属性:等真实响应就绪(readyState=4),
+ * 页面第一次读取时才同步套用 modify_response 并缓存结果。
+ * 这样不用跟页面的 onload / onreadystatechange 争先后顺序。
+ */
+ private _armTransform(): void {
+ const type = this.responseType
+ // 这几类拿不到同步的 ArrayBuffer,或语义不明,无法改写,原样放行
+ if (type === 'blob' || type === 'document') return
+
+ const self = this
+ const matched = this._fxaMatched!
+ const url = this._fxaUrl
+
+ const ensure = () => {
+ if (self._fxaApplied || self.readyState !== 4) return
+ // 归一一次并缓存:重复读 response 时要拿到同一个 ArrayBuffer 对象,
+ // 每次现编会让 xhr.response === xhr.response 变成 false
+ self._fxaSimple = resolveSimple(applyModifiers(url, matched, {
+ responseBody: readXhrBody(self),
+ statusCode: XHR_DESC.status.get!.call(self) as number,
+ responseHeaders: readXhrHeaders(self),
+ }))
+ self._fxaApplied = true
+ }
+
+ // status
+ Object.defineProperty(this, 'status', {
+ configurable: true,
+ get() {
+ ensure()
+ if (self._fxaApplied) return self._fxaSimple!.statusCode
+ return XHR_DESC.status.get!.call(self) as number
+ },
+ })
+
+ // responseText
+ Object.defineProperty(this, 'responseText', {
+ configurable: true,
+ get() {
+ ensure()
+ const t = self.responseType
+ if (!self._fxaApplied || (t !== '' && t !== 'text')) {
+ return XHR_DESC.responseText.get!.call(self)
+ }
+ return bodyToText(self._fxaSimple!.responseBody)
+ },
+ })
+
+ // response
+ Object.defineProperty(this, 'response', {
+ configurable: true,
+ get() {
+ ensure()
+ if (!self._fxaApplied) return XHR_DESC.response.get!.call(self)
+ const body = self._fxaSimple!.responseBody
+ switch (self.responseType) {
+ case '':
+ case 'text':
+ return bodyToText(body)
+ case 'json':
+ return JSON.parse(bodyToText(body))
+ case 'arraybuffer':
+ return body
+ default:
+ return XHR_DESC.response.get!.call(self)
+ }
+ },
+ })
+
+ // getAllResponseHeaders
+ this.getAllResponseHeaders = function getAllResponseHeaders(): string {
+ ensure()
+ if (!self._fxaApplied) return XHR_GETALL.call(self)
+ return Object.entries(self._fxaSimple!.responseHeaders)
+ .map(([k, v]) => `${k}: ${v}\r\n`)
+ .join('')
+ }
+
+ // getResponseHeader
+ this.getResponseHeader = function getResponseHeader(name: string): string | null {
+ ensure()
+ if (!self._fxaApplied) return XHR_GETONE.call(self, name)
+ const lower = name.toLowerCase()
+ const key = Object.keys(self._fxaSimple!.responseHeaders).find(k => k.toLowerCase() === lower)
+ return key ? self._fxaSimple!.responseHeaders[key]! : null
+ }
+ }
}
// 执行期hook,所以不用显式声明事件类hook
diff --git a/src/hooks/readerHook.ts b/src/hooks/readerHook.ts
index 19363d1..4e2b9f0 100644
--- a/src/hooks/readerHook.ts
+++ b/src/hooks/readerHook.ts
@@ -5,11 +5,14 @@ import { applyBookCss } from '../api/bookcss'
import { processFootnotes, bindFootnoteInteraction } from '../utils/footnote'
import { settings } from '../settings'
// import defaultcss from '../assets/default.css?raw';
-import { cloneElement } from '../utils';
+import { cloneElement, sleep } from '../utils';
import { fetchArrayBuffer } from '../utils/request';
import { type Book } from '../types'
import { decryptComicImage } from '../crypto/content';
import { initAudioPanel } from '../audioPanel'
+import { addToBookshelf, removeFromBookshelf, isInBookshelf } from '../api/bookshelf'
+import { addResponseModifier } from './fetchHook'
+import { userState } from '../api/user'
import {
closeAudiobook,
openAudiobook,
@@ -23,12 +26,77 @@ let currentBook: Book | null = null
let latestItemId: string | null = null
+let currentChapterWithContent: any = null
+
/** 脚本接管的正文容器 id。文字章与漫画章共用同一个,切章时复用 */
const SCRIPT_CONTAINER_ID = 'fqa-reader-content'
/** 漫画懒加载观察器,切章时要换掉旧的 */
let comicObserver: IntersectionObserver | null = null
+/*
+content-length
+39
+content-type
+application/json; charset=utf-8
+date
+Wed, 16 Sep 2026 08:58:41 GMT
+server
+volc-dcdn
+server-timing
+inner; dur=49, cdn-cache;desc=MISS, origin;dur=63, edge;dur=14, cdn-cache;desc=MISS, tt_agw; dur=45
+strict-transport-security
+max-age=63072000; includeSubDomains; preload
+tt-idc-switch
+10000@20260825024420
+tt_stable
+1
+via
+n172-005-142.sjzmp-agg.Creative,n173-115-143.nxzhongwei-mp01-cu.Creative
+x-agw-info
+tZz4wrQl57dt0wY2pTAgR2f48x1mv_TN71v9oOmqjxWS7rul6BjF9t8eZSMnCnTsdvrqH1P6-ELR41Ql_zMwMP_kZavRq2UkjiwjSI7ptI8EHk6qozYlJnJbaqiDWW2sO1RAbsmbFxPsUzXSsp3aUmh7JEMxEBn9_MgD_N49ZXPnqH1EJbjVod0JRshPQPmIzfsU_1PzITtNH20Wb96wrScoWjk=
+x-dsa-origin-status
+200
+x-dsa-trace-id
+17895491212d5366f9d0c2c959ffbc745c0148e43f
+x-request-ip
+2408:847a:718:cd2:20ed:81:8e36:a911
+x-tt-agw-login
+1
+x-tt-logid
+20260916165841775622D9BB198F0EF9C9
+x-tt-trace-host
+014917046478eae01e5ce9f4b5991634e2c2ac3d6c4ea331b6945fee3151af03e55859798c9e8c898e5508e08f78fb380fa48cd479a7ff5f96a2ef74435c7f420dcf4eb744b2aaca36db660c94c334414c30ce2bd99c166030189372edcc8a00b77f30aabb4f0447fece571b3a3c22a3de373bf420de4c924468d754783af3d783
+x-tt-trace-id
+00-260916165841775622D9BB198F0EF9C9-3157EADB1F76429E-00
+x-tt-trace-tag
+id=5
+*/
+
+// 确保脚本clone的书架按钮是干净不受账号状态影响的
+// 没登录不会请求这个api,所以不影响
+addResponseModifier({
+ matcher: (url: string) =>
+ url.indexOf('/reading/bookapi/bookshelf/check/') !== -1
+ && location.pathname.startsWith('/reader'),
+ should_break: () => true,
+ make_response: () => {
+ const body = JSON.stringify({
+ code: 0,
+ message: 'SUCCESS',
+ data: 0
+ })
+ return {
+ responseBody: body,
+ statusCode: 200,
+ responseHeaders: {
+ 'content-type': 'application/json; charset=utf-8',
+ 'content-length': String(new TextEncoder().encode(body).length)
+ }
+ }
+ },
+})
+
/**
* 取到脚本容器:没有就克隆一个插到原容器前面,有就复用并清空。
*
@@ -75,6 +143,7 @@ async function insertContent() {
return
}
console.log('Chapter:', chapter)
+ currentChapterWithContent = chapter
const pageState: any = (unsafeWindow as typeof unsafeWindow & {
__INITIAL_STATE__?: { reader?: { chapterData?: { title?: string } } }
@@ -292,7 +361,9 @@ async function insertContent() {
}
// 写入更精确的更新时间
let updateTimeSpans = muyeReaderSubtitle?.querySelectorAll('span.desc-item') || []
- if (updateTimeSpans.length >= 2 /* 应该是始终为3 */) {
+ console.log('spans len', updateTimeSpans.length, 'assertIsOffshelf', updateTimeSpans.length < 3)
+ if (updateTimeSpans.length >= 3 /* 应该是始终为3 */) {
+ console.log('if')
/* 最后一个一般是更新时间 */
let updateTimeSpan = updateTimeSpans[updateTimeSpans.length - 1] as HTMLSpanElement
let uttspan = updateTimeSpan.firstChild as HTMLSpanElement
@@ -303,11 +374,31 @@ async function insertContent() {
'更新时间:' +
currentChapterItem.update_time
} else {
+ console.log('else')
// 网页端屏蔽章节。只有一个本章字数 + 前面插入的卷名
- let updateTimeSpan = document.createElement('span')
+ // 有个默认显示的 0 字,给它删了
+ updateTimeSpans[updateTimeSpans.length - 1]?.remove()
+ const updateTimeSpan = document.createElement('span')
// let updateTimeSubSpan = document.createElement('span')
updateTimeSpan.className = 'desc-item'
updateTimeSpan.textContent = `更新时间:${currentChapterItem.update_time}`
+ console.log('assert equal', currentChapterItem.item_id === currentChapterWithContent?.novel_data?.item_id)
+ console.log('wordcnt', currentChapterWithContent?.novel_data?.chapter_word_number)
+ const c = document.getElementById('fqa-current-chapter-volume')
+ let b: HTMLSpanElement | null = null;
+ if (currentChapterItem.item_id === currentChapterWithContent?.novel_data?.item_id) {
+ const wordCntSpan = document.createElement('span')
+ wordCntSpan.className = 'desc-item'
+ wordCntSpan.textContent = `本章字数:${currentChapterWithContent?.novel_data?.chapter_word_number}字`
+ console.log(wordCntSpan)
+ if (c) {
+ console.log('insert wordcnt')
+ c.insertAdjacentElement('afterend', wordCntSpan)
+ }
+ b = wordCntSpan
+ }
+ console.log('insert update time');
+ (b || c)?.insertAdjacentElement('afterend', updateTimeSpan)
}
}
}
@@ -352,6 +443,14 @@ async function onHashChange(_previous?: string): Promise {
// TODO: 支持从hash里解析并跳转到指定行
}
async function onLoad(): Promise {
+ async function fetchBookInfo(): Promise {
+ const pageState: any = (unsafeWindow as typeof unsafeWindow & {
+ __INITIAL_STATE__?: { reader?: { chapterData?: { title?: string } } }
+ }).__INITIAL_STATE__
+ const bid = pageState?.reader?.chapterData?.bookId
+ currentBook = await getBookInfoAndCatalog(bid)
+ }
+ void fetchBookInfo() // 后台继续执行
/*
if (!document.getElementById('fqa-inject-css-reader')) {
const style = document.createElement('style')
@@ -382,6 +481,54 @@ async function onLoad(): Promise {
c.addEventListener('click', () => void startAudioPlay())
toolbar.appendChild(c)
}
+ const bookshelfButton = document.querySelector("div.reader-toolbar-item") // 第一个按钮一定是书架状态
+ // 没登录的话就走网页原线路,会要求登录
+ if (userState.isLogin && bookshelfButton && bookshelfButton.innerHTML.includes('书架')) {
+ const shelf = cloneElement(bookshelfButton)
+ bookshelfButton.replaceWith(shelf)
+
+ let inShelf = false
+ let ready = false
+ let busy = false
+
+ async function ensureBookshelfState(): Promise {
+ while (!currentBook) {
+ // 等 fetchBookInfo 飞回来
+ await sleep(50)
+ }
+ inShelf = await isInBookshelf(currentBook.book_id)
+ shelf.classList.toggle('reader-toolbar-item-disabled', inShelf)
+ const label = shelf.lastChild
+ if (label) label.textContent = inShelf ? '已在书架' : '加入书架'
+ ready = true
+ }
+ void ensureBookshelfState()
+
+ shelf.addEventListener('click', async () => {
+ const book = currentBook
+ // 状态还没同步好 / 书籍信息还没到,先不响应
+ if (!ready || busy || !book) return
+ busy = true
+ try {
+ if (inShelf) {
+ // 已加书架,按设置决定是否带确认移出书架
+ if (settings.shelfRemoveConfirm) {
+ if (!unsafeWindow.confirm(`确定要把《${book.title ?? '这本书'}》从书架移出吗?`)) return
+ }
+ await removeFromBookshelf(book.book_id)
+ } else {
+ // 未加书架,直接加入书架
+ await addToBookshelf(book.book_id)
+ }
+ await ensureBookshelfState()
+ } catch (error) {
+ console.error('[fqa:reader] 书架操作失败:', error)
+ unsafeWindow.alert(error instanceof Error ? error.message : '书架操作失败')
+ } finally {
+ busy = false
+ }
+ })
+ }
const btns = document.querySelector("div.muye-reader-btns") // single div
if (btns) {
// TODO: 直接覆写按钮行为,切章节由脚本完成
diff --git a/src/hooks/userHook.ts b/src/hooks/userHook.ts
index f5ff130..d6e7fae 100644
--- a/src/hooks/userHook.ts
+++ b/src/hooks/userHook.ts
@@ -4,6 +4,7 @@ import bookshelf from '../assets/bookshelf.svg?raw'
import settings from '../assets/settings.svg?raw'
import { openSettings } from '../settingsPanel'
import { cloneElement } from '../utils'
+import { settings as userSettings } from '../settings'
function formatReadingTime(readBookTime: bigint): string {
let minutes = readBookTime / 60_000n
@@ -77,6 +78,19 @@ async function mainHook(_previous?: string): Promise {
secondDiv.insertAdjacentElement('afterend', thirdDiv)
// 助手设置固定放在菜单最后
menuInner.appendChild(createMenuItem('助手设置', settings, openSettings))
+ menuInner.childNodes.forEach((node) => {
+ if (node.lastChild?.textContent?.trim() === '退出登录') {
+ const original = node as HTMLElement
+ const replaced = cloneElement(node as Element)
+ original.classList.add('fqa-hide')
+ original.insertAdjacentElement('afterend', replaced)
+ replaced.addEventListener('click', (_) => {
+ if (!userSettings.logoutConfirm || unsafeWindow.confirm('确认退出登录吗?')) {
+ original.click()
+ }
+ })
+ }
+ })
}
const scan = (root: HTMLElement) => {
diff --git a/src/settings.ts b/src/settings.ts
index 05de70b..e15c8e2 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -26,6 +26,10 @@ export interface Settings {
blockReport: boolean
/** 允许阅读器复制文本 */
allowCopy: boolean
+ /** 阅读页移出书架前弹确认框 */
+ shelfRemoveConfirm: boolean
+ /** 退出登录前弹确认框 */
+ logoutConfirm: boolean
/* --- 界面 --- */
/** 阅读器字体,空字符串表示跟随页面默认 */
@@ -84,6 +88,8 @@ export const DEFAULT_SETTINGS: Settings = {
decryptFont: true,
blockReport: true,
allowCopy: true,
+ shelfRemoveConfirm: true,
+ logoutConfirm: true,
readerFont: '',
customCssEnabled: false,
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
index 2ccac2d..83e1fdc 100644
--- a/src/views/SettingsView.vue
+++ b/src/views/SettingsView.vue
@@ -121,6 +121,16 @@ const GITHUB = 'https://github.com/naiyQAQ/fanqie-assistant'
允许阅读器复制文本
+
+
+
+