mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Remove fresh dashboard
This commit is contained in:
@@ -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 && \
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)} />;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
10
fresh.gen.ts
10
fresh.gen.ts
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
2
static/.gitignore
vendored
@@ -1,7 +1,5 @@
|
||||
bootstrap/
|
||||
flutter
|
||||
flutter/
|
||||
preact-material-components/
|
||||
sw.js
|
||||
sw.js.map
|
||||
sw.meta.json
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user