mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Add Download Task
This commit is contained in:
48
components/BCheckbox.tsx
Normal file
48
components/BCheckbox.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Component, ContextType } from "preact";
|
||||
import Checkbox from "preact-material-components/Checkbox";
|
||||
import { StateUpdater } from "preact/hooks";
|
||||
import { BCtx } from "./BContext.tsx";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
checked: boolean;
|
||||
name?: string;
|
||||
description?: string;
|
||||
set_value?: StateUpdater<boolean>;
|
||||
};
|
||||
|
||||
export default class BCheckbox extends Component<Props, unknown> {
|
||||
static contextType = BCtx;
|
||||
declare context: ContextType<typeof BCtx>;
|
||||
set_value(value: boolean) {
|
||||
if (this.props.set_value) {
|
||||
this.props.set_value(value);
|
||||
} else if (this.context) {
|
||||
this.context.set_value((v) => {
|
||||
v[this.props.name || ""] = value;
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
let label = null;
|
||||
if (this.props.description) {
|
||||
label = <label for={this.props.id}>{this.props.description}</label>;
|
||||
}
|
||||
return (
|
||||
<div class="bcheckbox">
|
||||
<Checkbox
|
||||
id={this.props.id}
|
||||
checked={this.props.checked}
|
||||
onInput={(ev: Event) => {
|
||||
if (ev.target) {
|
||||
const e = ev.target as HTMLInputElement;
|
||||
this.set_value(e.checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
29
components/BContext.tsx
Normal file
29
components/BContext.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Component, ComponentChild, createContext } from "preact";
|
||||
import { StateUpdater } from "preact/hooks";
|
||||
|
||||
type State = {
|
||||
set_value: StateUpdater<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: ComponentChild;
|
||||
set_value: StateUpdater<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
export const BCtx = createContext<State | null>(null);
|
||||
|
||||
export default class BContext extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
set_value: props.set_value,
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<BCtx.Provider value={this.state}>
|
||||
{this.props.children}
|
||||
</BCtx.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from "preact";
|
||||
import { Component, ContextType } from "preact";
|
||||
import Select from "preact-material-components/Select";
|
||||
import { Ref, StateUpdater, useRef } from "preact/hooks";
|
||||
import { BCtx } from "./BContext.tsx";
|
||||
|
||||
interface obj {
|
||||
toString(): string;
|
||||
@@ -19,7 +20,8 @@ type Props<T extends obj> = {
|
||||
/**@default {false}*/
|
||||
outlined?: boolean;
|
||||
hintText?: string;
|
||||
set_value: StateUpdater<T>;
|
||||
set_value?: StateUpdater<T>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -28,6 +30,8 @@ type State = {
|
||||
|
||||
export default class SettingsSelect<T extends obj>
|
||||
extends Component<Props<T>, State> {
|
||||
static contextType = BCtx;
|
||||
declare context: ContextType<typeof BCtx>;
|
||||
ref: Ref<Select | undefined> | undefined;
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
@@ -85,7 +89,14 @@ export default class SettingsSelect<T extends obj>
|
||||
}
|
||||
set_value(index: number) {
|
||||
const value = this.props.list[index].value;
|
||||
this.props.set_value(value);
|
||||
if (this.props.set_value) {
|
||||
this.props.set_value(value);
|
||||
} else if (this.context) {
|
||||
this.context.set_value((v) => {
|
||||
v[this.props.name || ""] = value;
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
update(index: number) {
|
||||
const e = this.ref?.current;
|
||||
|
||||
120
components/BTextField.tsx
Normal file
120
components/BTextField.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Component, ComponentChildren, ContextType } from "preact";
|
||||
import TextField from "preact-material-components/TextField";
|
||||
import { Ref, StateUpdater, useRef } from "preact/hooks";
|
||||
import { BCtx } from "./BContext.tsx";
|
||||
|
||||
interface TextType {
|
||||
text: string;
|
||||
password: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
text: never;
|
||||
password: never;
|
||||
number: number;
|
||||
}
|
||||
|
||||
type Props<T extends keyof TextType> = {
|
||||
value?: TextType[T];
|
||||
name?: string;
|
||||
description?: string;
|
||||
type: T;
|
||||
label?: string;
|
||||
helpertext?: string;
|
||||
textarea?: boolean;
|
||||
fullwidth?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: ComponentChildren;
|
||||
set_value?: StateUpdater<TextType[T]>;
|
||||
min?: DataType[T];
|
||||
max?: DataType[T];
|
||||
outlined?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export default class BTextField<T extends keyof TextType>
|
||||
extends Component<Props<T>, unknown> {
|
||||
static contextType = BCtx;
|
||||
ref: Ref<TextField | undefined> | undefined;
|
||||
declare context: ContextType<typeof BCtx>;
|
||||
update(value: TextType[T]) {
|
||||
const e = this.ref?.current;
|
||||
if (e) {
|
||||
const b = e.base;
|
||||
if (b) {
|
||||
const t = b as HTMLElement;
|
||||
const d = t.querySelector("input");
|
||||
if (d) {
|
||||
const type = this.props.type;
|
||||
// @ts-ignore Checked
|
||||
if (type === "text" || type === "password") d.value = value;
|
||||
// @ts-ignore Checked
|
||||
else d.valueAsNumber = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
set_value(value: TextType[T]) {
|
||||
if (this.props.set_value) {
|
||||
this.props.set_value(value);
|
||||
} else if (this.context) {
|
||||
this.context.set_value((v) => {
|
||||
v[this.props.name || ""] = value;
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
if (this.props.value !== undefined) this.update(this.props.value);
|
||||
}
|
||||
componentWillUpdate(
|
||||
nextProps: Readonly<Props<T>>,
|
||||
_nextState: Readonly<unknown>,
|
||||
_nextContext: unknown,
|
||||
) {
|
||||
if (nextProps.value !== undefined) 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>();
|
||||
let cn = "text";
|
||||
if (this.props.helpertext) cn += " helper";
|
||||
if (this.props.outlined) cn += " outlined";
|
||||
if (this.props.label) cn += " label";
|
||||
let desc = null;
|
||||
if (this.props.description) {
|
||||
desc = <label>{this.props.description}</label>;
|
||||
}
|
||||
return (
|
||||
<div class={cn} id={this.props.id}>
|
||||
{desc}
|
||||
<TextField
|
||||
fullwidth={this.props.fullwidth}
|
||||
textarea={this.props.textarea}
|
||||
type={this.props.type}
|
||||
disabled={this.props.disabled}
|
||||
helperText={this.props.helpertext}
|
||||
label={this.props.label}
|
||||
ref={this.ref}
|
||||
onInput={(ev: InputEvent) => {
|
||||
if (ev.target) {
|
||||
const e = ev.target as HTMLInputElement;
|
||||
this.set_value(this.get_value(e));
|
||||
}
|
||||
}}
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
outlined={this.props.outlined}
|
||||
/>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,16 @@ import Icon from "preact-material-components/Icon";
|
||||
import { set_state } from "../server/state.ts";
|
||||
import t from "../server/i18n.ts";
|
||||
import BSelect from "./BSelect.tsx";
|
||||
import { useState } from "preact/hooks";
|
||||
import { StateUpdater, useRef, useState } from "preact/hooks";
|
||||
import { TaskType } from "../task.ts";
|
||||
import BTextField from "./BTextField.tsx";
|
||||
import { parseUrl, UrlType } from "../url.ts";
|
||||
import { generate_download_cfg } from "../server/cfg.ts";
|
||||
import BCheckbox from "./BCheckbox.tsx";
|
||||
import BContext from "./BContext.tsx";
|
||||
import Button from "preact-material-components/Button";
|
||||
import { sendTaskMessage } from "../islands/TaskManager.tsx";
|
||||
import Snackbar from "preact-material-components/Snackbar";
|
||||
|
||||
export type NewTaskProps = {
|
||||
show: boolean;
|
||||
@@ -19,26 +27,168 @@ export default class NewTask extends Component<NewTaskProps> {
|
||||
if (!this.props.show) return null;
|
||||
const [task_type, set_task_type] = useState(TaskType.Download);
|
||||
let config_div = null;
|
||||
const close = () => {
|
||||
set_state((p) => p.slice(0, p.length - 4));
|
||||
};
|
||||
let submit: (() => boolean) | null = null;
|
||||
let clean: (() => void) | null = null;
|
||||
const snack = useRef<Snackbar>();
|
||||
const show_snack = (message: string) => {
|
||||
snack.current?.MDComponent?.show({ message });
|
||||
};
|
||||
if (task_type === TaskType.Download) {
|
||||
const [gid, set_gid1] = useState<number>();
|
||||
const [token, set_token1] = useState<string>();
|
||||
const [url, set_url1] = useState<string>();
|
||||
const [cfg, set_cfg] = useState(generate_download_cfg());
|
||||
const [overwrite_cfg, set_overwrite_cfg1] = useState(false);
|
||||
const set_url: StateUpdater<string> = (u) => {
|
||||
const n = typeof u === "string" ? u : u(url || "");
|
||||
set_url1(n);
|
||||
const p = parseUrl(n);
|
||||
if (p && p.type !== UrlType.Single) {
|
||||
set_gid1(p.gid);
|
||||
set_token1(p.token);
|
||||
}
|
||||
};
|
||||
const set_gid: StateUpdater<number> = (u) => {
|
||||
const g = typeof u === "number" ? u : u(gid || 0);
|
||||
set_gid1(g);
|
||||
if (g && token) {
|
||||
set_url1(`https://e-hentai.org/g/${g}/${token}/`);
|
||||
}
|
||||
};
|
||||
const set_token: StateUpdater<string> = (u) => {
|
||||
const n = typeof u === "string" ? u : u(url || "");
|
||||
set_token1(n);
|
||||
if (gid && n) {
|
||||
set_url1(`https://e-hentai.org/g/${gid}/${n}/`);
|
||||
}
|
||||
};
|
||||
const set_overwrite_cfg: StateUpdater<boolean> = (u) => {
|
||||
const n = typeof u === "boolean" ? u : u(overwrite_cfg);
|
||||
set_overwrite_cfg1(n);
|
||||
if (n === true) {
|
||||
set_cfg(generate_download_cfg());
|
||||
}
|
||||
};
|
||||
let cfg_div = null;
|
||||
if (overwrite_cfg) {
|
||||
cfg_div = (
|
||||
<div>
|
||||
<BContext set_value={set_cfg}>
|
||||
<BCheckbox
|
||||
id="d-download_original_img"
|
||||
name="download_original_img"
|
||||
checked={cfg.download_original_img || false}
|
||||
description={t(
|
||||
"settings.download_original_img",
|
||||
)}
|
||||
/>
|
||||
<BCheckbox
|
||||
id="d-mpv"
|
||||
name="mpv"
|
||||
checked={cfg.mpv || false}
|
||||
description={t("settings.mpv")}
|
||||
/>
|
||||
<BCheckbox
|
||||
id="d-remove_previous_gallery"
|
||||
name="remove_previous_gallery"
|
||||
checked={cfg.remove_previous_gallery || false}
|
||||
description={t(
|
||||
"settings.remove_previous_gallery",
|
||||
)}
|
||||
/>
|
||||
<BTextField
|
||||
id="d-max_download_img_count"
|
||||
name="max_download_img_count"
|
||||
value={cfg.max_download_img_count || 3}
|
||||
description={t(
|
||||
"settings.max_download_img_count",
|
||||
)}
|
||||
type="number"
|
||||
min={1}
|
||||
outlined={true}
|
||||
/>
|
||||
<BTextField
|
||||
id="d-max_retry_count"
|
||||
name="max_retry_count"
|
||||
value={cfg.max_retry_count || 3}
|
||||
description={t("settings.max_retry_count")}
|
||||
type="number"
|
||||
min={1}
|
||||
outlined={true}
|
||||
/>
|
||||
</BContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
config_div = (
|
||||
<div class="download">
|
||||
{t("task.gallery_url")}
|
||||
<BTextField
|
||||
value={url}
|
||||
type="text"
|
||||
description={t("task.gallery_url")}
|
||||
outlined={true}
|
||||
set_value={set_url}
|
||||
/>
|
||||
<BTextField
|
||||
value={gid}
|
||||
type="number"
|
||||
description={t("task.gallery_id")}
|
||||
outlined={true}
|
||||
set_value={set_gid}
|
||||
/>
|
||||
<BTextField
|
||||
value={token}
|
||||
type="text"
|
||||
description={t("task.gallery_token")}
|
||||
outlined={true}
|
||||
set_value={set_token}
|
||||
/>
|
||||
<BCheckbox
|
||||
id="d-cfg"
|
||||
checked={overwrite_cfg}
|
||||
description={t("task.overwrite_cfg")}
|
||||
set_value={set_overwrite_cfg}
|
||||
/>
|
||||
{cfg_div}
|
||||
</div>
|
||||
);
|
||||
if (gid && token) {
|
||||
submit = () => {
|
||||
return sendTaskMessage({
|
||||
type: "new_download_task",
|
||||
gid,
|
||||
token,
|
||||
cfg: overwrite_cfg ? cfg : undefined,
|
||||
});
|
||||
};
|
||||
clean = () => {
|
||||
set_gid1(undefined);
|
||||
set_token1(undefined);
|
||||
set_url1(undefined);
|
||||
};
|
||||
}
|
||||
}
|
||||
const sub = () => {
|
||||
if (submit) {
|
||||
const re = submit();
|
||||
if (re) {
|
||||
if (clean) clean();
|
||||
close();
|
||||
} else {
|
||||
show_snack(t("task.submit_failed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div class="new_task">
|
||||
<div class="container">
|
||||
<div class="top">
|
||||
<div class="title">{t("task.add")}</div>
|
||||
<Fab class="close" mini={true}>
|
||||
<Icon
|
||||
onClick={() => {
|
||||
set_state((p) => p.slice(0, p.length - 4));
|
||||
}}
|
||||
>
|
||||
close
|
||||
</Icon>
|
||||
<Icon onClick={close}>close</Icon>
|
||||
</Fab>
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -59,8 +209,13 @@ export default class NewTask extends Component<NewTaskProps> {
|
||||
</div>
|
||||
{config_div}
|
||||
</div>
|
||||
<div class="bottom"></div>
|
||||
<div class="bottom">
|
||||
<Button disabled={submit === null} onClick={sub}>
|
||||
{t("common.submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Snackbar ref={snack} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export const handler: Handlers = {
|
||||
return return_error(400, "token is needed.");
|
||||
},
|
||||
async PUT(req, _ctx) {
|
||||
const ct = req.headers.get("Content-Type").split(";")[0].trim() || "";
|
||||
const ct = req.headers.get("Content-Type")?.split(";")[0].trim() || "";
|
||||
if (ct === "application/json") {
|
||||
if (!req.body) return_error(1, "Body not found.");
|
||||
let b = null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { signal } from "@preact/signals";
|
||||
import { ConfigType } from "../config.ts";
|
||||
import { get_ws_host } from "./utils.ts";
|
||||
import { ConfigClientSocketData, ConfigSeverSocketData } from "./config.ts";
|
||||
import { DEFAULT_DOWNLOAD_CONFIG, DownloadConfig } from "../tasks/download.ts";
|
||||
|
||||
export const cfg = signal<ConfigType | undefined>(undefined);
|
||||
|
||||
@@ -23,3 +24,15 @@ export function initCfg() {
|
||||
sendMessage({ type: "close" });
|
||||
});
|
||||
}
|
||||
|
||||
export function generate_download_cfg(): DownloadConfig {
|
||||
if (!cfg.value) return DEFAULT_DOWNLOAD_CONFIG;
|
||||
const c = cfg.value;
|
||||
return {
|
||||
download_original_img: c.download_original_img,
|
||||
max_download_img_count: c.max_download_img_count,
|
||||
max_retry_count: c.max_retry_count,
|
||||
mpv: c.mpv,
|
||||
remove_previous_gallery: c.remove_previous_gallery,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings label {
|
||||
.settings label, div.new_task div.bcheckbox label {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
@@ -236,8 +236,8 @@ div.new_task {
|
||||
|
||||
div.new_task>div.container {
|
||||
position: relative;
|
||||
margin: 5% 10%;
|
||||
height: calc(100% - 10% - 64px);
|
||||
margin: 2.5% 10%;
|
||||
height: calc(100% - 5% - 64px);
|
||||
min-width: 400px;
|
||||
width: calc(100% - 20%);
|
||||
}
|
||||
@@ -259,6 +259,7 @@ div.new_task .top .title {
|
||||
|
||||
div.new_task>div.container>div.content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100% - 80px);
|
||||
min-height: 300px;
|
||||
}
|
||||
@@ -268,6 +269,18 @@ div.new_task>div.container>div.bottom {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
div.new_task div.content .text {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
div.new_task div.content .text .mdc-text-field {
|
||||
margin: 1px;
|
||||
width: calc(100% - 2px);
|
||||
}
|
||||
|
||||
@media (max-width:1280px) {
|
||||
.settings {
|
||||
margin: 0;
|
||||
@@ -316,4 +329,11 @@ div.new_task>div.container>div.bottom {
|
||||
.main {
|
||||
top: 56px;
|
||||
}
|
||||
|
||||
div.new_task>div.container {
|
||||
margin: 5px;
|
||||
min-width: 0;
|
||||
height: calc(100% - 74px);
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"save": "Save",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"darkmode": "Dark mode"
|
||||
"darkmode": "Dark mode",
|
||||
"submit": "Submit"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,9 @@
|
||||
"type": "Task type: ",
|
||||
"download": "Download",
|
||||
"export_zip": "Export as ZIP file",
|
||||
"gallery_url": "Gallery URL: "
|
||||
"gallery_url": "Gallery URL: ",
|
||||
"gallery_id": "Gallery ID: ",
|
||||
"gallery_token": "Gallery token: ",
|
||||
"overwrite_cfg": "Overwrite default config.",
|
||||
"submit_failed": "Failed to submit task."
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"save": "保存",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"darkmode": "夜间模式"
|
||||
"darkmode": "夜间模式",
|
||||
"submit": "提交"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,9 @@
|
||||
"type": "任务类型:",
|
||||
"download": "下载",
|
||||
"export_zip": "导出为ZIP文件",
|
||||
"gallery_url": "画廊地址:"
|
||||
"gallery_url": "画廊地址:",
|
||||
"gallery_id": "画廊ID:",
|
||||
"gallery_token": "画廊令牌:",
|
||||
"overwrite_cfg": "覆盖默认设置。",
|
||||
"submit_failed": "提交任务失败"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user