Remove fresh dashboard

This commit is contained in:
2024-05-24 18:13:54 +08:00
parent fd680931bd
commit e79744f6b6
30 changed files with 1 additions and 3456 deletions

View File

@@ -1,190 +0,0 @@
import { Head } from "$fresh/runtime.ts";
import { Component, ContextType } from "preact";
import { StateUpdater, useEffect, useState } from "preact/hooks";
import Icon from "preact-material-components/Icon.js";
import List from "preact-material-components/List.js";
import TopAppBar from "preact-material-components/TopAppBar.js";
import StyleSheet from "../components/StyleSheet.tsx";
import { GlobalCtx } from "../components/GlobalContext.tsx";
import Settings from "./Settings.tsx";
import t, { i18n_map, I18NMap } from "../server/i18n.ts";
import TaskManager from "./TaskManager.tsx";
import { initState, set_state } from "../server/state.ts";
import NewTask from "../components/NewTask.tsx";
import { parse_int } from "../server/parse.ts";
import { addDarkModeListener, detect_darkmode } from "../server/dark.ts";
import { registeServiceWorker } from "../server/sw.ts";
import { initCfg } from "../server/cfg.ts";
import { load_dmodule } from "../server/dmodule.ts";
import CreateRootUser from "../components/CreateRootUser.tsx";
import { check_auth_status } from "../server/auth.ts";
import Login from "../components/Login.tsx";
export type ContainerProps = {
i18n: I18NMap;
lang: string;
};
enum DarkMode {
Auto,
Light,
Dark,
}
function darkmode_next(d: DarkMode) {
if (d === DarkMode.Auto) return DarkMode.Light;
if (d === DarkMode.Dark) return DarkMode.Auto;
return DarkMode.Dark;
}
export default class Container extends Component<ContainerProps> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
i18n_map.value = this.props.i18n;
const [display, set_display] = useState(false);
const [state, set_state1] = useState("#/");
const [darkmode, set_darkmode1] = useState(DarkMode.Auto);
const [dmodule_loaded, set_dmodule_loaded] = useState(false);
const set_darkmode: StateUpdater<DarkMode> = (u) => {
const v = typeof u === "function" ? u(darkmode) : u;
set_darkmode1(v);
localStorage.setItem("darkmode", JSON.stringify(v));
if (v === DarkMode.Auto) {
if (detect_darkmode()) {
document.body.classList.add("dark-scheme");
} else {
document.body.classList.remove("dark-scheme");
}
} else if (v === DarkMode.Dark) {
document.body.classList.add("dark-scheme");
} else {
document.body.classList.remove("dark-scheme");
}
};
useEffect(() => {
const dm = parse_int(
localStorage.getItem("darkmode"),
DarkMode.Auto,
);
set_darkmode1(dm);
if (dm === DarkMode.Auto) {
if (detect_darkmode()) {
document.body.classList.add("dark-scheme");
}
} else if (dm === DarkMode.Dark) {
document.body.classList.add("dark-scheme");
}
registeServiceWorker("/sw.js", { updateViaCache: "all" }).catch(
(e) => {
console.error("Failed to registe service worker.");
console.error(e);
},
);
initCfg();
addDarkModeListener((e) => {
if (darkmode === DarkMode.Auto) {
if (e.matches) {
document.body.classList.add("dark-scheme");
} else {
document.body.classList.remove("dark-scheme");
}
}
});
load_dmodule().then(() => set_dmodule_loaded(true)).catch((e) => {
console.error(e);
});
initState(set_state1);
check_auth_status().catch((e) => {
console.error(e);
});
}, []);
let main = null;
if (dmodule_loaded) {
main = (
<div class="main">
<Settings show={state === "#/settings"} />
<TaskManager
base="#/task_manager"
show={state === "#/task_manager"}
/>
<NewTask show={state === "#/task_manager/new"} />
<CreateRootUser show={state === "#/create_root_user"} />
<Login show={state === "#/login"} />
</div>
);
}
return (
<div>
<Head>
<title>{t("common.title")}</title>
<link
rel="manifest"
href={`/manifest.json?lang=${this.props.lang}`}
/>
<GlobalCtx.Provider value={this.context}>
<StyleSheet href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<StyleSheet href="bootstrap/dist/css/bootstrap.min.css" />
<StyleSheet href="preact-material-components/style.css" />
<StyleSheet href="common.css" />
</GlobalCtx.Provider>
</Head>
<TopAppBar onNav={() => set_display(true)}>
<TopAppBar.Row>
<TopAppBar.Section align-start>
<TopAppBar.Icon navigation>menu</TopAppBar.Icon>
<TopAppBar.Title>
{t("common.title")}
</TopAppBar.Title>
</TopAppBar.Section>
<TopAppBar.Section align-end>
<TopAppBar.Icon
onClick={() => {
set_darkmode(darkmode_next(darkmode));
}}
navigation
>
{darkmode === DarkMode.Auto
? "brightness_auto"
: darkmode === DarkMode.Dark
? "dark_mode"
: "light_mode"}
</TopAppBar.Icon>
<TopAppBar.Icon>more_vert</TopAppBar.Icon>
</TopAppBar.Section>
</TopAppBar.Row>
</TopAppBar>
<List class={"nav-menu" + (display ? " open" : "")}>
<List.Item onClick={() => set_display(false)}>
<Icon>close</Icon>
</List.Item>
<List.Item
onClick={() => {
set_display(false);
set_state("#/");
}}
>
<Icon>home</Icon>
</List.Item>
<List.Item
onClick={() => {
set_display(false);
set_state("#/task_manager");
}}
>
<Icon>task</Icon>
</List.Item>
<List.Item
onClick={() => {
set_display(false);
set_state("#/settings");
}}
>
<Icon>settings</Icon>
</List.Item>
</List>
{main}
</div>
);
}
}

