diff --git a/inject/qd.js b/inject/qd.js new file mode 100644 index 0000000..e9aba1e --- /dev/null +++ b/inject/qd.js @@ -0,0 +1 @@ +console.clear = function () {}; diff --git a/manifest.json b/manifest.json index 317b6f4..2428717 100644 --- a/manifest.json +++ b/manifest.json @@ -30,6 +30,17 @@ "dist/qdchapter.js" ], "run_at": "document_start" + }, + { + "matches": [ + "https://*.qidian.com/*", + "http://*.qidian.com/*" + ], + "js": [ + "inject/qd.js" + ], + "run_at": "document_start", + "world": "MAIN" } ], "options_ui": { diff --git a/package.py b/package.py index ae9c64b..8159292 100644 --- a/package.py +++ b/package.py @@ -4,6 +4,7 @@ import os NEED_PACKED = [ 'dist', 'ico', + 'inject', 'manifest.json', 'LICENSE', ] diff --git a/src/db/indexedDb.ts b/src/db/indexedDb.ts new file mode 100644 index 0000000..ebe77c5 --- /dev/null +++ b/src/db/indexedDb.ts @@ -0,0 +1,115 @@ +import { IndexedDbConfig } from "../config"; +import type { QdChapterInfo } from "../types"; +import { compress } from "../utils"; + +async function make_storage_persist() { + const persisted = await navigator.storage.persisted(); + if (!persisted) { + await navigator.storage.persist(); + } +} + +async function save_data(db: IDBDatabase, storeName: string, data: T, key?: IDBValidKey) { + return new Promise((resolve, reject) => { + const tx = db.transaction(storeName, 'readwrite'); + const store = tx.objectStore(storeName); + const req = key !== undefined ? store.put(data, key) : store.put(data); + req.onsuccess = () => { + resolve(req.result); + } + req.onerror = () => { + reject(req.error); + } + }); +} + +type CompressedQdChapterInfo = { + compressed: Uint8Array; + bookId: number; + id: number; + time: number; +} + +export class IndexedDb { + compress: boolean; + _qddb?: IDBDatabase; + constructor(cfg: IndexedDbConfig) { + this.compress = cfg.compress; + } + get qddb() { + if (!this._qddb) { + throw new Error('Database not initialized'); + } + return this._qddb; + } + init_qddb() { + return new Promise((resolve, reject) => { + let dbreq = indexedDB.open('qd', 1); + dbreq.onupgradeneeded = function (event) { + let db = this.result; + console.log('Upgrading qd database from version', event.oldVersion, 'to', event.newVersion); + const nan = isNaN(event.oldVersion); + if (nan || event.oldVersion < 1) { + db.createObjectStore('books', { keyPath: 'id' }); + const chapters = db.createObjectStore('chapters', { keyPath: ['id', 'bookId', 'time'] }); + chapters.createIndex('bookId', 'bookId'); + chapters.createIndex('id', 'id'); + } + } + dbreq.onerror = () => { + reject(dbreq.error); + } + dbreq.onsuccess = () => { + this._qddb = dbreq.result; + resolve(); + } + }); + } + async init() { + make_storage_persist(); + await this.init_qddb(); + } + async saveQdChapter(info: QdChapterInfo) { + if (this.compress) { + const data = JSON.stringify(info); + const encoded = new TextEncoder().encode(data); + const compressed = await compress(encoded); + const compressedInfo: CompressedQdChapterInfo = { + compressed, + bookId: info.bookId, + id: info.id, + time: info.time, + } + await save_data(this.qddb, 'chapters', compressedInfo); + } else { + await save_data(this.qddb, 'chapters', info); + } + } + close() { + this.qddb.close(); + } +} + +function deleteIndexedDb(name: string) { + return new Promise((resolve, reject) => { + const req = indexedDB.deleteDatabase(name); + req.onsuccess = () => { + resolve(); + } + req.onerror = () => { + reject(req.error); + } + req.onblocked = () => { + reject(new Error('Delete blocked')); + } + }); +} + + +export async function clearIndexedDb() { + for (const info of await indexedDB.databases()) { + if (info?.name) { + await deleteIndexedDb(info.name); + } + } +} diff --git a/src/db/interfaces.ts b/src/db/interfaces.ts new file mode 100644 index 0000000..d38a4c2 --- /dev/null +++ b/src/db/interfaces.ts @@ -0,0 +1,21 @@ +import { DbConfig, DbType } from "../config"; +import { IndexedDb } from "./indexedDb"; +import type { QdChapterInfo } from "../types"; + +export interface Db { + init(): Promise; + saveQdChapter(info: QdChapterInfo): Promise; + close(): void; +} + +export async function createDb(): Promise { + const config = new DbConfig(); + await config.init(); + switch (config.DbType) { + case DbType.IndexedDb: + const db = new IndexedDb(config.IndexedDb); + return db; + default: + throw new Error('Unsupported database type'); + } +} diff --git a/src/models/QdChatperInfo.tsx b/src/models/QdChatperInfo.tsx index e9d8eef..796ef8d 100644 --- a/src/models/QdChatperInfo.tsx +++ b/src/models/QdChatperInfo.tsx @@ -1,16 +1,17 @@ import React from 'react'; import { Descriptions, Card, Typography, Tag, Space, Button } from 'antd'; -import type { BookInfo, ChapterInfo } from '../qdtypes'; +import type { QdChapterInfo } from '../types'; import { get_chapter_content, saveAsFile } from '../utils'; +import { createDb } from '../db/interfaces'; const { Title, Text, Paragraph } = Typography; interface QdChapterInfoProps { - bookInfo: BookInfo; - chapterInfo: ChapterInfo; + info: QdChapterInfo; } -export default function QdChapterInfo({ bookInfo, chapterInfo }: QdChapterInfoProps) { +export default function QdChapterInfo({ info }: QdChapterInfoProps) { + const { chapterInfo, bookInfo, contents } = info; // 格式化更新时间 const formatTime = (timestamp: number) => { return new Date(timestamp).toLocaleString('zh-CN'); @@ -34,7 +35,7 @@ export default function QdChapterInfo({ bookInfo, chapterInfo }: QdChapterInfoPr return {status}; }; - const chapters = get_chapter_content(chapterInfo.content); + const chapters = contents ?? get_chapter_content(chapterInfo.content); function saveAsTxt() { const chapterName = chapterInfo.chapterName.replace(/[\/\\?%*:|"<>]/g, '_'); @@ -44,6 +45,13 @@ export default function QdChapterInfo({ bookInfo, chapterInfo }: QdChapterInfoPr saveAsFile(filename, content); } + async function saveToDb() { + const db = await createDb(); + await db.init(); + await db.saveQdChapter(info); + db.close(); + } + return ( {/* 章节信息 */} @@ -73,6 +81,7 @@ export default function QdChapterInfo({ bookInfo, chapterInfo }: QdChapterInfoPr {/* 操作区域*/} + {/* 章节导航信息 */} diff --git a/src/popup.tsx b/src/popup.tsx index e69840f..19f8cf6 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; -import type { Message } from "./types"; +import type { Message, QdChapterInfo } from "./types"; import { getCurrentTab, parseUrlParams, sendMessageToTab } from "./utils"; import * as styles from "./popup.module.css"; import { Spin, Result } from "antd"; -import QdChapterInfo from "./models/QdChatperInfo"; +import QdChapterInfoModel from "./models/QdChatperInfo"; function PopupBody() { const [result, setResult] = useState(null); @@ -58,8 +58,10 @@ function PopupBody() { if (result) { console.log(result); if (result.ok && result.body?.type === 'QdChapterInfo') { - const { bookInfo, chapterInfo } = result.body; - return ; + const body: QdChapterInfo = result.body; + /**@ts-ignore*/ + delete body.type; + return ; } return ; } diff --git a/src/qdchapter.ts b/src/qdchapter.ts index 6aa374f..98eb854 100644 --- a/src/qdchapter.ts +++ b/src/qdchapter.ts @@ -19,6 +19,15 @@ function load() { } } +function getContents() { + const datas: string[] = []; + document.querySelectorAll('span.content-text').forEach(span => { + const e = span as HTMLElement; + datas.push(e.innerText); + }) + return datas; +} + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { const m = message as SendMessage; try { @@ -34,13 +43,25 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { sendResponse(msg); return; } + const chapterInfo = pageData.pageContext.pageProps.pageData.chapterInfo; + const bookInfo = pageData.pageContext.pageProps.pageData.bookInfo; + let contents: string[] | undefined = undefined; + if (chapterInfo.vipStatus !== 0) { + contents = getContents(); + // Clear encrypted content to reduce size. + chapterInfo.content = ''; + } const msg: Message = { ok: true, code: 0, body: { type: 'QdChapterInfo', - chapterInfo: pageData.pageContext.pageProps.pageData.chapterInfo, - bookInfo: pageData.pageContext.pageProps.pageData.bookInfo, + chapterInfo, + bookInfo, + bookId: bookInfo.bookId, + id: chapterInfo.chapterId, + contents, + time: Date.now(), }, for: m.type, }; diff --git a/src/types.ts b/src/types.ts index 0e9c98f..ff08181 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,13 @@ export type DiscriminatedUnion< export type QdChapterInfo = { chapterInfo: QdTypes.ChapterInfo; bookInfo: QdTypes.BookInfo; + bookId: number; + /**Chapter ID */ + id: number; + /**Decrypted contents of the chapter. May not obtained if chapter is a free chapter */ + contents?: string[]; + /**Timestamp of the chapter */ + time: number; } export type SendMessageMap = {