diff --git a/src/download.tsx b/src/download.tsx
index 43aae35..0e10d29 100644
--- a/src/download.tsx
+++ b/src/download.tsx
@@ -16,6 +16,13 @@ function Download() {
code: 0,
for: m.type,
} as Message);
+ } else if (m.type === 'DownloadQdBookAsTxtZip') {
+ setMessage(m);
+ sendResponse({
+ ok: true,
+ code: 0,
+ for: m.type,
+ } as Message);
}
});
}, []);
@@ -23,6 +30,9 @@ function Download() {
{message && message.type === 'DownloadQdBookAsEpub' &&
}
+ {message && message.type === 'DownloadQdBookAsTxtZip' &&
+
+ }
{!message && (null);
const [ok, setOk] = useState(false);
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(0);
const [msg, setMsg] = useState('');
async function save() {
+ const filePickerType: FilePickerAcceptType = save_type === 'epub' ? {
+ description: 'EPUB File',
+ accept: {'application/epub+zip': ['.epub']}
+ } : {
+ description: 'ZIP File',
+ accept: {'application/zip': ['.zip']}
+ };
const pickerOptions: SaveFilePickerOptions = {
- suggestedName: `${info.bookName}.${save_type}`,
- types: [{
- description: 'EPUB File',
- accept: {'application/epub+zip': ['.epub']}
- }],
+ suggestedName: `${info.bookName}.${ExtMap[save_type]}`,
+ types: [filePickerType],
};
const fileHandle = await window.showSaveFilePicker(pickerOptions);
const writable = await fileHandle.createWritable();
- const epub = new Epub(writable);
+ const epub = save_type === 'epub' ? new Epub(writable) : undefined;
+ const zip = save_type === 'txtzip' ? (makesure_zip_configured(), new ZipWriter(writable)) : undefined;
+ const xhtml = save_type === 'epub';
if (epub) {
setMsg('初始化EPUB文件');
await epub.init();
@@ -158,6 +188,7 @@ export default function Book({info, options, save_type}: QdBookProps) {
let batchChs: (QdChapterInfo | undefined)[] = []
const batchSize = db.batchSize();
const chKeys: Map = new Map();
+ const nameSets: Set = new Set();
let c = 0;
for (const ch of chapters) {
chKeys.set(ch.id, ch.primaryKey);
@@ -210,9 +241,9 @@ export default function Book({info, options, save_type}: QdBookProps) {
if (skipNotBoughtChapters && !ch.chapterInfo.isBuy) {
continue;
}
- page = generate_chapter_page(ch);
+ page = generate_chapter_page(ch, xhtml);
} else {
- page = generate_empty_chapter(chapter.name);
+ page = generate_empty_chapter(chapter.name, '未保存', xhtml);
it.name += '(未保存)';
}
if (epub) {
@@ -224,6 +255,17 @@ export default function Book({info, options, save_type}: QdBookProps) {
linear: vol.name !== '作品相关',
});
}
+ if (zip) {
+ const addVolFolder = txtzip?.addVolumeFolder ?? true;
+ const useChapterNameAsFileName = txtzip?.useChapterNameAsFileName ?? true;
+ const fileName = useChapterNameAsFileName ? filter_filename(it.name) : `ch_${chapter.id}`;
+ let path = addVolFolder ? `${filter_filename(vol.name)}/${fileName}` : fileName;
+ if (nameSets.has(path)) {
+ path += `_${chapter.id}`;
+ }
+ nameSets.add(path);
+ await zip.add(path + '.txt', new TextReader(page));
+ }
volNav.children!.push(it);
if (volNav.href === undefined) {
volNav.href = link;
@@ -240,6 +282,10 @@ export default function Book({info, options, save_type}: QdBookProps) {
await epub.save();
setOk(true);
}
+ if (zip) {
+ await zip.close();
+ setOk(true);
+ }
}
useEffect(() => {
save().catch(e => {
diff --git a/src/epub/base.ts b/src/epub/base.ts
index b05ba3a..1a606ab 100644
--- a/src/epub/base.ts
+++ b/src/epub/base.ts
@@ -1,24 +1,17 @@
-import { configure, ZipWriter, TextReader, BlobReader, WritableWriter } from "@zip.js/zip.js";
+import { ZipWriter, TextReader, BlobReader, WritableWriter } from "@zip.js/zip.js";
import type { Reader, ReadableReader, ZipWriterAddDataOptions } from "@zip.js/zip.js";
import { EpubPackage, EpubManifestItem, EpubItemRef } from "./package";
import { EpubNav, to_nav_xhtml } from "./nav";
+import { makesure_zip_configured } from "../utils/zip";
type StreamType = Reader | ReadableReader | ReadableStream | Reader[] | ReadableReader[] | ReadableStream[];
-let configured = false;
-
export class Epub {
zip: ZipWriter;
package;
#inited;
constructor(blob: T) {
- if (!configured) {
- configure({
- useWebWorkers: false,
- useCompressionStream: true,
- })
- configured = true;
- }
+ makesure_zip_configured();
this.zip = new ZipWriter(blob);
this.package = new EpubPackage();
this.#inited = false;
diff --git a/src/manage/qd/BookIndex.tsx b/src/manage/qd/BookIndex.tsx
index 2d8f962..8ff56df 100644
--- a/src/manage/qd/BookIndex.tsx
+++ b/src/manage/qd/BookIndex.tsx
@@ -9,7 +9,7 @@ import type { Volume } from "../../qdtypes";
import { ChapterShowMode, get_new_volumes } from "../../utils/qd";
import ShowMode from "./ShowMode";
import { sendMessageToTab, waitTabLoaded } from "../../utils";
-import { QdBookDownloadOptions } from "../../types";
+import { QdBookDownloadOptions, QdBookTxtZipOptions } from "../../types";
import SwitchLabel from "../../components/SwitchLabel";
import { useNavigate } from "react-router";
@@ -23,8 +23,9 @@ export default function BookIndex() {
const [bookStatus, setBookStatus] = useBookStatus();
const setItems = useBookContext();
const [err, setErr] = useState(null);
- const [saveChapterOpenAsEpub, setSaveChapterOpenAsEpub] = useState(false);
+ const [saveChapterOpenSaveAs, setSaveChapterOpenSaveAs] = useState<'epub' | 'txtzip' | false>(false);
const [downloadOptions, setDownloadOptions] = useState({});
+ const [txtzipOptions, setTxtZipOptions] = useState({});
const navigate = useNavigate();
function setChapterShowMode(chapterShowMode: ChapterShowMode) {
setBookStatus({ ...bookStatus, chapterShowMode });
@@ -38,17 +39,26 @@ export default function BookIndex() {
setErr(e instanceof Error ? e.message : String(e));
});
}
- async function handleSaveAsEpub() {
+ async function handleSave() {
const url = chrome.runtime.getURL('dist/download.html');
const tab = await chrome.tabs.create({ url });
if (tab.status !== 'complete') {
await waitTabLoaded(tab.id!);
}
- await sendMessageToTab(tab.id!, {
- type: 'DownloadQdBookAsEpub',
- info: bookInfo,
- options: downloadOptions,
- });
+ if (saveChapterOpenSaveAs === 'epub') {
+ await sendMessageToTab(tab.id!, {
+ type: 'DownloadQdBookAsEpub',
+ info: bookInfo,
+ options: downloadOptions,
+ });
+ } else if (saveChapterOpenSaveAs === 'txtzip') {
+ await sendMessageToTab(tab.id!, {
+ type: 'DownloadQdBookAsTxtZip',
+ info: bookInfo,
+ options: downloadOptions,
+ txtzip: txtzipOptions,
+ });
+ }
}
useEffect(() => {
handle();
@@ -87,15 +97,16 @@ export default function BookIndex() {
-
+
+
setSaveChapterOpenAsEpub(false)}
+ open={saveChapterOpenSaveAs !== false}
+ onCancel={() => setSaveChapterOpenSaveAs(false)}
onOk={() => {
- setSaveChapterOpenAsEpub(false);
- handleSaveAsEpub();
+ setSaveChapterOpenSaveAs(false);
+ handleSave();
}}
- title="保存为EPUB"
+ title={`保存为${saveChapterOpenSaveAs === 'epub' ? 'EPUB' : 'TXT ZIP'}`}
okText="保存"
cancelText="取消"
>
@@ -109,6 +120,18 @@ export default function BookIndex() {
onChange={(checked) => setDownloadOptions({ ...downloadOptions, skipUnsavedChapters: checked })}
label="跳过未保存章节"
/>
+ {saveChapterOpenSaveAs === 'txtzip' && (<>
+ setTxtZipOptions({ ...txtzipOptions, addVolumeFolder: checked })}
+ label="为每个卷创建文件夹"
+ />
+ setTxtZipOptions({ ...txtzipOptions, useChapterNameAsFileName: checked })}
+ label="使用章节名称作为文件名(否则使用章节ID)"
+ />
+ >)}
diff --git a/src/types.ts b/src/types.ts
index ec33650..72a44ff 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -55,6 +55,13 @@ export interface QdBookDownloadOptions {
skipNotBoughtChapters?: boolean;
}
+export interface QdBookTxtZipOptions {
+ /**@default {true} */
+ addVolumeFolder?: boolean;
+ /**@default {true} */
+ useChapterNameAsFileName?: boolean;
+}
+
export type SendMessageMap = {
GetQdChapterInfo: {};
GetQdBookInfo: {};
@@ -64,6 +71,11 @@ export type SendMessageMap = {
DownloadQdBookAsEpub: {
info: QdBookInfo;
options?: QdBookDownloadOptions;
+ },
+ DownloadQdBookAsTxtZip: {
+ info: QdBookInfo;
+ options?: QdBookDownloadOptions;
+ txtzip?: QdBookTxtZipOptions;
}
}
diff --git a/src/utils/filename.ts b/src/utils/filename.ts
new file mode 100644
index 0000000..45b5ad5
--- /dev/null
+++ b/src/utils/filename.ts
@@ -0,0 +1,4 @@
+export function filter_filename(name: string) {
+ // Remove characters that are not allowed in file names on Windows, macOS, and Linux
+ return name.replace(/[/\\?%*:|"<>]/g, '_');
+}
diff --git a/src/utils/zip.ts b/src/utils/zip.ts
new file mode 100644
index 0000000..bf76b55
--- /dev/null
+++ b/src/utils/zip.ts
@@ -0,0 +1,12 @@
+import { configure } from '@zip.js/zip.js';
+let configured = false;
+
+export function makesure_zip_configured() {
+ if (!configured) {
+ configure({
+ useWebWorkers: false,
+ useCompressionStream: true,
+ })
+ configured = true;
+ }
+}