mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-07-08 01:31:31 +08:00
Add support to save chapter to databases
This commit is contained in:
115
src/db/indexedDb.ts
Normal file
115
src/db/indexedDb.ts
Normal file
@@ -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<T>(db: IDBDatabase, storeName: string, data: T, key?: IDBValidKey) {
|
||||
return new Promise<IDBValidKey>((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<void>((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<void>((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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/db/interfaces.ts
Normal file
21
src/db/interfaces.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { DbConfig, DbType } from "../config";
|
||||
import { IndexedDb } from "./indexedDb";
|
||||
import type { QdChapterInfo } from "../types";
|
||||
|
||||
export interface Db {
|
||||
init(): Promise<void>;
|
||||
saveQdChapter(info: QdChapterInfo): Promise<void>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export async function createDb(): Promise<Db> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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 <Tag color={statusMap[status] || 'default'}>{status}</Tag>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{/* 章节信息 */}
|
||||
@@ -73,6 +81,7 @@ export default function QdChapterInfo({ bookInfo, chapterInfo }: QdChapterInfoPr
|
||||
{/* 操作区域*/}
|
||||
<Card title="操作" size="small">
|
||||
<Button type="primary" onClick={saveAsTxt}>保存为文本文件</Button>
|
||||
<Button onClick={saveToDb} style={{ marginLeft: 8 }}>保存到数据库</Button>
|
||||
</Card>
|
||||
|
||||
{/* 章节导航信息 */}
|
||||
|
||||
@@ -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<Message | null>(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 <QdChapterInfo bookInfo={bookInfo} chapterInfo={chapterInfo} />;
|
||||
const body: QdChapterInfo = result.body;
|
||||
/**@ts-ignore*/
|
||||
delete body.type;
|
||||
return <QdChapterInfoModel info={body} />;
|
||||
}
|
||||
return <Result status="error" title="错误" subTitle={result.msg || '未知错误'} />;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user