mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
✨ feat: Unity creator settings
This commit is contained in:
Vendored
+4
@@ -15,6 +15,8 @@ export const policies = {
|
||||
representedGroup: { ttl: 15 * 60_000, staleWhileRevalidate: 60 * 60_000 },
|
||||
group: { ttl: 30 * 60_000, staleWhileRevalidate: 2 * 60 * 60_000 },
|
||||
instance: { ttl: 30_000, staleWhileRevalidate: 60_000 },
|
||||
apiConfig: { ttl: 6 * 60 * 60_000, staleWhileRevalidate: 24 * 60 * 60_000 },
|
||||
unityChangeset: { ttl: 30 * 24 * 60 * 60_000 },
|
||||
} satisfies Record<string, CachePolicy>;
|
||||
|
||||
export const cacheKeys = {
|
||||
@@ -33,4 +35,6 @@ export const cacheKeys = {
|
||||
representedGroup: (id: string) => `user:group:represented:${id}`,
|
||||
group: (id: string) => `group:${id}`,
|
||||
instance: (location: string) => `instance:${location}`,
|
||||
apiConfig: () => "api:config",
|
||||
unityChangeset: (version: string) => `unity:changeset:${version}`,
|
||||
};
|
||||
|
||||
@@ -3,10 +3,11 @@ import { join } from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { writeFileAtomicSync } from "../lib/atomicFile";
|
||||
import type { AppConfig } from "../../shared/types/appConfig";
|
||||
import { detectedGamePath } from "../game/steam";
|
||||
|
||||
const path = () => join(app.getPath("userData"), "app-config.json");
|
||||
|
||||
type StoredConfig = Omit<AppConfig, "version">;
|
||||
type StoredConfig = Omit<AppConfig, "version" | "detectedGamePath">;
|
||||
const DEFAULTS: StoredConfig = { gamePath: null };
|
||||
|
||||
function readStored(): StoredConfig {
|
||||
@@ -17,8 +18,16 @@ function readStored(): StoredConfig {
|
||||
}
|
||||
}
|
||||
|
||||
function withDerived(stored: StoredConfig): AppConfig {
|
||||
return { version: app.getVersion(), detectedGamePath: detectedGamePath(), ...stored };
|
||||
}
|
||||
|
||||
export function gamePathOverride(): string | null {
|
||||
return readStored().gamePath;
|
||||
}
|
||||
|
||||
export function getConfig(): AppConfig {
|
||||
return { version: app.getVersion(), ...readStored() };
|
||||
return withDerived(readStored());
|
||||
}
|
||||
|
||||
export function setGamePath(gamePath: string | null): AppConfig {
|
||||
@@ -28,5 +37,5 @@ export function setGamePath(gamePath: string | null): AppConfig {
|
||||
}
|
||||
const next: StoredConfig = { ...readStored(), gamePath: trimmed };
|
||||
writeFileAtomicSync(path(), JSON.stringify(next, null, 2));
|
||||
return { version: app.getVersion(), ...next };
|
||||
return withDerived(next);
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,7 +1,7 @@
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { getConfig } from "../config/appConfig";
|
||||
import { gamePathOverride } from "../config/appConfig";
|
||||
|
||||
export const VRCHAT_APPID = "438100";
|
||||
|
||||
@@ -29,7 +29,7 @@ export function steamLibraries(): string[] {
|
||||
const bases = platformBases();
|
||||
const libs = new Set<string>();
|
||||
|
||||
const override = getConfig().gamePath;
|
||||
const override = gamePathOverride();
|
||||
if (override) {
|
||||
if (existsSync(join(override, "steamapps"))) libs.add(join(override, "steamapps"));
|
||||
if (existsSync(join(override, "compatdata"))) libs.add(override);
|
||||
@@ -60,3 +60,11 @@ export function vrchatPrefix(): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function detectedGamePath(): string | null {
|
||||
for (const lib of steamLibraries()) {
|
||||
const manifest = join(lib, `appmanifest_${VRCHAT_APPID}.acf`);
|
||||
if (existsSync(manifest)) return dirname(lib);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
|
||||
function hubConfigDir(): string {
|
||||
const home = homedir();
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "UnityHub");
|
||||
case "darwin":
|
||||
return join(home, "Library", "Application Support", "UnityHub");
|
||||
default:
|
||||
return join(home, ".config", "UnityHub");
|
||||
}
|
||||
}
|
||||
|
||||
function hubBinaries(): string[] {
|
||||
const home = homedir();
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return [
|
||||
join(process.env.ProgramFiles ?? "C:\\Program Files", "Unity Hub", "Unity Hub.exe"),
|
||||
join(
|
||||
process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)",
|
||||
"Unity Hub",
|
||||
"Unity Hub.exe",
|
||||
),
|
||||
];
|
||||
case "darwin":
|
||||
return ["/Applications/Unity Hub.app"];
|
||||
default:
|
||||
return ["/usr/bin/unityhub", "/opt/unityhub/unityhub", join(home, ".local", "bin", "unityhub")];
|
||||
}
|
||||
}
|
||||
|
||||
export function hubInstalled(): boolean {
|
||||
return hubBinaries().some(existsSync);
|
||||
}
|
||||
|
||||
function defaultEditorRoot(): string {
|
||||
const home = homedir();
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return join(process.env.ProgramFiles ?? "C:\\Program Files", "Unity", "Hub", "Editor");
|
||||
case "darwin":
|
||||
return "/Applications/Unity/Hub/Editor";
|
||||
default:
|
||||
return join(home, "Unity", "Hub", "Editor");
|
||||
}
|
||||
}
|
||||
|
||||
function customEditorRoot(): string | null {
|
||||
const file = join(hubConfigDir(), "secondaryInstallPath.json");
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(file, "utf8")) as unknown;
|
||||
const path = typeof raw === "string" ? raw : null;
|
||||
return path && path.trim() ? path.trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function editorRoot(): string | null {
|
||||
for (const root of [customEditorRoot(), defaultEditorRoot()]) {
|
||||
if (root && existsSync(root)) return root;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function editorBinaryName(): string {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return join("Editor", "Unity.exe");
|
||||
case "darwin":
|
||||
return join("Unity.app");
|
||||
default:
|
||||
return join("Editor", "Unity");
|
||||
}
|
||||
}
|
||||
|
||||
export function installedVersions(): string[] {
|
||||
const root = editorRoot();
|
||||
if (!root) return [];
|
||||
const binary = editorBinaryName();
|
||||
const out: string[] = [];
|
||||
try {
|
||||
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (existsSync(join(root, entry.name, binary))) out.push(entry.name);
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import * as users from "../vrchat/userService";
|
||||
import * as friends from "../vrchat/friendsService";
|
||||
import * as worlds from "../vrchat/worldService";
|
||||
import * as instances from "../vrchat/instanceService";
|
||||
import * as unity from "../vrchat/unityService";
|
||||
import * as avatars from "../vrchat/avatarService";
|
||||
import * as groups from "../vrchat/groupService";
|
||||
import * as settings from "../vrchat/settingsService";
|
||||
@@ -90,6 +91,13 @@ const handlers = {
|
||||
return appConfig.setGamePath(res.filePaths[0]);
|
||||
}),
|
||||
|
||||
"unity:status": () => guard(() => unity.unityStatus()),
|
||||
"unity:install": (url) =>
|
||||
guard(async () => {
|
||||
if (!url.startsWith("unityhub://")) throw new Error("Invalid Unity Hub link");
|
||||
await shell.openExternal(url);
|
||||
}),
|
||||
|
||||
"game:status": () => guard(() => game.status()),
|
||||
"game:launch": () => guard(() => game.launch()),
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { UnityStatus } from "../../shared/types/unity";
|
||||
import { cachedRead } from "./cachedRead";
|
||||
import { cacheKeys, policies } from "../cache/policies";
|
||||
import { getActiveClient } from "./client";
|
||||
import { userCache } from "./userService";
|
||||
import { hubInstalled, editorRoot, installedVersions } from "../game/unity";
|
||||
|
||||
async function requiredUnityVersion(): Promise<string | null> {
|
||||
if (!getActiveClient()) return null;
|
||||
try {
|
||||
return await cachedRead(cacheKeys.apiConfig(), policies.apiConfig, async (vrc) => {
|
||||
const { data } = await vrc.getConfig({ throwOnError: true });
|
||||
return data.sdkUnityVersion ?? null;
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function installUrl(version: string): Promise<string | null> {
|
||||
try {
|
||||
return await userCache.get(
|
||||
cacheKeys.unityChangeset(version),
|
||||
policies.unityChangeset,
|
||||
async () => {
|
||||
const res = await fetch(
|
||||
`https://services.api.unity.com/unity/editor/release/v1/releases?version=${version}`,
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const body = (await res.json()) as { results?: { unityHubDeepLink?: string }[] };
|
||||
return body.results?.[0]?.unityHubDeepLink ?? null;
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unityStatus(): Promise<UnityStatus> {
|
||||
const installed = installedVersions();
|
||||
const required = await requiredUnityVersion();
|
||||
|
||||
let match: UnityStatus["match"];
|
||||
if (!required) match = "unknown";
|
||||
else if (installed.length === 0) match = "no-editor";
|
||||
else if (installed.includes(required)) match = "ok";
|
||||
else match = "missing";
|
||||
|
||||
const url = required && match !== "ok" ? await installUrl(required) : null;
|
||||
|
||||
return {
|
||||
hubInstalled: hubInstalled(),
|
||||
editorRoot: editorRoot(),
|
||||
installedVersions: installed,
|
||||
requiredVersion: required,
|
||||
installUrl: url,
|
||||
match,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Boxes,
|
||||
Check,
|
||||
Download,
|
||||
Droplet,
|
||||
FolderOpen,
|
||||
Gamepad2,
|
||||
@@ -9,19 +12,23 @@ import {
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
Search,
|
||||
Sun,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "../../lib/ThemeContext";
|
||||
import { useI18n } from "../../lib/i18n";
|
||||
import { ACCENT_PRESETS, DEFAULT_ACCENT, type SchemeMode } from "../../lib/theme";
|
||||
import type { AppConfig } from "../../../../shared/types/appConfig";
|
||||
import type { UnityStatus } from "../../../../shared/types/unity";
|
||||
import { api, errorMessage } from "../../lib/api";
|
||||
import { useAsync } from "../../lib/useAsync";
|
||||
import { Button, Field, Tabs } from "../../components/ui";
|
||||
|
||||
const SHELL = "mx-auto flex w-full max-w-[760px] flex-col gap-[18px] px-12 pb-16 pt-10";
|
||||
const SECTIONS = "animate-rise flex flex-col gap-[18px]";
|
||||
|
||||
type SettingsTab = "appearance" | "game" | "about";
|
||||
type SettingsTab = "appearance" | "game" | "creator" | "about";
|
||||
|
||||
export function SettingsView() {
|
||||
const { t } = useI18n();
|
||||
@@ -37,6 +44,7 @@ export function SettingsView() {
|
||||
tabs={[
|
||||
{ id: "appearance", label: t("settings:tabs.appearance") },
|
||||
{ id: "game", label: t("settings:tabs.game") },
|
||||
{ id: "creator", label: t("settings:tabs.creator") },
|
||||
{ id: "about", label: t("settings:tabs.about") },
|
||||
]}
|
||||
active={tab}
|
||||
@@ -57,6 +65,12 @@ export function SettingsView() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tab === "creator" ? (
|
||||
<div className={SECTIONS}>
|
||||
<UnitySection />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tab === "about" ? (
|
||||
<div className={SECTIONS}>
|
||||
<AboutSection />
|
||||
@@ -312,6 +326,29 @@ function GameSection() {
|
||||
{t("settings:game.browse")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 text-[12.5px] text-muted">
|
||||
<Search size={13} className="shrink-0 text-faint" />
|
||||
<span className="shrink-0 font-semibold">{t("settings:game.detected")}:</span>
|
||||
{config?.detectedGamePath ? (
|
||||
<>
|
||||
<span className="truncate font-mono text-[12px]" title={config.detectedGamePath}>
|
||||
{config.detectedGamePath}
|
||||
</span>
|
||||
{config.detectedGamePath !== path.trim() ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPath(config.detectedGamePath ?? "")}
|
||||
className="ml-auto shrink-0 font-semibold text-accent hover:underline"
|
||||
>
|
||||
{t("settings:game.useDetected")}
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-faint">{t("settings:game.detectedNone")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2.5">
|
||||
<Button
|
||||
onClick={() =>
|
||||
@@ -343,6 +380,73 @@ function Notice({ error, ok }: { error?: string | null; ok?: string | null }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function UnitySection() {
|
||||
const { t } = useI18n();
|
||||
const status = useAsync(() => api.unity.status(), [], t("settings:unity.error"));
|
||||
|
||||
return (
|
||||
<Section
|
||||
title={t("settings:unity.title")}
|
||||
icon={<Boxes size={16} />}
|
||||
description={t("settings:unity.description")}
|
||||
>
|
||||
{status.status === "loading" ? (
|
||||
<p className="text-[13px] text-muted">{t("settings:unity.loading")}</p>
|
||||
) : status.status === "error" ? (
|
||||
<p className="text-[12.5px] text-[var(--danger)]">{status.message}</p>
|
||||
) : (
|
||||
<UnityStatusBody status={status.data} />
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function UnityStatusBody({ status }: { status: UnityStatus }) {
|
||||
const { t } = useI18n();
|
||||
const verdict = MATCH_META[status.match];
|
||||
return (
|
||||
<div className="flex flex-col gap-2 text-[13px]">
|
||||
<Row
|
||||
label={t("settings:unity.hub")}
|
||||
value={status.hubInstalled ? t("settings:unity.installed") : t("settings:unity.notFound")}
|
||||
/>
|
||||
<Row label={t("settings:unity.required")} value={status.requiredVersion ?? "—"} />
|
||||
<Row
|
||||
label={t("settings:unity.installedVersions")}
|
||||
value={status.installedVersions.length ? status.installedVersions.join(", ") : "—"}
|
||||
/>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-muted">{t("settings:unity.status")}</span>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 font-semibold"
|
||||
style={{ color: verdict.color }}
|
||||
>
|
||||
<verdict.icon size={15} />
|
||||
{t(verdict.key)}
|
||||
</span>
|
||||
</div>
|
||||
{status.match !== "ok" && status.installUrl ? (
|
||||
<div className="mt-1">
|
||||
<Button onClick={() => void api.unity.install(status.installUrl as string)}>
|
||||
<Download size={14} />
|
||||
{t("settings:unity.install", { version: status.requiredVersion ?? "" })}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MATCH_META: Record<
|
||||
UnityStatus["match"],
|
||||
{ key: string; icon: typeof Check; color: string }
|
||||
> = {
|
||||
ok: { key: "settings:unity.match.ok", icon: Check, color: "var(--status-active)" },
|
||||
missing: { key: "settings:unity.match.missing", icon: AlertTriangle, color: "var(--status-ask)" },
|
||||
"no-editor": { key: "settings:unity.match.noEditor", icon: X, color: "var(--danger)" },
|
||||
unknown: { key: "settings:unity.match.unknown", icon: AlertTriangle, color: "var(--muted)" },
|
||||
};
|
||||
|
||||
function AboutSection() {
|
||||
const { t } = useI18n();
|
||||
const [version, setVersion] = useState("");
|
||||
|
||||
@@ -100,6 +100,10 @@ export const api = {
|
||||
setGamePath: (gamePath: string | null) => call("config:setGamePath", { gamePath }),
|
||||
pickGamePath: () => call("config:pickGamePath"),
|
||||
},
|
||||
unity: {
|
||||
status: () => call("unity:status"),
|
||||
install: (url: string) => call("unity:install", url),
|
||||
},
|
||||
game: {
|
||||
status: () => call("game:status"),
|
||||
launch: () => call("game:launch"),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"tabs": {
|
||||
"appearance": "Appearance",
|
||||
"game": "Game",
|
||||
"creator": "Creator",
|
||||
"about": "About"
|
||||
},
|
||||
"appearance": {
|
||||
@@ -41,16 +42,38 @@
|
||||
"description": "The language used across the app."
|
||||
},
|
||||
"game": {
|
||||
"title": "Game Install Path",
|
||||
"description": "Where Steam or VRChat is installed. Leave empty to auto-detect. Set this if an enhancement can't find your install.",
|
||||
"label": "Install folder",
|
||||
"title": "Steam Path",
|
||||
"description": "Your Steam library folder that contains VRChat. Leave empty to auto-detect. Set this if an enhancement can't find your install.",
|
||||
"label": "Steam folder",
|
||||
"placeholder": "Auto-detect",
|
||||
"browse": "Browse",
|
||||
"save": "Save",
|
||||
"reset": "Reset to auto-detect",
|
||||
"saved": "Path saved.",
|
||||
"resetDone": "Reset to auto-detect.",
|
||||
"error": "Something went wrong."
|
||||
"error": "Something went wrong.",
|
||||
"detected": "Auto-detected",
|
||||
"detectedNone": "No Steam library with VRChat found automatically.",
|
||||
"useDetected": "Use this"
|
||||
},
|
||||
"unity": {
|
||||
"title": "Unity",
|
||||
"description": "The Unity Editor version VRChat uses, and whether you have it installed via Unity Hub.",
|
||||
"loading": "Checking your Unity install…",
|
||||
"error": "Couldn't check your Unity install.",
|
||||
"hub": "Unity Hub",
|
||||
"installed": "Installed",
|
||||
"notFound": "Not found",
|
||||
"required": "VRChat's Unity version",
|
||||
"installedVersions": "Installed editors",
|
||||
"status": "Status",
|
||||
"install": "Install {{version}} in Unity Hub",
|
||||
"match": {
|
||||
"ok": "Match",
|
||||
"missing": "Version not installed",
|
||||
"noEditor": "No editor installed",
|
||||
"unknown": "Sign in to check"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"tabs": {
|
||||
"appearance": "外観",
|
||||
"game": "ゲーム",
|
||||
"creator": "クリエイター",
|
||||
"about": "情報"
|
||||
},
|
||||
"appearance": {
|
||||
@@ -41,16 +42,38 @@
|
||||
"description": "アプリ全体で使用される言語。"
|
||||
},
|
||||
"game": {
|
||||
"title": "ゲームのインストール先",
|
||||
"description": "Steam または VRChat のインストール先です。空欄なら自動検出します。拡張機能がインストール先を見つけられないときに設定してください。",
|
||||
"label": "インストールフォルダ",
|
||||
"title": "Steam のパス",
|
||||
"description": "VRChat が入っている Steam ライブラリフォルダです。空欄なら自動検出します。拡張機能がインストール先を見つけられないときに設定してください。",
|
||||
"label": "Steam フォルダ",
|
||||
"placeholder": "自動検出",
|
||||
"browse": "参照",
|
||||
"save": "保存",
|
||||
"reset": "自動検出に戻す",
|
||||
"saved": "パスを保存しました。",
|
||||
"resetDone": "自動検出に戻しました。",
|
||||
"error": "問題が発生しました。"
|
||||
"error": "問題が発生しました。",
|
||||
"detected": "自動検出",
|
||||
"detectedNone": "VRChat を含む Steam ライブラリを自動で見つけられませんでした。",
|
||||
"useDetected": "これを使う"
|
||||
},
|
||||
"unity": {
|
||||
"title": "Unity",
|
||||
"description": "VRChat が使用している Unity エディターのバージョンと、Unity Hub でインストール済みかどうかを表示します。",
|
||||
"loading": "Unity のインストールを確認中…",
|
||||
"error": "Unity のインストールを確認できませんでした。",
|
||||
"hub": "Unity Hub",
|
||||
"installed": "インストール済み",
|
||||
"notFound": "見つかりません",
|
||||
"required": "VRChat の Unity バージョン",
|
||||
"installedVersions": "インストール済みエディター",
|
||||
"status": "状態",
|
||||
"install": "Unity Hub で {{version}} をインストール",
|
||||
"match": {
|
||||
"ok": "一致",
|
||||
"missing": "そのバージョンが未インストール",
|
||||
"noEditor": "エディター未インストール",
|
||||
"unknown": "確認するにはサインイン"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "情報",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"tabs": {
|
||||
"appearance": "รูปลักษณ์",
|
||||
"game": "เกม",
|
||||
"creator": "ครีเอเตอร์",
|
||||
"about": "เกี่ยวกับ"
|
||||
},
|
||||
"appearance": {
|
||||
@@ -41,16 +42,38 @@
|
||||
"description": "ภาษาที่ใช้ทั่วทั้งแอป"
|
||||
},
|
||||
"game": {
|
||||
"title": "ตำแหน่งติดตั้งเกม",
|
||||
"description": "ตำแหน่งที่ติดตั้ง Steam หรือ VRChat เว้นว่างไว้เพื่อตรวจหาอัตโนมัติ ตั้งค่านี้หากส่วนเสริมหาตำแหน่งติดตั้งไม่พบ",
|
||||
"label": "โฟลเดอร์ติดตั้ง",
|
||||
"title": "ตำแหน่ง Steam",
|
||||
"description": "โฟลเดอร์ไลบรารี Steam ที่มี VRChat อยู่ เว้นว่างไว้เพื่อตรวจหาอัตโนมัติ ตั้งค่านี้หากส่วนเสริมหาตำแหน่งติดตั้งไม่พบ",
|
||||
"label": "โฟลเดอร์ Steam",
|
||||
"placeholder": "ตรวจหาอัตโนมัติ",
|
||||
"browse": "เรียกดู",
|
||||
"save": "บันทึก",
|
||||
"reset": "รีเซ็ตเป็นตรวจหาอัตโนมัติ",
|
||||
"saved": "บันทึกตำแหน่งแล้ว",
|
||||
"resetDone": "รีเซ็ตเป็นตรวจหาอัตโนมัติแล้ว",
|
||||
"error": "เกิดข้อผิดพลาด"
|
||||
"error": "เกิดข้อผิดพลาด",
|
||||
"detected": "ตรวจพบอัตโนมัติ",
|
||||
"detectedNone": "ไม่พบไลบรารี Steam ที่มี VRChat โดยอัตโนมัติ",
|
||||
"useDetected": "ใช้ตำแหน่งนี้"
|
||||
},
|
||||
"unity": {
|
||||
"title": "Unity",
|
||||
"description": "เวอร์ชัน Unity Editor ที่ VRChat ใช้ และคุณติดตั้งไว้ผ่าน Unity Hub หรือไม่",
|
||||
"loading": "กำลังตรวจสอบการติดตั้ง Unity…",
|
||||
"error": "ตรวจสอบการติดตั้ง Unity ไม่ได้",
|
||||
"hub": "Unity Hub",
|
||||
"installed": "ติดตั้งแล้ว",
|
||||
"notFound": "ไม่พบ",
|
||||
"required": "เวอร์ชัน Unity ของ VRChat",
|
||||
"installedVersions": "เอดิเตอร์ที่ติดตั้ง",
|
||||
"status": "สถานะ",
|
||||
"install": "ติดตั้ง {{version}} ใน Unity Hub",
|
||||
"match": {
|
||||
"ok": "ตรงกัน",
|
||||
"missing": "ยังไม่ได้ติดตั้งเวอร์ชันนี้",
|
||||
"noEditor": "ยังไม่ได้ติดตั้งเอดิเตอร์",
|
||||
"unknown": "ลงชื่อเข้าใช้เพื่อตรวจสอบ"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "เกี่ยวกับ",
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
import type { SocialSnapshot, UserProfile, UserStatus } from "./types/user";
|
||||
import type { FavoriteWorldFolder, World, WorldSnapshot } from "./types/world";
|
||||
import type { Instance } from "./types/instance";
|
||||
import type { UnityStatus } from "./types/unity";
|
||||
import type { Avatar } from "./types/avatar";
|
||||
import type { RepoStats, StoredEntity } from "./types/repository";
|
||||
import type { AccountSettings, ContentFilterKey, Pending2Fa, RecoveryCode } from "./types/settings";
|
||||
@@ -91,6 +92,9 @@ export interface IpcRequests {
|
||||
"config:setGamePath": (p: { gamePath: string | null }) => IpcResult<AppConfig>;
|
||||
"config:pickGamePath": () => IpcResult<AppConfig>;
|
||||
|
||||
"unity:status": () => IpcResult<UnityStatus>;
|
||||
"unity:install": (url: string) => IpcResult<void>;
|
||||
|
||||
"game:status": () => IpcResult<GameStatus>;
|
||||
"game:launch": () => IpcResult<GameStatus>;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface AppConfig {
|
||||
version: string;
|
||||
gamePath: string | null;
|
||||
detectedGamePath: string | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface UnityStatus {
|
||||
hubInstalled: boolean;
|
||||
editorRoot: string | null;
|
||||
installedVersions: string[];
|
||||
requiredVersion: string | null;
|
||||
installUrl: string | null;
|
||||
match: "ok" | "missing" | "no-editor" | "unknown";
|
||||
}
|
||||
Reference in New Issue
Block a user