Add Db Settings

This commit is contained in:
2026-02-14 15:56:59 +08:00
parent deefa95918
commit d1a5b4aa3c
11 changed files with 274 additions and 4 deletions

View File

@@ -1,3 +1,4 @@
.switch-label > span {
cursor: pointer;
margin-left: 4px;
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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() {

View 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>
);
}

View File

@@ -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;
}