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

@@ -97,7 +97,7 @@ COPY ./LICENSE ./
ENV LD_LIBRARY_PATH=/app/lib
ENV PATH=/app/bin:$PATH
RUN deno task fetch && deno task server-build && deno task prebuild && \
RUN deno task server-build && deno task prebuild && \
deno task cache && rm -rf ~/.cache && \
mkdir -p ./thumbnails && chmod 777 ./thumbnails && \
mkdir -p ./downloads && chmod 777 ./downloads && \

View File

@@ -1,47 +0,0 @@
import { Component, ContextType } from "preact";
import Checkbox from "preact-material-components/Checkbox.js";
import { BCtx } from "./BContext.tsx";
type Props = {
id?: string;
checked: boolean;
name?: string;
description?: string;
set_value?: (v: boolean) => void;
};
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>
);
}
}

View File

@@ -1,29 +0,0 @@
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>
);
}
}

View File

@@ -1,141 +0,0 @@
import { Component, ComponentChildren, ContextType } from "preact";
import { BCtx } from "./BContext.tsx";
import { useState } from "preact/hooks";
import type { _MdOutlinedTextField as _TextField } from "../server/md3.ts";
import { MdOutlinedTextField } from "../server/dmodule.ts";
import List from "preact-material-components/List.js";
interface TextType {
text: string;
password: string;
number: number;
}
interface DataType {
text: never;
password: never;
number: number;
}
type Props<T extends keyof TextType> = {
/**@default {true} */
clear_cache?: boolean;
label?: string;
description?: string;
value?: TextType[T];
name?: string;
type: T;
disabled?: boolean;
children?: ComponentChildren;
set_value?: (v?: TextType[T]) => void;
min?: DataType[T];
max?: DataType[T];
id?: string;
list?: string;
datalist?: { value: TextType[T]; label?: string }[];
};
export default class BMd3TextField<T extends keyof TextType>
extends Component<Props<T>, unknown> {
static contextType = BCtx;
declare context: ContextType<typeof BCtx>;
get clear_cache() {
return this.props.clear_cache !== undefined
? this.props.clear_cache
: true;
}
get_value(e: _TextField): TextType[T] | undefined {
const type = this.props.type;
if (!e.value.length) return undefined;
// @ts-ignore Checked
if (type === "text" || type === "password") return e.value;
// @ts-ignore Checked
return e.valueAsNumber;
}
render() {
if (!MdOutlinedTextField.value) return null;
let datalist_div = null;
const [display_datalist, set_display_datalist] = useState(false);
let cn = "b-text-field md3 text";
if (this.props.label) cn += " label";
if (this.props.datalist && this.props.datalist.length) {
cn += " datalist";
let cn2 = "datalist";
if (display_datalist) cn2 += " open";
const v = this.props.value?.toString();
datalist_div = (
<List class={cn2}>
{this.props.datalist.map((d) => {
if (v !== undefined) {
if (!d.value.toString().startsWith(v)) return null;
}
let label_div = null;
if (d.label) {
label_div = <div class="label">{d.label}</div>;
}
return (
<List.Item
onMousedown={() => {
this.set_value(d.value);
}}
>
<div class="value">{d.value}</div>
{label_div}
</List.Item>
);
})}
</List>
);
}
const TextField = MdOutlinedTextField.value;
let desc = null;
if (this.props.description) {
desc = <label>{this.props.description}</label>;
}
let value: string | undefined;
if (this.props.value !== undefined) {
if (typeof this.props.value === "string") {
value = this.props.value;
} else {
value = this.props.value.toString();
}
} else if (this.clear_cache) {
value = "";
}
return (
<div class={cn} id={this.props.id}>
{desc}
<TextField
value={value}
type={this.props.type}
label={this.props.label}
disabled={this.props.disabled}
min={this.props.min ? this.props.min.toString() : undefined}
max={this.props.max ? this.props.max.toString() : undefined}
list={this.props.list}
/**@ts-ignore */
onInput={(ev: InputEvent) => {
if (ev.target) {
const e = ev.target as _TextField;
this.set_value(this.get_value(e));
}
}}
onFocus={() => set_display_datalist(true)}
onBlur={() => set_display_datalist(false)}
/>
{datalist_div}
{this.props.children}
</div>
);
}
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;
});
}
}
}

View File

@@ -1,85 +0,0 @@
import { Component, ContextType } from "preact";
import Md3Select from "./Md3Select.tsx";
import { StateUpdater } from "preact/hooks";
import { BCtx } from "./BContext.tsx";
interface obj {
toString(): string;
}
type Props<T extends obj> = {
id?: string;
list: { value: T; text?: string; disabled?: boolean }[];
/**@default {0}*/
selectedIndex?: number;
selectedValue?: T;
/**@default {false}*/
disabled?: boolean;
hintText?: string;
set_value?: StateUpdater<T>;
name?: string;
};
type State = {
selectedIndex: number;
};
export default class BSelect<T extends obj> extends Component<Props<T>, State> {
static contextType = BCtx;
declare context: ContextType<typeof BCtx>;
constructor(props: Props<T>) {
super(props);
if (!props.list.length) throw Error("No list.");
let index = props.selectedValue
? props.list.findIndex((v) => v.value === props.selectedValue)
: props.selectedIndex;
if (index === -1) index = 0;
this.state = { selectedIndex: index || 0 };
}
componentWillReceiveProps(
nextProps: Readonly<Props<T>>,
_nextContext: unknown,
): void {
const index = nextProps.selectedValue
? nextProps.list.findIndex((v) =>
v.value === nextProps.selectedValue
)
: nextProps.selectedIndex;
if (index === -1) return;
const selectedIndex = index || 0;
this.setState({ selectedIndex });
}
render() {
return (
<Md3Select
id={this.props.id}
supportingText={this.props.hintText}
disabled={this.props.disabled}
set_index={(selectedIndex) => {
this.setState({ selectedIndex });
this.set_value(selectedIndex);
}}
selectedIndex={this.state.selectedIndex}
>
{this.props.list.map((v) => {
const t = v.text ? v.text : v.value.toString();
return <Md3Select.Option disabled={v.disabled} value={t} />;
})}
</Md3Select>
);
}
get selectedIndex() {
return this.state.selectedIndex;
}
set_value(index: number) {
const value = this.props.list[index].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;
});
}
}
}

View File

