diff --git a/src/db/indexedDb.ts b/src/db/indexedDb.ts index 1382c62..fd31e8e 100644 --- a/src/db/indexedDb.ts +++ b/src/db/indexedDb.ts @@ -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(db: IDBDatabase, storeName: string, key: ID }); } -function get_datas_with_convert(db: IDBDatabase, storeName: string, convert: (key: IDBValidKey, data: T, list: U[]) => Promise | void, key?: IDBValidKey | IDBKeyRange, index?: string): Promise { +function get_datas_with_convert(db: IDBDatabase, storeName: string, convert: (key: IDBValidKey, data: T, list: U[]) => Promise | void, key?: IDBValidKey | IDBKeyRange, index?: string, direction?: IDBCursorDirection): Promise { return new Promise((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 { + return await get_datas_with_convert(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 { return await get_data_with_convert(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 { + 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(); diff --git a/src/db/interfaces.ts b/src/db/interfaces.ts index 7d6a2f4..4d5a52d 100644 --- a/src/db/interfaces.ts +++ b/src/db/interfaces.ts @@ -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; @@ -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; getQdBook(id: number): Promise; @@ -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; + /** + * 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; /** * Get the latest (which time is biggest) chapter of a chapter. if not found, return undefined. * @param id Chapter ID */ getLatestQdChapter(id: number): Promise; + /** + * 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; close(): void; } diff --git a/src/db/pocketBase.ts b/src/db/pocketBase.ts index 860ed29..c707eca 100644 --- a/src/db/pocketBase.ts +++ b/src/db/pocketBase.ts @@ -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 { + 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 { 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 { + await this.client.collection(`${this.cfg.prefix}qd_chapters`).update(String(key), { + time: Date.now(), + }); + return key; } close(): void { this.client.cancelAllRequests(); diff --git a/src/manage/qd/BookChapter.tsx b/src/manage/qd/BookChapter.tsx index 9973afa..341370c 100644 --- a/src/manage/qd/BookChapter.tsx +++ b/src/manage/qd/BookChapter.tsx @@ -100,7 +100,7 @@ export default function BookChapter() { title="数据加载失败" subTitle={err} extra={} />} - {chapter && } + {chapter && } {!chapter && !err && } diff --git a/src/manage/qd/ChapterEditor.module.css b/src/manage/qd/ChapterEditor.module.css index 317ac59..aae708f 100644 --- a/src/manage/qd/ChapterEditor.module.css +++ b/src/manage/qd/ChapterEditor.module.css @@ -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; +} diff --git a/src/manage/qd/ChapterEditor.tsx b/src/manage/qd/ChapterEditor.tsx index 3806537..ed09a4f 100644 --- a/src/manage/qd/ChapterEditor.tsx +++ b/src/manage/qd/ChapterEditor.tsx @@ -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 { ref; + db?: Db; constructor(props: ChapterEditorProps) { super(props); this.ref = createRef(); @@ -37,6 +45,9 @@ export default class ChapterEditor extends Component, _prevState: Readonly, _snapshot?: unknown): void { @@ -47,6 +58,23 @@ export default class ChapterEditor extends Component { + 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 - { (db) => <> + { (db) => {this.db = db; return <> @@ -128,6 +160,40 @@ export default class ChapterEditor extends Component + {this.props.history && { + this.setState({ historyPanelOpen: true }); + }} />} + {this.props.history && this.setState({ historyPanelOpen: false })} + open={this.state.historyPanelOpen} + > + + {this.state.history && this.state.history.length === 0 && } + {this.state.history && this.state.history.length > 0 && this.state.history.map((item, ind) => ( + + + {item.name} + {item.time === this.props.chapter.time && (当前)} + + 保存时间: {new Date(item.time).toLocaleString()} + + {ind !== 0 && } + + + ))} + {this.state.loadingHistory && } + + }
@@ -144,7 +210,7 @@ export default class ChapterEditor extends Component
- } + }}
); } diff --git a/src/types.ts b/src/types.ts index e51339d..4638324 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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;