feat & refactor: 支持app端签名的请求,支持右键操作书架书籍,逻辑大幅重构,更改默认的设备逻辑为自动注册,支持载入某些书籍内置的CSS、支持跳转正文注释等等

This commit is contained in:
naiy committed 2026-08-03 16:17:49 +08:00
1 parent 2f2ee606c0
commit 3586b01dbb
47 files changed
+3594 -243

No files matched your search

+2 -1
View File
@@ -3,6 +3,7 @@ import type { HookConfig } from '../config'
import BookshelfView from '../views/BookshelfView.vue'
import bookshelfcss from '../assets/bookshelf.css?raw'
import { waitForElement } from '../utils'
import { userState } from '../api/user'
const CONTAINER_ID = 'fqa-bookshelf-root'
const STYLE_ID = 'fqa-bookshelf-style'
@@ -102,7 +103,7 @@ async function mainHook(_previous?: string): Promise<void> {
}
function filter(path: string, _query: URLSearchParams, _hash: string) {
return isBookshelfPath(path) || !!app
return (isBookshelfPath(path) || !!app) && userState.isLogin
}
const _exports: HookConfig[] = [
+3
View File
@@ -1,4 +1,5 @@
import type { HookConfig } from "../config"
import { settings } from "../settings"
// disable report
const blackList = [
@@ -16,6 +17,8 @@ const BLOCKED_BODY = JSON.stringify({
})
function checkBlack(url: string): boolean {
// 设置里关掉后即时放行,无需刷新页面
if (!settings.blockReport) return false
return blackList.some(black => url.includes(black))
}
+27 -9
View File
@@ -1,11 +1,14 @@
import { type HookConfig } from '../config'
import { getChapter } from '../api/content'
import { getBookInfoAndCatalog } from '../api/book'
import { decryptComicImage } from '../utils/crypto'
import { applyBookCss } from '../api/bookcss'
import { processFootnotes, bindFootnoteInteraction } from '../utils/footnote'
import { settings } from '../settings'
import { fetch } from '../config'
// import defaultcss from '../assets/default.css?raw';
import { cloneElement } from '../utils';
import { type Book } from '../types'
import { decryptComicImage } from '../crypto/content';
// import moment from 'moment'
let currentBook: Book | null = null
@@ -36,20 +39,34 @@ async function insertContent() {
}).__INITIAL_STATE__
const chapterTitle = chapter.novel_data?.title || pageState?.reader?.chapterData?.title
if (typeof chapter.content === 'string') {
// TODO: 支持部分书籍的CSS内容
// 书籍自带的排版样式(css_map),作用域限定在正文容器内后注入
void applyBookCss(chapter.novel_data?.css_map, '#fqa-reader-content')
const dp = new DOMParser()
const doc = dp.parseFromString(chapter.content, 'text/html')
const body = doc.body
const article = body.querySelector('article')
const toProcess = article || body
// XHTML 里的 <link href="Styles/xxx.css"> 是 EPUB 相对路径,页面里解析不到,
// 留着只会产生 404 请求,样式已由 applyBookCss 处理
body.querySelectorAll('link[rel="stylesheet"]').forEach((el) => el.remove())
let article = body.querySelector('article')
let toProcess = article || body
// 章内注释:把裂图标记换成可点的上标序号,并重排文末注释列表
processFootnotes(toProcess)
for (let i = 0; i < toProcess.childNodes.length; i++) {
if (i < 1 && (toProcess.childNodes[i] as HTMLElement)?.innerHTML.includes(chapterTitle)) {
// console.log('Child node:', toProcess.childNodes[i] as HTMLElement)
if (i < 2 && (toProcess.childNodes[i] as HTMLElement)?.innerHTML?.includes(chapterTitle)) {
// remove duplicate title
toProcess.removeChild(toProcess.childNodes[i] as HTMLElement)
break
}
}
if (!article) {
article = document.createElement('article')
article.innerHTML = toProcess.innerHTML
toProcess = article
}
const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)")
if (readerContainer) {
// 防止插入多次
@@ -58,13 +75,14 @@ async function insertContent() {
scriptContainer = cloneElement(readerContainer) as HTMLDivElement
scriptContainer.id = 'fqa-reader-content'
scriptContainer.classList.add('fqa')
scriptContainer.classList.remove('noselect') // allow text selection
if (settings.allowCopy) scriptContainer.classList.remove('noselect')
readerContainer.insertAdjacentElement('beforebegin', scriptContainer)
}
scriptContainer.innerHTML = ''
// 隐藏原来的内容div
readerContainer.classList.add('fqa-hide')
scriptContainer.appendChild(toProcess)
bindFootnoteInteraction(scriptContainer)
}
} else if (chapter.content.picInfos) { // comic
if (chapter.content.encrypt) {
@@ -92,7 +110,7 @@ async function insertContent() {
scriptContainer.id = 'fqa-reader-content'
scriptContainer.classList.add('fqa')
scriptContainer.classList.add('fqa-comic-reader')
scriptContainer.classList.remove('noselect')
if (settings.allowCopy) scriptContainer.classList.remove('noselect')
readerContainer.insertAdjacentElement('beforebegin', scriptContainer)
}
scriptContainer.innerHTML = ''
@@ -156,7 +174,7 @@ async function insertContent() {
scriptContainer.id = 'fqa-reader-content'
scriptContainer.classList.add('fqa')
scriptContainer.classList.add('fqa-comic-reader')
scriptContainer.classList.remove('noselect')
if (settings.allowCopy) scriptContainer.classList.remove('noselect')
readerContainer.insertAdjacentElement('beforebegin', scriptContainer)
}
scriptContainer.innerHTML = ''
@@ -170,7 +188,7 @@ async function insertContent() {
let muyeReaderSubtitle = document.querySelector("div.muye-reader-subtitle")
document.querySelector('#fqa-subtitle')?.remove() // prevent duplicate subtitle
if (muyeReaderSubtitle) {
// 脱离原页面 Vue tree ,防止被二次修改
// 脱离原页面防止被二次修改
let _cloned = cloneElement(muyeReaderSubtitle)
muyeReaderSubtitle.classList.add('fqa-hide')
_cloned.id = 'fqa-subtitle'
+63
View File
@@ -1,6 +1,8 @@
import { getDetailedUserInfo, userState } from '../api/user'
import type { HookConfig } from '../config'
import bookshelf from '../assets/bookshelf.svg?raw'
import { openSettings } from '../settingsPanel'
import { cloneElement } from '../utils'
function formatReadingTime(readBookTime: bigint): string {
let minutes = readBookTime / 60_000n
@@ -26,6 +28,12 @@ function createMenuItem(text: string, icon?: string, onclick?: () => void): HTML
if (icon) {
const iconDiv = document.createElement('div')
iconDiv.classList.add('slogin-user-avatar__menu-item__icon')
if (document.querySelector('div.muye-reader-dark')) {
console.log('isdark')
iconDiv.classList.add('fqa-icon-dark')
} else {
console.log('islight')
}
iconDiv.innerHTML = icon
inner1.appendChild(iconDiv)
}
@@ -62,6 +70,8 @@ async function mainHook(_previous?: string): Promise<void> {
window.open('https://fanqienovel.com/bookshelf', '_blank')
})
secondDiv.insertAdjacentElement('afterend', thirdDiv)
// 助手设置固定放在菜单最后
menuInner.appendChild(createMenuItem('助手设置', undefined, openSettings))
}
const scan = (root: HTMLElement) => {
@@ -89,16 +99,69 @@ async function mainHook(_previous?: string): Promise<void> {
scan(document.body)
}
/**
* 未登录时没有用户菜单,改为复制「注册」按钮做一个「助手设置」入口。
*/
async function guestHook(_previous?: string): Promise<void> {
const injected = new WeakSet<HTMLElement>()
const inject = (registerBtn: HTMLElement) => {
const parent = registerBtn.parentElement
if (!parent || injected.has(parent)) return
if (parent.querySelector('.fqa-settings-entry')) return
injected.add(parent)
const entry = cloneElement(registerBtn)
entry.classList.add('fqa-settings-entry')
entry.textContent = '助手设置'
entry.addEventListener('click', event => {
// 原按钮带跳转逻辑,别让它继续冒泡
event.preventDefault()
event.stopPropagation()
openSettings()
})
registerBtn.insertAdjacentElement('afterend', entry)
}
const scan = (root: ParentNode) => {
root.querySelectorAll<HTMLElement>('.slogin-user-avatar__buttons__item').forEach(el => {
if (el.classList.contains('fqa-settings-entry')) return
if (el.textContent?.trim() === '注册') inject(el)
})
}
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement) scan(node)
}
}
})
observer.observe(document.body, { childList: true, subtree: true })
scan(document)
}
function filter(path: string, _query: URLSearchParams, _hash: string): boolean {
return userState.isLogin && !path.startsWith('/writer') && !path.startsWith('/welfare')
}
function guestFilter(path: string, _query: URLSearchParams, _hash: string): boolean {
return !userState.isLogin && !path.startsWith('/writer') && !path.startsWith('/welfare')
}
const _exports: HookConfig[] = [
{
id: 'userHook',
event: 'load',
filter,
handler: mainHook
},
{
id: 'userHook_guest',
event: 'load',
filter: guestFilter,
handler: guestHook
}
]