@@ -1,217 +0,0 @@
import { Component, ComponentChildren, ContextType } from "preact";
import TextField from "preact-material-components/TextField.js";
import { Ref, useRef, useState } from "preact/hooks";
import { BCtx } from "./BContext.tsx";
import List from "preact-material-components/List.js";
interface TextType {
text: string;
password: string;
number: number;
}
interface DataType {
text: never;
password: never;
number: number;
}
type Props<T extends keyof TextType> = {
/**@default {true} */
clear_cache?: boolean;
value?: TextType[T];
name?: string;
description?: string;
type: T;
label?: string;
helpertext?: string;
textarea?: boolean;
fullwidth?: boolean;
disabled?: boolean;
children?: ComponentChildren;
set_value?: (v?: TextType[T]) => void;
min?: DataType[T];
max?: DataType[T];
outlined?: boolean;
id?: string;
list?: string;
datalist?: { value: TextType[T]; label?: string }[];
onPaste?: (
clipboard: string,
) => { text: string; overwrite?: boolean } | undefined;
};
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>;
clear() {
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) {
d.value = "";
}
}
}
}
get clear_cache() {
return this.props.clear_cache !== undefined
? this.props.clear_cache
: true;
}
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);
else if (this.clear_cache) this.clear();
}
componentDidUpdate(
previousProps: Readonly<Props<T>>,
previousState: Readonly<unknown>,
snapshot: unknown,
): void {
if (this.props.value !== undefined) this.update(this.props.value);
else if (this.clear_cache) this.clear();
}
get value(): TextType[T] | undefined {
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) {
return this.get_value(d);
}
}
}
return undefined;
}
get_value(e: HTMLInputElement): TextType[T] | undefined {
const type = this.props.type;
if (!e.value.length) return undefined;
// @ts-ignore Checked
if (type === "text" || type === "password") return e.value;
// @ts-ignore Checked
return e.valueAsNumber;
}
render() {
this.ref = useRef<TextField>();
let cn = "b-text-field text";
let datalist_div = null;
const [display_datalist, set_display_datalist] = useState(false);
if (this.props.helpertext) cn += " helper";
if (this.props.outlined) cn += " outlined";
if (this.props.label) cn += " label";
if (this.props.datalist && this.props.datalist.length) {
cn += " datalist";
let cn2 = "datalist";
if (display_datalist) cn2 += " open";
const v = this.value?.toString();
datalist_div = (
<List class={cn2}>
{this.props.datalist.map((d) => {
if (v !== undefined) {
if (!d.value.toString().startsWith(v)) return null;
}
let label_div = null;
if (d.label) {
label_div = <div class="label">{d.label}</div>;
}
return (
<List.Item
onMousedown={() => {
this.set_value(d.value);
}}
>
<div class="value">{d.value}</div>
{label_div}
</List.Item>
);
})}
</List>
);
}
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}
list={this.props.list}
onFocus={() => set_display_datalist(true)}
onBlur={() => set_display_datalist(false)}
onPaste={this.props.onPaste
? ((e: ClipboardEvent) => {
if (!this.props.onPaste) return;
const clipboard =
e.clipboardData?.getData("text") || "";
const v = this.props.onPaste(clipboard);
if (!v) return;
e.preventDefault();
if (e.target) {
const i = e.target as HTMLInputElement;
if (v.overwrite) {
i.value = v.text;
} else {
i.setRangeText(v.text);
}
this.set_value(this.get_value(i));
}
})
: undefined}
/>
{datalist_div}
{this.props.children}
</div>
);
}
}

View File

@@ -1,105 +0,0 @@
import { Component, ContextType } from "preact";
import { GlobalCtx } from "./GlobalContext.tsx";
import BMd3TextField from "./BMd3TextField.tsx";
import t from "../server/i18n.ts";
import { useState } from "preact/hooks";
import { MdTextButton, MdTonalButton } from "../server/dmodule.ts";
import { set_state } from "../server/state.ts";
import pbkdf2Hmac from "pbkdf2-hmac/?target=es2022";
import { encodeBase64 as encode } from "std/encoding/base64.ts";
type Props = {
show: boolean;
};
export default class CreateRootUser extends Component<Props> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
if (!this.props.show) return null;
if (!MdTonalButton.value) return null;
if (!MdTextButton.value) return null;
const [username, set_username] = useState<string>();
const [password, set_password] = useState<string>();
const [disabled, set_disabled] = useState(false);
const create_user = async (username: string, password: string) => {
set_disabled(true);
const body = new URLSearchParams();
body.append("name", username);
body.append("password", password);
const re = await fetch("/api/user", { method: "PUT", body });
if (re.status !== 201) {
throw Error(re.statusText);
}
const b = new URLSearchParams();
b.append("username", username);
const p = new Uint8Array(
await pbkdf2Hmac(
password,
"eh-downloader-salt",
210000,
64,
"SHA-512",
),
);
const t = (new Date()).getTime();
const p2 = encode(
new Uint8Array(
await pbkdf2Hmac(p, t.toString(), 1000, 64, "SHA-512"),
),
);
b.append("password", p2);
b.append("t", t.toString());
b.append("set_cookie", "1");
if (document.location.protocol === "https:") {
b.append("secure", "1");
}
const re2 = await fetch("/api/token", { method: "PUT", body: b });
const token = await re2.json();
if (!token.ok) {
throw Error(token.error);
}
};
const Button = MdTonalButton.value;
const TextButton = MdTextButton.value;
return (
<div>
<BMd3TextField
label={t("user.username")}
type="text"
value={username}
set_value={set_username}
/>
<BMd3TextField
label={t("user.password")}
type="password"
value={password}
set_value={set_password}
/>
<TextButton
onClick={() => {
localStorage.setItem("skip_create_root_user", "1");
set_state("#/");
}}
>
{t("user.skip")}
</TextButton>
<Button
disabled={disabled || !username || !password}
onClick={() => {
if (!username || !password) return;
create_user(username, password).then(() => {
set_disabled(false);
set_state("#/");
}).catch((e) => {
console.error(e);
set_disabled(false);
});
}}
>
{t("user.create_root_user")}
</Button>
</div>
);
}
}

View File

@@ -1,100 +0,0 @@
import { Component, ContextType } from "preact";
import { GlobalCtx } from "./GlobalContext.tsx";
import BMd3TextField from "./BMd3TextField.tsx";
import t from "../server/i18n.ts";
import { useState } from "preact/hooks";
import { MdTonalButton } from "../server/dmodule.ts";
import { set_state } from "../server/state.ts";
import pbkdf2Hmac from "pbkdf2-hmac/?target=es2022";
import { encodeBase64 as encode } from "std/encoding/base64.ts";
import { UserAgent } from "std/http/user_agent.ts";
type Props = {
show: boolean;
};
export default class Login extends Component<Props> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
if (!this.props.show) return null;
if (!MdTonalButton.value) return null;
const [username, set_username] = useState<string>();
const [password, set_password] = useState<string>();
const [disabled, set_disabled] = useState(false);
const login = async (username: string, password: string) => {
set_disabled(true);
const b = new URLSearchParams();
b.append("username", username);
const p = new Uint8Array(
await pbkdf2Hmac(
password,
"eh-downloader-salt",
210000,
64,
"SHA-512",
),
);
const t = (new Date()).getTime();
const p2 = encode(
new Uint8Array(
await pbkdf2Hmac(p, t.toString(), 1000, 64, "SHA-512"),
),
);
b.append("password", p2);
b.append("t", t.toString());
b.append("set_cookie", "1");
if (document.location.protocol === "https:") {
b.append("secure", "1");
}
b.append("client", "fresh");
b.append("client_version", "0.0.1");
b.append("client_platform", "web");
const ua = new UserAgent(navigator.userAgent);
let name = ua.browser.name;
if (name && ua.browser.version) {
name += " " + ua.browser.version;
}
if (name) {
b.append("device", name);
}
const re2 = await fetch("/api/token", { method: "PUT", body: b });
const token = await re2.json();
if (!token.ok) {
throw Error(token.error);
}
};
const Button = MdTonalButton.value;
return (
<div>
<BMd3TextField
label={t("user.username")}
type="text"
value={username}
set_value={set_username}
/>
<BMd3TextField
label={t("user.password")}
type="password"
value={password}
set_value={set_password}
/>
<Button
disabled={disabled || !username || !password}
onClick={() => {
if (!username || !password) return;
login(username, password).then(() => {
set_disabled(false);
set_state("#/");
}).catch((e) => {
console.error(e);
set_disabled(false);
});
}}
>
{t("user.login")}
</Button>
</div>
);
}
}

