Setting Page responsive layout

This commit is contained in:
FeiBam
2023-06-23 14:14:10 +09:00
17 changed files with 350 additions and 65 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ config.json
test/
downloads/
utt.lock
thumbnails/

View File

@@ -20,6 +20,7 @@ export type ConfigType = {
meili_update_api_key?: string;
ffmpeg_path: string;
thumbnail_method: ThumbnailMethod;
thumbnail_dir: string;
};
export enum ThumbnailMethod {
@@ -124,6 +125,9 @@ export class Config {
if (n < 0 || n > 1) return ThumbnailMethod.FFMPEG_BINARY;
return n as ThumbnailMethod;
}
get thumbnail_dir() {
return this._return_string("thumbnail_dir") || "./thumbnails";
}
to_json(): ConfigType {
return {
cookies: typeof this.cookies === "string",
@@ -144,6 +148,7 @@ export class Config {
meili_update_api_key: this.meili_update_api_key,
ffmpeg_path: this.ffmpeg_path,
thumbnail_method: this.thumbnail_method,
thumbnail_dir: this.thumbnail_dir,
};
}
}

8
db.ts
View File

@@ -1,5 +1,9 @@
import { DB } from "sqlite/mod.ts";
import { compare as compare_ver, parse as parse_ver } from "std/semver/mod.ts";
import {
compare as compare_ver,
format as format_ver,
parse as parse_ver,
} from "std/semver/mod.ts";
import { unescape } from "std/html/mod.ts";
import { join, resolve } from "std/path/mod.ts";
import { SqliteError } from "sqlite/mod.ts";
@@ -401,7 +405,7 @@ export class EhDb {
this.db.transaction(() => {
this.db.query("INSERT OR REPLACE INTO version VALUES (?, ?);", [
"eh",
this.version.toString(),
format_ver(this.version),
]);
});
}

View File

@@ -12,8 +12,10 @@ import * as $6 from "./routes/api/file/random.ts";
import * as $7 from "./routes/api/filemeta.ts";
import * as $8 from "./routes/api/filemeta/[token].ts";
import * as $9 from "./routes/api/gallery/[gid].ts";
import * as $10 from "./routes/api/task.ts";
import * as $11 from "./routes/index.tsx";
import * as $10 from "./routes/api/status.ts";
import * as $11 from "./routes/api/task.ts";
import * as $12 from "./routes/api/thumbnail/[id].ts";
import * as $13 from "./routes/index.tsx";
import * as $$0 from "./islands/Container.tsx";
import * as $$1 from "./islands/Settings.tsx";
import * as $$2 from "./islands/TaskManager.tsx";
@@ -30,8 +32,10 @@ const manifest = {
"./routes/api/filemeta.ts": $7,
"./routes/api/filemeta/[token].ts": $8,
"./routes/api/gallery/[gid].ts": $9,
"./routes/api/task.ts": $10,
"./routes/index.tsx": $11,
"./routes/api/status.ts": $10,
"./routes/api/task.ts": $11,
"./routes/api/thumbnail/[id].ts": $12,
"./routes/index.tsx": $13,
},
islands: {
"./islands/Container.tsx": $$0,

View File

@@ -98,7 +98,7 @@ export default class Settings extends Component<SettingsProps> {
set_changed={set_changed}
set_settings={set_settings}
>
<div>
<div class="check-box">
<SettingsCheckbox
name="download_original_img"
checked={settings.download_original_img}
@@ -120,7 +120,29 @@ export default class Settings extends Component<SettingsProps> {
description={t("settings.export_zip_jpn_title")}
/>
</div>
<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}
outlined={true}
/>
<SettingsText
name="port"
value={settings.port}
description={t("settings.port")}
type="number"
min={0}
max={65535}
outlined={true}
/>
<SettingsText
name="base"
value={settings.base}
@@ -128,40 +150,29 @@ export default class Settings extends Component<SettingsProps> {
type="text"
outlined={true}
/>
<SettingsText
name="ua"
value={settings.ua ? settings.ua : ""}
description={t("settings.ua")}
type="text"
outlined={true}
ref={ref}
>
<Button
onClick={() => {
if (ref.current) {
const ua = navigator.userAgent;
const t = ref.current;
t.update(ua);
t.set_value(ua);
}
}}
<div class="ua">
<SettingsText
name="ua"
value={settings.ua ? settings.ua : ""}
description={t("settings.ua")}
type="text"
outlined={true}
ref={ref}
>
{t("settings.ua_now")}
</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>
</SettingsText>
<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}
/>
</div>
<SettingsText
name="max_task_count"
value={settings.max_task_count}
@@ -194,15 +205,6 @@ export default class Settings extends Component<SettingsProps> {
helpertext={t("settings.db_path_help")}
outlined={true}
/>
<SettingsText
name="port"
value={settings.port}
description={t("settings.port")}
type="number"
min={0}
max={65535}
outlined={true}
/>
<SettingsText
name="hostname"
value={settings.hostname}
@@ -238,17 +240,17 @@ export default class Settings extends Component<SettingsProps> {
type="text"
outlined={true}
/>
<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="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}
/>
</div>

View File

@@ -55,7 +55,9 @@ export class MeiliSearchServer {
}
#gallery_update(e: Event) {
const ev = e as CustomEvent<number>;
this.updateGallery(ev.detail);
this.updateGallery(ev.detail).catch((e) => {
console.log(e);
});
}
async #updateGMetaSettings() {
if (this.#gmeta) {

View File

@@ -1,6 +1,7 @@
import { DOMParser, Element } from "deno_dom/deno-dom-wasm-noinit.ts";
import { Client } from "../client.ts";
import { initDOMParser, parse_bool } from "../utils.ts";
import { initDOMParser, map, parse_bool } from "../utils.ts";
import { parseUrl, UrlType } from "../url.ts";
class GalleryPage {
dom;
@@ -11,6 +12,7 @@ class GalleryPage {
#meta_script: string | undefined = undefined;
#gid: number | undefined = undefined;
#token: string | undefined = undefined;
#new_version: Array<{ gid: number; token: string }> | undefined = undefined;
constructor(html: string, client: Client) {
const dom = (new DOMParser()).parseFromString(html, "text/html");
if (!dom) {
@@ -86,6 +88,24 @@ class GalleryPage {
if (!ele) throw Error("Failed to find gallery's name.");
return ele.innerText;
}
get new_version() {
if (this.#new_version === undefined) {
const eles = this.doc.querySelectorAll("#gnd > a");
const d = <{ gid: number; token: string }[]> map(eles, (e) => {
const b = e as Element;
const u = b.getAttribute("href");
if (!u) return null;
const d = parseUrl(u);
if (d?.type === UrlType.Gallery) {
return { gid: d.gid, token: d.token };
} else {
return null;
}
}).filter((d) => d !== null);
this.#new_version = d;
return d;
} else return this.#new_version;
}
get japanese_name() {
return this.doc.getElementById("gj")?.innerText;
}

View File

@@ -36,4 +36,18 @@ Deno.test({
assertEquals(re.language, "Chinese");
assertEquals(re.gid, 2552611);
assertEquals(re.token, "3132307627");
assertEquals(re.new_version.length, 0);
});
Deno.test({
name: "GalleryPage_test2",
permissions: API_PERMISSION,
}, async () => {
const cfg = await load_settings("./config.json");
const client = new Client(cfg);
const re = await client.fetchGalleryPage(2209409, "8c8b2b1fc3");
assertEquals(re.name, "[Fanbox] houk1se1 (2022.03.08 - 2022.05.01)");
assertEquals(re.japanese_name, "");
assertEquals(re.length, 42);
assertEquals(re.new_version[0], { gid: 2223198, token: "2a5788135e" });
});

View File

@@ -8,8 +8,10 @@ export const handler: Handlers = {
const u = new URL(req.url);
const is_nsfw = await parse_bool(u.searchParams.get("is_nsfw"), null);
const is_ad = await parse_bool(u.searchParams.get("is_ad"), null);
const thumb = await parse_bool(u.searchParams.get("thumb"), false);
const f = m.db.get_random_file(is_nsfw, is_ad);
if (!f) return new Response("File not found.", { status: 404 });
return Response.redirect(`${u.origin}/api/file/${f.id}`);
const t = thumb ? "thumbnail" : "file";
return Response.redirect(`${u.origin}/api/${t}/${f.id}`);
},
};

28
routes/api/status.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Handlers } from "$fresh/server.ts";
import { get_task_manager } from "../../server.ts";
import { StatusData } from "../../server/status.ts";
import { return_data } from "../../server/utils.ts";
import { check_ffmpeg_binary } from "../../thumbnail/ffmpeg_binary.ts";
export const handler: Handlers = {
async GET(_req, _ctx) {
const m = get_task_manager();
const ffmpeg_binary_enabled = await check_ffmpeg_binary(
m.cfg.ffmpeg_path,
);
const meilisearch_enabled = m.meilisearch !== undefined;
const meilisearch = meilisearch_enabled && m.cfg.meili_host &&
m.cfg.meili_update_api_key
? {
host: m.cfg.meili_host,
key: m.cfg.meili_search_api_key ||
m.cfg.meili_update_api_key,
}
: undefined;
return return_data<StatusData>({
ffmpeg_binary_enabled,
meilisearch_enabled,
meilisearch,
});
},
};

View File

@@ -0,0 +1,80 @@
import { Handlers } from "$fresh/server.ts";
import { exists } from "std/fs/exists.ts";
import { get_task_manager } from "../../../server.ts";
import { parse_bool, parse_int } from "../../../server/parse_form.ts";
import { generate_filename, ThumbnailConfig } from "../../../thumbnail/base.ts";
import { sure_dir } from "../../../utils.ts";
import { ThumbnailMethod } from "../../../config.ts";
import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts";
import {
get_file_response,
GetFileResponseOptions,
} from "../../../server/get_file_response.ts";
export const handler: Handlers = {
async GET(req, ctx) {
const id = parseInt(ctx.params.id);
if (isNaN(id)) {
return new Response("Bad Request", { status: 400 });
}
const m = get_task_manager();
const b = m.cfg.thumbnail_dir;
const method = m.cfg.thumbnail_method;
await sure_dir(b);
const f = m.db.get_file(id);
if (!f) {
return new Response("File not found.", { status: 404 });
}
const u = new URL(req.url);
const max = await parse_int(u.searchParams.get("max"), 1200);
const width = await parse_int(u.searchParams.get("width"), null);
const height = await parse_int(u.searchParams.get("height"), null);
const quality = await parse_int(u.searchParams.get("quality"), 1);
const force = await parse_bool(u.searchParams.get("force"), false);
const cfg: ThumbnailConfig = { width: 0, height: 0, quality };
if (width !== null && height !== null) {
cfg.width = width;
cfg.height = height;
} else if (width !== null) {
cfg.width = width;
cfg.height = Math.round(f.height / f.width * width);
} else if (height !== null) {
cfg.height = height;
cfg.width = Math.round(f.width / f.height * height);
} else {
if (f.width > f.height) {
cfg.width = max;
cfg.height = Math.round(f.height / f.width * max);
} else {
cfg.height = max;
cfg.width = Math.round(f.width / f.height * max);
}
}
if (!force) {
if (cfg.width > f.width || cfg.height > f.height) {
return Response.redirect(`${u.origin}/api/file/${f.id}`);
}
}
const output = generate_filename(b, f, cfg);
if (!(await exists(output))) {
if (method === ThumbnailMethod.FFMPEG_BINARY) {
const re = await fb_generate_thumbnail(
m.cfg.ffmpeg_path,
f.path,
output,
cfg,
);
if (!re) {
return new Response("Failed to generate thumbnail.", {
status: 500,
});
}
}
}
const opts: GetFileResponseOptions = {};
opts.range = req.headers.get("range");
opts.if_modified_since = req.headers.get("If-Modified-Since");
opts.if_unmodified_since = req.headers.get("If-Unmodified-Since");
return await get_file_response(output, opts);
},
};

8
server/status.ts Normal file
View File

@@ -0,0 +1,8 @@
export type StatusData = {
ffmpeg_binary_enabled: boolean;
meilisearch_enabled: boolean;
meilisearch?: {
host: string;
key: string;
};
};

View File

@@ -19,6 +19,14 @@
top: 64px;
}
.settings {
margin: 0 18%;
padding-top: 40px;
transition: 0.6s;
}
.settings div.text {
width: 100%;
}
@@ -60,3 +68,43 @@
.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;
}
@media (max-width:767px) {
}
@media (max-width:1280px) {
.settings {
margin: 0;
}
}

22
thumbnail/base.ts Normal file
View File

@@ -0,0 +1,22 @@
import { join } from "std/path/mod.ts";
import { filterFilename } from "../utils.ts";
import { EhFile } from "../db.ts";
export type ThumbnailConfig = {
width: number;
height: number;
quality: number;
};
export function generate_filename(
base: string,
f: EhFile,
cfg: ThumbnailConfig,
) {
return join(
base,
filterFilename(
`${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}.jpg`,
),
);
}

View File

@@ -0,0 +1,43 @@
import { ThumbnailConfig } from "./base.ts";
export async function check_ffmpeg_binary(p: string) {
const cmd = new Deno.Command(p, {
stdout: "null",
stderr: "null",
args: ["-h"],
});
const c = cmd.spawn();
const o = await c.output();
return o.code === 0;
}
export async function fb_generate_thumbnail(
p: string,
i: string,
o: string,
cfg: ThumbnailConfig,
) {
const args = [
"-i",
i,
"-vf",
`scale=${cfg.width}:${cfg.height}`,
"-qmin",
`${cfg.quality}`,
"-qmax",
`${cfg.quality}`,
o,
];
const cmd = new Deno.Command(p, { args, stdout: "null", stderr: "piped" });
const c = cmd.spawn();
const s = await c.output();
if (s.code !== 0) {
try {
const d = (new TextDecoder()).decode(s.stderr);
console.log(d);
} catch (_) {
console.log(s.stderr);
}
}
return s.code === 0;
}

View File

@@ -27,5 +27,6 @@
"ffmpeg_path": "The path to the ffmpeg binary: ",
"thumbnail_method": "The method used to generate thumbnail: ",
"thumbnail_method0": "ffmpeg binary",
"thumbnail_method1": "ffmpeg API"
"thumbnail_method1": "ffmpeg API",
"thumbnail_dir": "The folder used to store thumbnails: "
}

View File

@@ -27,5 +27,6 @@
"ffmpeg_path": "FFMPEG二进制的位置:",
"thumbnail_method": "生成缩略图的方式:",
"thumbnail_method0": "FFMPEG二进制",
"thumbnail_method1": "FFMPEG API"
"thumbnail_method1": "FFMPEG API",
"thumbnail_dir": "存放缩略图的文件夹:"
}