View File

@@ -1,333 +0,0 @@
import { Component, ContextType } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import Button from "preact-material-components/Button.js";
import Snackbar from "preact-material-components/Snackbar.js";
import { tw } from "twind";
import { GlobalCtx } from "../components/GlobalContext.tsx";
import type { ConfigType } from "../config.ts";
import { ThumbnailMethod } from "../config.ts";
import SettingsCheckbox from "../components/SettingsCheckbox.tsx";
import SettingsContext from "../components/SettingsContext.tsx";
import SettingsText from "../components/SettingsText.tsx";
import t from "../server/i18n.ts";
import SettingsSelect from "../components/SettingsSelect.tsx";
import type { _MdDialog, DialogAction } from "../server/md3.ts";
import { MdDialog, MdTextButton } from "../server/dmodule.ts";
import StringRecordsBox from "../components/StringRecordsBox.tsx";
export type SettingsProps = {
show: boolean;
};
export default class Settings extends Component<SettingsProps> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
if (!this.props.show) return;
if (!MdDialog.value) return;
if (!MdTextButton.value) return;
const Dialog = MdDialog.value;
const TextButton = MdTextButton.value;
const [settings, set_settings] = useState<ConfigType | undefined>();
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 [disabled, set_disabled] = useState(false);
const fetchSettings = async () => {
const re = await fetch("/api/config");
set_settings(await re.json());
set_changed(new Set());
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) => {
set_error(t("settings.failed"));
console.error(e);
});
};
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<"text">>();
const dlg = useRef<_MdDialog>();
const showDlg = () => {
if (!changed.size) {
show_snack(t("settings.no_changed"));
return;
}
dlg.current?.show();
};
const save = () => {
set_disabled(true);
saveSettings().then((d) => {
set_disabled(false);
show_snack(
t("settings.saved") +
(d.is_unsafe ? t("settings.need_restart") : ""),
);
loadData();
}).catch((e) => {
set_disabled(false);
show_snack(t("settings.failed"));
console.error(e);
});
};
data = (
<div class="settings">
<SettingsContext
set_changed={set_changed}
set_settings={set_settings}
>
<div class="check-box">
<SettingsCheckbox
name="download_original_img"
checked={settings.download_original_img}
description={t(
"settings.download_original_img",
)}
/>
<SettingsCheckbox
name="ex"
checked={settings.ex}
description={t("settings.ex")}
/>
<SettingsCheckbox
name="mpv"
checked={settings.mpv}
description={t("settings.mpv")}
/>
<SettingsCheckbox
name="export_zip_jpn_title"
checked={settings.export_zip_jpn_title}
description={t("settings.export_zip_jpn_title")}
/>
<SettingsCheckbox
name="remove_previous_gallery"
checked={settings.remove_previous_gallery}
description={t(
"settings.remove_previous_gallery",
)}
/>
</div>
<div class="text-box">
<SettingsSelect
name="thumbnail_method"
list={[{
value: ThumbnailMethod.FFMPEG_BINARY,
text: t("settings.thumbnail_method0"),
}, {
value: ThumbnailMethod.FFMPEG_API,
text: t("settings.thumbnail_method1"),
}]}
description={t("settings.thumbnail_method")}
selectedIndex={settings.thumbnail_method}
/>
<SettingsText
name="port"
value={settings.port}
description={t("settings.port")}
type="number"
min={0}
max={65535}
outlined={true}
/>
<SettingsText
name="base"
value={settings.base}
description={t("settings.base")}
type="text"
outlined={true}
/>
<div class="ua">
<SettingsText
name="ua"
value={settings.ua ? settings.ua : ""}
description={t("settings.ua")}
type="text"
outlined={true}
ref={ref}
>
</SettingsText>
<Button
onClick={() => {
if (ref.current) {
const ua = navigator.userAgent;
const t = ref.current;
t.update(ua);
t.set_value(ua);
}
}}
>
{t("settings.ua_now")}
</Button>
</div>
<SettingsText
name="max_task_count"
value={settings.max_task_count}
description={t("settings.max_task_count")}
type="number"
min={1}
outlined={true}
/>
<SettingsText
name="max_retry_count"
value={settings.max_retry_count}
description={t("settings.max_retry_count")}
type="number"
min={1}
outlined={true}
/>
<SettingsText
name="max_download_img_count"
value={settings.max_download_img_count}
description={t(
"settings.max_download_img_count",
)}
type="number"
min={1}
outlined={true}
/>
<SettingsText
name="db_path"
value={settings.db_path || ""}
type="text"
description={t("settings.db_path")}
helpertext={t("settings.db_path_help")}
outlined={true}
/>
<SettingsText
name="hostname"
value={settings.hostname}
description={t("settings.hostname")}
type="text"
outlined={true}
/>
<SettingsText
name="meili_host"
value={settings.meili_host || ""}
description={t("settings.meili_host")}
type="text"
outlined={true}
/>
<SettingsText
name="meili_update_api_key"
value={settings.meili_update_api_key || ""}
description={t("settings.meili_update_api_key")}
type="text"
outlined={true}
/>
<SettingsText
name="meili_search_api_key"
value={settings.meili_search_api_key || ""}
description={t("settings.meili_search_api_key")}
type="text"
outlined={true}
/>
<SettingsText
name="ffmpeg_path"
value={settings.ffmpeg_path}
description={t("settings.ffmpeg_path")}
type="text"
outlined={true}
/>
<SettingsText
name="cookies"
value={new_cookies}
description={t("settings.cookies")}
type="text"
set_value={set_new_cookies}
label={t(
`settings.enter${
settings.cookies ? "_new" : ""
}_cookies`,
)}
outlined={true}
/>
<SettingsText
name="img_verify_secret"
value={settings.img_verify_secret || ""}
description={t("settings.img_verify_secret")}
type="text"
outlined={true}
/>
<div>
<label style={{ display: "block" }}>
{t("settings.meili_hosts")}
</label>
<StringRecordsBox
value={settings.meili_hosts || {}}
sign=":"
set_value={(_) => {
set_changed((v) => {
v.add("meili_hosts");
return v;
});
}}
/>
</div>
</div>
</SettingsContext>
<Button onClick={loadData}>{t("common.reload")}</Button>
<Button onClick={showDlg} disabled={disabled}>
{t("common.save")}
</Button>
<Dialog
/**@ts-ignore */
ref={dlg}
onclosed={(ev) => {
const e = ev as CustomEvent<DialogAction>;
if (e.detail.action === "yes") {
save();
}
}}
>
<span slot="headline">{t("settings.save_dlg")}</span>
<div slot="footer">
<TextButton dialog-action="yes">
{t("common.yes")}
</TextButton>
<TextButton dialog-action="close">
{t("common.no")}
</TextButton>
</div>
</Dialog>
</div>
);
} else {
data = <div class={tw`text-red-500`}>{t("common.loading")}</div>;
}
return (
<div>
{data}
<Snackbar ref={snack} />
</div>
);
}
}