View File

@@ -1,102 +0,0 @@
import { Component, VNode } from "preact";
import { MdOutlinedSelect, MdSelectOption } from "../server/dmodule.ts";
import type { _MdOutlinedSelect } from "../server/md3.ts";
type OProps = {
value: string;
headline?: string;
selected?: boolean;
disabled?: boolean;
};
class Md3Option extends Component<OProps> {
render() {
if (!MdSelectOption.value) return null;
const Option = MdSelectOption.value;
return (
<Option
value={this.props.value}
headline={this.props.headline || this.props.value}
selected={this.props.selected}
disabled={this.props.disabled}
/>
);
}
}
type Props = {
id?: string;
children: VNode<Md3Option>[] | VNode<Md3Option>;
/**@default {false} */
quick?: boolean;
/**@default {false} */
required?: boolean;
/**@default {false} */
disabled?: boolean;
errorText?: string;
label?: string;
supportingText?: string;
/**@default {false} */
error?: boolean;
menuFixed?: boolean;
typeaheadDelay?: number;
hasLeadingIcon?: boolean;
hasTrailingIcon?: boolean;
displayText?: string;
selectedIndex?: number;
set_index?: (index: number) => void;
};
type State = {
selectedIndex: number;
};
export default class Md3Select extends Component<Props, State> {
static readonly Option = Md3Option;
constructor(props: Props) {
super(props);
this.state = { selectedIndex: props.selectedIndex || 0 };
}
componentWillReceiveProps(
nextProps: Readonly<Props>,
_nextContext: unknown,
): void {
const selectedIndex = nextProps.selectedIndex || 0;
this.setState({ selectedIndex });
}
get selectedIndex() {
return this.state.selectedIndex;
}
render() {
if (!MdOutlinedSelect.value) return null;
const Select = MdOutlinedSelect.value;
return (
<Select
id={this.props.id}
quick={this.props.quick}
required={this.props.required}
disabled={this.props.disabled}
errorText={this.props.errorText}
label={this.props.label}
supportingText={this.props.supportingText}
error={this.props.error}
menuFixed={this.props.menuFixed}
typeaheadDelay={this.props.typeaheadDelay}
hasLeadingIcon={this.props.hasLeadingIcon}
hasTrailingIcon={this.props.hasTrailingIcon}
displayText={this.props.displayText}
selectedIndex={this.state.selectedIndex}
onChange={(e) => {
const t = e.target as _MdOutlinedSelect;
this.setState({ selectedIndex: t.selectedIndex });
if (this.props.set_index) {
this.props.set_index(t.selectedIndex);
}
}}
>
{/**@ts-ignore */}
{this.props.children}
</Select>
);
}
}

View File

