diff --git a/package.json b/package.json index 977fdaf..6072fa9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@types/chrome": "^0.1.36", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/wicg-file-system-access": "^2023.10.7", "@zip.js/zip.js": "^2.8.21", "antd": "^6.3.0", "colors": "^1.4.0", diff --git a/src/db/indexedDb.ts b/src/db/indexedDb.ts index 9e63ca4..11c8acc 100644 --- a/src/db/indexedDb.ts +++ b/src/db/indexedDb.ts @@ -75,7 +75,7 @@ function get_datas(db: IDBDatabase, storeName: string, key?: IDBValidKey | ID }); } -function get_data_with_convert(db: IDBDatabase, storeName: string, key: IDBValidKey | IDBKeyRange, convert: (key: IDBValidKey, data: T) => Promise | U, index?: string, direction?: IDBCursorDirection): Promise { +function get_data_with_convert(db: IDBDatabase, storeName: string, key: IDBValidKey | IDBKeyRange | undefined, convert: (key: IDBValidKey, data: T) => Promise | U, index?: string, direction?: IDBCursorDirection): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); @@ -418,6 +418,20 @@ export class IndexedDb implements Db { return data; }, 'id', 'prev'); } + async getQdNewChapterId(): Promise { + const smallest_id = await get_data_with_convert(this.qddb, 'chapters', undefined, async (key, data) => { + if ('compressed' in data) { + const decompressed = await decompress(data.compressed); + const decoded = new TextDecoder().decode(decompressed); + data = JSON.parse(decoded) as QdChapterInfo; + } + return data.id; + }, 'id', 'prev'); + if (smallest_id === undefined || smallest_id >= 0) { + return -1; + } + return smallest_id - 1; + } async setAsLatestQdChapter(key: unknown): Promise { const chapter = await this.getQdChapter(key); if (!chapter) { diff --git a/src/db/interfaces.ts b/src/db/interfaces.ts index c02dd5c..5e542a3 100644 --- a/src/db/interfaces.ts +++ b/src/db/interfaces.ts @@ -61,6 +61,10 @@ export interface Db { * @param id Chapter ID */ getLatestQdChapter(id: number): Promise; + /** + * Returns a new chapter ID (<0) that is not used in the database. + */ + getQdNewChapterId(): 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. diff --git a/src/db/pocketBase.ts b/src/db/pocketBase.ts index 68cb80f..c60d72a 100644 --- a/src/db/pocketBase.ts +++ b/src/db/pocketBase.ts @@ -346,6 +346,18 @@ export class PocketBaseDb implements Db { } return record; } + async getQdNewChapterId(): Promise { + const smallest_id = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getList(1, 1, { + filter: `chapterId < 0`, + fields: 'chapterId', + sort: 'chapterId', + }); + if (smallest_id.totalItems === 0) { + return -1; + } + const id = smallest_id.items[0].chapterId; + return id - 1; + } async setAsLatestQdChapter(key: unknown): Promise { await this.client.collection(`${this.cfg.prefix}qd_chapters`).update(String(key), { time: Date.now(), diff --git a/src/download/qd/Book.tsx b/src/download/qd/Book.tsx index d557a1f..6403228 100644 --- a/src/download/qd/Book.tsx +++ b/src/download/qd/Book.tsx @@ -91,7 +91,7 @@ export default function Book({info, options, save_type}: QdBookProps) { const [total, setTotal] = useState(0); const [current, setCurrent] = useState(0); async function save() { - const pickerOptions = { + const pickerOptions: SaveFilePickerOptions = { suggestedName: `${info.bookName}.${save_type}`, types: [{ description: 'EPUB File', diff --git a/src/manage.tsx b/src/manage.tsx index ab8f4b4..7e29d96 100644 --- a/src/manage.tsx +++ b/src/manage.tsx @@ -10,6 +10,7 @@ import { DbContext } from "./manage/dbProvider"; import QdBook from "./manage/qd/Book"; import QdBookIndex from "./manage/qd/BookIndex"; import QdBookChapter from "./manage/qd/BookChapter"; +import QdBookNewChapter from "./manage/qd/BookNewChapter"; const router = createHashRouter([ { @@ -31,6 +32,10 @@ const router = createHashRouter([ { path: "chapter/:chapterId", element: + }, + { + path: "chapter/new", + element: } ], } diff --git a/src/manage/qd/BookIndex.tsx b/src/manage/qd/BookIndex.tsx index 17038b0..0e071b9 100644 --- a/src/manage/qd/BookIndex.tsx +++ b/src/manage/qd/BookIndex.tsx @@ -11,6 +11,7 @@ import ShowMode from "./ShowMode"; import { sendMessageToTab, waitTabLoaded } from "../../utils"; import { QdBookDownloadOptions } from "../../types"; import SwitchLabel from "../../components/SwitchLabel"; +import { useNavigate } from "react-router"; const { Paragraph, Link } = Typography; @@ -24,6 +25,7 @@ export default function BookIndex() { const [err, setErr] = useState(null); const [saveChapterOpenAsEpub, setSaveChapterOpenAsEpub] = useState(false); const [downloadOptions, setDownloadOptions] = useState({}); + const navigate = useNavigate(); function setChapterShowMode(chapterShowMode: ChapterShowMode) { setBookStatus({ ...bookStatus, chapterShowMode }); } @@ -104,6 +106,7 @@ export default function BookIndex() { label="跳过未保存章节" /> + diff --git a/src/manage/qd/BookNewChapter.tsx b/src/manage/qd/BookNewChapter.tsx new file mode 100644 index 0000000..d0f1049 --- /dev/null +++ b/src/manage/qd/BookNewChapter.tsx @@ -0,0 +1,161 @@ +import { useBookContext } from "./BookStatusProvider"; +import type { QdChapterInfo } from "../../types"; +import { useEffect, useState } from "react"; +import { useDb } from "../dbProvider"; +import { useBookInfo } from "./BookInfoProvider"; +import { Result, Skeleton } from "antd"; + +export default function BookNewChapter() { + const setItems = useBookContext(); + const [chapter, setChapter] = useState(null); + const [err, setErr] = useState(null); + const bookInfo = useBookInfo(); + const db = useDb(); + setItems([ + { title: "新章节" }, + ]) + async function load() { + const new_id = await db.getQdNewChapterId(); + console.log("new chapter id", new_id); + const time = Date.now(); + setChapter({ + id: new_id, + bookId: bookInfo.id, + contents: [], + time, + bookInfo: { + bookId: bookInfo.id, + bookName: bookInfo.bookName, + sbookid: 0, + authorId: Number(bookInfo.bookInfo.pageJson.authorInfo.authorId), + authorName: bookInfo.bookInfo.pageJson.authorInfo.authorName, + cAuthorId: '', + chanId: bookInfo.bookInfo.chanId, + chanName: '', + chanUrl: '', + chanAlias: '', + subCateId: 0, + subCateName: '', + unitCategoryId: 0, + unitSubCategoryId: 0, + isVip: bookInfo.bookInfo.pageJson.isVip, + bookType: bookInfo.bookInfo.pageJson.bookType, + form: 0, + chargetype: 0, + totalprice: 0, + fineLayout: 0, + isPreCollection: 0, + bookStore: { + member: false, + app: false, + story: false, + }, + bookStatus: '', + actionStatus: '', + signStatus: bookInfo.bookInfo.pageJson.signStatus, + joinTime: '', + collect: 0, + updChapterId: 0, + updChapterName: '', + updTime: 0, + updChapterUrl: '', + cbid: '', + editorNickname: '', + bookLabels: [], + bookTag: { + tagName: '', + }, + updInfo: { + desc: '', + tag: '', + updStatus: '', + }, + supplierId: '', + interact: { + recTicketEnable: 0, + monthTicketEnable: 0, + donateEnable: 0, + }, + joinTimes: 0, + isSign: bookInfo.bookInfo.pageJson.isSign, + noRewardMonthTic: 0, + bookAllAuth: 0, + }, + chapterInfo: { + actualWords: 0, + authorRecommend: [], + authorSay: '', + cbid: '', + ccid: '', + chapterId: new_id, + chapterName: '', + chapterOrder: 0, + chapterType: 0, + cvid: '', + extra: { + nextCcid: '', + nextName: '', + nextVipStatus: 0, + preCcid: '', + prevName: '', + prevVipStatus: 0, + volumeBody: false, + volumeName: '', + nextUrl: '', + preUrl: '', + }, + fineLayout: 0, + freeStatus: 0, + modifyTime: time, + multiModal: 0, + nextCcid: '', + prevCcid: '', + seq: 0, + updateTime: new Date(time).toISOString(), + uuid: 0, + vipStatus: 0, + volumeId: 0, + wordsCount: 0, + isFirst: 0, + content: '', + riskInfo: { + banId: 0, + banMessage: '', + sessionKey: '', + captchaAId: '', + captchaURL: '', + phoneNumber: '', + gt: '', + challenge: '', + offline: 0, + newCaptcha: 0, + captchaType: 0, + }, + riskbe: { + be: 0, + message: '', + }, + updateTimestamp: time, + isBuy: 0, + limitFree: 0, + authorWords: { + content: '', + }, + eFW: 0, + cES: 0, + guidMark: '', + fEnS: 0, + }, + }) + } + useEffect(() => { + load().catch(e => { + console.warn(e); + setErr(e instanceof Error ? e.message : String(e)); + }); + }, []); + return (<> + {err && } + {!err && !chapter && } + ); +} diff --git a/src/manage/qd/ChapterEditor.tsx b/src/manage/qd/ChapterEditor.tsx index bf26144..ea5e565 100644 --- a/src/manage/qd/ChapterEditor.tsx +++ b/src/manage/qd/ChapterEditor.tsx @@ -37,6 +37,8 @@ export interface ChapterEditorState { export default class ChapterEditor extends Component { ref; + #editorContainerRef = createRef(); + #resizeObserver?: ResizeObserver; db?: Db; constructor(props: ChapterEditorProps) { super(props); @@ -51,6 +53,17 @@ export default class ChapterEditor extends Component { + this.layout(); + }); + this.#resizeObserver.observe(this.#editorContainerRef.current); + } + } + componentWillUnmount() { + this.#resizeObserver?.disconnect(); + } componentDidUpdate(prevProps: Readonly, _prevState: Readonly, _snapshot?: unknown): void { if (prevProps.chapter.id !== this.props.chapter.id) { this.setState({ @@ -208,7 +221,7 @@ export default class ChapterEditor extends Component} -
+