From c7c6e30a56e41407e94d171e4f0df087275236ef Mon Sep 17 00:00:00 2001 From: noelorin Date: Fri, 25 Sep 2026 04:51:16 +0800 Subject: [PATCH] feat(web): library grid + bangumi manual bind --- app/web/src/components/pages/LibraryBody.tsx | 157 +++++++++++++++++++ app/web/src/pages/[locale]/library.astro | 19 +++ app/web/src/views/library.astro | 40 +++++ packages/i18n/src/messages/en.ts | 11 ++ packages/i18n/src/messages/zh-CN.ts | 11 ++ 5 files changed, 238 insertions(+) create mode 100644 app/web/src/components/pages/LibraryBody.tsx create mode 100644 app/web/src/pages/[locale]/library.astro create mode 100644 app/web/src/views/library.astro diff --git a/app/web/src/components/pages/LibraryBody.tsx b/app/web/src/components/pages/LibraryBody.tsx new file mode 100644 index 0000000..cc76cda --- /dev/null +++ b/app/web/src/components/pages/LibraryBody.tsx @@ -0,0 +1,157 @@ +import type { Locale } from "@app/i18n"; +import { t } from "@app/i18n"; +import { Button, Input } from "@app/ui"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState } from "react"; +import { createTRPCReactClient, trpc } from "~/lib/trpc"; + +function LibraryInner(props: { locale: Locale }) { + const { locale } = props; + const [filter, setFilter] = useState<"" | "unmatched" | "ok">(""); + const [q, setQ] = useState(""); + const list = trpc.media.libraryList.useQuery({ + limit: 24, + offset: 0, + scrapeStatus: filter || undefined, + q: q || undefined, + }); + const progressList = list.data?.rows ?? []; + const [matchId, setMatchId] = useState(null); + const [searchQ, setSearchQ] = useState(""); + const search = trpc.media.scrapeSearch.useQuery( + { q: searchQ }, + { enabled: searchQ.length > 1 && matchId !== null }, + ); + const bind = trpc.media.scrapeBind.useMutation({ + onSuccess: () => { + setMatchId(null); + list.refetch(); + }, + }); + + return ( +
+
+

{t(locale, "library.title")}

+
+ setQ(e.target.value)} + /> + +
+
+ +
+ {progressList.map((item) => ( +
+
+ {item.posterUrl ? ( + + ) : ( + t(locale, "library.noPoster") + )} +
+
+
+ {item.title} +
+
+ + {item.scrapeStatus !== "ok" && ( + + )} +
+
+
+ ))} + {progressList.length === 0 && ( +

+ {t(locale, "library.empty")} +

+ )} +
+ + {matchId && ( +
+
+

{t(locale, "library.matchTitle")}

+ setSearchQ(e.target.value)} + /> +
    + {(search.data ?? []).map((hit) => ( +
  • + {hit.nameCn || hit.name} + +
  • + ))} +
+ +
+
+ )} +
+ ); +} + +const client = createTRPCReactClient(); +const qc = new QueryClient(); + +export function LibraryBody(props: { locale: Locale }) { + return ( + + + + + + ); +} diff --git a/app/web/src/pages/[locale]/library.astro b/app/web/src/pages/[locale]/library.astro new file mode 100644 index 0000000..acdb7a2 --- /dev/null +++ b/app/web/src/pages/[locale]/library.astro @@ -0,0 +1,19 @@ +--- +import { COOKIE_LANG, isLocale, localePath, resolvePreferredLocale, type Locale } from "@app/i18n"; +import Library from "~/views/library.astro"; + +const raw = Astro.params["locale"]; +const normalized = raw === "zh-cn" ? "zh-CN" : raw; + +if (!normalized || !isLocale(normalized)) { + const preferred = resolvePreferredLocale( + Astro.cookies.get(COOKIE_LANG)?.value, + Astro.request.headers.get("accept-language"), + ); + return Astro.redirect(localePath(preferred, "/library")); +} + +const locale: Locale = normalized; +--- + + diff --git a/app/web/src/views/library.astro b/app/web/src/views/library.astro new file mode 100644 index 0000000..741a7d2 --- /dev/null +++ b/app/web/src/views/library.astro @@ -0,0 +1,40 @@ +--- +import { alternatePath, type Locale, t } from "@app/i18n"; +import { Toaster } from "@app/ui"; +import { AppShell } from "~/components/AppShell"; +import { LibraryBody } from "~/components/pages/LibraryBody"; +import AppLayout from "~/layouts/AppLayout.astro"; + +export interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const other: Locale = locale === "zh-CN" ? "en" : "zh-CN"; +const labels: Record = { + "meta.brand": t(locale, "meta.brand"), + "nav.library": t(locale, "nav.library"), + "nav.browse": t(locale, "nav.browse"), + "nav.mounts": t(locale, "nav.mounts"), + "nav.danmaku": t(locale, "nav.danmaku"), +}; +--- + + + + + + + diff --git a/packages/i18n/src/messages/en.ts b/packages/i18n/src/messages/en.ts index d23562e..6b90894 100644 --- a/packages/i18n/src/messages/en.ts +++ b/packages/i18n/src/messages/en.ts @@ -40,6 +40,17 @@ export const en: Record = { "library.scan": "Scan mounts", "library.unmatched": "Unmatched", "library.continue": "Continue watching", + "library.searchPlaceholder": "Search title", + "library.filterAll": "All", + "library.filterOk": "Scraped", + "library.noPoster": "No poster", + "library.play": "Play", + "library.match": "Match", + "library.empty": "No media yet. Scan a mount in the file browser first.", + "library.matchTitle": "Manual Bangumi match", + "library.searchWork": "Search by title", + "library.bind": "Bind", + "library.cancel": "Cancel", "browse.title": "File browser", "browse.play": "Play", diff --git a/packages/i18n/src/messages/zh-CN.ts b/packages/i18n/src/messages/zh-CN.ts index 135676f..645e334 100644 --- a/packages/i18n/src/messages/zh-CN.ts +++ b/packages/i18n/src/messages/zh-CN.ts @@ -41,6 +41,17 @@ export const zhCN = { "library.scan": "扫描挂载", "library.unmatched": "未匹配", "library.continue": "继续播放", + "library.searchPlaceholder": "搜索标题", + "library.filterAll": "全部", + "library.filterOk": "已刮削", + "library.noPoster": "无封面", + "library.play": "播放", + "library.match": "匹配", + "library.empty": "暂无媒体,请先在文件浏览中扫描挂载。", + "library.matchTitle": "手动匹配 Bangumi", + "library.searchWork": "搜索作品名", + "library.bind": "绑定", + "library.cancel": "取消", "browse.title": "文件浏览", "browse.play": "播放",