@@ -1,421 +0,0 @@
import { Component, ContextType } from "preact";
import { GlobalCtx } from "./GlobalContext.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 BSelect from "./BSelect.tsx";
import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import { TaskType } from "../task.ts";
import BTextField from "./BTextField.tsx";
import { parseUrl, UrlType } from "../url.ts";
import {
cfg,
generate_download_cfg,
generate_export_zip_cfg,
} from "../server/cfg.ts";
import BCheckbox from "./BCheckbox.tsx";
import BContext from "./BContext.tsx";
import Button from "preact-material-components/Button.js";
import { sendTaskMessage } from "../islands/TaskManager.tsx";
import Snackbar from "preact-material-components/Snackbar.js";
import type { GalleryResult } from "../server/gallery.ts";
import { tw } from "twind";
import type { ExportZipConfig } from "../tasks/export_zip.ts";
import type { GMeta } from "../db.ts";
import type { GalleryListResult } from "../server/gallery.ts";
export type NewTaskProps = {
show: boolean;
};
type State = {
gids?: GMeta[];
};
export default class NewTask extends Component<NewTaskProps, State> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
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 });
};
const [dgid, set_dgid1] = useState<number>();
const [token, set_token1] = useState<string>();
const [url, set_url1] = useState<string>();
const [dcfg, set_dcfg] = useState(generate_download_cfg());
const [overwrite_cfg, set_overwrite_cfg1] = useState(false);
const [ezgid, set_ezgid1] = useState<number>();
const [ginfo, set_ginfo] = useState<GalleryResult>();
const [abort, set_abort] = useState<AbortController>();
const [ezcfg, set_ezcfg1] = useState(generate_export_zip_cfg());
const [overwrite_ezcfg, set_overwrite_ezcfg] = useState(false);
const onPasteGid = (clipboard: string) => {
const p = parseUrl(clipboard);
if (p) {
return { text: p.gid.toString(), overwrite: true };
}
return;
};
const onPasteToken = (clipboard: string) => {
const p = parseUrl(clipboard);
if (p && p.type !== UrlType.Single) {
return { text: p.token, overwrite: true };
}
return;
};
const fetchGidsData = async () => {
const re = await fetch(
"/api/gallery/list?all=1&fields=gid,title,title_jpn",
);
const d: GalleryListResult = await re.json();
if (d.ok) {
this.setState({ ...this.state, gids: d.data });
}
};
useEffect(() => {
if (task_type === TaskType.ExportZip) {
fetchGidsData().catch((e) => console.error(e));
}
}, [task_type]);
if (task_type === TaskType.Download) {
const set_url: StateUpdater<string | undefined> = (u) => {
const n = typeof u === "string" ? u : u ? u(url) : u;
set_url1(n);
if (n) {
const p = parseUrl(n);
if (p && p.type !== UrlType.Single) {
set_dgid(p.gid);
set_token1(p.token);
}
}
};
const set_dgid: StateUpdater<number | undefined> = (u) => {
const g = typeof u === "number" ? u : u ? u(dgid) : u;
set_dgid1(g);
if (g && token) {
set_url1(`https://e-hentai.org/g/${g}/${token}/`);
}
};
const set_token: StateUpdater<string | undefined> = (u) => {
const n = typeof u === "string" ? u : u ? u(url) : u;
set_token1(n);
if (dgid && n) {
set_url1(`https://e-hentai.org/g/${dgid}/${n}/`);
}
};
const set_overwrite_cfg = (n: boolean) => {
set_overwrite_cfg1(n);
if (n) {
set_dcfg(generate_download_cfg());
}
};
let cfg_div = null;
if (overwrite_cfg) {
cfg_div = (
<div>
<BContext set_value={set_dcfg}>
<BCheckbox
id="d-download_original_img"
name="download_original_img"
checked={dcfg.download_original_img || false}
description={t(
"settings.download_original_img",
)}
/>
<BCheckbox
id="d-mpv"
name="mpv"
checked={dcfg.mpv || false}
description={t("settings.mpv")}
/>
<BCheckbox
id="d-remove_previous_gallery"
name="remove_previous_gallery"
checked={dcfg.remove_previous_gallery || false}
description={t(
"settings.remove_previous_gallery",
)}
/>
<BTextField
id="d-max_download_img_count"
name="max_download_img_count"
value={dcfg.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={dcfg.max_retry_count || 3}
description={t("settings.max_retry_count")}
type="number"
min={1}
outlined={true}
/>
</BContext>
</div>
);
}
config_div = (
<div class="download">
<BTextField
value={url}
type="text"
description={t("task.gallery_url")}
outlined={true}
set_value={set_url}
/>
<BTextField
value={dgid}
type="number"
description={t("task.gallery_id")}
outlined={true}
set_value={set_dgid}
onPaste={onPasteGid}
/>
<BTextField
value={token}
type="text"
description={t("task.gallery_token")}
outlined={true}
set_value={set_token}
onPaste={onPasteToken}
/>
<BCheckbox
id="d-cfg"
checked={overwrite_cfg}
description={t("task.overwrite_cfg")}
set_value={set_overwrite_cfg}
/>
{cfg_div}
</div>
);
if (dgid && token) {
submit = () => {
return sendTaskMessage({
type: "new_download_task",
gid: dgid,
token,
cfg: overwrite_cfg ? dcfg : undefined,
});
};
clean = () => {
set_dgid1(undefined);
set_token1(undefined);
set_url1(undefined);
};
}
} else if (task_type === TaskType.ExportZip) {
const export_ad = overwrite_ezcfg
? ezcfg.export_ad || false
: false;
const jpn_title = overwrite_ezcfg
? ezcfg.jpn_title || false
: cfg.value
? cfg.value.export_zip_jpn_title
: false;
const fetch_ginfo = (gid: number) => {
set_abort(new AbortController());
fetch(`/api/gallery/${gid}`).then(async (res) => {
try {
set_ginfo(await res.json());
} catch (e) {
set_ginfo({
ok: false,
status: -1,
error: e.toString(),
});
}
}).catch((e) => {
set_ginfo({ ok: false, status: -1, error: e.toString() });
}).finally(() => {
set_abort(undefined);
});
};
const set_ezgid = (g: number | undefined) => {
if (abort) abort.abort();
set_ezgid1(g);
if (g !== undefined && !isNaN(g)) fetch_ginfo(g);
};
let ginfo_div = null;
if (ginfo?.ok) {
let title = ginfo.data.meta.title;
if (jpn_title && ginfo.data.meta.title_jpn) {
title = ginfo.data.meta.title_jpn;
}
const count = export_ad
? ginfo.data.pages.length
: ginfo.data.pages.reduce((p, c) => c.is_ad ? p : p + 1, 0);
ginfo_div = (
<div>
<div>
{t("task.gallery_title")}
{title}
</div>
<div>
{t("task.gallery_page")}
{count}
</div>
</div>
);
} else if (ginfo?.ok === false) {
ginfo_div = (
<div>
<div class={tw`text-red-500`}>{ginfo.error}</div>
</div>
);
}
const set_ezcfg: StateUpdater<ExportZipConfig> = (v) => {
set_ezcfg1(v);
this.forceUpdate();
};
const set_overwrite_cfg = (v: boolean) => {
set_overwrite_ezcfg(v);
if (v) {
set_ezcfg(Object.assign(ezcfg, generate_export_zip_cfg()));
}
};
let cfg_div = null;
if (overwrite_ezcfg) {
cfg_div = (
<div>
<BContext set_value={set_ezcfg}>
<BCheckbox
id="ez-jpn_title"
name="jpn_title"
checked={ezcfg.jpn_title || false}
description={t("task.ezcfg_jpn_title")}
/>
<BCheckbox
id="ez-export_ad"
name="export_ad"
checked={ezcfg.export_ad || false}
description={t("task.ezcfg_export_ad")}
/>
<BTextField
name="output"
value={ezcfg.output}
description={t("task.ezcfg_output")}
type="text"
outlined={true}
/>
<BTextField
name="max_length"
value={ezcfg.max_length}
description={t("task.ezcfg_max_length")}
type="number"
outlined={true}
min={0}
/>
</BContext>
</div>
);
}
const datalist: { value: number; label?: string }[] = [];
if (this.state.gids) {
this.state.gids.forEach((g) => {
const t = jpn_title && g.title_jpn ? g.title_jpn : g.title;
datalist.push({ value: g.gid, label: t });
});
}
config_div = (
<div class="export_zip">
<BTextField
value={ezgid}
description={t("task.gallery_id")}
type="number"
outlined={true}
set_value={set_ezgid}
datalist={datalist}
onPaste={onPasteGid}
/>
{ginfo_div}
<BCheckbox
id="ez-cfg"
checked={overwrite_ezcfg}
description={t("task.overwrite_cfg")}
set_value={set_overwrite_cfg}
/>
{cfg_div}
</div>
);
if (ezgid && ginfo?.ok && ginfo.data.meta.gid === ezgid) {
submit = () => {
return sendTaskMessage({
type: "new_export_zip_task",
gid: ezgid,
cfg: overwrite_ezcfg ? ezcfg : undefined,
});
};
clean = () => {
set_ezcfg((c) => {
c.output = undefined;
return c;
});
set_ginfo(undefined);
set_ezgid1(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} onClick={close}>
<Icon>close</Icon>
</Fab>
</div>
<div class="content">
<div class="type">
{t("task.type")}
<BSelect
list={[{
value: TaskType.Download,
text: t("task.download"),
}, {
value: TaskType.ExportZip,
text: t("task.export_zip"),
}]}
selectedValue={task_type}
set_value={set_task_type}
/>
</div>
{config_div}
</div>
<div class="bottom">
<Button disabled={submit === null} onClick={sub}>
{t("common.submit")}
</Button>
</div>
</div>
<Snackbar ref={snack} />
</div>
);
}
}

View File

@@ -1,109 +0,0 @@
import {
Component,
ComponentChildren,
ContextType,
createContext,
} from "preact";
import round from "lodash/round";
type CtxProps = {
min: number;
max: number;
striped: boolean;
animated: boolean;
precision: number;
};
const PCtx = createContext<CtxProps | null>(null);
type BarProps = {
value: number;
striped?: boolean;
animated?: boolean;
class?: string;
label?: string;
set_label?: (
per: number,
value: number,
max: number,
min: number,
) => string;
precision?: number;
};
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;
const precision =
(this.props.precision === undefined
? this.context?.precision
: this.props.precision) || 2;
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 per = (v - min) / (max - min) * 100;
const style = `width: ${per}%;`;
let label = this.props.label;
if (this.props.set_label) {
label = this.props.set_label(round(per, precision), v, max, min);
}
return (
<div
class={cls}
style={style}
role="progressbar"
aria-valuenow={v}
aria-valuemin={min}
aria-valuemax={max}
>
{label}
</div>
);
}
}
type Props = {
/**@default {0} */
min?: number;
/**@default {100} */
max?: number;
/**@default {false} */
striped?: boolean;
/**@default {false} */
animated?: boolean;
children: ComponentChildren;
/**@default {2} */
precision?: number;
};
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,
precision: this.props.precision || 2,
}}
>
{this.props.children}
</PCtx.Provider>
</div>
);
}
}

View File

@@ -1,46 +0,0 @@
import { Component, ContextType } from "preact";
import { SettingsCtx } from "./SettingsContext.tsx";
import type { ConfigType } from "../config.ts";
import Checkbox from "preact-material-components/Checkbox.js";
export type SettingsCheckboxProps = {
checked: boolean;
name: keyof ConfigType;
description: string;
};
export default class SettingsCheckbox
extends Component<SettingsCheckboxProps, unknown> {
static contextType = SettingsCtx;
declare context: ContextType<typeof SettingsCtx>;
render() {
const id = `s-${this.props.name}`;
return (
<div>
<Checkbox
id={id}
checked={this.props.checked}
onInput={(ev: Event) => {
if (ev.target && this.context) {
const e = ev.target as HTMLInputElement;
this.context.set_settings((v) => {
if (v) {
const t: Record<string, unknown> = v;
t[this.props.name] = e.checked;
if (this.context) {
this.context.set_changed((v) => {
v.add(this.props.name);
return v;
});
}
return t as ConfigType;
}
});
}
}}
/>
<label for={id}>{this.props.description}</label>
</div>
);
}
}

