From 21a17dd9d787c64f4e73a6a439d94a672c59db10 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 27 May 2023 11:15:04 +0800 Subject: [PATCH] Update settings page --- components/SettingsText.tsx | 55 +++++++++++++----- config.ts | 4 ++ deno.lock | 12 ++++ islands/Settings.tsx | 112 ++++++++++++++++++++++++++++++++---- routes/api/config.ts | 32 ++++++++++- 5 files changed, 187 insertions(+), 28 deletions(-) diff --git a/components/SettingsText.tsx b/components/SettingsText.tsx index 557dc64..a7fce76 100644 --- a/components/SettingsText.tsx +++ b/components/SettingsText.tsx @@ -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 = { + 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; - ignore_update_value?: boolean; + set_value?: StateUpdater; + min?: DataType[T]; + max?: DataType[T]; }; -export default class SettingsText - extends Component { +export default class SettingsText + extends Component, unknown> { static contextType = SettingsCtx; ref: Ref | undefined; declare context: ContextType; - 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, + nextProps: Readonly>, _nextState: Readonly, _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(); @@ -78,7 +103,7 @@ export default class SettingsText { 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} diff --git a/config.ts b/config.ts index c95bfba..63c7aac 100644 --- a/config.ts +++ b/config.ts @@ -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 }); +} diff --git a/deno.lock b/deno.lock index 21c361b..3709c9e 100644 --- a/deno.lock +++ b/deno.lock @@ -196,8 +196,10 @@ "https://esm.sh/*preact-render-to-string@5.2.6": "88ec8d8706b6a3f1e0fdad3862a2690dcd9b350d87bdc8e7bd0e27fbc0f7d29e", "https://esm.sh/preact-material-components@1.6.1/Button": "bc60923d511c6e2e33a7064339b3e643a9c15e3ef232ab063ef570af2ef83dc8", "https://esm.sh/preact-material-components@1.6.1/Checkbox": "bf34f5cd8c6d015916d854d91aab2caf115463e97be9a461f8dd3370ea11a49c", + "https://esm.sh/preact-material-components@1.6.1/Dialog": "b0ff8da9c770456748f7e065fecda2fc90f5364ea66cae75ff5f51d57f6a87eb", "https://esm.sh/preact-material-components@1.6.1/Icon": "e15153f88485e448a8d5ee9d1399205717b41d9cb72bd5824c3b42e1dc463cf6", "https://esm.sh/preact-material-components@1.6.1/List": "5eefaedd7a0f66843c1a1de658f4058b6fc1bbdcd0af1ec627c0f9412f4510c5", + "https://esm.sh/preact-material-components@1.6.1/Snackbar": "49c2072f1ffceda0c9089b43d64a13f2ed9bbc009eb0477700a83f3c3b5387bc", "https://esm.sh/preact-material-components@1.6.1/TextField": "a03efbe74b561d1bb95d2ff59163ba50cfe27ef72d8c29f136d0be7e248eca76", "https://esm.sh/preact-material-components@1.6.1/TopAppBar": "e418485020aecb866d9cc44216cd52c5ac1ff120bd1ad422b318428c274b3474", "https://esm.sh/preact@10.13.1": "ae382301328ab874e2c42bee76e261bb8d094673fe76ca7eb71917636d43d8ad", @@ -226,8 +228,11 @@ "https://esm.sh/v122/@babel/runtime@7.21.5/deno/helpers/interopRequireDefault.js": "273ebefd452416883e9e517a620b46197b732a9ee384dfcb2ef33f731b7293d4", "https://esm.sh/v122/@babel/runtime@7.21.5/deno/helpers/possibleConstructorReturn.js": "c48587b6a4194dac1f823e4706c67a286187cec9f29d35aac06df940443baa51", "https://esm.sh/v122/@babel/runtime@7.21.5/deno/helpers/typeof.js": "ea8e5723c88f83ac7cfbb5728d38e20449f7387093b2825e6c8c81fdc50a7f32", + "https://esm.sh/v122/@material/animation@0.39.0/deno/index.js": "e3a5feaecaada6d930cdee2e4c48eab2d7a44e0c37074614993463d2d74002a3", "https://esm.sh/v122/@material/base@0.39.0/deno/component.js": "031f373ae497c52e9b7050165e31dc6e708aebe2b2df027b25c897ce4b7cd43a", "https://esm.sh/v122/@material/base@0.39.0/deno/foundation.js": "4edd0737b3601305ffe599556e98e4861542210744da83af19194c8c4f50c0e0", + "https://esm.sh/v122/@material/base@0.39.0/deno/index.js": "bb7da3f9d447f6235ae7947648917928d27ad37a0ccb9558068f7ef3f6bdfb87", + "https://esm.sh/v122/@material/dialog@0.39.3/deno/dialog.mjs": "b9cbae5215472c168e2b2e5e6a89e2e3a1339110a6a3f8c52706c848300224d9", "https://esm.sh/v122/@material/floating-label@0.39.1/deno/adapter.js": "05ef2078de04c85a8fd4567ef122eefa4ff04418048602f0885f78bbd156418a", "https://esm.sh/v122/@material/floating-label@0.39.1/deno/constants.js": "d3a83d71e3c72466ef6323cc53fb7ca1b7387667d9f9c0c2c58165ae334c1cab", "https://esm.sh/v122/@material/floating-label@0.39.1/deno/foundation.js": "3c818e3221af9d8b07846044f2c7ba6bcd356ca3dce862eff5a89edb67e4952e", @@ -246,23 +251,30 @@ "https://esm.sh/v122/@material/ripple@0.39.3/deno/index.js": "379c671f75219f71074e9d5fc1d790325d4ef469e0268d509147afaab67c6e8a", "https://esm.sh/v122/@material/ripple@0.39.3/deno/ripple.mjs": "a915efddd4cf20bc2bbb187fc44655b1d9919d563be67053fa9b71b78cd05df5", "https://esm.sh/v122/@material/ripple@0.39.3/deno/util.js": "4df787664fc517d4545bb7d502f85de3b34cfa613e34edd6b4630ea21e673cf2", + "https://esm.sh/v122/@material/snackbar@0.39.1/deno/snackbar.mjs": "b306beb44f6c56a8c2edc7a7097eaacb033cb722871ee6d2c2964c89907bbfa8", "https://esm.sh/v122/@material/textfield@0.39.3/deno/textfield.mjs": "b5b70ecde6fb97e99b8f2dfbd052cf14e3caaf99ba1c19a63fb3d52e2ee91876", "https://esm.sh/v122/@material/top-app-bar@0.39.3/deno/top-app-bar.mjs": "e7de012030af2ce0baac3cff6c96adc206f763cea07924a8c714fd4a83b6a18f", "https://esm.sh/v122/bind-decorator@1.0.11/deno/bind-decorator.mjs": "21a126bdebaca6e38120139d903c3485c4f57e15487646666e79c1b5c15d0e44", + "https://esm.sh/v122/focus-trap@2.4.6/deno/focus-trap.mjs": "618450ba6b10b5bdd715e8ff78cb2bf4bd5c86d44bd0b43631925149d7a2996c", "https://esm.sh/v122/preact-material-components@1.6.1/Base/MaterialComponent.d.ts": "af619a28f2d2f76113e29e952308a2ce46577438f7ed40531458b880ff9a141f", "https://esm.sh/v122/preact-material-components@1.6.1/Base/types.d.ts": "4ec93636595145b9c5cdd628111f28e66148821c23c8ef6273f84bb84a6b8eb8", "https://esm.sh/v122/preact-material-components@1.6.1/Button/index.d.ts": "3744521d359e3ccf04f115ac731e3304ff8d5901b7d7bcb617de5d88833fdb5c", + "https://esm.sh/v122/preact-material-components@1.6.1/Dialog/index.d.ts": "c3cc63c506ad3b2bf43332d9144f73d73a80403002ef462cbc31731beb3e3091", "https://esm.sh/v122/preact-material-components@1.6.1/Icon/index.d.ts": "b2c27c8b912f07f20025951d9d05b2e4fb07c773d43db567b930e59b1c444f1c", "https://esm.sh/v122/preact-material-components@1.6.1/List/index.d.ts": "b4159279b6622abbf057385c2b155875dc7e80bdd4403def72d1c0cf58324c73", + "https://esm.sh/v122/preact-material-components@1.6.1/Snackbar/index.d.ts": "75e9221ab99f29e82adb81145067fc56d3499b10aacf6e7db9b59c56a4c026db", "https://esm.sh/v122/preact-material-components@1.6.1/TextField/index.d.ts": "107e98cbbf663f9ad39aca61600e885935ccb84cf670397739ecf9685a7208d5", "https://esm.sh/v122/preact-material-components@1.6.1/TopAppBar/index.d.ts": "d921bd9336541339b0fff19e0769cea149b798bc172d261a7b7a1595f6b90266", "https://esm.sh/v122/preact-material-components@1.6.1/deno/Base/MaterialComponent.js": "7dca052dee2ab5010232055eac4a3159c8455e9b2857870525f7002cb50160bd", "https://esm.sh/v122/preact-material-components@1.6.1/deno/Button.js": "d60a9ea2eff985808ee0f491579b88dd30e6ce8bf0cb95ac2e7e9c2da9b0c0f3", + "https://esm.sh/v122/preact-material-components@1.6.1/deno/Dialog.js": "f8062ca1e6cbb91d4546638b75bb8454d77d35f0471920473d2f7f616ff2213d", "https://esm.sh/v122/preact-material-components@1.6.1/deno/Icon.js": "44e17e354a08a2421fffcca3493efa60cc92e2357d410ed387e40f5d34da0ee2", "https://esm.sh/v122/preact-material-components@1.6.1/deno/List.js": "6e908d2bac752351683097bcc22ea26d8e46009c60bc3cee2312728af2f60776", + "https://esm.sh/v122/preact-material-components@1.6.1/deno/Snackbar.js": "a9f08f06d2862ee6358a6073929b1ac64dd17edd487f94885d1f4d2ce296545a", "https://esm.sh/v122/preact-material-components@1.6.1/deno/TextField.js": "111d412f9eb75a09cdc4cccd3b929d655432f8ecd1c4745a7da5db29f2793fef", "https://esm.sh/v122/preact-material-components@1.6.1/deno/TopAppBar.js": "ab7ec4fa168d7867961b9fd818e10d138ffb075c20ea4b11f6d231318d897145", "https://esm.sh/v122/preact-material-components@1.6.1/deno/themeUtils/generateThemeClass.js": "5ec3dd1474bfb68d60ae2c06c90a3faab4e1f79597807d1e72ff5608b05f0a44", + "https://esm.sh/v122/tabbable@1.1.3/deno/tabbable.mjs": "0a7dea7674457de3db98582b8bb532e49689aaa4e8b813a02ba7e4253d378d96", "https://esm.sh/v124/@babel/runtime@7.21.5/deno/helpers/classCallCheck.js": "678d4a1fc837f06947628e73a957e1f017fe30c77e8500008ec9a5977563c4f9", "https://esm.sh/v124/@babel/runtime@7.21.5/deno/helpers/createClass.js": "4205e44f1238343af0a0e0ebc92cfb043382600f54c3e53aa4e6801c9bec7a44", "https://esm.sh/v124/@babel/runtime@7.21.5/deno/helpers/get.js": "2021a8fa9d0d4880ece83c1d93266ac1d7414430f4f4671691a336ef3aa25dbe", diff --git a/islands/Settings.tsx b/islands/Settings.tsx index 01cece3..fc5f5c9 100644 --- a/islands/Settings.tsx +++ b/islands/Settings.tsx @@ -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 { const [error, set_error] = useState(); const [changed, set_changed] = useState>(new Set()); const [new_cookies, set_new_cookies] = useState(""); - const cookies_ref = useRef(); + 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 = settings; + const d: Record = {}; + 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 { }); }; useEffect(loadData, []); + const snack = useRef(); + const show_snack = (message: string) => { + snack.current?.MDComponent?.show({ message }); + }; let data; if (error) { + show_snack(error); data =
{error}
; } else if (settings) { - const ref = useRef(); + const ref = useRef>(); + const dlg = useRef(); + 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 = (
{ name="base" value={settings.base} description="Download location:" + type="text" /> - + + + + Do you want to save settings? + + + + Yes + + + No + + +
); } else { - data =
Loading...
; + data =
Loading...
; } - return
{data}
; + return ( +
+ {data} + +
+ ); } } diff --git a/routes/api/config.ts b/routes/api/config.ts index 25d39ed..45ab56e 100644 --- a/routes/api/config.ts +++ b/routes/api/config.ts @@ -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 }); + } + }, };