View File

@@ -1,212 +0,0 @@
import { Component, ContextType } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { signal } from "@preact/signals";
import { GlobalCtx } from "../components/GlobalContext.tsx";
import type { TaskDetail } from "../task.ts";
import { TaskStatus } from "../task.ts";
import { Sortable } from "sortable";
import type {
TaskClientSocketData,
TaskServerSocketData,
} from "../server/task.ts";
import { get_ws_host } from "../server/utils.ts";
import Task from "../components/Task.tsx";
import Fab from "preact-material-components/Fab.js";
import Icon from "preact-material-components/Icon.js";
import { set_state } from "../server/state.ts";
import t from "../server/i18n.ts";
import { TaskFilterBar, TaskStatusFlag } from "../components/TaskFilterBar.tsx";
export type TaskManagerProps = {
base: string;
show: boolean;
};
const tasks = signal(new Map<number, TaskDetail>());
const task_list = signal(new Array<number>());
export const task_ws = signal<WebSocket | undefined>(undefined);
export function sendTaskMessage(mes: TaskClientSocketData) {
const ws = task_ws.value;
if (ws && ws.readyState === ws.OPEN) {
ws.send(JSON.stringify(mes));
return true;
} else return false;
}
type SortableEvent = CustomEvent & {
oldIndex: number | undefined;
newIndex: number | undefined;
};
export default class TaskManager extends Component<TaskManagerProps> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
sortable?: Sortable;
render() {
const ul = useRef<HTMLDivElement>();
useEffect(() => {
if (!this.props.show) {
this.sortable?.destroy();
this.sortable = undefined;
return;
}
this.sortable = new Sortable(ul.current, {
handle: ".task_handle",
onSort: (evt: SortableEvent) => {
if (
evt.newIndex === undefined || evt.oldIndex === undefined
) return;
const tl = task_list.value;
tl.splice(evt.newIndex, 0, tl.splice(evt.oldIndex, 1)[0]);
},
});
}, [this.props.show]);
useEffect(() => {
const ws = new WebSocket(`${get_ws_host()}/api/task`);
console.log(ws);
task_ws.value = ws;
function sendMessage(mes: TaskClientSocketData) {
ws.send(JSON.stringify(mes));
}
ws.onopen = () => {
sendMessage({ type: "task_list" });
};
ws.onmessage = (e) => {
const t: TaskServerSocketData = JSON.parse(e.data);
function sendTaskChangedEvent(id: number) {
self.dispatchEvent(
new CustomEvent("task_changed", { detail: id }),
);
}
if (t.type == "close") {
ws.close();
} else if (t.type == "tasks") {
let running_index = -1;
t.tasks.forEach((ta) => {
const is_running = t.running.includes(ta.id);
tasks.value.set(ta.id, {
base: ta,
status: is_running
? TaskStatus.Running
: TaskStatus.Wait,
});
const tl = task_list.value;
if (!tl.includes(ta.id)) {
tl.push(ta.id);
if (is_running) {
if (tl.length) {
tl.splice(
running_index + 1,
0,
tl.splice(tl.length - 1, 1)[0],
);
}
running_index += 1;
}
}
});
this.forceUpdate();
} else if (t.type == "new_task") {
tasks.value.set(t.detail.id, {
base: t.detail,
status: TaskStatus.Wait,
});
if (!task_list.value.includes(t.detail.id)) {
task_list.value.push(t.detail.id);
}
this.forceUpdate();
} else if (t.type == "task_started") {
const task = tasks.value.get(t.detail.id);
if (task === undefined) {
tasks.value.set(t.detail.id, {
base: t.detail,
status: TaskStatus.Running,
});
const tl = task_list.value;
if (!tl.includes(t.detail.id)) {
tl.push(t.detail.id);
if (tl.length) {
tl.splice(0, 0, tl.splice(tl.length - 1, 1)[0]);
}
}
this.forceUpdate();
} else {
task.status = TaskStatus.Running;
sendTaskChangedEvent(t.detail.id);
const tl = task_list.value;
const ind = tl.indexOf(task.base.id);
if (ind > 0) {
tl.splice(0, 0, tl.splice(ind, 1)[0]);
this.sortable?.sort(tl.map((t) => t.toString()));
}
}
} else if (t.type == "task_finished") {
const task = tasks.value.get(t.detail.id);
if (task !== undefined) {
task.status = TaskStatus.Finished;
sendTaskChangedEvent(t.detail.id);
const tl = task_list.value;
const ind = tl.indexOf(task.base.id);
if (ind < tl.length - 1 && ind > -1) {
tl.splice(tl.length - 1, 0, tl.splice(ind, 1)[0]);
this.sortable?.sort(tl.map((t) => t.toString()));
}
}
} else if (t.type == "task_progress") {
const task = tasks.value.get(t.detail.task_id);
if (task !== undefined) {
task.progress = t.detail.detail;
sendTaskChangedEvent(t.detail.task_id);
}
} else if (t.type == "task_updated") {
const task = tasks.value.get(t.detail.id);
if (task) {
task.base = t.detail;
}
} else if (t.type === "task_error") {
const task = tasks.value.get(t.detail.task.id);
if (task) {
task.status = TaskStatus.Failed;
task.error = t.detail.error;
task.fataled = t.detail.fatal;
sendTaskChangedEvent(task.base.id);
}
}
};
self.addEventListener("beforeunload", () => {
sendMessage({ type: "close" });
});
}, []);
if (!this.props.show) return null;
const [flags, set_flags] = useState(TaskStatusFlag.All);
return (
<div class="task_manager">
<Fab
class="new_task"
onClick={() => {
set_state(`${this.props.base}/new`);
}}
>
<Icon>add</Icon>
</Fab>
<div class="task_amounts">
<TaskFilterBar value={flags} set_value={set_flags} />
</div>
<div
class="task_details"
// @ts-ignore Checked
ref={ul}
>
{task_list.value.map((k) => {
const t = tasks.value.get(k);
if (t) {
return <Task task={t} flags={flags} />;
} else {
return <div data-id={k}></div>;
}
})}
</div>
</div>
);
}
}