mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-07-02 03:11:18 +08:00
Add support to load ch in manage db
This commit is contained in:
@@ -40,4 +40,7 @@ export default defineConfig([
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build.js'],
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"esbuild-plugin-eslint": "^0.3.12",
|
||||
"eslint": "9",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"pocketbase": "^0.26.8",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
||||
@@ -61,7 +61,37 @@ function get_datas<T>(db: IDBDatabase, storeName: string, key?: IDBValidKey | ID
|
||||
});
|
||||
}
|
||||
|
||||
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[]> {
|
||||
function get_data_with_convert<T, U>(db: IDBDatabase, storeName: string, key: IDBValidKey | IDBKeyRange, convert: (key: IDBValidKey, data: T) => Promise<U> | U, index?: string, direction?: IDBCursorDirection): Promise<U | undefined> {
|
||||
return new Promise<U | undefined>((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
const req = index ? store.index(index).openCursor(key, direction) : store.openCursor(key, direction);
|
||||
req.onsuccess = () => {
|
||||
const cursor = req.result;
|
||||
if (cursor) {
|
||||
try {
|
||||
const res = convert(cursor.primaryKey, cursor.value);
|
||||
if (res instanceof Promise) {
|
||||
res.then(resolve).catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
resolve(res);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
};
|
||||
req.onerror = () => {
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function get_datas_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');
|
||||
@@ -258,10 +288,10 @@ export class IndexedDb implements Db {
|
||||
async getQdBook(id: number): Promise<QdBookInfo | undefined> {
|
||||
return await get_data(this.qddb, 'books', id);
|
||||
}
|
||||
async getChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]> {
|
||||
async getQdChapterSimpleInfos(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) => {
|
||||
return await get_datas_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);
|
||||
@@ -277,6 +307,7 @@ export class IndexedDb implements Db {
|
||||
id: data.id,
|
||||
name: data.chapterInfo.chapterName,
|
||||
bookId: data.bookId,
|
||||
time: data.time,
|
||||
};
|
||||
} else if (!oldValue) {
|
||||
currents.set(data.id, value);
|
||||
@@ -285,10 +316,34 @@ export class IndexedDb implements Db {
|
||||
id: data.id,
|
||||
name: data.chapterInfo.chapterName,
|
||||
bookId: data.bookId,
|
||||
time: data.time,
|
||||
});
|
||||
}
|
||||
}, bookId, 'bookId');
|
||||
}
|
||||
async getQdChapter(key: unknown): Promise<QdChapterInfo | undefined> {
|
||||
const k = key as QdChapterKey;
|
||||
const data = await get_data<CompressedQdChapterInfo | QdChapterInfo>(this.qddb, 'chapters', k);
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if ('compressed' in data) {
|
||||
const decompressed = await decompress(data.compressed);
|
||||
const decoded = new TextDecoder().decode(decompressed);
|
||||
return JSON.parse(decoded) as QdChapterInfo;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
async getLatestQdChapter(id: number): Promise<QdChapterInfo | undefined> {
|
||||
return await get_data_with_convert<CompressedQdChapterInfo | QdChapterInfo, QdChapterInfo>(this.qddb, 'chapters', id, async (key, data) => {
|
||||
if ('compressed' in data) {
|
||||
const decompressed = await decompress(data.compressed);
|
||||
const decoded = new TextDecoder().decode(decompressed);
|
||||
return JSON.parse(decoded) as QdChapterInfo;
|
||||
}
|
||||
return data;
|
||||
}, 'id', 'prevunique');
|
||||
}
|
||||
close() {
|
||||
this.qddb.close();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,17 @@ export interface Db {
|
||||
* Primary key should be the latest (time is biggest) saved chapter.
|
||||
* @param bookId Book ID
|
||||
*/
|
||||
getChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]>;
|
||||
getQdChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]>;
|
||||
/**
|
||||
* Get chapter info by primary key. if not found, return undefined.
|
||||
* @param key Primary key of the chapter, which is determined by the database implementation.
|
||||
*/
|
||||
getQdChapter(key: unknown): Promise<QdChapterInfo | undefined>;
|
||||
/**
|
||||
* Get the latest (which time is biggest) chapter of a chapter. if not found, return undefined.
|
||||
* @param id Chapter ID
|
||||
*/
|
||||
getLatestQdChapter(id: number): Promise<QdChapterInfo | undefined>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ export class PocketBaseDb implements Db {
|
||||
});
|
||||
return records.totalItems > 0 ? records.items[0].data : undefined;
|
||||
}
|
||||
async getChapterSimpleInfos(bookId: number): Promise<QdChapterSimpleInfo[]> {
|
||||
async getQdChapterSimpleInfos(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({
|
||||
@@ -239,6 +239,7 @@ export class PocketBaseDb implements Db {
|
||||
id: item.chapterId,
|
||||
name: data.chapterInfo.chapterName,
|
||||
bookId: item.bookId,
|
||||
time: item.time,
|
||||
};
|
||||
} else if (!oldValue) {
|
||||
currents.set(key, value);
|
||||
@@ -247,11 +248,28 @@ export class PocketBaseDb implements Db {
|
||||
id: item.chapterId,
|
||||
name: data.chapterInfo.chapterName,
|
||||
bookId: item.bookId,
|
||||
time: item.time,
|
||||
});
|
||||
}
|
||||
}
|
||||
return re;
|
||||
}
|
||||
async getQdChapter(key: unknown): Promise<QdChapterInfo | undefined> {
|
||||
const k = String(key);
|
||||
const record = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getList(1, 1, {
|
||||
filter: `id = "${k}"`,
|
||||
fields: 'data',
|
||||
});
|
||||
return record.totalItems > 0 ? record.items[0].data : undefined;
|
||||
}
|
||||
async getLatestQdChapter(id: number): Promise<QdChapterInfo | undefined> {
|
||||
const records = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getList(1, 1, {
|
||||
filter: `chapterId = ${id}`,
|
||||
fields: 'data',
|
||||
sort: '-time',
|
||||
});
|
||||
return records.totalItems > 0 ? records.items[0].data : undefined;
|
||||
}
|
||||
close(): void {
|
||||
this.client.cancelAllRequests();
|
||||
this.client.authStore.clear();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Result } from 'antd';
|
||||
import { DbContext } from "./manage/dbProvider";
|
||||
import QdBook from "./manage/qd/Book";
|
||||
import QdBookIndex from "./manage/qd/BookIndex";
|
||||
import QdBookChapter from "./manage/qd/BookChapter";
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
@@ -26,6 +27,10 @@ const router = createHashRouter([
|
||||
{
|
||||
index: true,
|
||||
element: <QdBookIndex />
|
||||
},
|
||||
{
|
||||
path: "chapter/:chapterId",
|
||||
element: <QdBookChapter />
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { QdBookInfo } from "../../types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BookInfoContext } from "./BookInfoProvider";
|
||||
import { BookStatusContext, createBookStatus } from "./BookStatusProvider";
|
||||
import type { ItemType } from "antd/es/breadcrumb/Breadcrumb";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
export default function Book() {
|
||||
const db = useDb();
|
||||
@@ -12,6 +14,7 @@ export default function Book() {
|
||||
const [book, setBook] = useState<QdBookInfo | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [bookStatus, setBookStatus] = useState(createBookStatus());
|
||||
const [items, setItems] = useState<ItemType[]>([]);
|
||||
async function load() {
|
||||
const data = await db.getQdBook(Number(id));
|
||||
if (data) {
|
||||
@@ -20,6 +23,11 @@ export default function Book() {
|
||||
setErr("书籍不存在");
|
||||
}
|
||||
}
|
||||
function setItemsIfNeeded(newItems: ItemType[]) {
|
||||
if (!isEqual(items, newItems)) {
|
||||
setItems(newItems);
|
||||
}
|
||||
}
|
||||
function handle() {
|
||||
load().catch(e => {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
@@ -28,6 +36,7 @@ export default function Book() {
|
||||
useEffect(() => {
|
||||
handle();
|
||||
}, [id]);
|
||||
const title = book ? `书籍详情:${book.bookName}` : '书籍详情';
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={
|
||||
@@ -38,8 +47,10 @@ export default function Book() {
|
||||
}, {
|
||||
title: <NavLink to="/qd/books">按书籍管理</NavLink>
|
||||
}, {
|
||||
title: book ? `书籍详情:${book.bookName}` : '书籍详情'
|
||||
}]
|
||||
title: items.length > 0 ? <NavLink to={`/qd/book/${id}`}>{title}</NavLink> : title
|
||||
},
|
||||
...items,
|
||||
]
|
||||
} />
|
||||
{!book && !err && <Skeleton active />}
|
||||
{err && <Result
|
||||
@@ -49,7 +60,7 @@ export default function Book() {
|
||||
extra={<Button type="primary" onClick={() => { setErr(null); handle(); }}>重试</Button>} />}
|
||||
{book && (<BookInfoContext.Provider value={book}>
|
||||
<BookStatusContext.Provider value={[bookStatus, setBookStatus]}>
|
||||
<Outlet />
|
||||
<Outlet context={setItemsIfNeeded} />
|
||||
</BookStatusContext.Provider>
|
||||
</BookInfoContext.Provider>)}
|
||||
</>
|
||||
|
||||
50
src/manage/qd/BookChapter.tsx
Normal file
50
src/manage/qd/BookChapter.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Button, Result } from "antd";
|
||||
import { useParams } from "react-router";
|
||||
import { useBookContext, useBookStatus } from "./BookStatusProvider";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDb } from "../dbProvider";
|
||||
import type { QdChapterInfo } from "../../types";
|
||||
|
||||
export default function BookChapter() {
|
||||
const setItems = useBookContext();
|
||||
const { chapterId } = useParams();
|
||||
const id = parseInt(chapterId ?? '');
|
||||
const [bookStatus, setBookStatus] = useBookStatus();
|
||||
const db = useDb();
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [chapter, setChapter] = useState<QdChapterInfo | null>(null);
|
||||
async function load() {
|
||||
const primaryKey = bookStatus.chapterLists?.find(chapter => chapter.id === id)?.primaryKey;
|
||||
const data = await (primaryKey ? db.getQdChapter(primaryKey) : db.getLatestQdChapter(id));
|
||||
if (data) {
|
||||
setChapter(data);
|
||||
} else {
|
||||
setErr("章节不存在");
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isNaN(id)) {
|
||||
return;
|
||||
}
|
||||
load().catch(e => {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
});
|
||||
}, [id]);
|
||||
setItems([{
|
||||
title: chapter ? `章节详情:${chapter.chapterInfo.chapterName}` : '章节详情'
|
||||
}])
|
||||
if (isNaN(id)) {
|
||||
return <Result
|
||||
status="error"
|
||||
title="章节ID无效"
|
||||
/>;
|
||||
}
|
||||
if (err) {
|
||||
return <Result
|
||||
status="error"
|
||||
title="数据加载失败"
|
||||
subTitle={err}
|
||||
extra={<Button type="primary" onClick={() => { setErr(null); load(); }}>重试</Button>} />;
|
||||
}
|
||||
return (<></>)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Affix, Flex, Space, Tag, Typography, Switch, Skeleton, Result, Button } from "antd";
|
||||
import { useBookInfo } from "./BookInfoProvider";
|
||||
import styles from './BookIndex.module.css';
|
||||
import { loadChapterListsIfNeeded, useBookStatus } from "./BookStatusProvider";
|
||||
import { loadChapterListsIfNeeded, useBookContext, useBookStatus } from "./BookStatusProvider";
|
||||
import VolumesList from "./VolumesList";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDb } from "../dbProvider";
|
||||
@@ -16,6 +16,7 @@ export default function BookIndex() {
|
||||
const bookInfo = useBookInfo();
|
||||
const db = useDb();
|
||||
const [bookStatus, setBookStatus] = useBookStatus();
|
||||
const setItems = useBookContext();
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
function setShowSavedOnly(showSavedOnly: boolean) {
|
||||
setBookStatus({ ...bookStatus, showSavedOnly });
|
||||
@@ -32,6 +33,7 @@ export default function BookIndex() {
|
||||
useEffect(() => {
|
||||
handle();
|
||||
}, [bookInfo.id]);
|
||||
setItems([]);
|
||||
let vols: Volume[] = bookInfo.volumes;
|
||||
if (bookStatus.chapterLists) {
|
||||
vols = get_new_volumes(bookStatus.chapterLists, bookInfo.volumes, !bookStatus.showSavedOnly);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createContext, useContext, Dispatch, SetStateAction } from "react";
|
||||
import { QdChapterSimpleInfo } from "../../types";
|
||||
import { Db } from "../../db/interfaces";
|
||||
import { useOutletContext } from "react-router";
|
||||
import type { ItemType } from "antd/es/breadcrumb/Breadcrumb";
|
||||
|
||||
export type BookStatus = {
|
||||
showSavedOnly: boolean;
|
||||
@@ -14,7 +16,7 @@ export function createBookStatus(): BookStatus {
|
||||
}
|
||||
|
||||
export async function loadChapterLists(bookId: number, setBookStatus: Dispatch<SetStateAction<BookStatus>>, db: Db) {
|
||||
const list = await db.getChapterSimpleInfos(bookId);
|
||||
const list = await db.getQdChapterSimpleInfos(bookId);
|
||||
setBookStatus((status) => ({ ...status, chapterLists: list }));
|
||||
}
|
||||
|
||||
@@ -30,3 +32,7 @@ export function useBookStatus() {
|
||||
return useContext(BookStatusContext);
|
||||
}
|
||||
|
||||
export function useBookContext() {
|
||||
return useOutletContext<Dispatch<ItemType[]>>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Collapse, Flex, Typography } from "antd";
|
||||
import { Collapse, Flex } from "antd";
|
||||
import type { Volume } from "../../qdtypes";
|
||||
import styles from './VolumesList.module.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
import { Link } from "react-router";
|
||||
|
||||
export type VolumesListProps = {
|
||||
volumes: Volume[];
|
||||
@@ -18,7 +17,7 @@ export default function VolumesList({ volumes, bookId }: VolumesListProps) {
|
||||
extra: v.isVip ? <span style={{ color: 'red' }}>VIP卷</span> : null,
|
||||
children: <Flex wrap>
|
||||
{v.chapters.map(chapter => (
|
||||
<Text className={styles.ch} key={chapter.id}>{chapter.name}</Text>
|
||||
<Link to={`/qd/book/${bookId}/chapter/${chapter.id}`} className={styles.ch} key={chapter.id}>{chapter.name}</Link>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export type QdChapterSimpleInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
bookId: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type QdBookInfo = {
|
||||
|
||||
@@ -2007,6 +2007,11 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
|
||||
Reference in New Issue
Block a user