View File

@@ -1,33 +0,0 @@
import { Component, ComponentChild, createContext } from "preact";
import { StateUpdater } from "preact/hooks";
import type { ConfigType } from "../config.ts";
export const SettingsCtx = createContext<State | null>(null);
type State = {
set_settings: StateUpdater<ConfigType | undefined>;
set_changed: StateUpdater<Set<string>>;
};
type Props = {
children: ComponentChild;
set_settings: StateUpdater<ConfigType | undefined>;
set_changed: StateUpdater<Set<string>>;
};
export default class SettingsContext extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
set_settings: props.set_settings,
set_changed: props.set_changed,
};
}
render() {
return (
<SettingsCtx.Provider value={this.state}>
{this.props.children}
</SettingsCtx.Provider>
);
}
}

View File

@@ -1,91 +0,0 @@
import { Component, ContextType } from "preact";
import { SettingsCtx } from "./SettingsContext.tsx";
import type { ConfigType } from "../config.ts";
import Md3Select from "./Md3Select.tsx";
import { StateUpdater } from "preact/hooks";
interface obj {
toString(): string;
}
type Props<T extends obj> = {
name: keyof ConfigType;
list: { value: T; text?: string; disabled?: boolean }[];
description: string;
/**@default {0}*/
selectedIndex?: number;
/**@default {false}*/
disabled?: boolean;
hintText?: string;
set_value?: StateUpdater<T>;
};
type State = {
selectedIndex: number;
};
export default class SettingsSelect<T extends obj>
extends Component<Props<T>, State> {
static contextType = SettingsCtx;
declare context: ContextType<typeof SettingsCtx>;
constructor(props: Props<T>) {
super(props);
if (!props.list.length) throw Error("No list.");
this.state = { selectedIndex: props.selectedIndex || 0 };
}
componentWillReceiveProps(
nextProps: Readonly<Props<T>>,
_nextContext: unknown,
): void {
const selectedIndex = nextProps.selectedIndex || 0;
this.setState({ selectedIndex });
}
render() {
const id = `s-${this.props.name}`;
return (
<div class="s-select" id={id}>
<label>{this.props.description}</label>
<Md3Select
supportingText={this.props.hintText}
disabled={this.props.disabled}
selectedIndex={this.state.selectedIndex}
set_index={(selectedIndex) => {
this.setState({ selectedIndex });
this.set_value(selectedIndex);
}}
>
{this.props.list.map((v) => {
const t = v.text ? v.text : v.value.toString();
return (
<Md3Select.Option disabled={v.disabled} value={t} />
);
})}
</Md3Select>
</div>
);
}
set_changed() {
if (this.context) {
this.context.set_changed((v) => {
v.add(this.props.name);
return v;
});
}
}
set_value(index: number) {
const value = this.props.list[index].value;
if (this.props.set_value) {
this.props.set_value(value);
this.set_changed();
} else if (this.context) {
this.context.set_settings((v) => {
if (v) {
const t: Record<string, unknown> = v;
t[this.props.name] = value;
this.set_changed();
return t as ConfigType;
}
});
}
}
}

View File

@@ -1,130 +0,0 @@
import { Component, ComponentChildren, ContextType } from "preact";
import { SettingsCtx } from "./SettingsContext.tsx";
import type { ConfigType } from "../config.ts";
import TextField from "preact-material-components/TextField.js";
import { Ref, StateUpdater, useRef } from "preact/hooks";
interface TextType {
text: string;
password: string;
number: number;
}
interface DataType {
text: never;
password: never;
number: number;
}
export type SettingsTextProps<T extends keyof TextType> = {
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<TextType[T]>;
min?: DataType[T];
max?: DataType[T];
outlined?: boolean;
};
export default class SettingsText<T extends keyof TextType>
extends Component<SettingsTextProps<T>, unknown> {
static contextType = SettingsCtx;
ref: Ref<TextField | undefined> | undefined;
declare context: ContextType<typeof SettingsCtx>;
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_changed() {
if (this.context) {
this.context.set_changed((v) => {
v.add(this.props.name);
return v;
});
}
}
set_value(value: TextType[T]) {
if (this.props.set_value) {
this.props.set_value(value);
this.set_changed();
} else if (this.context) {
this.context.set_settings((v) => {
if (v) {
const t: Record<string, unknown> = v;
t[this.props.name] = value;
this.set_changed();
return t as ConfigType;
}
});
}
}
componentDidMount() {
this.update(this.props.value);
}
componentWillUpdate(
nextProps: Readonly<SettingsTextProps<T>>,
_nextState: Readonly<unknown>,
_nextContext: unknown,
) {
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>();
const id = `s-${this.props.name}`;
let cn = "text";
if (this.props.helpertext) cn += " helper";
if (this.props.outlined) cn += " outlined";
if (this.props.label) cn += " label";
return (
<div class={cn} id={id}>
<label>{this.props.description}</label>
<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>
);
}
}

View File

@@ -1,183 +0,0 @@
import { Component, ContextType, createContext } from "preact";
import BMd3TextField from "./BMd3TextField.tsx";
import { MdTonalButton } from "../server/dmodule.ts";
type CtxProps = {
set_value: (index: number, key?: string, value?: string) => void;
append: () => void;
delete: (index: number) => void;
sign: string;
};
const Ctx = createContext<CtxProps | null>(null);
type SRProps = {
k?: string;
value?: string;
index: number;
disable?: boolean;
};
type SRState = {
k: string;
value: string;
};
class StringRecord extends Component<SRProps, SRState> {
static contextType = Ctx;
declare context: ContextType<typeof Ctx>;
constructor(props: SRProps) {
super(props);
this.state = { k: props.k || "", value: props.value || "" };
}
componentWillReceiveProps(
nextProps: Readonly<SRProps>,
nextContext: unknown,
): void {
this.setState({ k: nextProps.k || "", value: nextProps.value || "" });
}
render() {
if (!MdTonalButton.value) return null;
const Button = MdTonalButton.value;
return (
<div class="string-record">
<BMd3TextField
type="text"
value={this.state.k}
set_value={(k) => {
this.context?.set_value(
this.props.index,
k,
this.state.value,
);
this.setState({ k, value: this.state.value });
}}
/>
{this.context?.sign || "="}
<BMd3TextField
type="text"
value={this.state.value}
set_value={(v) => {
this.context?.set_value(
this.props.index,
this.state.k,
v,
);
this.setState({ k: this.state.k, value: v });
}}
/>
<Button
onClick={() => {
this.context?.append();
}}
>
+
</Button>
<Button
onClick={() => {
this.context?.delete(this.props.index);
}}
disabled={this.props.disable}
>
-
</Button>
</div>
);
}
}
type State = {
list: SRProps[];
append: () => void;
set_value: (index: number, key?: string, value?: string) => void;
delete: (index: number) => void;
};
type Props = {
value: Record<string, string>;
set_value?: (v: Record<string, string>) => void;
sign?: string;
};
export default class StringRecordsBox extends Component<Props, State> {
index = 0;
constructor(props: Props) {
super(props);
const keys = Object.getOwnPropertyNames(props.value);
this.state = {
list: keys.length
? keys.map((k) => {
return { k: k, value: props.value[k], index: this.index++ };
})
: [{ index: this.index++ }],
append: () => {
this.append();
},
set_value: (index, key, value) => {
this.set_value(index, key, value);
},
delete: (index) => {
this.delete(index);
},
};
}
append() {
this.state.list.push({ index: this.index++ });
this.forceUpdate();
}
delete(index: number) {
const i = this.state.list.findIndex((i) => i.index === index);
if (i === -1) return;
const d = this.state.list.splice(i, 1)[0];
if (d.k) {
delete this.props.value[d.k];
if (this.props.set_value) this.props.set_value(this.props.value);
}
this.forceUpdate();
}
set_value(index: number, key?: string, value?: string) {
const i = this.state.list.findIndex((i) => i.index === index);
let changed = false;
let pkey: string | undefined;
if (i !== -1) {
pkey = this.state.list[i].k;
this.state.list[i] = { index, k: key, value };
}
console.log(index, i, key, pkey, value);
if (pkey !== undefined && pkey !== "" && pkey !== key) {
delete this.props.value[pkey];
changed = true;
}
if (key !== undefined) {
if (value !== undefined) this.props.value[key] = value;
else delete this.props.value[key];
changed = true;
}
if (changed && this.props.set_value) {
this.props.set_value(this.props.value);
}
}
render() {
return (
<div class="string-records-box">
<Ctx.Provider
value={{
append: this.state.append,
set_value: this.state.set_value,
delete: this.state.delete,
sign: this.props.sign || "=",
}}
>
{this.state.list.map((v) => (
<StringRecord
k={v.k}
value={v.value}
index={v.index}
disable={this.state.list.length == 1}
/>
))}
</Ctx.Provider>
</div>
);
}
}

