mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-07-01 19:00:30 +08:00
Add Db Settings
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.crx
|
||||
|
||||
39
build.js
39
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 });
|
||||
|
||||
@@ -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"
|
||||
|
||||
26
package.py
Normal file
26
package.py
Normal file
@@ -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()
|
||||
@@ -1,3 +1,4 @@
|
||||
.switch-label > span {
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ export type SwitchLabelProps = {
|
||||
export default function SwitchLabel({ label, checked, onChange }: SwitchLabelProps) {
|
||||
return (
|
||||
<div className={styles["switch-label"]}>
|
||||
<span onClick={() => onChange(!checked)}>{label}</span>
|
||||
<Switch checked={checked} onChange={onChange} />
|
||||
<span onClick={() => onChange(!checked)}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<DbConfigData>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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': <DbSettings />,
|
||||
},
|
||||
{
|
||||
'key': '2',
|
||||
'label': `起点设置`,
|
||||
'children': <QdSettings />,
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
function Settings() {
|
||||
|
||||
71
src/settings/DbSettings.tsx
Normal file
71
src/settings/DbSettings.tsx
Normal file
@@ -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 (<>
|
||||
<SwitchLabel label="启用数据压缩" checked={compress} onChange={handleCompressChange} />
|
||||
</>)
|
||||
}
|
||||
|
||||
export default function DbSettings() {
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
const [config] = useState(new DbConfig());
|
||||
const [alert, setAlert] = useState<{ title?: string; content: string } | null>(null);
|
||||
const [dbType, setDbType] = useState<DbType>(DbType.IndexedDb);
|
||||
const [indexedDbConfig, setIndexedDbConfig] = useState<IndexedDbConfig>(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 (
|
||||
<div ref={setContainer}>
|
||||
<Affix target={() => container}>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={saveSettings}>保存设置</Button>
|
||||
<Button onClick={resetSettings} style={{ marginLeft: 8 }} icon={<SyncOutlined />}>重置设置</Button>
|
||||
</Affix>
|
||||
<br />
|
||||
<Space orientation="vertical">
|
||||
<Select value={dbType} onChange={value => setDbType(value)} style={{ width: 200 }} options={[
|
||||
{
|
||||
label: "IndexedDb",
|
||||
value: DbType.IndexedDb,
|
||||
},
|
||||
]} />
|
||||
{dbType === DbType.IndexedDb && <IndexedDbSettings config={indexedDbConfig} />}
|
||||
</Space>
|
||||
<FloatButton icon={<SaveTwoTone />} tooltip="保存设置" onClick={saveSettings} />
|
||||
{alert && <AlertWarn title={alert.title} content={alert.content} onClose={() => setAlert(null)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
src/utils.ts
61
src/utils.ts
@@ -70,3 +70,64 @@ export async function loadConfig<T>(key: string, defaultValue: T): Promise<T> {
|
||||
}
|
||||
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<Uint8Array<ArrayBuffer>> {
|
||||
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<Uint8Array<ArrayBuffer>> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user