mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-07-03 20:01:24 +08:00
Update settings page
This commit is contained in:
@@ -4,26 +4,40 @@ import { ConfigType } from "../config.ts";
|
||||
import TextField from "preact-material-components/TextField";
|
||||
import { Ref, StateUpdater, useRef } from "preact/hooks";
|
||||
|
||||
export type SettingsTextProps = {
|
||||
value: string;
|
||||
interface TextType {
|
||||
text: string;
|
||||
password: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
text: never;
|
||||
password: never;
|
||||
number: number;
|
||||
}
|
||||
|
||||
export type SettingsTextProps<T extends keyof TextType> = {
|
||||
value: TextType[T];
|
||||
name: keyof ConfigType;
|
||||
description: string;
|
||||
type: T;
|
||||
label?: string;
|
||||
helpertext?: string;
|
||||
textarea?: boolean;
|
||||
fullwidth?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: ComponentChildren;
|
||||
set_value?: StateUpdater<string>;
|
||||
ignore_update_value?: boolean;
|
||||
set_value?: StateUpdater<TextType[T]>;
|
||||
min?: DataType[T];
|
||||
max?: DataType[T];
|
||||
};
|
||||
|
||||
export default class SettingsText
|
||||
extends Component<SettingsTextProps, unknown> {
|
||||
export default class SettingsText<T extends keyof TextType>
|
||||
extends Component<SettingsTextProps<T>, unknown> {
|
||||
static contextType = SettingsCtx;
|
||||
ref: Ref<TextField | undefined> | undefined;
|
||||
declare context: ContextType<typeof SettingsCtx>;
|
||||
update(value: string) {
|
||||
update(value: TextType[T]) {
|
||||
const e = this.ref?.current;
|
||||
if (e) {
|
||||
const b = e.base;
|
||||
@@ -31,7 +45,11 @@ export default class SettingsText
|
||||
const t = b as HTMLElement;
|
||||
const d = t.querySelector("input");
|
||||
if (d) {
|
||||
d.value = value;
|
||||
const type = this.props.type;
|
||||
// @ts-ignore Checked
|
||||
if (type === "text" || type === "password") d.value = value;
|
||||
// @ts-ignore Checked
|
||||
else d.valueAsNumber = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +62,7 @@ export default class SettingsText
|
||||
});
|
||||
}
|
||||
}
|
||||
set_text(value: string) {
|
||||
set_value(value: TextType[T]) {
|
||||
if (this.props.set_value) {
|
||||
this.props.set_value(value);
|
||||
this.set_changed();
|
||||
@@ -60,14 +78,21 @@ export default class SettingsText
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
if (!this.props.ignore_update_value) this.update(this.props.value);
|
||||
this.update(this.props.value);
|
||||
}
|
||||
componentWillUpdate(
|
||||
nextProps: Readonly<SettingsTextProps>,
|
||||
nextProps: Readonly<SettingsTextProps<T>>,
|
||||
_nextState: Readonly<unknown>,
|
||||
_nextContext: unknown,
|
||||
) {
|
||||
if (!this.props.ignore_update_value) this.update(nextProps.value);
|
||||
this.update(nextProps.value);
|
||||
}
|
||||
get_value(e: HTMLInputElement): TextType[T] {
|
||||
const type = this.props.type;
|
||||
// @ts-ignore Checked
|
||||
if (type === "text" || type === "password") return e.value;
|
||||
// @ts-ignore Checked
|
||||
return e.valueAsNumber;
|
||||
}
|
||||
render() {
|
||||
this.ref = useRef<TextField>();
|
||||
@@ -78,7 +103,7 @@ export default class SettingsText
|
||||
<TextField
|
||||
fullwidth={this.props.fullwidth}
|
||||
textarea={this.props.textarea}
|
||||
type="text"
|
||||
type={this.props.type}
|
||||
disabled={this.props.disabled}
|
||||
helperText={this.props.helpertext}
|
||||
label={this.props.label}
|
||||
@@ -86,9 +111,11 @@ export default class SettingsText
|
||||
onInput={(ev: InputEvent) => {
|
||||
if (ev.target) {
|
||||
const e = ev.target as HTMLInputElement;
|
||||
this.set_text(e.value);
|
||||
this.set_value(this.get_value(e));
|
||||
}
|
||||
}}
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
/>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
@@ -106,3 +106,7 @@ export async function load_settings(path: string) {
|
||||
const s = (new TextDecoder()).decode(await Deno.readFile(path));
|
||||
return new Config(parse(s));
|
||||
}
|
||||
|
||||
export function save_settings(path: string, cfg: Config, signal?: AbortSignal) {
|
||||
return Deno.writeTextFile(path, JSON.stringify(cfg._data), { signal });
|
||||
}
|
||||
|
||||
12
deno.lock
generated
12
deno.lock
generated
@@ -196,8 +196,10 @@
|
||||
"https://esm.sh/*[email protected]": "88ec8d8706b6a3f1e0fdad3862a2690dcd9b350d87bdc8e7bd0e27fbc0f7d29e",
|
||||
"https://esm.sh/[email protected]/Button": "bc60923d511c6e2e33a7064339b3e643a9c15e3ef232ab063ef570af2ef83dc8",
|
||||
"https://esm.sh/[email protected]/Checkbox": "bf34f5cd8c6d015916d854d91aab2caf115463e97be9a461f8dd3370ea11a49c",
|
||||
"https://esm.sh/[email protected]/Dialog": "b0ff8da9c770456748f7e065fecda2fc90f5364ea66cae75ff5f51d57f6a87eb",
|
||||
"https://esm.sh/[email protected]/Icon": "e15153f88485e448a8d5ee9d1399205717b41d9cb72bd5824c3b42e1dc463cf6",
|
||||
"https://esm.sh/[email protected]/List": "5eefaedd7a0f66843c1a1de658f4058b6fc1bbdcd0af1ec627c0f9412f4510c5",
|
||||
"https://esm.sh/[email protected]/Snackbar": "49c2072f1ffceda0c9089b43d64a13f2ed9bbc009eb0477700a83f3c3b5387bc",
|
||||
"https://esm.sh/[email protected]/TextField": "a03efbe74b561d1bb95d2ff59163ba50cfe27ef72d8c29f136d0be7e248eca76",
|
||||
"https://esm.sh/[email protected]/TopAppBar": "e418485020aecb866d9cc44216cd52c5ac1ff120bd1ad422b318428c274b3474",
|
||||
"https://esm.sh/[email protected]": "ae382301328ab874e2c42bee76e261bb8d094673fe76ca7eb71917636d43d8ad",
|
||||
@@ -226,8 +228,11 @@
|
||||
"https://esm.sh/v122/@babel/[email protected]/deno/helpers/interopRequireDefault.js": "273ebefd452416883e9e517a620b46197b732a9ee384dfcb2ef33f731b7293d4",
|
||||
"https://esm.sh/v122/@babel/[email protected]/deno/helpers/possibleConstructorReturn.js": "c48587b6a4194dac1f823e4706c67a286187cec9f29d35aac06df940443baa51",
|
||||
"https://esm.sh/v122/@babel/[email protected]/deno/helpers/typeof.js": "ea8e5723c88f83ac7cfbb5728d38e20449f7387093b2825e6c8c81fdc50a7f32",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/index.js": "e3a5feaecaada6d930cdee2e4c48eab2d7a44e0c37074614993463d2d74002a3",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/component.js": "031f373ae497c52e9b7050165e31dc6e708aebe2b2df027b25c897ce4b7cd43a",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/foundation.js": "4edd0737b3601305ffe599556e98e4861542210744da83af19194c8c4f50c0e0",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/index.js": "bb7da3f9d447f6235ae7947648917928d27ad37a0ccb9558068f7ef3f6bdfb87",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/dialog.mjs": "b9cbae5215472c168e2b2e5e6a89e2e3a1339110a6a3f8c52706c848300224d9",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/adapter.js": "05ef2078de04c85a8fd4567ef122eefa4ff04418048602f0885f78bbd156418a",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/constants.js": "d3a83d71e3c72466ef6323cc53fb7ca1b7387667d9f9c0c2c58165ae334c1cab",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/foundation.js": "3c818e3221af9d8b07846044f2c7ba6bcd356ca3dce862eff5a89edb67e4952e",
|
||||
@@ -246,23 +251,30 @@
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/index.js": "379c671f75219f71074e9d5fc1d790325d4ef469e0268d509147afaab67c6e8a",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/ripple.mjs": "a915efddd4cf20bc2bbb187fc44655b1d9919d563be67053fa9b71b78cd05df5",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/util.js": "4df787664fc517d4545bb7d502f85de3b34cfa613e34edd6b4630ea21e673cf2",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/snackbar.mjs": "b306beb44f6c56a8c2edc7a7097eaacb033cb722871ee6d2c2964c89907bbfa8",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/textfield.mjs": "b5b70ecde6fb97e99b8f2dfbd052cf14e3caaf99ba1c19a63fb3d52e2ee91876",
|
||||
"https://esm.sh/v122/@material/[email protected]/deno/top-app-bar.mjs": "e7de012030af2ce0baac3cff6c96adc206f763cea07924a8c714fd4a83b6a18f",
|
||||
"https://esm.sh/v122/[email protected]/deno/bind-decorator.mjs": "21a126bdebaca6e38120139d903c3485c4f57e15487646666e79c1b5c15d0e44",
|
||||
"https://esm.sh/v122/[email protected]/deno/focus-trap.mjs": "618450ba6b10b5bdd715e8ff78cb2bf4bd5c86d44bd0b43631925149d7a2996c",
|
||||
"https://esm.sh/v122/[email protected]/Base/MaterialComponent.d.ts": "af619a28f2d2f76113e29e952308a2ce46577438f7ed40531458b880ff9a141f",
|
||||
"https://esm.sh/v122/[email protected]/Base/types.d.ts": "4ec93636595145b9c5cdd628111f28e66148821c23c8ef6273f84bb84a6b8eb8",
|
||||
"https://esm.sh/v122/[email protected]/Button/index.d.ts": "3744521d359e3ccf04f115ac731e3304ff8d5901b7d7bcb617de5d88833fdb5c",
|
||||
"https://esm.sh/v122/[email protected]/Dialog/index.d.ts": "c3cc63c506ad3b2bf43332d9144f73d73a80403002ef462cbc31731beb3e3091",
|
||||
"https://esm.sh/v122/[email protected]/Icon/index.d.ts": "b2c27c8b912f07f20025951d9d05b2e4fb07c773d43db567b930e59b1c444f1c",
|
||||
"https://esm.sh/v122/[email protected]/List/index.d.ts": "b4159279b6622abbf057385c2b155875dc7e80bdd4403def72d1c0cf58324c73",
|
||||
"https://esm.sh/v122/[email protected]/Snackbar/index.d.ts": "75e9221ab99f29e82adb81145067fc56d3499b10aacf6e7db9b59c56a4c026db",
|
||||
"https://esm.sh/v122/[email protected]/TextField/index.d.ts": "107e98cbbf663f9ad39aca61600e885935ccb84cf670397739ecf9685a7208d5",
|
||||
"https://esm.sh/v122/[email protected]/TopAppBar/index.d.ts": "d921bd9336541339b0fff19e0769cea149b798bc172d261a7b7a1595f6b90266",
|
||||
"https://esm.sh/v122/[email protected]/deno/Base/MaterialComponent.js": "7dca052dee2ab5010232055eac4a3159c8455e9b2857870525f7002cb50160bd",
|
||||
"https://esm.sh/v122/[email protected]/deno/Button.js": "d60a9ea2eff985808ee0f491579b88dd30e6ce8bf0cb95ac2e7e9c2da9b0c0f3",
|
||||
"https://esm.sh/v122/[email protected]/deno/Dialog.js": "f8062ca1e6cbb91d4546638b75bb8454d77d35f0471920473d2f7f616ff2213d",
|
||||
"https://esm.sh/v122/[email protected]/deno/Icon.js": "44e17e354a08a2421fffcca3493efa60cc92e2357d410ed387e40f5d34da0ee2",
|
||||
"https://esm.sh/v122/[email protected]/deno/List.js": "6e908d2bac752351683097bcc22ea26d8e46009c60bc3cee2312728af2f60776",
|
||||
"https://esm.sh/v122/[email protected]/deno/Snackbar.js": "a9f08f06d2862ee6358a6073929b1ac64dd17edd487f94885d1f4d2ce296545a",
|
||||
"https://esm.sh/v122/[email protected]/deno/TextField.js": "111d412f9eb75a09cdc4cccd3b929d655432f8ecd1c4745a7da5db29f2793fef",
|
||||
"https://esm.sh/v122/[email protected]/deno/TopAppBar.js": "ab7ec4fa168d7867961b9fd818e10d138ffb075c20ea4b11f6d231318d897145",
|
||||
"https://esm.sh/v122/[email protected]/deno/themeUtils/generateThemeClass.js": "5ec3dd1474bfb68d60ae2c06c90a3faab4e1f79597807d1e72ff5608b05f0a44",
|
||||
"https://esm.sh/v122/[email protected]/deno/tabbable.mjs": "0a7dea7674457de3db98582b8bb532e49689aaa4e8b813a02ba7e4253d378d96",
|
||||
"https://esm.sh/v124/@babel/[email protected]/deno/helpers/classCallCheck.js": "678d4a1fc837f06947628e73a957e1f017fe30c77e8500008ec9a5977563c4f9",
|
||||
"https://esm.sh/v124/@babel/[email protected]/deno/helpers/createClass.js": "4205e44f1238343af0a0e0ebc92cfb043382600f54c3e53aa4e6801c9bec7a44",
|
||||
"https://esm.sh/v124/@babel/[email protected]/deno/helpers/get.js": "2021a8fa9d0d4880ece83c1d93266ac1d7414430f4f4671691a336ef3aa25dbe",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, ContextType } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import Button from "preact-material-components/Button";
|
||||
import Dialog from "preact-material-components/Dialog";
|
||||
import Snackbar from "preact-material-components/Snackbar";
|
||||
import { tw } from "twind";
|
||||
import { GlobalCtx } from "../components/GlobalContext.tsx";
|
||||
import { ConfigType } from "../config.ts";
|
||||
@@ -21,15 +23,32 @@ export default class Settings extends Component<SettingsProps> {
|
||||
const [error, set_error] = useState<string | undefined>();
|
||||
const [changed, set_changed] = useState<Set<string>>(new Set());
|
||||
const [new_cookies, set_new_cookies] = useState<string>("");
|
||||
const cookies_ref = useRef<SettingsText>();
|
||||
const [disabled, set_disabled] = useState(false);
|
||||
const fetchSettings = async () => {
|
||||
const re = await fetch("/api/config");
|
||||
set_settings(await re.json());
|
||||
set_changed(new Set());
|
||||
if (cookies_ref.current) {
|
||||
const t = cookies_ref.current;
|
||||
t.update("");
|
||||
set_new_cookies("");
|
||||
};
|
||||
const saveSettings = async () => {
|
||||
if (!settings) return;
|
||||
const s: Record<string, unknown> = settings;
|
||||
const d: Record<string, unknown> = {};
|
||||
for (const i of changed) {
|
||||
if (i === "cookies") d[i] = new_cookies;
|
||||
else d[i] = s[i];
|
||||
}
|
||||
const re = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(d),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const r = await re.json();
|
||||
set_changed(new Set());
|
||||
set_new_cookies("");
|
||||
return r;
|
||||
};
|
||||
const loadData = () => {
|
||||
fetchSettings().catch((e) => {
|
||||
@@ -38,11 +57,40 @@ export default class Settings extends Component<SettingsProps> {
|
||||
});
|
||||
};
|
||||
useEffect(loadData, []);
|
||||
const snack = useRef<Snackbar>();
|
||||
const show_snack = (message: string) => {
|
||||
snack.current?.MDComponent?.show({ message });
|
||||
};
|
||||
let data;
|
||||
if (error) {
|
||||
show_snack(error);
|
||||
data = <div class={tw`text-red-500`}>{error}</div>;
|
||||
} else if (settings) {
|
||||
const ref = useRef<SettingsText>();
|
||||
const ref = useRef<SettingsText<"text">>();
|
||||
const dlg = useRef<Dialog>();
|
||||
const showDlg = () => {
|
||||
if (!changed.size) {
|
||||
show_snack("Nothing was changed.");
|
||||
return;
|
||||
}
|
||||
dlg.current?.MDComponent?.show();
|
||||
};
|
||||
const save = () => {
|
||||
set_disabled(true);
|
||||
saveSettings().then((d) => {
|
||||
set_disabled(false);
|
||||
show_snack(
|
||||
"Saved." +
|
||||
(d.is_unsafe
|
||||
? " Some settings require a restart to take effect."
|
||||
: ""),
|
||||
);
|
||||
}).catch((e) => {
|
||||
set_disabled(false);
|
||||
show_snack("Failed to save settings.");
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
data = (
|
||||
<div class="settings">
|
||||
<SettingsContext
|
||||
@@ -68,11 +116,13 @@ export default class Settings extends Component<SettingsProps> {
|
||||
name="base"
|
||||
value={settings.base}
|
||||
description="Download location:"
|
||||
type="text"
|
||||
/>
|
||||
<SettingsText
|
||||
name="ua"
|
||||
value={settings.ua ? settings.ua : ""}
|
||||
description="User Agent:"
|
||||
type="text"
|
||||
ref={ref}
|
||||
>
|
||||
<Button
|
||||
@@ -81,7 +131,7 @@ export default class Settings extends Component<SettingsProps> {
|
||||
const ua = navigator.userAgent;
|
||||
const t = ref.current;
|
||||
t.update(ua);
|
||||
t.set_text(ua);
|
||||
t.set_value(ua);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -90,23 +140,61 @@ export default class Settings extends Component<SettingsProps> {
|
||||
</SettingsText>
|
||||
<SettingsText
|
||||
name="cookies"
|
||||
value=""
|
||||
value={new_cookies}
|
||||
description="Cookies:"
|
||||
type="text"
|
||||
set_value={set_new_cookies}
|
||||
label={`Enter${
|
||||
settings.cookies ? " new" : ""
|
||||
} cookies here.`}
|
||||
ignore_update_value={true}
|
||||
ref={cookies_ref}
|
||||
/>
|
||||
<SettingsText
|
||||
name="max_task_count"
|
||||
value={settings.max_task_count}
|
||||
description="Maximum number of parallel tasks:"
|
||||
type="number"
|
||||
min={1}
|
||||
/>
|
||||
<SettingsText
|
||||
name="max_retry_count"
|
||||
value={settings.max_retry_count}
|
||||
description="Maximum retry count:"
|
||||
type="number"
|
||||
min={1}
|
||||
/>
|
||||
<SettingsText
|
||||
name="max_download_img_count"
|
||||
value={settings.max_download_img_count}
|
||||
description="Maximum number of parallel downloads of images:"
|
||||
type="number"
|
||||
min={1}
|
||||
/>
|
||||
</SettingsContext>
|
||||
<Button onClick={loadData}>Reload</Button>
|
||||
<Button disabled={changed.size === 0}>Save</Button>
|
||||
<Button onClick={showDlg} disabled={disabled}>Save</Button>
|
||||
<Dialog ref={dlg} onAccept={save}>
|
||||
<Dialog.Header>
|
||||
Do you want to save settings?
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer>
|
||||
<Dialog.FooterButton accept={true}>
|
||||
Yes
|
||||
</Dialog.FooterButton>
|
||||
<Dialog.FooterButton cancel={true}>
|
||||
No
|
||||
</Dialog.FooterButton>
|
||||
</Dialog.Footer>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
data = <div>Loading...</div>;
|
||||
data = <div class={tw`text-red-500`}>Loading...</div>;
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
return (
|
||||
<div>
|
||||
{data}
|
||||
<Snackbar ref={snack} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { load_settings } from "../../config.ts";
|
||||
import { get_cfg_path } from "../../server.ts";
|
||||
import { ConfigType, load_settings, save_settings } from "../../config.ts";
|
||||
import { get_cfg_path, get_task_manager } from "../../server.ts";
|
||||
|
||||
const UNSAFE_TYPE: (keyof ConfigType)[] = ["base", "db_path"];
|
||||
const UNSAFE_TYPE2 = UNSAFE_TYPE as string[];
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_req, _ctx) {
|
||||
@@ -10,4 +13,29 @@ export const handler: Handlers = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
},
|
||||
async POST(req, _ctx) {
|
||||
const content_type = req.headers.get("Content-Type");
|
||||
if (content_type === "application/json") {
|
||||
const d = await req.json();
|
||||
const path = get_cfg_path();
|
||||
const m = get_task_manager();
|
||||
let is_unsafe = false;
|
||||
const cfg = await load_settings(path);
|
||||
Object.getOwnPropertyNames(d).forEach((k) => {
|
||||
if (UNSAFE_TYPE2.indexOf(k) === -1) {
|
||||
cfg._data[k] = d[k];
|
||||
m.cfg._data[k] = d[k];
|
||||
} else {
|
||||
cfg._data[k] = d[k];
|
||||
is_unsafe = true;
|
||||
}
|
||||
});
|
||||
await save_settings(path, cfg, m.force_aborts);
|
||||
return new Response(JSON.stringify({ is_unsafe }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} else {
|
||||
return new Response("Bad Request", { status: 400 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user