From 42cb28e38e02422b8134214e7024e42b8a73efb3 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 29 Jun 2023 19:04:09 +0800 Subject: [PATCH] Add export zip task --- components/BCheckbox.tsx | 3 +- components/BTextField.tsx | 4 +- components/GidDataList.tsx | 44 ++++++++++++++++++ components/NewTask.tsx | 90 +++++++++++++++++++++++++++++++++--- db.ts | 14 ++++++ fresh.gen.ts | 18 ++++---- routes/api/gallery/list.ts | 46 ++++++++++++++++++ server/cfg.ts | 12 +++++ server/gallery.ts | 2 + static/common.css | 2 +- translation/en/task.jsonc | 6 ++- translation/zh-cn/task.jsonc | 6 ++- 12 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 components/GidDataList.tsx create mode 100644 routes/api/gallery/list.ts diff --git a/components/BCheckbox.tsx b/components/BCheckbox.tsx index 6eb3c28..44e083d 100644 --- a/components/BCheckbox.tsx +++ b/components/BCheckbox.tsx @@ -1,6 +1,5 @@ import { Component, ContextType } from "preact"; import Checkbox from "preact-material-components/Checkbox"; -import { StateUpdater } from "preact/hooks"; import { BCtx } from "./BContext.tsx"; type Props = { @@ -8,7 +7,7 @@ type Props = { checked: boolean; name?: string; description?: string; - set_value?: StateUpdater; + set_value?: (v: boolean) => void; }; export default class BCheckbox extends Component { diff --git a/components/BTextField.tsx b/components/BTextField.tsx index 8027b01..8f271b4 100644 --- a/components/BTextField.tsx +++ b/components/BTextField.tsx @@ -1,6 +1,6 @@ import { Component, ComponentChildren, ContextType } from "preact"; import TextField from "preact-material-components/TextField"; -import { Ref, StateUpdater, useRef } from "preact/hooks"; +import { Ref, useRef } from "preact/hooks"; import { BCtx } from "./BContext.tsx"; interface TextType { @@ -33,6 +33,7 @@ type Props = { max?: DataType[T]; outlined?: boolean; id?: string; + list?: string; }; export default class BTextField @@ -134,6 +135,7 @@ export default class BTextField min={this.props.min} max={this.props.max} outlined={this.props.outlined} + list={this.props.list} /> {this.props.children} diff --git a/components/GidDataList.tsx b/components/GidDataList.tsx new file mode 100644 index 0000000..f1872cf --- /dev/null +++ b/components/GidDataList.tsx @@ -0,0 +1,44 @@ +import { Component } from "preact"; +import { GMeta } from "../db.ts"; +import { useEffect } from "preact/hooks"; +import { GalleryListResult } from "../server/gallery.ts"; + +type Props = { + /**@default {false}*/ + jpn_title?: boolean; + id: string; +}; + +type State = { + data?: GMeta[]; +}; + +export default class GidDataList extends Component { + render() { + const fetchData = 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({ data: d.data }); + } + }; + useEffect(() => { + fetchData().catch((e) => console.error(e)); + }, []); + return ( + + {this.state.data + ? this.state.data.map((d) => { + let title = d.title; + if (this.props.jpn_title && d.title_jpn) { + title = d.title_jpn; + } + return ; + }) + : null} + + ); + } +} diff --git a/components/NewTask.tsx b/components/NewTask.tsx index 361abdc..4b814d6 100644 --- a/components/NewTask.tsx +++ b/components/NewTask.tsx @@ -9,7 +9,11 @@ import { StateUpdater, useRef, useState } from "preact/hooks"; import { TaskType } from "../task.ts"; import BTextField from "./BTextField.tsx"; import { parseUrl, UrlType } from "../url.ts"; -import { generate_download_cfg } from "../server/cfg.ts"; +import { + 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"; @@ -17,6 +21,8 @@ import { sendTaskMessage } from "../islands/TaskManager.tsx"; import Snackbar from "preact-material-components/Snackbar"; import { GalleryResult } from "../server/gallery.ts"; import { tw } from "twind"; +import { ExportZipConfig } from "../tasks/export_zip.ts"; +import GidDataList from "./GidDataList.tsx"; export type NewTaskProps = { show: boolean; @@ -46,6 +52,8 @@ export default class NewTask extends Component { const [ezgid, set_ezgid1] = useState(); const [ginfo, set_ginfo] = useState(); const [abort, set_abort] = useState(); + const [ezcfg, set_ezcfg1] = useState(generate_export_zip_cfg()); + const [overwrite_ezcfg, set_overwrite_ezcfg] = useState(false); if (task_type === TaskType.Download) { const set_url: StateUpdater = (u) => { const n = typeof u === "string" ? u : u(url || ""); @@ -70,10 +78,9 @@ export default class NewTask extends Component { set_url1(`https://e-hentai.org/g/${dgid}/${n}/`); } }; - const set_overwrite_cfg: StateUpdater = (u) => { - const n = typeof u === "boolean" ? u : u(overwrite_cfg); + const set_overwrite_cfg = (n: boolean) => { set_overwrite_cfg1(n); - if (n === true) { + if (n) { set_dcfg(generate_download_cfg()); } }; @@ -176,6 +183,14 @@ export default class NewTask extends Component { }; } } 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) => { @@ -201,15 +216,22 @@ export default class NewTask extends Component { }; 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 = (
{t("task.gallery_title")} - {ginfo.data.meta.title} + {title}
{t("task.gallery_page")} - {ginfo.data.pages.length} + {count}
); @@ -220,17 +242,71 @@ export default class NewTask extends Component { ); } + const set_ezcfg: StateUpdater = (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 = ( +
+ + + + + + +
+ ); + } config_div = (
+ {ginfo_div} + + {cfg_div}
); } diff --git a/db.ts b/db.ts index 6b50e0b..fc792bc 100644 --- a/db.ts +++ b/db.ts @@ -659,6 +659,7 @@ export class EhDb { } convert_gmeta(m: GMetaRaw[]): GMeta[] { return m.map((m) => { + if (m.expunged === undefined) return m; const b = m.expunged !== 0; const t = m; t.expunged = b; @@ -752,6 +753,19 @@ export class EhDb { [limit, offset], ).map((n) => n[0]); } + get_gmetas(offset = 0, limit = 20, fields = "*") { + return this.convert_gmeta( + this.db.queryEntries( + `SELECT ${fields} FROM gmeta LIMIT ? OFFSET ?;`, + [limit, offset], + ), + ); + } + get_gmetas_all(fields = "*") { + return this.convert_gmeta( + this.db.queryEntries(`SELECT ${fields} FROM gmeta;`), + ); + } get_gmeta_by_gid(gid: number) { const s = this.convert_gmeta( this.db.queryEntries( diff --git a/fresh.gen.ts b/fresh.gen.ts index d7105d4..f3e7675 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -14,10 +14,11 @@ import * as $8 from "./routes/api/filemeta.ts"; import * as $9 from "./routes/api/filemeta/[token].ts"; import * as $10 from "./routes/api/files/[token].ts"; import * as $11 from "./routes/api/gallery/[gid].ts"; -import * as $12 from "./routes/api/status.ts"; -import * as $13 from "./routes/api/task.ts"; -import * as $14 from "./routes/api/thumbnail/[id].ts"; -import * as $15 from "./routes/index.tsx"; +import * as $12 from "./routes/api/gallery/list.ts"; +import * as $13 from "./routes/api/status.ts"; +import * as $14 from "./routes/api/task.ts"; +import * as $15 from "./routes/api/thumbnail/[id].ts"; +import * as $16 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"; @@ -36,10 +37,11 @@ const manifest = { "./routes/api/filemeta/[token].ts": $9, "./routes/api/files/[token].ts": $10, "./routes/api/gallery/[gid].ts": $11, - "./routes/api/status.ts": $12, - "./routes/api/task.ts": $13, - "./routes/api/thumbnail/[id].ts": $14, - "./routes/index.tsx": $15, + "./routes/api/gallery/list.ts": $12, + "./routes/api/status.ts": $13, + "./routes/api/task.ts": $14, + "./routes/api/thumbnail/[id].ts": $15, + "./routes/index.tsx": $16, }, islands: { "./islands/Container.tsx": $$0, diff --git a/routes/api/gallery/list.ts b/routes/api/gallery/list.ts new file mode 100644 index 0000000..d566dd8 --- /dev/null +++ b/routes/api/gallery/list.ts @@ -0,0 +1,46 @@ +import { Handlers } from "$fresh/server.ts"; +import { get_task_manager } from "../../../server.ts"; +import { parse_bool, parse_int } from "../../../server/parse_form.ts"; +import { return_data, return_error } from "../../../server/utils.ts"; + +const ALLOW_FIELDS = [ + "gid", + "token", + "title", + "title_jpn", + "category", + "uploader", + "posted", + "filecount", + "filesize", + "expunged", + "rating", + "parent_gid", + "parent_key", + "first_gid", + "first_key", +]; + +export const handler: Handlers = { + async GET(req, _ctx) { + const u = new URL(req.url); + const t = get_task_manager(); + const all = await parse_bool(u.searchParams.get("all"), false); + const offset = await parse_int(u.searchParams.get("offset"), 0); + const limit = await parse_int(u.searchParams.get("limit"), 20); + const fields = u.searchParams.get("fields") || "*"; + if (fields !== "*") { + const fs = fields.split(","); + const ok = fs.every((d) => { + const c = d.trim(); + return ALLOW_FIELDS.includes(c); + }); + if (!ok) return return_error(1, "Some fields not allowed."); + } + if (all) { + return return_data(t.db.get_gmetas_all(fields)); + } else { + return return_data(t.db.get_gmetas(offset, limit, fields)); + } + }, +}; diff --git a/server/cfg.ts b/server/cfg.ts index e5caa70..821be9d 100644 --- a/server/cfg.ts +++ b/server/cfg.ts @@ -3,6 +3,10 @@ import { ConfigType } from "../config.ts"; import { get_ws_host } from "./utils.ts"; import { ConfigClientSocketData, ConfigSeverSocketData } from "./config.ts"; import { DEFAULT_DOWNLOAD_CONFIG, DownloadConfig } from "../tasks/download.ts"; +import { + DEFAULT_EXPORT_ZIP_CONFIG, + ExportZipConfig, +} from "../tasks/export_zip.ts"; export const cfg = signal(undefined); @@ -36,3 +40,11 @@ export function generate_download_cfg(): DownloadConfig { remove_previous_gallery: c.remove_previous_gallery, }; } + +export function generate_export_zip_cfg(): ExportZipConfig { + if (!cfg.value) return DEFAULT_EXPORT_ZIP_CONFIG; + const c = cfg.value; + return { + jpn_title: c.export_zip_jpn_title, + }; +} diff --git a/server/gallery.ts b/server/gallery.ts index aeffc71..7cbc587 100644 --- a/server/gallery.ts +++ b/server/gallery.ts @@ -8,3 +8,5 @@ export type GalleryData = { }; export type GalleryResult = JSONResult; + +export type GalleryListResult = JSONResult; diff --git a/static/common.css b/static/common.css index 0b64d46..f7ab2f0 100644 --- a/static/common.css +++ b/static/common.css @@ -251,7 +251,7 @@ div.new_task>div.container { height: calc(100% - 5% - 64px); min-width: 400px; width: calc(100% - 20%); - margin-top: 8rem; + margin-top: 2.5%; } div.new_task .top { diff --git a/translation/en/task.jsonc b/translation/en/task.jsonc index 6cfea99..3c72093 100644 --- a/translation/en/task.jsonc +++ b/translation/en/task.jsonc @@ -15,5 +15,9 @@ "overwrite_cfg": "Overwrite default config.", "submit_failed": "Failed to submit task.", "gallery_title": "Gallery title: ", - "gallery_page": "Number of pages: " + "gallery_page": "Number of pages: ", + "ezcfg_output": "The path to output file: ", + "ezcfg_jpn_title": "Use japanese title first.", + "ezcfg_export_ad": "Export pages which marked as ads.", + "ezcfg_max_length": "Maximum length of filenames in Zip files: " } diff --git a/translation/zh-cn/task.jsonc b/translation/zh-cn/task.jsonc index 1209e9d..86325ae 100644 --- a/translation/zh-cn/task.jsonc +++ b/translation/zh-cn/task.jsonc @@ -15,5 +15,9 @@ "overwrite_cfg": "覆盖默认设置。", "submit_failed": "提交任务失败", "gallery_title": "画廊标题:", - "gallery_page": "页数:" + "gallery_page": "页数:", + "ezcfg_output": "输出文件的位置:", + "ezcfg_jpn_title": "优先使用日语标题。", + "ezcfg_export_ad": "导出标记为广告的页面。", + "ezcfg_max_length": "Zip文件中文件名的最大长度:" }