View File

@@ -1,21 +0,0 @@
import { asset } from "$fresh/runtime.ts";
import { Component, ContextType } from "preact";
import { GlobalCtx } from "./GlobalContext.tsx";
export type StyleSheetType = {
href: string;
};
export default class StyleSheet extends Component<StyleSheetType, unknown> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
const href = this.props.href;
if (this.context) {
const sheets = this.context.stylesheets;
if (sheets.has(href)) return null;
sheets.add(href);
}
return <link rel="stylesheet" href={asset(this.props.href)} />;
}
}

View File

@@ -1,138 +0,0 @@
import { Component } from "preact";
import Icon from "preact-material-components/Icon.js";
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";
import { TaskStatusFlag } from "./TaskFilterBar.tsx";
import { filesize } from "filesize";
type Props = {
task: TaskDetail;
flags: TaskStatusFlag;
};
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",
};
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;
}
export default class Task extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
task_changed: (e) => this.task_changed(e),
};
}
task_changed(d: Event) {
const e = d as unknown as CustomEvent<number>;
if (e.detail == this.props.task.base.id) {
e.stopImmediatePropagation();
this.forceUpdate();
}
}
render() {
const task = this.props.task;
if (!(this.props.flags & map_taskstatus(task.status))) {
return <div data-id={task.base.id}></div>;
}
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];
const b_progress_div = (
<Progress max={d.total_page} animated={true}>
<Progress.Bar
class="bg-success"
value={d.downloaded_page}
set_label={(per, v) =>
`${v} (${per}%)`}
/>
<Progress.Bar
class="bg-danger"
value={d.failed_page}
set_label={(per, v) =>
`${v} (${per}%)`}
/>
</Progress>
);
progress_div = (
<div>
{b_progress_div}
{d.details.map((v) => {
return (
<div>
<div>{v.name}</div>
<Progress
max={v.total || v.downloaded}
animated={true}
>
<Progress.Bar
class="bg-success"
value={v.downloaded}
set_label={(per, v) =>
`${
filesize(v, {
base: 2,
round: 3,
})
} (${per}%)`}
/>
</Progress>
</div>
);
})}
</div>
);
}
}
return (
<div data-id={task.base.id}>
<Icon class="task_handle">unfold_more</Icon>
<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>
);
}
componentDidMount(): void {
self.addEventListener("task_changed", this.state.task_changed);
}
componentWillUnmount(): void {
self.removeEventListener("task_changed", this.state.task_changed);
}
}

View File

@@ -1,97 +0,0 @@
// @ts-nocheck Chips
import { Component } from "preact";
import { Ref, useRef } from "preact/hooks";
import Chips from "preact-material-components/Chips.js";
import t from "../server/i18n.ts";
export enum TaskStatusFlag {
None = 0,
Running = 1 << 0,
Waiting = 1 << 1,
Failed = 1 << 2,
Finished = 1 << 3,
All = ~(~0 << 4),
}
type Props = {
value: TaskStatusFlag;
set_value: (v: TaskStatusFlag) => void;
};
export class TaskFilterBar extends Component<Props> {
ref: Ref<Chips | null> | undefined;
get is_all() {
return this.props.value === TaskStatusFlag.All;
}
get is_failed() {
return (this.props.value & TaskStatusFlag.Failed) !== 0;
}
get is_finished() {
return (this.props.value & TaskStatusFlag.Finished) !== 0;
}
get is_running() {
return (this.props.value & TaskStatusFlag.Running) !== 0;
}
get is_waiting() {
return (this.props.value & TaskStatusFlag.Waiting) !== 0;
}
get value() {
if (this.ref && this.ref.current) {
const com = this.ref.current.MDComponent;
if (com) {
return com.chips.slice(1).reduce(
(p, v, i) => v.selected ? p | 1 << i : p,
0,
);
}
}
return TaskStatusFlag.None;
}
render() {
this.ref = useRef<Chips>(null);
const onClick = () => {
const co = () => this.props.set_value(this.value);
setTimeout(co, 0);
};
return (
<Chips ref={this.ref} filter>
<Chips.Chip
selected={this.is_all}
onClick={() => {
const co = () => {
if (this.ref && this.ref.current) {
const com = this.ref.current.MDComponent;
if (com) {
com.chips.slice(1).forEach((v) => {
v.selected = com.chips[0].selected;
});
}
}
this.props.set_value(this.value);
};
setTimeout(co, 0);
}}
>
<Chips.Checkmark />
<Chips.Text>{t("task.all")}</Chips.Text>
</Chips.Chip>
<Chips.Chip selected={this.is_running} onClick={onClick}>
<Chips.Checkmark />
<Chips.Text>{t("task.running")}</Chips.Text>
</Chips.Chip>
<Chips.Chip selected={this.is_waiting} onClick={onClick}>
<Chips.Checkmark />
<Chips.Text>{t("task.waiting")}</Chips.Text>
</Chips.Chip>
<Chips.Chip selected={this.is_failed} onClick={onClick}>
<Chips.Checkmark />
<Chips.Text>{t("task.failed")}</Chips.Text>
</Chips.Chip>
<Chips.Chip selected={this.is_finished} onClick={onClick}>
<Chips.Checkmark />
<Chips.Text>{t("task.finished")}</Chips.Text>
</Chips.Chip>
</Chips>
);
}
}

View File

