Add settings page

This commit is contained in:
2026-02-13 23:48:43 +08:00
parent 92f5073774
commit dc252af38a
10 changed files with 139 additions and 31 deletions

View File

@@ -7,13 +7,13 @@ const is_dev = process.argv.includes('--dev');
const is_dbg = process.argv.includes('--debug');
const no_source_map = process.argv.includes('--no-sourcemap');
function sourcemap() {
function sourcemap(is_content_script = false) {
if (no_source_map) return false;
if (is_dev) return "inline";
if (is_dev && is_content_script) return "inline";
return true;
}
async function build(name) {
async function build(name, is_content_script = true) {
return await esbuild.build({
entryPoints: [`src/${name}.ts`],
bundle: true,
@@ -21,42 +21,36 @@ async function build(name) {
outfile: `dist/${name}.js`,
platform: 'browser',
target: ['chrome100'],
sourcemap: sourcemap(),
sourcemap: sourcemap(is_content_script),
})
}
async function buildTsx(name) {
async function buildTsx(names) {
const entryPoints = [];
for (const name of names) {
entryPoints.push(`src/${name}.tsx`);
}
await esbuild.build({
entryPoints: [`src/${name}.tsx`],
entryPoints: entryPoints,
bundle: true,
minify: !is_dbg,
outfile: `dist/${name}.js`,
outdir: 'dist',
platform: 'browser',
target: ['chrome100'],
sourcemap: sourcemap(),
jsx: 'automatic',
loader: { '.css': 'global-css', '.module.css': 'local-css' },
splitting: true,
format: 'esm',
});
// 确保输出目录存在并写入同名 HTML
fs.mkdirSync(path.dirname(`dist/${name}.html`), { recursive: true });
const cssFile = `dist/${name}.css`;
const cssLink = fs.existsSync(cssFile) ? `<link rel="stylesheet" href="./${name}.css"/>` : '';
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>${name}</title>
${cssLink}
</head>
<body>
<div id="root"></div>
<script src="./${name}.js"></script>
</body>
</html>`;
fs.writeFileSync(`dist/${name}.html`, html, 'utf8');
for (const name of names) {
const srcHtmlPath = path.join('src', `${name}.html`);
const distHtmlPath = path.join('dist', `${name}.html`);
fs.copyFileSync(srcHtmlPath, distHtmlPath);
}
}
fs.rmSync('dist', { recursive: true, force: true });
fs.mkdirSync('dist', { recursive: true });
await build('qdchapter');
await buildTsx('popup');
await buildTsx(['popup', 'settings']);

View File

@@ -31,5 +31,9 @@
],
"run_at": "document_start"
}
]
],
"options_ui": {
"open_in_tab": true,
"page": "dist/settings.html"
}
}

View File

@@ -1,6 +1,7 @@
{
"name": "bookdownload",
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@types/chrome": "^0.1.36",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",

30
src/config.ts Normal file
View File

@@ -0,0 +1,30 @@
import { loadConfig, saveConfig } from "./utils";
type QdConfigData = {
AutoSaveChapter?: boolean;
}
export class QdConfig {
static STORAGE_KEY = 'qd_config';
config?: QdConfigData;
constructor() {
}
async init() {
this.config = await loadConfig<QdConfigData>(QdConfig.STORAGE_KEY, {});
}
async save() {
if (!this.config) {
throw new Error('Config not initialized');
}
await saveConfig(QdConfig.STORAGE_KEY, this.config);
}
get AutoSaveChapter(): boolean {
return this.config?.AutoSaveChapter ?? false;
}
set AutoSaveChapter(value: boolean) {
if (!this.config) {
throw new Error('Config not initialized');
}
this.config.AutoSaveChapter = value;
}
}

View File

@@ -3,7 +3,7 @@ import { Descriptions, Card, Typography, Tag, Space, Button } from 'antd';
import type { BookInfo, ChapterInfo } from '../qdtypes';
import { get_chapter_content, saveAsFile } from '../utils';
const { Title, Text } = Typography;
const { Title, Text, Paragraph } = Typography;
interface QdChapterInfoProps {
bookInfo: BookInfo;
@@ -127,7 +127,7 @@ export default function QdChapterInfo({ bookInfo, chapterInfo }: QdChapterInfoPr
{/* 章节内容 */}
<Card title="章节内容" size="small">
{chapters.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
<Paragraph key={index}>{paragraph}</Paragraph>
))}
</Card>
</Space>

13
src/popup.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>扩展弹出页</title>
<link rel="stylesheet" href="./popup.css"/>
</head>
<body>
<div id="root"></div>
<script src="./popup.js" type="module"></script>
</body>
</html>

13
src/settings.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>扩展设置</title>
<!--<link rel="stylesheet" href="./settings.css"/>-->
</head>
<body>
<div id="root"></div>
<script src="./settings.js" type="module"></script>
</body>
</html>

23
src/settings.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { createRoot } from "react-dom/client";
import { Typography, Tabs } from "antd";
import type { TabsProps } from 'antd';
import QdSettings from "./settings/QdSettings";
const { Title } = Typography;
const items: TabsProps['items'] = [
{
'key': '1',
'label': `起点设置`,
'children': <QdSettings />,
}
];
function Settings() {
return (<>
<Title ></Title>
<Tabs defaultActiveKey="1" items={items} />
</>);
}
createRoot(document.getElementById("root")!).render(<Settings />);

View File

@@ -0,0 +1,18 @@
import { Typography, Switch, FloatButton, Affix, Button } from "antd";
import { SaveTwoTone, SaveOutlined } from "@ant-design/icons";
import { useState } from "react";
const { Title } = Typography;
export default function QdSettings() {
const [container, setContainer] = useState<HTMLElement | null>(null);
return (
<div ref={setContainer}>
<Title level={2}></Title>
<Affix target={() => container}>
<Button type="primary" icon={<SaveOutlined />}></Button>
</Affix>
<FloatButton icon={<SaveTwoTone />} tooltip="保存设置" />
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { UrlParams, SendMessage, Message } from "./types";
import type { UrlParams, SendMessage, Message } from "./types";
export const QD_CHAPTER_URLPATH_REGEX = /^\/chapter\/(\d+)\/(\d+)\/?$/;
@@ -58,3 +58,15 @@ export function saveAsFile(filename: string, content: string, mimeType: string =
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export async function saveConfig(key: string, config: any): Promise<void> {
return await chrome.storage.managed.set({ [key]: config });
}
export async function loadConfig<T>(key: string, defaultValue: T): Promise<T> {
const r = await chrome.storage.managed.get<{[key]: T | undefined}>(key);
if (r[key] === undefined) {
return defaultValue;
}
return r[key];
}