mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-06-22 03:45:07 +08:00
Add history model
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { IndexedDbConfig } from "../config";
|
||||
import type { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo } from "../types";
|
||||
import type { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo, QdChapterHistoryInfo } from "../types";
|
||||
import { compress, decompress, isServiceWorker } from "../utils";
|
||||
import { hash_qdchapter_info } from "../utils/qd";
|
||||
import type { Db } from "./interfaces";
|
||||
@@ -105,12 +105,12 @@ function get_data_with_convert<T, U>(db: IDBDatabase, storeName: string, key: ID
|
||||
});
|
||||
}
|
||||
|
||||
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[]> {
|
||||
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, direction?: IDBCursorDirection): 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);
|
||||
const req = index ? store.index(index).openCursor(key, direction) : store.openCursor(key, direction);
|
||||
req.onsuccess = () => {
|
||||
const cursor = req.result;
|
||||
if (cursor) {
|
||||
@@ -371,6 +371,20 @@ export class IndexedDb implements Db {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
async getQdChapterHistory(chapterId: number): Promise<QdChapterHistoryInfo[]> {
|
||||
return await get_datas_with_convert<CompressedQdChapterInfo | QdChapterInfo, QdChapterHistoryInfo>(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;
|
||||
}
|
||||
list.push({
|
||||
primaryKey: key,
|
||||
name: data.chapterInfo.chapterName,
|
||||
time: data.time,
|
||||
})
|
||||
}, chapterId, 'id', 'prev');
|
||||
}
|
||||
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) {
|
||||
@@ -379,7 +393,32 @@ export class IndexedDb implements Db {
|
||||
return JSON.parse(decoded) as QdChapterInfo;
|
||||
}
|
||||
return data;
|
||||
}, 'id', 'prevunique');
|
||||
}, 'id', 'prev');
|
||||
}
|
||||
async setAsLatestQdChapter(key: unknown): Promise<unknown> {
|
||||
const chapter = await this.getQdChapter(key);
|
||||
if (!chapter) {
|
||||
return undefined;
|
||||
}
|
||||
chapter.time = Date.now();
|
||||
const hash = hash_qdchapter_info(chapter);
|
||||
if (this.compress) {
|
||||
chapter.hash = undefined;
|
||||
const data = JSON.stringify(chapter);
|
||||
const encoded = new TextEncoder().encode(data);
|
||||
const compressed = await compress(encoded);
|
||||
const compressedInfo: CompressedQdChapterInfo = {
|
||||
compressed,
|
||||
bookId: chapter.bookId,
|
||||
id: chapter.id,
|
||||
time: chapter.time,
|
||||
hash,
|
||||
}
|
||||
return await save_data(this.qddb, 'chapters', compressedInfo);
|
||||
} else {
|
||||
chapter.hash = hash;
|
||||
return await save_data(this.qddb, 'chapters', chapter);
|
||||
}
|
||||
}
|
||||
close() {
|
||||
this.qddb.close();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DbConfig, DbType } from "../config";
|
||||
import { IndexedDb } from "./indexedDb";
|
||||
import { PocketBaseDb } from "./pocketBase";
|
||||
import type { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo } from "../types";
|
||||
import type { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo, QdChapterHistoryInfo } from "../types";
|
||||
|
||||
export interface Db {
|
||||
init(): Promise<void>;
|
||||
@@ -18,7 +18,7 @@ export interface Db {
|
||||
/**
|
||||
* Update chapter info in database.
|
||||
* @param info The chapter info to update. time will be updated to current time in database implementation so mannual update is not needed. Primary key was chapterId, bookId and time.
|
||||
* @return Primary key of the updated chapter, which is determined by the database implementation.
|
||||
* @returns Primary key of the updated chapter, which is determined by the database implementation.
|
||||
*/
|
||||
updateQdChapter(info: QdChapterInfo): Promise<unknown>;
|
||||
getQdBook(id: number): Promise<QdBookInfo | undefined>;
|
||||
@@ -34,11 +34,23 @@ export interface Db {
|
||||
* @param key Primary key of the chapter, which is determined by the database implementation.
|
||||
*/
|
||||
getQdChapter(key: unknown): Promise<QdChapterInfo | undefined>;
|
||||
/**
|
||||
* Get chapter history by chapter ID. if not found, return empty array.
|
||||
* @param chapterId Chapter ID
|
||||
* @returns Chapter history, sorted by time in descending order (latest first)
|
||||
*/
|
||||
getQdChapterHistory(chapterId: number): Promise<QdChapterHistoryInfo[]>;
|
||||
/**
|
||||
* 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>;
|
||||
/**
|
||||
* Set the latest chapter by primary key. This function will update the time of the chapter.
|
||||
* @param key Primary key of the chapter, which is determined by the database implementation.
|
||||
* @returns Primary key of the chapter set as latest, which is determined by the database implementation. if the chapter is not found, return undefined.
|
||||
*/
|
||||
setAsLatestQdChapter(key: unknown): Promise<unknown>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Db } from "./interfaces";
|
||||
import PocketBase from "pocketbase";
|
||||
import type { CollectionModel } from "pocketbase";
|
||||
import { PocketBaseConfig } from "../config";
|
||||
import { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo } from "../types";
|
||||
import { QdChapterInfo, QdBookInfo, PagedData, QdChapterSimpleInfo, QdChapterHistoryInfo } from "../types";
|
||||
import { hash_qdchapter_info } from "../utils/qd";
|
||||
import { loadConfig, saveConfig } from "../utils";
|
||||
|
||||
@@ -279,17 +279,43 @@ export class PocketBaseDb implements Db {
|
||||
const k = String(key);
|
||||
const record = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getList(1, 1, {
|
||||
filter: `id = "${k}"`,
|
||||
fields: 'data',
|
||||
fields: 'data,time',
|
||||
});
|
||||
return record.totalItems > 0 ? record.items[0].data : undefined;
|
||||
const re = record.totalItems > 0 ? record.items[0].data : undefined;
|
||||
if (re) {
|
||||
re.time = record.items[0].time;
|
||||
}
|
||||
return re;
|
||||
}
|
||||
async getQdChapterHistory(chapterId: number): Promise<QdChapterHistoryInfo[]> {
|
||||
const records = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getFullList({
|
||||
filter: `chapterId = ${chapterId}`,
|
||||
fields: 'id,time,data.chapterInfo.chapterName',
|
||||
sort: '-time',
|
||||
});
|
||||
return records.map(item => ({
|
||||
primaryKey: item.id,
|
||||
name: item.data.chapterInfo.chapterName,
|
||||
time: item.time,
|
||||
}));
|
||||
}
|
||||
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',
|
||||
fields: 'data,time',
|
||||
sort: '-time',
|
||||
});
|
||||
return records.totalItems > 0 ? records.items[0].data : undefined;
|
||||
const record = records.totalItems > 0 ? records.items[0].data : undefined;
|
||||
if (record) {
|
||||
record.time = records.items[0].time;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
async setAsLatestQdChapter(key: unknown): Promise<unknown> {
|
||||
await this.client.collection(`${this.cfg.prefix}qd_chapters`).update(String(key), {
|
||||
time: Date.now(),
|
||||
});
|
||||
return key;
|
||||
}
|
||||
close(): void {
|
||||
this.client.cancelAllRequests();
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function BookChapter() {
|
||||
title="数据加载失败"
|
||||
subTitle={err}
|
||||
extra={<Button type="primary" onClick={() => { setErr(null); handle_load(); }}>重试</Button>} />}
|
||||
{chapter && <ChapterEditor ref={editorRef} chapter={chapter} onChapterSaveAs={setChapter} />}
|
||||
{chapter && <ChapterEditor ref={editorRef} chapter={chapter} onChapterSaveAs={setChapter} history />}
|
||||
{!chapter && !err && <Skeleton active />}
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
|
||||
@@ -95,3 +95,32 @@
|
||||
.reset:hover {
|
||||
color: #4096ff;
|
||||
}
|
||||
|
||||
.showHistory {
|
||||
color: gray;
|
||||
cursor: pointer;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.showHistory:hover {
|
||||
color: #4096ff;
|
||||
}
|
||||
|
||||
.showHistory.disabled {
|
||||
color: lightgray;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.showHistory.disabled:hover {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.history .item {
|
||||
width: 100%;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.history .current {
|
||||
color: #4096ff;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, createRef } from "react";
|
||||
import { QdChapterInfo } from "../../types";
|
||||
import { QdChapterHistoryInfo, QdChapterInfo } from "../../types";
|
||||
import MonacoEditor, { MonacoEditorHandle } from 'react-monaco-editor';
|
||||
import styles from './ChapterEditor.module.css';
|
||||
import { Flex, Tooltip, Typography } from "antd";
|
||||
import { Button, Card, Empty, Drawer, Flex, Skeleton, Tooltip, Typography } from "antd";
|
||||
import Icon from "../../components/Icon";
|
||||
import EditOutlined from "../../../node_modules/@material-icons/svg/svg/edit/outline.svg";
|
||||
import SaveOutlined from "../../../node_modules/@material-icons/svg/svg/save/outline.svg";
|
||||
@@ -11,12 +11,15 @@ import ReplayOutlined from "../../../node_modules/@material-icons/svg/svg/replay
|
||||
import { DbContext } from "../dbProvider";
|
||||
import Notification from "../../components/Notification";
|
||||
import SaveAsOutlined from "../../../node_modules/@material-icons/svg/svg/save_as/outline.svg";
|
||||
import type { Db } from "../../db/interfaces";
|
||||
import HistoryOutlined from "../../../node_modules/@material-icons/svg/svg/history/outline.svg";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export interface ChapterEditorProps {
|
||||
chapter: QdChapterInfo;
|
||||
onChapterSaveAs?: (chapter: QdChapterInfo) => void;
|
||||
history?: boolean;
|
||||
}
|
||||
|
||||
export interface ChapterEditorState {
|
||||
@@ -25,10 +28,15 @@ export interface ChapterEditorState {
|
||||
editingChapterName: boolean;
|
||||
eChapterName?: string;
|
||||
changed: boolean;
|
||||
history?: QdChapterHistoryInfo[];
|
||||
loadingHistory: boolean;
|
||||
loadHistoryFailed: boolean;
|
||||
historyPanelOpen: boolean;
|
||||
}
|
||||
|
||||
export default class ChapterEditor extends Component<ChapterEditorProps, ChapterEditorState> {
|
||||
ref;
|
||||
db?: Db;
|
||||
constructor(props: ChapterEditorProps) {
|
||||
super(props);
|
||||
this.ref = createRef<MonacoEditorHandle>();
|
||||
@@ -37,6 +45,9 @@ export default class ChapterEditor extends Component<ChapterEditorProps, Chapter
|
||||
chapterName: props.chapter.chapterInfo.chapterName,
|
||||
editingChapterName: false,
|
||||
changed: false,
|
||||
loadingHistory: false,
|
||||
loadHistoryFailed: false,
|
||||
historyPanelOpen: false,
|
||||
};
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<ChapterEditorProps>, _prevState: Readonly<ChapterEditorState>, _snapshot?: unknown): void {
|
||||
@@ -47,6 +58,23 @@ export default class ChapterEditor extends Component<ChapterEditorProps, Chapter
|
||||
editingChapterName: false,
|
||||
eChapterName: undefined,
|
||||
changed: false,
|
||||
history: undefined,
|
||||
loadingHistory: false,
|
||||
loadHistoryFailed: false,
|
||||
});
|
||||
} else if (prevProps.history && !this.props.history) {
|
||||
this.setState({ history: undefined, loadingHistory: false, loadHistoryFailed: false });
|
||||
}
|
||||
if (this.props.history && !this.state.history && !this.state.loadingHistory && this.db && !this.state.loadHistoryFailed) {
|
||||
this.setState({ loadingHistory: true });
|
||||
this.db.getQdChapterHistory(this.props.chapter.id).then(history => {
|
||||
console.log(history);
|
||||
this.setState({ history, loadingHistory: false });
|
||||
}).catch(err => {
|
||||
console.warn(err);
|
||||
const errmsg = err instanceof Error ? err.message : String(err);
|
||||
Notification(`加载章节历史失败: ${errmsg}`, 'error');
|
||||
this.setState({ loadingHistory: false, loadHistoryFailed: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -62,9 +90,13 @@ export default class ChapterEditor extends Component<ChapterEditorProps, Chapter
|
||||
if (!this.state.changed) {
|
||||
chapterSaveClass += ` ${styles.disabled}`;
|
||||
}
|
||||
let showHistoryClass = styles.showHistory;
|
||||
if (this.state.loadingHistory) {
|
||||
showHistoryClass += ` ${styles.disabled}`;
|
||||
}
|
||||
return (<>
|
||||
<DbContext.Consumer>
|
||||
{ (db) => <>
|
||||
{ (db) => {this.db = db; return <>
|
||||
<Flex vertical className={styles.container}>
|
||||
<Flex className={styles.header}>
|
||||
<Flex className={nameClass}>
|
||||
@@ -128,6 +160,40 @@ export default class ChapterEditor extends Component<ChapterEditorProps, Chapter
|
||||
changed: false,
|
||||
});
|
||||
}} /></Icon></Tooltip>
|
||||
{this.props.history && <Tooltip title="查看章节历史" placement="left"><Icon><HistoryOutlined fill="currentColor" className={showHistoryClass} onClick={this.state.loadingHistory ? undefined : () => {
|
||||
this.setState({ historyPanelOpen: true });
|
||||
}} /></Icon></Tooltip>}
|
||||
{this.props.history && <Drawer
|
||||
title="章节历史"
|
||||
placement="right"
|
||||
onClose={() => this.setState({ historyPanelOpen: false })}
|
||||
open={this.state.historyPanelOpen}
|
||||
>
|
||||
<Flex vertical align="center" className={styles.history}>
|
||||
{this.state.history && this.state.history.length === 0 && <Empty description="没有历史记录" />}
|
||||
{this.state.history && this.state.history.length > 0 && this.state.history.map((item, ind) => (
|
||||
<Card key={String(item.primaryKey)} className={styles.item}>
|
||||
<Flex align="center">
|
||||
<Text>{item.name}</Text>
|
||||
{item.time === this.props.chapter.time && <Text className={styles.current}>(当前)</Text>}
|
||||
</Flex>
|
||||
<Text>保存时间: {new Date(item.time).toLocaleString()}</Text>
|
||||
<Flex align="center">
|
||||
{ind !== 0 && <Button onClick={() => {
|
||||
db.setAsLatestQdChapter(item.primaryKey).then(() => {
|
||||
this.setState({ loadHistoryFailed: false, history: undefined, loadingHistory: false, });
|
||||
}).catch((err) => {
|
||||
console.warn(err);
|
||||
const errmsg = err instanceof Error ? err.message : String(err);
|
||||
Notification(`设为最新版本失败: ${errmsg}`, 'error');
|
||||
});
|
||||
}}>设为最新版本</Button>}
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
{this.state.loadingHistory && <Skeleton active />}
|
||||
</Flex>
|
||||
</Drawer>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<div className={styles.editor}>
|
||||
@@ -144,7 +210,7 @@ export default class ChapterEditor extends Component<ChapterEditorProps, Chapter
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</>}
|
||||
</>}}
|
||||
</DbContext.Consumer>
|
||||
</>);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ export type QdChapterSimpleInfo = {
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type QdChapterHistoryInfo = {
|
||||
primaryKey: unknown;
|
||||
name: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type QdBookInfo = {
|
||||
bookInfo: QdTypes.BookGData;
|
||||
bookName: string;
|
||||
|
||||
Reference in New Issue
Block a user