From 6766362d39bbf2d0390bbff2bcd676ae127e6e5f Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 22 Jul 2023 15:11:32 +0800 Subject: [PATCH] Update task render --- components/Progress.tsx | 85 ++++++++++++++++++++++++++++++++++++ components/Task.tsx | 51 ++++++++++++++++++++-- db.ts | 5 +++ fetch_static_files.ts | 1 + import_map.json | 3 +- islands/Container.tsx | 1 + islands/TaskManager.tsx | 9 ++++ server/bs5.ts | 3 ++ server/dmodule.ts | 5 +++ static/.gitignore | 1 + static/common.css | 5 +++ static/sw.ts | 1 + task.ts | 3 ++ task_manager.ts | 11 ++++- tasks/download.ts | 4 +- translation/en/task.jsonc | 6 ++- translation/zh-cn/task.jsonc | 6 ++- 17 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 components/Progress.tsx create mode 100644 server/bs5.ts diff --git a/components/Progress.tsx b/components/Progress.tsx new file mode 100644 index 0000000..9912cc0 --- /dev/null +++ b/components/Progress.tsx @@ -0,0 +1,85 @@ +import { + Component, + ComponentChildren, + ContextType, + createContext, +} from "preact"; + +type CtxProps = { + min: number; + max: number; + striped: boolean; + animated: boolean; +}; + +const PCtx = createContext(null); + +type BarProps = { + value: number; + striped?: boolean; + animated?: boolean; + class?: string; +}; + +class ProgressBar extends Component { + static contextType = PCtx; + declare context: ContextType; + render() { + let cls = "progress-bar"; + const striped = this.props.striped === undefined + ? this.context?.striped + : this.props.striped; + const animated = this.props.animated === undefined + ? this.context?.animated + : this.props.animated; + if (striped || animated) cls += " progress-bar-striped"; + if (animated) cls += " progress-bar-animated"; + if (this.props.class) cls += " " + this.props.class; + const max = this.context?.max || 100; + const min = this.context?.min || 0; + const v = this.props.value; + const style = `width: ${(v - min) / (max - min) * 100}%;`; + return ( +
+ ); + } +} + +type Props = { + /**@default {0} */ + min?: number; + /**@default {100} */ + max?: number; + /**@default {false} */ + striped?: boolean; + /**@default {false} */ + animated?: boolean; + children: ComponentChildren; +}; + +export default class Progress extends Component { + static readonly Bar = ProgressBar; + render() { + return ( +
+ + {this.props.children} + +
+ ); + } +} diff --git a/components/Task.tsx b/components/Task.tsx index 339866b..ee199e9 100644 --- a/components/Task.tsx +++ b/components/Task.tsx @@ -1,7 +1,10 @@ import { Component } from "preact"; import Icon from "preact-material-components/Icon"; -import type { TaskDetail } from "../task.ts"; +import type { TaskDetail, TaskProgressBasicType } from "../task.ts"; +import { TaskStatus, TaskType } from "../task.ts"; import t from "../server/i18n.ts"; +import { tw } from "twind"; +import Progress from "./Progress.tsx"; type Props = { task: TaskDetail; @@ -11,6 +14,20 @@ type State = { task_changed: (d: Event) => void; }; +const Types: Record = { + [TaskType.Download]: "download", + [TaskType.ExportZip]: "export_zip", + [TaskType.UpdateMeiliSearchData]: "update_meilisearch_data", + [TaskType.FixGalleryPage]: "fix_gallery_page", +}; + +const Status: Record = { + [TaskStatus.Wait]: "waiting", + [TaskStatus.Running]: "running", + [TaskStatus.Finished]: "finished", + [TaskStatus.Failed]: "failed", +}; + export default class Task extends Component { constructor(props: Props) { super(props); @@ -28,11 +45,39 @@ export default class Task extends Component { render() { const task = this.props.task; console.log(task); + let error_div = null; + if (task.status === TaskStatus.Failed) { + error_div = ( +
+ {t("task.error_msg")} +
{task.error}
+
+ ); + } + let progress_div = null; + if (task.status === TaskStatus.Running && task.progress) { + if (task.base.type === TaskType.Download) { + const d = task + .progress as TaskProgressBasicType[TaskType.Download]; + progress_div = ( + + + + + ); + } + } return (
unfold_more - {t("task.id")} - {task.base.id} +
{t("task.id")}{task.base.id}
+
{t("task.type")}{t(`task.${Types[task.base.type]}`)}
+
{t("task.status")}{t(`task.${Status[task.status]}`)}
+ {error_div} + {progress_div}
); } diff --git a/db.ts b/db.ts index fcfe89c..85688d7 100644 --- a/db.ts +++ b/db.ts @@ -848,6 +848,11 @@ export class EhDb { this.db.query("DELETE FROM task WHERE id = ?;", [task.id]); }); } + delete_task_by_id(id: number) { + return this.transaction(() => { + this.db.query("DELETE FROM task WHERE id = ?;", [id]); + }); + } delete_token(token: string) { this.db.query("DELETE FROM token WHERE token = ?;", [token]); } diff --git a/fetch_static_files.ts b/fetch_static_files.ts index 64f9a9b..1f84b3b 100644 --- a/fetch_static_files.ts +++ b/fetch_static_files.ts @@ -4,6 +4,7 @@ import { sure_dir } from "./utils.ts"; const map = JSON.parse(await Deno.readTextFile("./import_map.json")).imports; const LIST: string[] = [ "preact-material-components/style.css", + "bootstrap/dist/css/bootstrap.min.css", ]; function get_url(i: string) { diff --git a/import_map.json b/import_map.json index 4daa2de..c4ba93d 100644 --- a/import_map.json +++ b/import_map.json @@ -25,6 +25,7 @@ "pbkdf2-hmac/": "https://esm.sh/pbkdf2-hmac@1.2.1/", "randomstring": "https://esm.sh/randomstring@1.3.0", "@material/web/": "https://unpkg.lifegpc.workers.dev/@material/web@1.0.0-pre.13/", - "@lit-labs/react/": "https://esm.sh/@lit-labs/react@1.2.1/" + "@lit-labs/react/": "https://esm.sh/@lit-labs/react@1.2.1/", + "bootstrap/": "https://esm.sh/bootstrap@5.3.0/" } } diff --git a/islands/Container.tsx b/islands/Container.tsx index c0e1849..c382932 100644 --- a/islands/Container.tsx +++ b/islands/Container.tsx @@ -124,6 +124,7 @@ export default class Container extends Component { /> + diff --git a/islands/TaskManager.tsx b/islands/TaskManager.tsx index c6f8055..c2e4fba 100644 --- a/islands/TaskManager.tsx +++ b/islands/TaskManager.tsx @@ -26,6 +26,7 @@ function map_taskstatus(s: TaskStatus) { if (s === TaskStatus.Wait) return TaskStatusFlag.Waiting; else if (s === TaskStatus.Running) return TaskStatusFlag.Running; else if (s === TaskStatus.Finished) return TaskStatusFlag.Finished; + else if (s === TaskStatus.Failed) return TaskStatusFlag.Failed; return TaskStatusFlag.None; } @@ -170,6 +171,14 @@ export default class TaskManager extends Component { 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", () => { diff --git a/server/bs5.ts b/server/bs5.ts new file mode 100644 index 0000000..1053428 --- /dev/null +++ b/server/bs5.ts @@ -0,0 +1,3 @@ +import * as bs5 from "bootstrap/?target=es2022"; + +export default bs5; diff --git a/server/dmodule.ts b/server/dmodule.ts index 24fb6c8..9ee9b74 100644 --- a/server/dmodule.ts +++ b/server/dmodule.ts @@ -10,6 +10,7 @@ import type { MdTextButton as _MdTextButton, MdTonalButton as _MdTonalButton, } from "./md3.ts"; +import type _bs5 from "./bs5.ts"; export const MdOutlinedTextField = signal< typeof _MdOutlinedTextField | undefined @@ -39,6 +40,8 @@ export const MdMenu = signal(undefined); export const MdMenuItem = signal(undefined); +export const bs5 = signal(undefined); + export async function load_dmodule() { const md3 = await import("./md3.ts"); MdOutlinedTextField.value = md3.MdOutlinedTextField; @@ -50,4 +53,6 @@ export async function load_dmodule() { MdTextButton.value = md3.MdTextButton; MdMenu.value = md3.MdMenu; MdMenuItem.value = md3.MdMenuItem; + const b = await import("./bs5.ts"); + bs5.value = b.default; } diff --git a/static/.gitignore b/static/.gitignore index b030b30..347db2c 100644 --- a/static/.gitignore +++ b/static/.gitignore @@ -1,3 +1,4 @@ +bootstrap/ preact-material-components/ sw.js sw.js.map diff --git a/static/common.css b/static/common.css index 2204edd..b735114 100644 --- a/static/common.css +++ b/static/common.css @@ -1,3 +1,8 @@ +a { + color: inherit; + text-decoration: inherit; +} + body { min-width: 100vw; min-height: 100vh; diff --git a/static/sw.ts b/static/sw.ts index 324d675..7c07340 100644 --- a/static/sw.ts +++ b/static/sw.ts @@ -33,6 +33,7 @@ const CACHES = [ "/preact-material-components/style.css", "/favicon.ico", "/logo.svg", + "/bootstrap/dist/css/bootstrap.min.css", ]; function match_url(u: URL) { diff --git a/task.ts b/task.ts index a362ca8..88f387d 100644 --- a/task.ts +++ b/task.ts @@ -52,10 +52,13 @@ export enum TaskStatus { Wait, Running, Finished, + Failed, } export type TaskDetail = { base: Task; progress?: TaskProgressBasicType[T]; status: TaskStatus; + error?: string; + fataled?: boolean; }; diff --git a/task_manager.ts b/task_manager.ts index a4da2d6..441a09f 100644 --- a/task_manager.ts +++ b/task_manager.ts @@ -27,13 +27,15 @@ import { export class AlreadyClosedError extends Error { } +export class RecoverableError extends Error {} + type EventMap = { current_cfg_updated: ConfigType; new_task: Task; task_started: Task; task_finished: Task; task_progress: TaskProgress; - task_error: { task: Task; error: string }; + task_error: { task: Task; error: string; fatal: boolean }; task_updated: Task; }; @@ -189,6 +191,7 @@ export class TaskManager extends EventTarget { async check_running_tasks() { this.#check_closed(); const removed_task: number[] = []; + const fataled_task: number[] = []; for (const [id, task] of this.running_tasks) { const status = await promiseState(task.task); if (status.status == PromiseStatus.Fulfilled && status.value) { @@ -198,10 +201,13 @@ export class TaskManager extends EventTarget { } else if (status.status == PromiseStatus.Rejected) { if (status.reason && !this.aborted) { console.log(status.reason); + const fatal = !(status.reason instanceof RecoverableError); this.dispatchEvent("task_error", { task: task.base, error: status.reason.toString(), + fatal, }); + if (fatal) fataled_task.push(id); } removed_task.push(id); } @@ -209,6 +215,9 @@ export class TaskManager extends EventTarget { for (const id of removed_task) { this.running_tasks.delete(id); } + for (const id of fataled_task) { + await this.db.delete_task_by_id(id); + } } close() { if (this.#closed) { diff --git a/tasks/download.ts b/tasks/download.ts index ac7af0a..6f41bf0 100644 --- a/tasks/download.ts +++ b/tasks/download.ts @@ -3,7 +3,7 @@ import { Client } from "../client.ts"; import type { Config } from "../config.ts"; import type { EhDb, EhFile, PMeta } from "../db.ts"; import { Task, TaskDownloadProgess, TaskType } from "../task.ts"; -import { TaskManager } from "../task_manager.ts"; +import { RecoverableError, TaskManager } from "../task_manager.ts"; import { add_suffix_to_path, asyncFilter, @@ -319,7 +319,7 @@ export async function download_task( } } await m.join(); - if (m.has_failed_task) throw Error("Some tasks failed."); + if (m.has_failed_task) throw new RecoverableError("Some tasks failed."); if (abort.aborted || force_abort.aborted) throw Error("aborted"); if (remove_previous_gallery && gmeta.first_gid && gmeta.first_key) { let replaced_gallery = dcfg.replaced_gallery; diff --git a/translation/en/task.jsonc b/translation/en/task.jsonc index 3c72093..0a17037 100644 --- a/translation/en/task.jsonc +++ b/translation/en/task.jsonc @@ -19,5 +19,9 @@ "ezcfg_output": "The path to output file: ", "ezcfg_jpn_title": "Use japanese title first.", "ezcfg_export_ad": "Export pages which marked as ads.", - "ezcfg_max_length": "Maximum length of filenames in Zip files: " + "ezcfg_max_length": "Maximum length of filenames in Zip files: ", + "update_meilisearch_data": "Synchronize data with meilisearch server", + "fix_gallery_page": "Fix broken gallery data", + "status": "Task status: ", + "error_msg": "Error message: " } diff --git a/translation/zh-cn/task.jsonc b/translation/zh-cn/task.jsonc index 86325ae..8ff7176 100644 --- a/translation/zh-cn/task.jsonc +++ b/translation/zh-cn/task.jsonc @@ -19,5 +19,9 @@ "ezcfg_output": "输出文件的位置:", "ezcfg_jpn_title": "优先使用日语标题。", "ezcfg_export_ad": "导出标记为广告的页面。", - "ezcfg_max_length": "Zip文件中文件名的最大长度:" + "ezcfg_max_length": "Zip文件中文件名的最大长度:", + "update_meilisearch_data": "与Meiliserach服务器同步数据", + "fix_gallery_page": "修复缺损的画廊数据", + "status": "任务状态:", + "error_msg": "错误消息:" }