diff --git a/.gitignore b/.gitignore
index b947077..1f3a407 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
node_modules/
dist/
+*.crx
diff --git a/build.js b/build.js
index 69808ea..c3fe140 100644
--- a/build.js
+++ b/build.js
@@ -2,6 +2,7 @@ import esbuild from 'esbuild';
import process from 'node:process';
import fs from 'node:fs';
import path from 'node:path';
+import colors from 'colors';
const is_dev = process.argv.includes('--dev');
const is_dbg = process.argv.includes('--debug');
@@ -13,8 +14,36 @@ function sourcemap(is_content_script = false) {
return true;
}
+function displaySize(bytes) {
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
+ let i = 0;
+ while (bytes >= 1024 && i < units.length - 1) {
+ bytes /= 1024;
+ i++;
+ }
+ return `${bytes.toFixed(2)} ${units[i]}`;
+}
+
+/**@param {esbuild.BuildResult} result */
+function displayResult(result) {
+ for (const outfile in result.metafile.outputs) {
+ const output = result.metafile.outputs[outfile];
+ let totalInBytes = 0;
+ for (const input in output.inputs) {
+ const inp = result.metafile.inputs[input];
+ totalInBytes += inp.bytes;
+ }
+ let inInfo = '';
+ if (totalInBytes > 0) {
+ const ratio = (output.bytes / totalInBytes * 100).toFixed(2);
+ inInfo = ` - Input size: ${colors.green(displaySize(totalInBytes))} (${totalInBytes} B), Ratio: ${colors.yellow(ratio + '%')}`;
+ }
+ console.log(`${colors.cyan(outfile)}: ${colors.yellow(displaySize(output.bytes))} (${output.bytes} B)${inInfo}`);
+ }
+}
+
async function build(name, is_content_script = true) {
- return await esbuild.build({
+ const result = await esbuild.build({
entryPoints: [`src/${name}.ts`],
bundle: true,
minify: !is_dbg,
@@ -22,7 +51,10 @@ async function build(name, is_content_script = true) {
platform: 'browser',
target: ['chrome100'],
sourcemap: sourcemap(is_content_script),
+ metafile: true,
})
+ displayResult(result);
+ return result;
}
async function buildTsx(names) {
@@ -30,7 +62,7 @@ async function buildTsx(names) {
for (const name of names) {
entryPoints.push(`src/${name}.tsx`);
}
- await esbuild.build({
+ const result = await esbuild.build({
entryPoints: entryPoints,
bundle: true,
minify: !is_dbg,
@@ -42,12 +74,15 @@ async function buildTsx(names) {
loader: { '.css': 'global-css', '.module.css': 'local-css' },
splitting: true,
format: 'esm',
+ metafile: true,
});
for (const name of names) {
const srcHtmlPath = path.join('src', `${name}.html`);
const distHtmlPath = path.join('dist', `${name}.html`);
fs.copyFileSync(srcHtmlPath, distHtmlPath);
}
+ displayResult(result);
+ return result;
}
fs.rmSync('dist', { recursive: true, force: true });
diff --git a/package.json b/package.json
index 8e69bd4..c57a984 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"antd": "^6.3.0",
+ "colors": "^1.4.0",
"esbuild": "^0.27.3",
"react": "^19.2.4",
"react-dom": "^19.2.4"
diff --git a/package.py b/package.py
new file mode 100644
index 0000000..ae9c64b
--- /dev/null
+++ b/package.py
@@ -0,0 +1,26 @@
+from zipfile import ZipFile, ZIP_DEFLATED
+import os
+
+NEED_PACKED = [
+ 'dist',
+ 'ico',
+ 'manifest.json',
+ 'LICENSE',
+]
+
+def pack():
+ with ZipFile('bookdownload.crx', 'w', compression=ZIP_DEFLATED, compresslevel=9) as zip:
+ for item in NEED_PACKED:
+ if not os.path.exists(item):
+ print(f'Warning: {item} does not exist, skipping.')
+ continue
+ if os.path.isdir(item):
+ for foldername, subfolders, filenames in os.walk(item):
+ for filename in filenames:
+ zip.write(os.path.join(foldername, filename))
+ else:
+ zip.write(item)
+
+
+if __name__ == '__main__':
+ pack()
diff --git a/src/components/SwitchLabel.module.css b/src/components/SwitchLabel.module.css
index 0b4ef82..b263984 100644
--- a/src/components/SwitchLabel.module.css
+++ b/src/components/SwitchLabel.module.css
@@ -1,3 +1,4 @@
.switch-label > span {
cursor: pointer;
+ margin-left: 4px;
}
diff --git a/src/components/SwitchLabel.tsx b/src/components/SwitchLabel.tsx
index abd00bb..ba6054f 100644
--- a/src/components/SwitchLabel.tsx
+++ b/src/components/SwitchLabel.tsx
@@ -10,8 +10,8 @@ export type SwitchLabelProps = {
export default function SwitchLabel({ label, checked, onChange }: SwitchLabelProps) {
return (
- onChange(!checked)}>{label}
+ onChange(!checked)}>{label}
);
}
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index b09a732..f581019 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -31,3 +31,66 @@ export class QdConfig {
this.config.AutoSaveChapter = value;
}
}
+
+type IndexedDbConfigData = {
+ compress?: boolean;
+}
+
+export enum DbType {
+ IndexedDb,
+}
+
+type DbConfigData = {
+ IndexedDb?: IndexedDbConfigData;
+ DbType?: DbType;
+}
+
+export class IndexedDbConfig {
+ config: IndexedDbConfigData;
+ constructor(config: IndexedDbConfigData) {
+ this.config = config;
+ }
+ get compress(): boolean {
+ return this.config.compress ?? false;
+ }
+ set compress(value: boolean) {
+ this.config.compress = value;
+ }
+}
+
+export class DbConfig {
+ static STORAGE_KEY = 'db_config';
+ config?: DbConfigData;
+ constructor() {
+ }
+ async init() {
+ this.config = await loadConfig(DbConfig.STORAGE_KEY, {});
+ }
+ async save() {
+ if (!this.config) {
+ throw new Error('Config not initialized');
+ }
+ await saveConfig(DbConfig.STORAGE_KEY, this.config);
+ }
+ reset() {
+ this.config = {};
+ }
+ get IndexedDb(): IndexedDbConfig {
+ if (!this.config) {
+ throw new Error('Config not initialized');
+ }
+ if (!this.config.IndexedDb) {
+ this.config.IndexedDb = {};
+ }
+ return new IndexedDbConfig(this.config.IndexedDb);
+ }
+ get DbType(): DbType {
+ return this.config?.DbType ?? DbType.IndexedDb;
+ }
+ set DbType(value: DbType) {
+ if (!this.config) {
+ throw new Error('Config not initialized');
+ }
+ this.config.DbType = value;
+ }
+}
diff --git a/src/settings.tsx b/src/settings.tsx
index 3e29f35..fb9541d 100644
--- a/src/settings.tsx
+++ b/src/settings.tsx
@@ -2,15 +2,21 @@ import { createRoot } from "react-dom/client";
import { Typography, Tabs } from "antd";
import type { TabsProps } from 'antd';
import QdSettings from "./settings/QdSettings";
+import DbSettings from "./settings/DbSettings";
const { Title } = Typography;
const items: TabsProps['items'] = [
{
'key': '1',
+ 'label': `数据库设置`,
+ 'children': ,
+ },
+ {
+ 'key': '2',
'label': `起点设置`,
'children': ,
- }
+ },
];
function Settings() {
diff --git a/src/settings/DbSettings.tsx b/src/settings/DbSettings.tsx
new file mode 100644
index 0000000..e2cabab
--- /dev/null
+++ b/src/settings/DbSettings.tsx
@@ -0,0 +1,71 @@
+import { FloatButton, Affix, Button, Space, Select, Input } from "antd";
+import { SaveTwoTone, SaveOutlined, SyncOutlined } from "@ant-design/icons";
+import { useEffect, useState } from "react";
+import { DbConfig, DbType, IndexedDbConfig } from "../config";
+import AlertWarn from "../components/AlertWarn";
+import SwitchLabel from "../components/SwitchLabel";
+
+function IndexedDbSettings({config}: { config: IndexedDbConfig }) {
+ const [compress, setCompress] = useState(false);
+ useEffect(() => {
+ setCompress(config.compress);
+ }, [config]);
+ function handleCompressChange(value: boolean) {
+ setCompress(value);
+ config.compress = value;
+ }
+ return (<>
+
+ >)
+}
+
+export default function DbSettings() {
+ const [container, setContainer] = useState(null);
+ const [config] = useState(new DbConfig());
+ const [alert, setAlert] = useState<{ title?: string; content: string } | null>(null);
+ const [dbType, setDbType] = useState(DbType.IndexedDb);
+ const [indexedDbConfig, setIndexedDbConfig] = useState(new IndexedDbConfig({}));
+ function handleConfig() {
+ setDbType(config.DbType);
+ setIndexedDbConfig(config.IndexedDb);
+ }
+ useEffect(() => {
+ config.init().then(() => {
+ handleConfig();
+ }).catch(e => {
+ setAlert({ content: "加载设置失败:" + (e instanceof Error ? e.message : "未知错误"), title: "错误" });
+ });
+ }, []);
+ function saveSettings() {
+ config.DbType = dbType;
+ config.save().then(() => {
+ setAlert({ content: "设置已保存!", title: "通知" });
+ }).catch(e => {
+ setAlert({ content: e instanceof Error ? e.message : "未知错误", title: "错误" });
+ });
+ }
+ function resetSettings() {
+ config.reset();
+ handleConfig();
+ }
+ return (
+
+
container}>
+ } onClick={saveSettings}>保存设置
+ }>重置设置
+
+
+
+
+
} tooltip="保存设置" onClick={saveSettings} />
+ {alert &&
setAlert(null)} />}
+
+ );
+}
diff --git a/src/utils.ts b/src/utils.ts
index 1925813..399ca98 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -70,3 +70,64 @@ export async function loadConfig(key: string, defaultValue: T): Promise {
}
return r[key];
}
+
+/**
+ * Compress data using CompressionStream API. The result is a Uint8Array with the first 4 bytes representing the original data length (little-endian), followed by the compressed data.
+ */
+export async function compress(data: BufferSource, method: CompressionFormat = 'deflate'): Promise> {
+ if (typeof CompressionStream === 'undefined') {
+ throw new Error('CompressionStream is not supported in this browser');
+ }
+ const stream = new CompressionStream(method);
+ const writer = stream.writable.getWriter();
+ const dataLength = data.byteLength;
+ writer.write(data);
+ writer.close();
+ const reader = stream.readable.getReader();
+ const chunks: Uint8Array[] = [];
+ let totalLength = 0;
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) {
+ break;
+ }
+ chunks.push(value);
+ totalLength += value.length;
+ }
+ const result = new Uint8Array(totalLength + 4);
+ const view = new DataView(result.buffer);
+ view.setUint32(0, dataLength, true);
+ let offset = 4;
+ for (const chunk of chunks) {
+ result.set(chunk, offset);
+ offset += chunk.length;
+ }
+ return result;
+}
+
+/**
+ * Decompress data using DecompressionStream API. The input should be a Uint8Array where the first 4 bytes represent the original data length (little-endian), followed by the compressed data. The output is a Uint8Array containing the decompressed data.
+ */
+export async function decompress(data: BufferSource, method: CompressionFormat = 'deflate'): Promise> {
+ if (typeof DecompressionStream === 'undefined') {
+ throw new Error('DecompressionStream is not supported in this browser');
+ }
+ const stream = new DecompressionStream(method);
+ const view = new DataView(data instanceof ArrayBuffer ? data : data.buffer);
+ const originalLength = view.getUint32(0, true);
+ const writer = stream.writable.getWriter();
+ writer.write(view.buffer.slice(4));
+ writer.close();
+ const reader = stream.readable.getReader();
+ const result = new Uint8Array(originalLength);
+ let offset = 0;
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) {
+ break;
+ }
+ result.set(value, offset);
+ offset += value.length;
+ }
+ return result;
+}
diff --git a/yarn.lock b/yarn.lock
index 440d096..e27830f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -701,6 +701,11 @@ clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+colors@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+ integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
compute-scroll-into-view@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa"