mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-19 18:34:15 +08:00
Update task render
This commit is contained in:
85
components/Progress.tsx
Normal file
85
components/Progress.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentChildren,
|
||||
ContextType,
|
||||
createContext,
|
||||
} from "preact";
|
||||
|
||||
type CtxProps = {
|
||||
min: number;
|
||||
max: number;
|
||||
striped: boolean;
|
||||
animated: boolean;
|
||||
};
|
||||
|
||||
const PCtx = createContext<CtxProps | null>(null);
|
||||
|
||||
type BarProps = {
|
||||
value: number;
|
||||
striped?: boolean;
|
||||
animated?: boolean;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
class ProgressBar extends Component<BarProps> {
|
||||
static contextType = PCtx;
|
||||
declare context: ContextType<typeof PCtx>;
|
||||
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 (
|
||||
<div
|
||||
class={cls}
|
||||
style={style}
|
||||
role="progressbar"
|
||||
aria-valuenow={v}
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<Props> {
|
||||
static readonly Bar = ProgressBar;
|
||||
render() {
|
||||
return (
|
||||
<div class="progress">
|
||||
<PCtx.Provider
|
||||
value={{
|
||||
min: this.props.min || 0,
|
||||
max: this.props.max || 100,
|
||||
striped: this.props.striped || false,
|
||||
animated: this.props.animated || false,
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</PCtx.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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, string> = {
|
||||
[TaskType.Download]: "download",
|
||||
[TaskType.ExportZip]: "export_zip",
|
||||
[TaskType.UpdateMeiliSearchData]: "update_meilisearch_data",
|
||||
[TaskType.FixGalleryPage]: "fix_gallery_page",
|
||||
};
|
||||
|
||||
const Status: Record<TaskStatus, string> = {
|
||||
[TaskStatus.Wait]: "waiting",
|
||||
[TaskStatus.Running]: "running",
|
||||
[TaskStatus.Finished]: "finished",
|
||||
[TaskStatus.Failed]: "failed",
|
||||
};
|
||||
|
||||
export default class Task extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -28,11 +45,39 @@ export default class Task extends Component<Props, State> {
|
||||
render() {
|
||||
const task = this.props.task;
|
||||
console.log(task);
|
||||
let error_div = null;
|
||||
if (task.status === TaskStatus.Failed) {
|
||||
error_div = (
|
||||
<div class="error">
|
||||
{t("task.error_msg")}
|
||||
<div class={tw`text-red-500`}>{task.error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 = (
|
||||
<Progress max={d.total_page} animated={true}>
|
||||
<Progress.Bar
|
||||
class="bg-success"
|
||||
value={d.downloaded_page}
|
||||
/>
|
||||
<Progress.Bar class="bg-danger" value={d.failed_page} />
|
||||
</Progress>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div data-id={task.base.id}>
|
||||
<Icon class="task_handle">unfold_more</Icon>
|
||||
{t("task.id")}
|
||||
{task.base.id}
|
||||
<div>{t("task.id")}{task.base.id}</div>
|
||||
<div>{t("task.type")}{t(`task.${Types[task.base.type]}`)}</div>
|
||||
<div>{t("task.status")}{t(`task.${Status[task.status]}`)}</div>
|
||||
{error_div}
|
||||
{progress_div}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
5
db.ts
5
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]);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"pbkdf2-hmac/": "https://esm.sh/[email protected]/",
|
||||
"randomstring": "https://esm.sh/[email protected]",
|
||||
"@material/web/": "https://unpkg.lifegpc.workers.dev/@material/[email protected]/",
|
||||
"@lit-labs/react/": "https://esm.sh/@lit-labs/[email protected]/"
|
||||
"@lit-labs/react/": "https://esm.sh/@lit-labs/[email protected]/",
|
||||
"bootstrap/": "https://esm.sh/[email protected]/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ export default class Container extends Component<ContainerProps> {
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -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<TaskManagerProps> {
|
||||
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", () => {
|
||||
|
||||
3
server/bs5.ts
Normal file
3
server/bs5.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as bs5 from "bootstrap/?target=es2022";
|
||||
|
||||
export default bs5;
|
||||
@@ -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<typeof _MdMenu | undefined>(undefined);
|
||||
|
||||
export const MdMenuItem = signal<typeof _MdMenuItem | undefined>(undefined);
|
||||
|
||||
export const bs5 = signal<typeof _bs5 | undefined>(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;
|
||||
}
|
||||
|
||||
1
static/.gitignore
vendored
1
static/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
bootstrap/
|
||||
preact-material-components/
|
||||
sw.js
|
||||
sw.js.map
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
3
task.ts
3
task.ts
@@ -52,10 +52,13 @@ export enum TaskStatus {
|
||||
Wait,
|
||||
Running,
|
||||
Finished,
|
||||
Failed,
|
||||
}
|
||||
|
||||
export type TaskDetail<T extends TaskType = TaskType> = {
|
||||
base: Task<T>;
|
||||
progress?: TaskProgressBasicType[T];
|
||||
status: TaskStatus;
|
||||
error?: string;
|
||||
fataled?: boolean;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "
|
||||
}
|
||||
|
||||
@@ -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": "错误消息:"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user