@@ -9,7 +9,6 @@
"run": "deno run --allow-read=./ --allow-write=./ --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID,DOCKER,DB_USE_FFI --allow-ffi --allow-net",
"compile": "deno compile --allow-read=./ --allow-write=./ --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID --allow-net",
"compile_full": "deno compile --allow-read --allow-write --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID --allow-net",
"fetch": "deno run --allow-read=./ --allow-write=./ --allow-net fetch_static_files.ts",
"gen_meili_server_key": "deno run --allow-net scripts/gen_meili_server_key.ts",
"server-build": "deno run -A server-dev.ts build",
"prebuild": "deno run -A scripts/prebuild.ts",

View File

@@ -1,50 +0,0 @@
import { dirname, join } from "std/path/mod.ts";
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) {
if (i.startsWith("preact-material-components/")) {
return i.replace(
"preact-material-components/",
"https://esm.sh/[email protected]/",
);
}
for (const v of Object.getOwnPropertyNames(map)) {
if (v.endsWith("/") && i.startsWith(v)) {
return i.replace(v, map[v]);
}
}
for (const v of Object.getOwnPropertyNames(map)) {
if (i.startsWith(v)) {
return i.replace(v, map[v]);
}
}
return i;
}
async function fetch_file(u: string | URL, p: string) {
await sure_dir(dirname(p));
const f = await Deno.open(p, { create: true, write: true, truncate: true });
try {
const r = await fetch(u);
if (!r.body) throw Error("No body.");
await r.body.pipeTo(f.writable);
} finally {
try {
f.close();
} catch (_) {
null;
}
}
}
for (const i of LIST) {
const u = get_url(i);
const p = join("./static", i);
await fetch_file(u, p);
}

View File

@@ -30,14 +30,9 @@ import * as $api_user from "./routes/api/user.ts";
import * as $file_id_ from "./routes/file/[id].ts";
import * as $file_verify_id_ from "./routes/file/[verify]/[id].ts";
import * as $file_middleware from "./routes/file/_middleware.ts";
import * as $index from "./routes/index.tsx";
import * as $manifest_json from "./routes/manifest.json.ts";
import * as $thumbnail_id_ from "./routes/thumbnail/[id].ts";
import * as $thumbnail_middleware from "./routes/thumbnail/_middleware.ts";
import * as $upload from "./routes/upload.tsx";
import * as $Container from "./islands/Container.tsx";
import * as $Settings from "./islands/Settings.tsx";
import * as $TaskManager from "./islands/TaskManager.tsx";
import * as $Upload from "./islands/Upload.tsx";
import { type Manifest } from "$fresh/server.ts";
@@ -72,16 +67,11 @@ const manifest = {
"./routes/file/[id].ts": $file_id_,
"./routes/file/[verify]/[id].ts": $file_verify_id_,
"./routes/file/_middleware.ts": $file_middleware,
"./routes/index.tsx": $index,
"./routes/manifest.json.ts": $manifest_json,
"./routes/thumbnail/[id].ts": $thumbnail_id_,
"./routes/thumbnail/_middleware.ts": $thumbnail_middleware,
"./routes/upload.tsx": $upload,
},
islands: {
"./islands/Container.tsx": $Container,
"./islands/Settings.tsx": $Settings,
"./islands/TaskManager.tsx": $TaskManager,
"./islands/Upload.tsx": $Upload,
},
baseUrl: import.meta.url,

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>
);
}
}

View File

@@ -1,61 +0,0 @@
import { Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import GlobalContext from "../components/GlobalContext.tsx";
import Container from "../islands/Container.tsx";
import { get_i18nmap, i18n_handle_request } from "../server/i18ns.ts";
import { get_task_manager } from "../server.ts";
import { UserAgent } from "std/http/user_agent.ts";
import { exists } from "std/fs/exists.ts";
import { get_host } from "../server/utils.ts";
type Props = {
lang: string;
userAgent: string | null;
};
export const handler: Handlers<Props> = {
async GET(req, ctx) {
const re = i18n_handle_request(req);
const m = get_task_manager();
if (typeof re === "string") {
return ctx.render({
lang: re,
userAgent: req.headers.get("User-Agent"),
});
} else if (m.cfg.redirect_to_flutter) {
let flutter_base = import.meta.resolve("../static/flutter").slice(
7,
);
if (Deno.build.os === "windows") {
flutter_base = flutter_base.slice(1);
}
if (m.cfg.flutter_frontend) {
flutter_base = m.cfg.flutter_frontend;
}
if (!await exists(flutter_base)) {
return re;
}
return Response.redirect(`${get_host(req)}/flutter/`);
}
return re;
},
};
export default function Index({ data }: PageProps<Props>) {
const i18n = get_i18nmap(data.lang);
const ua = new UserAgent(data.userAgent || "");
const is_windows_chrome = ua.browser.name === "Chrome" &&
ua.os.name === "Windows";
return (
<body>
<Head>
{is_windows_chrome
? <link rel="stylesheet" href="scrollBar.css" />
: null}
</Head>
<GlobalContext>
<Container i18n={i18n} lang={data.lang} />
</GlobalContext>
</body>
);
}

View File

@@ -1,47 +0,0 @@
import { Handlers } from "$fresh/server.ts";
import { get_i18nmap, i18n_handle_request } from "../server/i18ns.ts";
import t, { i18n_map } from "../server/i18n.ts";
export const handler: Handlers = {
GET(req, _ctx) {
const i18n = i18n_handle_request(req);
if (typeof i18n === "string") {
const m = get_i18nmap(i18n);
i18n_map.value = m;
const base = `/?lang=${i18n}`;
const data = {
name: t("common.title"),
lang: i18n,
start_url: base,
display: "standalone",
icons: [
{
src: "/favicon.ico",
sizes: "64x64",
"type": "image/vnd.microsoft.icon",
},
{
src: "/logo.svg",
sizes: "any",
"type": "image/svg+xml",
},
],
scope: base,
shortcuts: [
{
name: t("task.add"),
url: `${base}#/task_manager/new`,
},
{
name: t("common.settings"),
url: `${base}#/settings`,
},
],
};
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/manifest+json" },
});
}
return i18n;
},
};

2
static/.gitignore vendored
View File

@@ -1,7 +1,5 @@
bootstrap/
flutter
flutter/
preact-material-components/
sw.js
sw.js.map
sw.meta.json

View File

