diff --git a/frontend/public/kun.webp b/frontend/public/kun.webp new file mode 100644 index 0000000..f4b5f81 Binary files /dev/null and b/frontend/public/kun.webp differ diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 625a369..3025186 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -67,7 +67,7 @@ class App { getPreFetchData() { const preFetchDataElement = document.getElementById("pre_fetch_data"); if (preFetchDataElement) { - let content = preFetchDataElement.textContent + let content = preFetchDataElement.textContent; if (!content) { return null; } diff --git a/frontend/src/components/navigator.tsx b/frontend/src/components/navigator.tsx index 41adff8..ce0752f 100644 --- a/frontend/src/components/navigator.tsx +++ b/frontend/src/components/navigator.tsx @@ -87,8 +87,8 @@ export default function Navigator() { }} > -
-
+
+
-
+
{resource.image != null && (
> { + try { + const client = axios.create({ + validateStatus(status) { + return status === 200 || status === 404; // Accept only 200 and 404 responses + }, + }); + const res = await client.get( + `https://www.moyu.moe/api/hikari?vndb_id=${id}`, + ); + if (res.status === 404) { + return { + success: false, + message: "404", + }; + } + if (res.status !== 200) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return { + success: true, + message: "ok", + data: res.data.data, + }; + } catch (error) { + console.error("Error fetching files:", error); + return { success: false, message: "Failed to fetch files" }; + } + }, +}; + +export default KunApi; + +export interface KunUser { + id: number; + name: string; + avatar: string; +} + +export interface KunPatchResponse { + id: number; + name: string; + // e.g. "vndb_id": "v19658", + vndb_id: string; + banner: string; + introduction: string; + // e.g. "released": "2016-11-25", + released: string; + status: number; + download: number; + view: number; + resource_update_time: Date; + type: string[]; + language: string[]; + engine: string[]; + platform: string[]; + user_id: number; + user: KunUser; + created: Date; + updated: Date; + resource: KunPatchResourceResponse[]; +} + +export interface KunPatchResourceResponse { + id: number; + storage: "s3" | "user"; + name: string; + model_name: string; + size: string; + code: string; + password: string; + note: string; + hash: string; + type: string[]; + language: string[]; + platform: string[]; + download: number; + status: number; + update_time: Date; + user_id: number; + patch_id: number; + created: Date; + user: KunUser; +} + +export interface HikariResponse { + success: boolean; + message: string; + data: KunPatchResponse | null; +} + +const SUPPORTED_LANGUAGE_MAP: Record = { + "zh-Hans": "简体中文", + "zh-Hant": "繁體中文", + "ja": "日本語", + "en": "English", + "other": "其它", +}; + +export function kunLanguageToString(language: string): string { + return SUPPORTED_LANGUAGE_MAP[language] || language; +} + +const SUPPORTED_PLATFORM_MAP: Record = { + windows: "Windows", + android: "Android", + macos: "MacOS", + ios: "iOS", + linux: "Linux", + other: "其它", +}; + +export function kunPlatformToString(platform: string): string { + return SUPPORTED_PLATFORM_MAP[platform] || platform; +} + +const resourceTypes = [ + { + value: "manual", + label: "人工翻译补丁", + }, + { + value: "ai", + label: "AI 翻译补丁", + }, + { + value: "machine_polishing", + label: "机翻润色", + }, + { + value: "machine", + label: "机翻补丁", + }, + { + value: "save", + label: "全 CG 存档", + }, + { + value: "crack", + label: "破解补丁", + }, + { + value: "fix", + label: "修正补丁", + }, + { + value: "mod", + label: "魔改补丁", + }, + { + value: "other", + label: "其它", + }, +]; + +export function kunResourceTypeToString(type: string): string { + const resourceType = resourceTypes.find((t) => t.value === type); + return resourceType ? resourceType.label : type; +} diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index 89fe26a..555ce6d 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -50,6 +50,13 @@ import { BiLogoSteam } from "react-icons/bi"; import { CommentTile } from "../components/comment_tile.tsx"; import { CommentInput } from "../components/comment_input.tsx"; import { useNavigator } from "../components/navigator.tsx"; +import KunApi, { + kunLanguageToString, + KunPatchResourceResponse, + KunPatchResponse, + kunPlatformToString, + kunResourceTypeToString, +} from "../network/kun.ts"; export default function ResourcePage() { const params = useParams(); @@ -63,6 +70,8 @@ export default function ResourcePage() { const [page, setPage] = useState(0); + const [visitedTabs, setVisitedTabs] = useState>(new Set([])); + const location = useLocation(); const navigator = useNavigator(); @@ -113,10 +122,12 @@ export default function ResourcePage() { if (resource) { document.title = resource.title; if (resource.images.length > 0) { - navigator.setBackground(network.getResampledImageUrl(resource.images[0].id)); + navigator.setBackground( + network.getResampledImageUrl(resource.images[0].id), + ); } } - }, [resource]) + }, [resource]); const navigate = useNavigate(); @@ -136,6 +147,7 @@ export default function ResourcePage() { // 初始状态读取hash useEffect(() => { setPage(getPageFromHash(window.location.hash)); + setVisitedTabs(new Set([getPageFromHash(window.location.hash)])); // 监听hash变化 const onHashChange = () => { setPage(getPageFromHash(window.location.hash)); @@ -149,6 +161,8 @@ export default function ResourcePage() { const handleTabChange = (idx: number) => { setPage(idx); setHashByPage(idx); + // Mark tab as visited when switched to + setVisitedTabs((prev) => new Set(prev).add(idx)); }; if (isNaN(id)) { @@ -222,9 +236,12 @@ export default function ResourcePage() {

)} -
+
-
+ {visitedTabs.has(0) &&
}
- + {visitedTabs.has(1) && ( + + )}
- + {visitedTabs.has(2) && }
@@ -315,11 +334,15 @@ function Tags({ tags }: { tags: Tag[] }) { <> {Array.from(tagsMap.entries()).map(([type, tags]) => (

- {type == "" ? t("Other") : type} + + {type == "" ? t("Other") : type} + {tags.map((tag) => ( { navigate(`/tag/${tag.name}`); }} @@ -603,7 +626,7 @@ function RelatedResourceCard({ height: imgHeight, objectFit: "cover", }} - className={"h-full min-h-0 object-cover min-w-0"} + className={"h-full object-cover min-w-0"} alt={"cover"} src={network.getImageUrl(r.image?.id)} /> @@ -702,32 +725,33 @@ function FileTile({ file }: { file: RFile }) {

- { - file.size > 10 * 1024 * 1024 ? ( - - ) : ( - - - - ) - } + {file.size > 10 * 1024 * 1024 ? ( + + ) : ( + + + + )}
@@ -777,7 +801,13 @@ function CloudflarePopup({ file }: { file: RFile }) { ); } -function Files({ files, resourceID }: { files: RFile[]; resourceID: number }) { +function Files({ + files, + resource, +}: { + files: RFile[]; + resource: ResourceDetails; +}) { return (
{files.map((file) => { @@ -786,9 +816,10 @@ function Files({ files, resourceID }: { files: RFile[]; resourceID: number }) {
{(app.canUpload() || app.allowNormalUserUpload) && (
- +
)} +
); } @@ -1412,3 +1443,138 @@ function DeleteFileDialog({ ); } + +function KunFiles({ resource }: { resource: ResourceDetails }) { + let vnid = ""; + for (const link of resource.links) { + if (link.label.toLowerCase() === "vndb") { + vnid = link.url.split("/").pop() || ""; + break; + } + } + + const [data, setData] = useState(null); + + const [isLoading, setLoading] = useState(true); + + const [error, setError] = useState(null); + + const { t } = useTranslation(); + + useEffect(() => { + if (!vnid || !KunApi.isAvailable()) { + return; + } + KunApi.getPatch(vnid).then((res) => { + if (res.success) { + setData(res.data!); + } else if (res.message === "404") { + // ignore + } else { + setError(res.message); + } + setLoading(false); + }); + }, [vnid]); + + if (error) { + return ; + } + + if (isLoading) { + return ; + } + + if (!vnid || !KunApi.isAvailable() || data === null) { + return <>; + } + + return ( + <> + + {data && ( +
+ {data.resource.map((file) => { + return ; + })} + {data.resource.length === 0 && ( +

+ {t("No patches found for this VN.")} +

+ )} +
+ )} + + ); +} + +function KunFile({ + file, + patchID, +}: { + file: KunPatchResourceResponse; + patchID: number; +}) { + const tags: string[] = []; + if (file.model_name) { + tags.push(file.model_name); + } + tags.push(...file.platform.map((p) => kunPlatformToString(p))); + tags.push(...file.language.map((l) => kunLanguageToString(l))); + tags.push(...file.type.map((t) => kunResourceTypeToString(t))); + + return ( +
+
+
+

{file.name}

+

{file.note}

+

+ + + {"avatar"} + {file.user.name} + + + + + {file.size} + + {tags.map((p) => ( + + {p} + + ))} +

+
+
+ + + +
+
+
+ ); +} diff --git a/frontend/src/pages/tags_page.tsx b/frontend/src/pages/tags_page.tsx index f8043c0..1b2ac0e 100644 --- a/frontend/src/pages/tags_page.tsx +++ b/frontend/src/pages/tags_page.tsx @@ -65,7 +65,9 @@ export default function TagsPage() { navigate(`/tag/${tag.name}`); }} key={tag.name} - className={"m-1 cursor-pointer badge-soft badge-primary shadow-xs"} + className={ + "m-1 cursor-pointer badge-soft badge-primary shadow-xs" + } > {tag.name + (tag.resources_count > 0 ? ` (${tag.resources_count})` : "")} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7986745..023c6b9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,6 +12,10 @@ export default defineConfig({ // target: "https://res.nyne.dev", changeOrigin: true, }, + "https://www.moyu.moe": { + target: "https://www.moyu.moe", + changeOrigin: true, + }, }, }, });