From 9b90140cfd9421e63e497853f489cc57f4e1b98a Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 30 Jun 2026 21:16:05 +0700 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20centralize=20a?= =?UTF-8?q?pp=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/config/appConfig.ts | 46 +++++++++++++- src/main/ipc/handlers.ts | 1 + src/renderer/src/components/AppShell.tsx | 26 ++++---- .../src/features/settings/SettingsView.tsx | 60 +++++++++++++------ src/renderer/src/lib/AppConfigContext.tsx | 52 ++++++++++++++++ src/renderer/src/lib/ThemeContext.tsx | 42 +++++++++---- src/renderer/src/lib/api.ts | 3 +- src/renderer/src/lib/debugSettings.ts | 16 +++++ src/renderer/src/lib/i18n/I18nContext.tsx | 15 +++-- src/renderer/src/lib/i18n/index.ts | 2 - .../src/lib/i18n/locales/en/settings.json | 7 +++ .../src/lib/i18n/locales/ja/settings.json | 7 +++ .../src/lib/i18n/locales/th/settings.json | 7 +++ src/renderer/src/lib/i18n/registry.ts | 33 ++-------- src/renderer/src/lib/i18n/types.ts | 4 +- src/renderer/src/lib/theme.ts | 46 +------------- src/renderer/src/main.tsx | 25 ++++---- src/shared/ipc.ts | 3 +- src/shared/locales.ts | 13 ++++ src/shared/types/appConfig.ts | 10 ++++ 20 files changed, 284 insertions(+), 134 deletions(-) create mode 100644 src/renderer/src/lib/AppConfigContext.tsx create mode 100644 src/renderer/src/lib/debugSettings.ts create mode 100644 src/shared/locales.ts diff --git a/src/main/config/appConfig.ts b/src/main/config/appConfig.ts index 560d02e..fec1f2b 100644 --- a/src/main/config/appConfig.ts +++ b/src/main/config/appConfig.ts @@ -2,18 +2,48 @@ import { app } from "electron"; import { join } from "node:path"; import { existsSync } from "node:fs"; import { jsonFile } from "../lib/jsonFile"; -import type { AppConfig, PreferredRegion } from "../../shared/types/appConfig"; +import type { AppConfig, AppPreferences, PreferredRegion } from "../../shared/types/appConfig"; +import { DEFAULT_LOCALE, isAppLocale } from "../../shared/locales"; import { detectedGamePath } from "../game/steam"; const path = () => join(app.getPath("userData"), "app-config.json"); type StoredConfig = Omit; -const DEFAULTS: StoredConfig = { gamePath: null, preferredRegion: "auto" }; +const DEFAULT_PREFERENCES: AppPreferences = { + schemeMode: "auto", + accent: null, + locale: DEFAULT_LOCALE, + showDebugNav: false, +}; +const DEFAULTS: StoredConfig = { + gamePath: null, + preferredRegion: "auto", + preferences: DEFAULT_PREFERENCES, +}; + +function normalizePreferences(raw: Partial | undefined): AppPreferences { + const schemeMode = raw?.schemeMode; + const locale = raw?.locale; + const accent = raw?.accent?.trim() || null; + return { + schemeMode: schemeMode === "light" || schemeMode === "dark" || schemeMode === "auto" ? schemeMode : "auto", + accent: accent && /^#[\da-f]{6}$/i.test(accent) ? accent : null, + locale: isAppLocale(locale) ? locale : DEFAULT_LOCALE, + showDebugNav: raw?.showDebugNav === true, + }; +} const configFile = jsonFile( path, () => ({ ...DEFAULTS }), - (raw) => ({ ...DEFAULTS, ...(raw as Partial) }), + (raw) => { + const stored = raw as Partial; + return { + ...DEFAULTS, + ...stored, + preferences: normalizePreferences(stored.preferences), + }; + }, ); const readStored = configFile.read; @@ -39,6 +69,16 @@ export function setPreferredRegion(region: PreferredRegion): AppConfig { return withDerived(next); } +export function setPreferences(patch: Partial): AppConfig { + const stored = readStored(); + const next: StoredConfig = { + ...stored, + preferences: normalizePreferences({ ...stored.preferences, ...patch }), + }; + writeStored(next); + return withDerived(next); +} + export function getConfig(): AppConfig { return withDerived(readStored()); } diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 4abc546..5da4e9e 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -135,6 +135,7 @@ const handlers = { if (p.region === "auto") void region.detectBestRegion(); return next; }), + "config:setPreferences": (p) => guard(async () => appConfig.setPreferences(p)), "region:detect": () => guard(() => region.detectBestRegion()), "region:ping": () => diff --git a/src/renderer/src/components/AppShell.tsx b/src/renderer/src/components/AppShell.tsx index b525265..8b2971f 100644 --- a/src/renderer/src/components/AppShell.tsx +++ b/src/renderer/src/components/AppShell.tsx @@ -33,6 +33,7 @@ import { LaunchButton } from "../features/game/LaunchButton"; import { NavProvider, useNav, type View } from "../features/navigation/NavContext"; import { useI18n } from "../lib/i18n"; import { api, events } from "../lib/api"; +import { useDebugNavVisible } from "../lib/debugSettings"; import "../styles/app-shell.css"; export function AppShell() { @@ -58,6 +59,7 @@ function Shell() { const { t } = useI18n(); const [leftOpen, setLeftOpen] = useState(true); const [friendsOpen, setFriendsOpen] = useState(true); + const showDebugNav = useDebugNavVisible(); useEffect( () => @@ -119,16 +121,20 @@ function Shell() { divider: true, onClick: () => nav.openSettings(), }, - { - id: "debug", - label: t("nav:debug"), - icon: Settings, - external: true, - onClick: (e) => { - if (e.ctrlKey || e.metaKey) void api.debug.cacheClear(); - else void api.debug.openWindow(); - }, - }, + ...(showDebugNav + ? [ + { + id: "debug", + label: t("nav:debug"), + icon: Settings, + external: true, + onClick: (e) => { + if (e.ctrlKey || e.metaKey) void api.debug.cacheClear(); + else void api.debug.openWindow(); + }, + } satisfies NavItem, + ] + : []), ]; const openProfile = (id: "me" | string) => nav.openUser(id); diff --git a/src/renderer/src/features/settings/SettingsView.tsx b/src/renderer/src/features/settings/SettingsView.tsx index 600018e..bf2573a 100644 --- a/src/renderer/src/features/settings/SettingsView.tsx +++ b/src/renderer/src/features/settings/SettingsView.tsx @@ -3,6 +3,7 @@ import { AlertTriangle, Boxes, Check, + Bug, Download, Droplet, FolderOpen, @@ -26,6 +27,8 @@ import type { AppConfig, PreferredRegion, RegionPing } from "../../../../shared/ import type { UnityStatus } from "../../../../shared/types/unity"; import { api, errorMessage } from "../../lib/api"; import { useAsync } from "../../lib/useAsync"; +import { useSetDebugNavVisible, useDebugNavVisible } from "../../lib/debugSettings"; +import { useAppConfig } from "../../lib/AppConfigContext"; import { regionFlag, regionLabel } from "../../lib/vrchat"; import { Button, @@ -34,12 +37,13 @@ import { PAGE_TITLE, SettingsSection as Section, Tabs, + Toggle, } 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" | "creator" | "about"; +type SettingsTab = "appearance" | "game" | "creator" | "debug" | "about"; export function SettingsView() { const { t } = useI18n(); @@ -56,6 +60,7 @@ export function SettingsView() { { id: "appearance", label: t("settings:tabs.appearance") }, { id: "game", label: t("settings:tabs.game") }, { id: "creator", label: t("settings:tabs.creator") }, + { id: "debug", label: t("settings:tabs.debug") }, { id: "about", label: t("settings:tabs.about") }, ]} active={tab} @@ -83,6 +88,12 @@ export function SettingsView() { ) : null} + {tab === "debug" ? ( +
+ +
+ ) : null} + {tab === "about" ? (
@@ -92,6 +103,27 @@ export function SettingsView() { ); } +function DebugSection() { + const { t } = useI18n(); + const showDebugNav = useDebugNavVisible(); + const setDebugNavVisible = useSetDebugNavVisible(); + return ( +
} + description={t("settings:debug.description")} + > +
+
+
{t("settings:debug.sidebar")}
+
{t("settings:debug.sidebarHint")}
+
+ +
+
+ ); +} + const SCHEME_OPTIONS: { mode: SchemeMode; labelKey: string; hintKey: string; icon: ReactNode }[] = [ { mode: "auto", @@ -259,18 +291,15 @@ function LanguageSection() { function GameSection() { const { t } = useI18n(); - const [config, setConfig] = useState(null); + const { config, setGamePath, pickGamePath } = useAppConfig(); const [path, setPath] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [ok, setOk] = useState(null); useEffect(() => { - api.config.get().then((c) => { - setConfig(c); - setPath(c.gamePath ?? ""); - }); - }, []); + setPath(config?.gamePath ?? ""); + }, [config?.gamePath]); async function run(p: Promise, okMsg: string) { setBusy(true); @@ -278,7 +307,6 @@ function GameSection() { setOk(null); try { const next = await p; - setConfig(next); setPath(next.gamePath ?? ""); setOk(okMsg); } catch (e) { @@ -307,7 +335,7 @@ function GameSection() {