+
{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 (
+
+ );
+}
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,
+ },
},
},
});