import { type HookConfig } from '../config' import { getChapter } from '../api/content' import { getBookInfoAndCatalog } from '../api/book' import { decryptComicImage } from '../utils/crypto' import { fetch } from '../config' // import defaultcss from '../assets/default.css?raw'; import { cloneElement } from '../utils'; import { type Book } from '../types' // import moment from 'moment' let currentBook: Book | null = null let latestItemId: string | null = null async function insertContent() { const itemId = window.location.pathname.split('/').pop()?.substring(0, 19) || '' if (!itemId) { console.warn('No item_id found in URL') return } latestItemId = itemId const chapter = await getChapter(itemId) if (!chapter) { console.warn('No chapter found for item_id:', itemId) return } if (latestItemId !== itemId) { console.debug('Stale chapter response discarded:', itemId) return } console.log('Chapter:', chapter) const pageState: any = (unsafeWindow as typeof unsafeWindow & { __INITIAL_STATE__?: { reader?: { chapterData?: { title?: string } } } }).__INITIAL_STATE__ const chapterTitle = chapter.novel_data?.title || pageState?.reader?.chapterData?.title if (typeof chapter.content === 'string') { // TODO: 支持部分书籍的CSS内容 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 for (let i = 0; i < toProcess.childNodes.length; i++) { if (i < 1 && (toProcess.childNodes[i] as HTMLElement)?.innerHTML.includes(chapterTitle)) { // remove duplicate title toProcess.removeChild(toProcess.childNodes[i] as HTMLElement) } } const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)") if (readerContainer) { // 防止插入多次 let scriptContainer = document.getElementById('fqa-reader-content') as HTMLDivElement | null if (!scriptContainer) { scriptContainer = cloneElement(readerContainer) as HTMLDivElement scriptContainer.id = 'fqa-reader-content' scriptContainer.classList.add('fqa') scriptContainer.classList.remove('noselect') // allow text selection readerContainer.insertAdjacentElement('beforebegin', scriptContainer) } scriptContainer.innerHTML = '' // 隐藏原来的内容div readerContainer.classList.add('fqa-hide') scriptContainer.appendChild(toProcess) } } else if (chapter.content.picInfos) { // comic if (chapter.content.encrypt) { // 加密漫画图片:懒加载解密 const imgs: HTMLImageElement[] = [] for (let i = 0; i < chapter.content.picInfos.length; i++) { const picInfo = chapter.content.picInfos[i] const img = document.createElement('img') img.className = 'fqa-comic-img fqa-comic-encrypted' img.alt = `第${i + 1}页` img.dataset.encryptedUrl = picInfo.picUrl img.dataset.encryptKey = chapter.content.encrypt_key img.dataset.pageIndex = i.toString() // 占位符:加载中 img.style.minHeight = '500px' img.style.backgroundColor = '#f0f0f0' imgs.push(img) } const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)") if (readerContainer) { let scriptContainer = document.getElementById('fqa-comic-content') as HTMLDivElement | null if (!scriptContainer) { scriptContainer = cloneElement(readerContainer) as HTMLDivElement scriptContainer.id = 'fqa-reader-content' scriptContainer.classList.add('fqa') scriptContainer.classList.add('fqa-comic-reader') scriptContainer.classList.remove('noselect') readerContainer.insertAdjacentElement('beforebegin', scriptContainer) } scriptContainer.innerHTML = '' readerContainer.classList.add('fqa-hide') imgs.forEach(img => scriptContainer.appendChild(img)) const observer = new IntersectionObserver( async (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const img = entry.target as HTMLImageElement if (img.dataset.encryptedUrl && img.dataset.encryptKey && !img.src) { observer.unobserve(img) // 只解密一次 try { const response = await fetch(img.dataset.encryptedUrl) const encryptedBuffer = await response.arrayBuffer() const decryptedBuffer = await decryptComicImage( encryptedBuffer, img.dataset.encryptKey ) const blob = new Blob([decryptedBuffer], { type: 'image/jpeg' }) const blobUrl = URL.createObjectURL(blob) img.src = blobUrl img.style.minHeight = '' img.style.backgroundColor = '' img.onload = () => { URL.revokeObjectURL(blobUrl) } } catch (error) { console.error(`解密图片失败 (页 ${img.dataset.pageIndex}):`, error) img.alt = `第${Number(img.dataset.pageIndex) + 1}页 - 解密失败` img.style.backgroundColor = '#ffebee' } } } } }, { rootMargin: '200px' } ) imgs.forEach(img => observer.observe(img)) } } else { const imgs: HTMLImageElement[] = [] for (let i = 0; i < chapter.content.picInfos.length; i++) { const picInfo = chapter.content.picInfos[i] const img = document.createElement('img') img.className = 'fqa-comic-img' img.alt = `第${i + 1}页` img.src = picInfo.picUrl imgs.push(img) } const readerContainer = document.querySelector("div.muye-reader-content:not(.fqa)") if (readerContainer) { // 防止插入多次 let scriptContainer = document.getElementById('fqa-comic-content') as HTMLDivElement | null if (!scriptContainer) { scriptContainer = cloneElement(readerContainer) as HTMLDivElement scriptContainer.id = 'fqa-reader-content' scriptContainer.classList.add('fqa') scriptContainer.classList.add('fqa-comic-reader') scriptContainer.classList.remove('noselect') readerContainer.insertAdjacentElement('beforebegin', scriptContainer) } scriptContainer.innerHTML = '' // 隐藏原来的内容div readerContainer.classList.add('fqa-hide') imgs.forEach(img => scriptContainer.appendChild(img)) } } } const muyeReaderTitle = document.querySelector("h1.muye-reader-title") let muyeReaderSubtitle = document.querySelector("div.muye-reader-subtitle") 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' muyeReaderSubtitle.insertAdjacentElement('afterend', _cloned) muyeReaderSubtitle = _cloned _cloned.classList.remove('fqa-hide') console.log('clone subtitle: ', _cloned) } if (muyeReaderTitle) { muyeReaderTitle.textContent = chapterTitle } //