@@ -1,435 +0,0 @@
a {
color: inherit;
text-decoration: inherit;
}
body {
min-width: 100vw;
min-height: 100vh;
}
:root {
--mdc-theme-primary: #007bff;
--mdc-theme-secondary: #007bff;
--dark-backgroud: #343a40;
color-scheme: only light;
}
body {
background-color: white;
}
.nav-menu {
z-index: 100;
position: fixed;
background-color: white;
box-sizing: border-box;
display: block;
left: -64px;
transition: all 0.5s ease;
height: 100vh;
max-width: 64px;
}
.nav-menu.open {
left: 0px;
}
.main {
position: relative;
top: 64px;
}
.b-text-field .datalist {
width: 100%;
display: none;
max-height: 386px;
height: auto;
overflow: auto;
border: solid 1px rgba(0, 0, 0, 0.24);
border-radius: 3px;
position: absolute;
top: 182px;
background: white;
z-index: 10;
}
.b-text-field .datalist.open {
display: grid;
}
.b-text-field .datalist .mdc-list-item {
display: grid;
}
.b-text-field .datalist .mdc-list-item:hover {
color: white;
background-color: var(--mdc-theme-primary);
}
.b-text-field .datalist .mdc-list-item .value {
width: 100%;
}
.b-text-field .datalist .mdc-list-item .label {
width: 100%;
font-size: smaller;
}
.settings {
margin: 0 18%;
padding-top: 40px;
transition: 0.6s;
overflow-x: hidden;
}
.settings div.text {
width: 100%;
}
.settings label, div.new_task div.bcheckbox label {
line-height: 40px;
}
.settings .mdc-text-field label {
line-height: revert;
top: 22px;
bottom: auto;
}
.settings .text-box div.text:not(.helper, #s-port) .mdc-text-field {
min-width: 50%;
}
.settings .text-box div.text.helper>div {
min-width: 50%;
}
.settings .text-box div.text.helper .mdc-text-field {
min-width: 100%;
}
.settings #s-port .mdc-text-field {
min-width: 100px;
}
.settings div.text.helper {
display: flex;
}
.settings div.text.helper label {
margin-top: 14px;
}
.settings div.text.helper.outlined label {
margin-top: 7px;
}
.settings div.text.outlined .mdc-text-field label {
top: 14px;
}
.settings div.text.outlined .mdc-text-field.mdc-text-field--focused label {
top: 22px;
}
.settings div.text.outlined.label {
margin-top: 6px;
}
.settings .text-box {
margin: 0 10px;
}
.settings .text-box .text {
display: flex;
justify-content: space-between;
align-items: center;
}
.settings .text-box .text .mdc-text-field::after {
width: 0;
}
.settings .text-box .text .mdc-text-field::before {
width: 0;
}
.settings .ua {
position: relative;
}
.settings .ua>button {
position: absolute;
top: 30px;
left: -10px;
}
.task_manager {
padding: 16px;
display: flex;
flex-direction: column;
}
.task_amounts {
display: flex;
height: 64px;
}
.btn-success {
background-color: #28a745
}
.btn-warning {
background-color: #ffc107
}
.btn-danger {
background-color: #dc3545
}
.btn-primary {
background-color: #007bff
}
.dark-scheme {
background: var(--dark-backgroud);
color: white;
color-scheme: dark;
}
.dark-scheme .mdc-top-app-bar {
background-color: black;
}
.dark-scheme .mdc-text-field__input {
color: white !important;
}
.dark-scheme .mdc-select__native-control {
color: white !important;
}
.dark-scheme .mdc-text-field__input {
color: white !important;
}
.dark-scheme .mdc-floating-label {
color: white !important;
}
.dark-scheme .mdc-select-item {
background-color: black !important;
}
.dark-scheme .mdc-list {
background-color: #272727;
}
.dark-scheme .mdc-list-item {
color: white;
}
.dark-scheme .mdc-notched-outline__idle {
color: white !important;
}
.dark-scheme .mdc-dialog__surface {
background-color: var(--dark-backgroud);
}
.dark-scheme .mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__idle {
border-color: white;
}
.dark-scheme .mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused) .mdc-text-field__input:hover~.mdc-notched-outline__idle, .mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused) .mdc-text-field__icon:hover~.mdc-notched-outline__idle {
border-color: var(--mdc-theme-primary);
}
.dark-scheme .mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate)~.mdc-checkbox__background {
border-color: white;
}
.mdc-top-app-bar__section--align-end>* {
margin: 0 5px;
}
.task_manager .new_task {
position: fixed;
right: 50px;
bottom: 50px;
}
div.new_task {
overflow: auto;
width: 100vw;
height: calc(100vh - 64px);
max-width: 100vw;
max-height: calc(100vh - 64px);
}
div.new_task>div.container {
position: relative;
margin: auto;
height: calc(100% - 5% - 64px);
min-width: 400px;
width: calc(100% - 20%);
margin-top: 2.5%;
}
div.new_task .top {
display: flex;
min-height: 40px;
line-height: 40px;
justify-content: space-between;
align-items: center;
}
div.new_task .top .title {
margin-left: 40px;
width: calc(100% - 80px);
font-size: 24px;
text-align: center;
}
div.new_task>div.container>div.content {
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 80px);
min-height: 300px;
}
div.new_task>div.container>div.bottom {
line-height: 40px;
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);
position: relative;
}
.dark-scheme {
--md-sys-color-primary: #D0BCFF;
--md-sys-color-primary-container: #4F378B;
--md-sys-color-on-primary: #371E73;
--md-sys-color-on-primary-container: #EADDFF;
--md-sys-color-inverse-primary: #6750A4;
--md-sys-color-secondary: #CCC2DC;
--md-sys-color-secondary-container: #4A4458;
--md-sys-color-on-secondary: #332D41;
--md-sys-color-on-secondary-container: #E8DEF8;
--md-sys-color-tertiary: #EFB8C8;
--md-sys-color-tertiary-container: #633B48;
--md-sys-color-on-tertiary: #492532;
--md-sys-color-on-tertiary-container: #FFD8E4;
--md-sys-color-surface: #141218;
--md-sys-color-surface-dim: #141218;
--md-sys-color-surface-bright: #3B383E;
--md-sys-color-surface-container-lowest: #0F0D13;
--md-sys-color-surface-container-low: #1D1B20;
--md-sys-color-surface-container: #211F26;
--md-sys-color-surface-container-high: #2B2930;
--md-sys-color-surface-container-highest: #36343B;
--md-sys-color-surface-variant: #49454F;
--md-sys-color-on-surface: #E6E1E5;
--md-sys-color-on-surface-variant: #CAC4D0;
--md-sys-color-inverse-surface: #E6E1E5;
--md-sys-color-inverse-on-surface: #313033;
--md-sys-color-background: #141218;
--md-sys-color-on-background: #E6E1E5;
--md-sys-color-error: #F2B8B5;
--md-sys-color-error-container: #8C1D18;
--md-sys-color-on-error: #601410;
--md-sys-color-on-error-container: #F9DEDC;
--md-sys-color-outline: #938F99;
--md-sys-color-outline-variant: #444746;
--md-sys-color-shadow: #000000;
--md-sys-color-surface-tint-color: #D0BCFF;
--md-sys-color-scrim: #000000;
}
.b-text-field.md3.label md-outlined-text-field {
margin-top: 6px;
}
div.string-record {
display: inline-flex;
line-height: 56px;
}
div.string-record > div.b-text-field.md3.text {
margin: 0 5px;
}
div.string-records-box {
display: inline-grid;
display: -ms-inline-grid;
display: -moz-inline-grid;
}
@media (max-width:1280px) {
.settings {
margin: 0;
padding-top: 10px;
}
}
@media (max-width:810px) {
.settings {
padding-top: 0;
}
.settings>div:not(.check-box) label {
line-height: normal;
}
.settings .ua>button {
position: relative;
top: 0;
}
.settings .text-box .text {
flex-direction: column;
align-items: start;
}
.settings .text-box .mdc-text-field {
margin: 1px;
width: 100%;
}
.settings .text-box .label .mdc-text-field {
margin-top: 5px;
}
.settings .text-box div.text.helper>div {
min-width: 100%;
}
button.mdc-button {
margin: 5px;
}
}
@media (max-width: 599px) {
.main {
top: 56px;
}
div.new_task>div.container {
margin: 5px;
min-width: 0;
height: calc(100% - 74px);
width: calc(100% - 10px);
}
}

View File

@@ -1,19 +0,0 @@
::-webkit-scrollbar {
width: 6px;
height: 1px;
}
::-webkit-scrollbar-thumb {
border-radius: 100%;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
border-radius: 10px;
background: #EDEDED;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.45);
border-radius: 6px;
}