mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-07-08 01:31:31 +08:00
Add support to load chapter lists from databases
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { IndexedDbConfig } from "../config";
|
import { IndexedDbConfig } from "../config";
|
||||||
import type { QdChapterInfo, QdBookInfo, PagedData } from "../types";
|
import type { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo } from "../types";
|
||||||
import { compress, isServiceWorker } from "../utils";
|
import { compress, decompress, isServiceWorker } from "../utils";
|
||||||
import { hash_qdchapter_info } from "../utils/qd";
|
import { hash_qdchapter_info } from "../utils/qd";
|
||||||
import type { Db } from "./interfaces";
|
import type { Db } from "./interfaces";
|
||||||
|
|
||||||
@@ -61,6 +61,37 @@ async function get_datas<T>(db: IDBDatabase, storeName: string, key?: IDBValidKe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function get_data_with_convert<T, U>(db: IDBDatabase, storeName: string, convert: (key: IDBValidKey, data: T, list: U[]) => Promise<void> | void , key?: IDBValidKey | IDBKeyRange, index?: string): Promise<U[]> {
|
||||||
|
return new Promise<U[]>((resolve, reject) => {
|
||||||
|
const list: U[] = [];
|
||||||
|
const tx = db.transaction(storeName, 'readonly');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const req = index ? store.index(index).openCursor(key) : store.openCursor(key);
|
||||||
|
req.onsuccess = () => {
|
||||||
|
const cursor = req.result;
|
||||||
|
if (cursor) {
|
||||||
|
try {
|
||||||
|
const res = convert(cursor.primaryKey, cursor.value, list);
|
||||||
|
if (res instanceof Promise) {
|
||||||
|
res.then(() => {
|
||||||
|
cursor.continue();
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function get_keys<K extends IDBValidKey = IDBValidKey>(db: IDBDatabase, storeName: string, query?: IDBValidKey | IDBKeyRange, options?: GetAllOptions): Promise<K[]> {
|
async function get_keys<K extends IDBValidKey = IDBValidKey>(db: IDBDatabase, storeName: string, query?: IDBValidKey | IDBKeyRange, options?: GetAllOptions): Promise<K[]> {
|
||||||
return new Promise<K[]>((resolve, reject) => {
|
return new Promise<K[]>((resolve, reject) => {
|
||||||
const tx = db.transaction(storeName, 'readonly');
|
const tx = db.transaction(storeName, 'readonly');
|
||||||
@@ -145,7 +176,7 @@ async function get_paged_data<T>(db: IDBDatabase, storeName: string, page: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CompressedQdChapterInfo = {
|
type CompressedQdChapterInfo = {
|
||||||
compressed: Uint8Array;
|
compressed: Uint8Array<ArrayBuffer>;
|
||||||
bookId: number;
|
bookId: number;
|
||||||
id: number;
|
id: number;
|
||||||
time: number;
|
time: number;
|
||||||
@@ -227,6 +258,37 @@ export class IndexedDb implements Db {
|
|||||||
async getQdBook(id: number): Promise<QdBookInfo | undefined> {
|
async getQdBook(id: number): Promise<QdBookInfo | undefined> {
|
||||||
return await get_data(this.qddb, 'books', id);
|
return await get_data(this.qddb, 'books', id);
|
||||||
}
|
}
|
||||||
|
async getChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]> {
|
||||||
|
// chapterId-> [index, time]
|
||||||
|
const currents: Map<number, [number, number]> = new Map();
|
||||||
|
return await get_data_with_convert<QdChapterInfo | CompressedQdChapterInfo, QdChapterSimpleInfo>(this.qddb, 'chapters', async (key, data, list) => {
|
||||||
|
if ('compressed' in data) {
|
||||||
|
const decompressed = await decompress(data.compressed);
|
||||||
|
const decoded = new TextDecoder().decode(decompressed);
|
||||||
|
data = JSON.parse(decoded) as QdChapterInfo;
|
||||||
|
}
|
||||||
|
const value: [number, number] = [list.length, data.time];
|
||||||
|
const oldValue = currents.get(data.id);
|
||||||
|
if (oldValue && value[1] > oldValue[1]) {
|
||||||
|
value[0] = oldValue[0];
|
||||||
|
currents.set(data.id, value);
|
||||||
|
list[oldValue[0]] = {
|
||||||
|
primaryKey: key,
|
||||||
|
id: data.id,
|
||||||
|
name: data.chapterInfo.chapterName,
|
||||||
|
bookId: data.bookId,
|
||||||
|
};
|
||||||
|
} else if (!oldValue) {
|
||||||
|
currents.set(data.id, value);
|
||||||
|
list.push({
|
||||||
|
primaryKey: key,
|
||||||
|
id: data.id,
|
||||||
|
name: data.chapterInfo.chapterName,
|
||||||
|
bookId: data.bookId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, bookId, 'bookId');
|
||||||
|
}
|
||||||
close() {
|
close() {
|
||||||
this.qddb.close();
|
this.qddb.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DbConfig, DbType } from "../config";
|
import { DbConfig, DbType } from "../config";
|
||||||
import { IndexedDb } from "./indexedDb";
|
import { IndexedDb } from "./indexedDb";
|
||||||
import { PocketBaseDb } from "./pocketBase";
|
import { PocketBaseDb } from "./pocketBase";
|
||||||
import type { QdChapterInfo, QdBookInfo, PagedData } from "../types";
|
import type { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo } from "../types";
|
||||||
|
|
||||||
export interface Db {
|
export interface Db {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
@@ -17,6 +17,12 @@ export interface Db {
|
|||||||
saveQdBook(info: QdBookInfo): Promise<void>;
|
saveQdBook(info: QdBookInfo): Promise<void>;
|
||||||
getQdBook(id: number): Promise<QdBookInfo | undefined>;
|
getQdBook(id: number): Promise<QdBookInfo | undefined>;
|
||||||
getQdBooks(page: number, pageSize: number): Promise<PagedData<QdBookInfo>>;
|
getQdBooks(page: number, pageSize: number): Promise<PagedData<QdBookInfo>>;
|
||||||
|
/**
|
||||||
|
* Retrieve chapter list of a book. if bookId is not found, return empty array.
|
||||||
|
* Primary key should be the latest (time is biggest) saved chapter.
|
||||||
|
* @param bookId Book ID
|
||||||
|
*/
|
||||||
|
getChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]>;
|
||||||
close(): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Db } from "./interfaces";
|
|||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
import type { CollectionModel } from "pocketbase";
|
import type { CollectionModel } from "pocketbase";
|
||||||
import { PocketBaseConfig } from "../config";
|
import { PocketBaseConfig } from "../config";
|
||||||
import { QdChapterInfo, QdBookInfo, PagedData } from "../types";
|
import { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo } from "../types";
|
||||||
import { hash_qdchapter_info } from "../utils/qd";
|
import { hash_qdchapter_info } from "../utils/qd";
|
||||||
import { loadConfig, saveConfig } from "../utils";
|
import { loadConfig, saveConfig } from "../utils";
|
||||||
|
|
||||||
@@ -218,6 +218,40 @@ export class PocketBaseDb implements Db {
|
|||||||
});
|
});
|
||||||
return records.totalItems > 0 ? records.items[0].data : undefined;
|
return records.totalItems > 0 ? records.items[0].data : undefined;
|
||||||
}
|
}
|
||||||
|
async getChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]> {
|
||||||
|
// chapterId -> [index, time]
|
||||||
|
const currents: Map<number, [number, number]> = new Map();
|
||||||
|
const list = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getFullList({
|
||||||
|
filter: `bookId = ${bookId}`,
|
||||||
|
fields: 'id,chapterId,bookId,time,data.chapterInfo.chapterName',
|
||||||
|
});
|
||||||
|
const re: QdChapterSimpleInfo[] = [];
|
||||||
|
for (const item of list) {
|
||||||
|
const data: QdChapterInfo = item.data;
|
||||||
|
const key: number = item.chapterId;
|
||||||
|
const value: [number, number] = [re.length, item.time];
|
||||||
|
const oldValue = currents.get(key);
|
||||||
|
if (oldValue && value[1] > oldValue[1]) {
|
||||||
|
value[0] = oldValue[0];
|
||||||
|
currents.set(key, value);
|
||||||
|
re[oldValue[0]] = {
|
||||||
|
primaryKey: item.id,
|
||||||
|
id: item.chapterId,
|
||||||
|
name: data.chapterInfo.chapterName,
|
||||||
|
bookId: item.bookId,
|
||||||
|
};
|
||||||
|
} else if (!oldValue) {
|
||||||
|
currents.set(key, value);
|
||||||
|
re.push({
|
||||||
|
primaryKey: item.id,
|
||||||
|
id: item.chapterId,
|
||||||
|
name: data.chapterInfo.chapterName,
|
||||||
|
bookId: item.bookId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return re;
|
||||||
|
}
|
||||||
close(): void {
|
close(): void {
|
||||||
this.client.cancelAllRequests();
|
this.client.cancelAllRequests();
|
||||||
this.client.authStore.clear();
|
this.client.authStore.clear();
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Affix, Flex, Space, Tag, Typography, Switch } from "antd";
|
import { Affix, Flex, Space, Tag, Typography, Switch, Skeleton, Result, Button } from "antd";
|
||||||
import { useBookInfo } from "./BookInfoProvider";
|
import { useBookInfo } from "./BookInfoProvider";
|
||||||
import styles from './BookIndex.module.css';
|
import styles from './BookIndex.module.css';
|
||||||
import { useBookStatus } from "./BookStatusProvider";
|
import { loadChapterListsIfNeeded, useBookStatus } from "./BookStatusProvider";
|
||||||
import VolumesList from "./VolumesList";
|
import VolumesList from "./VolumesList";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDb } from "../dbProvider";
|
||||||
|
import type { Volume } from "../../qdtypes";
|
||||||
|
import { get_new_volumes } from "../../utils/qd";
|
||||||
|
|
||||||
const { Paragraph, Link } = Typography;
|
const { Paragraph, Link } = Typography;
|
||||||
|
|
||||||
@@ -10,10 +14,30 @@ const QD_BOOK_TAG_COLOR = ['blue', 'cyan', 'orange'];
|
|||||||
|
|
||||||
export default function BookIndex() {
|
export default function BookIndex() {
|
||||||
const bookInfo = useBookInfo();
|
const bookInfo = useBookInfo();
|
||||||
|
const db = useDb();
|
||||||
const [bookStatus, setBookStatus] = useBookStatus();
|
const [bookStatus, setBookStatus] = useBookStatus();
|
||||||
|
const [err, setErr] = useState<string | null>(null);
|
||||||
function setShowSavedOnly(showSavedOnly: boolean) {
|
function setShowSavedOnly(showSavedOnly: boolean) {
|
||||||
setBookStatus({ ...bookStatus, showSavedOnly });
|
setBookStatus({ ...bookStatus, showSavedOnly });
|
||||||
}
|
}
|
||||||
|
function handle() {
|
||||||
|
if (err) {
|
||||||
|
setErr(null);
|
||||||
|
}
|
||||||
|
loadChapterListsIfNeeded(bookInfo.id, bookStatus, setBookStatus, db).catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
setErr(e instanceof Error ? e.message : String(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
handle();
|
||||||
|
}, [bookInfo.id]);
|
||||||
|
let vols: Volume[] = bookInfo.volumes;
|
||||||
|
if (bookStatus.chapterLists) {
|
||||||
|
vols = get_new_volumes(bookStatus.chapterLists, bookInfo.volumes, !bookStatus.showSavedOnly);
|
||||||
|
} else if (bookStatus.showSavedOnly) {
|
||||||
|
vols = [];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.c}>
|
<div className={styles.c}>
|
||||||
<Flex justify="center" align="center">
|
<Flex justify="center" align="center">
|
||||||
@@ -41,7 +65,9 @@ export default function BookIndex() {
|
|||||||
<Switch checked={bookStatus.showSavedOnly} onChange={setShowSavedOnly} checkedChildren={"仅显示已保存章节"} unCheckedChildren={"显示所有章节"} />
|
<Switch checked={bookStatus.showSavedOnly} onChange={setShowSavedOnly} checkedChildren={"仅显示已保存章节"} unCheckedChildren={"显示所有章节"} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Affix>
|
</Affix>
|
||||||
{!bookStatus.showSavedOnly && <VolumesList bookId={bookInfo.id} volumes={bookInfo.volumes} />}
|
{bookStatus.showSavedOnly && err && <Result status="error" title="加载章节列表失败" subTitle={err} extra={<Button type="primary" onClick={handle}>重试</Button>} />}
|
||||||
|
{bookStatus.showSavedOnly && !bookStatus.chapterLists && !err && <Skeleton active />}
|
||||||
|
{vols.length > 0 && <VolumesList bookId={bookInfo.id} volumes={vols} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { createContext, useContext, Dispatch } from "react";
|
import { createContext, useContext, Dispatch, SetStateAction } from "react";
|
||||||
|
import { QdChapterSimpleInfo } from "../../types";
|
||||||
|
import { Db } from "../../db/interfaces";
|
||||||
|
|
||||||
export type BookStatus = {
|
export type BookStatus = {
|
||||||
showSavedOnly: boolean;
|
showSavedOnly: boolean;
|
||||||
|
chapterLists?: QdChapterSimpleInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBookStatus(): BookStatus {
|
export function createBookStatus(): BookStatus {
|
||||||
@@ -10,7 +13,18 @@ export function createBookStatus(): BookStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BookStatusContext = createContext<[BookStatus, Dispatch<BookStatus>]>(null as any);
|
export async function loadChapterLists(bookId: number, setBookStatus: Dispatch<SetStateAction<BookStatus>>, db: Db) {
|
||||||
|
const list = await db.getChapterSimpleInfos(bookId);
|
||||||
|
setBookStatus((status) => ({ ...status, chapterLists: list }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadChapterListsIfNeeded(bookId: number, bookStatus: BookStatus, setBookStatus: Dispatch<SetStateAction<BookStatus>>, db: Db) {
|
||||||
|
if (!bookStatus.chapterLists) {
|
||||||
|
await loadChapterLists(bookId, setBookStatus, db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookStatusContext = createContext<[BookStatus, Dispatch<SetStateAction<BookStatus>>]>(null as any);
|
||||||
|
|
||||||
export function useBookStatus() {
|
export function useBookStatus() {
|
||||||
return useContext(BookStatusContext);
|
return useContext(BookStatusContext);
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ export type QdChapterInfo = {
|
|||||||
hash?: string;
|
hash?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QdChapterSimpleInfo = {
|
||||||
|
primaryKey: unknown;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
bookId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type QdBookInfo = {
|
export type QdBookInfo = {
|
||||||
bookInfo: QdTypes.BookGData;
|
bookInfo: QdTypes.BookGData;
|
||||||
bookName: string;
|
bookName: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SHA256 } from "@stablelib/sha256";
|
import { SHA256 } from "@stablelib/sha256";
|
||||||
import type { QdChapterInfo } from "../types";
|
import type { QdChapterInfo, QdChapterSimpleInfo } from "../types";
|
||||||
import { get_chapter_content, toHex } from "../utils";
|
import { get_chapter_content, toHex } from "../utils";
|
||||||
|
import type { Chapter, Volume } from "../qdtypes";
|
||||||
|
|
||||||
export function hash_qdchapter_info(info: QdChapterInfo): string {
|
export function hash_qdchapter_info(info: QdChapterInfo): string {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
@@ -27,3 +28,46 @@ export function hash_qdchapter_info(info: QdChapterInfo): string {
|
|||||||
const hash = h.digest();
|
const hash = h.digest();
|
||||||
return toHex(hash);
|
return toHex(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function get_new_volumes(chapterLists: QdChapterSimpleInfo[], volumes: Volume[], keep=true): Volume[] {
|
||||||
|
const vols: Volume[] = [];
|
||||||
|
if (keep) {
|
||||||
|
const volMap: Map<number, string> = new Map();
|
||||||
|
for (const vol of volumes) {
|
||||||
|
const id = vol.id;
|
||||||
|
for (const ch of vol.chapters) {
|
||||||
|
volMap.set(ch.id, vol.name);
|
||||||
|
}
|
||||||
|
vols.push(vol);
|
||||||
|
}
|
||||||
|
const volCh: Chapter[] = [];
|
||||||
|
for (const ch of chapterLists) {
|
||||||
|
if (!volMap.has(ch.id)) {
|
||||||
|
volCh.push({
|
||||||
|
id: ch.id,
|
||||||
|
name: ch.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (volCh.length > 0) {
|
||||||
|
vols.unshift({
|
||||||
|
id: 'vol_new',
|
||||||
|
name: '其他已保存章节',
|
||||||
|
chapters: volCh,
|
||||||
|
isVip: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const chIds = new Set(chapterLists.map(ch => ch.id));
|
||||||
|
for (const vol of volumes) {
|
||||||
|
const newChs = vol.chapters.filter(ch => chIds.has(ch.id));
|
||||||
|
if (newChs.length > 0) {
|
||||||
|
vols.push({
|
||||||
|
...vol,
|
||||||
|
chapters: newChs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vols;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user