mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
♻️ refactor: centralize app settings
This commit is contained in:
@@ -2,18 +2,48 @@ import { app } from "electron";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { jsonFile } from "../lib/jsonFile";
|
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";
|
import { detectedGamePath } from "../game/steam";
|
||||||
|
|
||||||
const path = () => join(app.getPath("userData"), "app-config.json");
|
const path = () => join(app.getPath("userData"), "app-config.json");
|
||||||
|
|
||||||
type StoredConfig = Omit<AppConfig, "version" | "detectedGamePath">;
|
type StoredConfig = Omit<AppConfig, "version" | "detectedGamePath">;
|
||||||
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<AppPreferences> | 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<StoredConfig>(
|
const configFile = jsonFile<StoredConfig>(
|
||||||
path,
|
path,
|
||||||
() => ({ ...DEFAULTS }),
|
() => ({ ...DEFAULTS }),
|
||||||
(raw) => ({ ...DEFAULTS, ...(raw as Partial<StoredConfig>) }),
|
(raw) => {
|
||||||
|
const stored = raw as Partial<StoredConfig>;
|
||||||
|
return {
|
||||||
|
...DEFAULTS,
|
||||||
|
...stored,
|
||||||
|
preferences: normalizePreferences(stored.preferences),
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const readStored = configFile.read;
|
const readStored = configFile.read;
|
||||||
|
|
||||||
@@ -39,6 +69,16 @@ export function setPreferredRegion(region: PreferredRegion): AppConfig {
|
|||||||
return withDerived(next);
|
return withDerived(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setPreferences(patch: Partial<AppPreferences>): AppConfig {
|
||||||
|
const stored = readStored();
|
||||||
|
const next: StoredConfig = {
|
||||||
|
...stored,
|
||||||
|
preferences: normalizePreferences({ ...stored.preferences, ...patch }),
|
||||||
|
};
|
||||||
|
writeStored(next);
|
||||||
|
return withDerived(next);
|
||||||
|
}
|
||||||
|
|
||||||
export function getConfig(): AppConfig {
|
export function getConfig(): AppConfig {
|
||||||
return withDerived(readStored());
|
return withDerived(readStored());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ const handlers = {
|
|||||||
if (p.region === "auto") void region.detectBestRegion();
|
if (p.region === "auto") void region.detectBestRegion();
|
||||||
return next;
|
return next;
|
||||||
}),
|
}),
|
||||||
|
"config:setPreferences": (p) => guard(async () => appConfig.setPreferences(p)),
|
||||||
|
|
||||||
"region:detect": () => guard(() => region.detectBestRegion()),
|
"region:detect": () => guard(() => region.detectBestRegion()),
|
||||||
"region:ping": () =>
|
"region:ping": () =>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { LaunchButton } from "../features/game/LaunchButton";
|
|||||||
import { NavProvider, useNav, type View } from "../features/navigation/NavContext";
|
import { NavProvider, useNav, type View } from "../features/navigation/NavContext";
|
||||||
import { useI18n } from "../lib/i18n";
|
import { useI18n } from "../lib/i18n";
|
||||||
import { api, events } from "../lib/api";
|
import { api, events } from "../lib/api";
|
||||||
|
import { useDebugNavVisible } from "../lib/debugSettings";
|
||||||
import "../styles/app-shell.css";
|
import "../styles/app-shell.css";
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
@@ -58,6 +59,7 @@ function Shell() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [leftOpen, setLeftOpen] = useState(true);
|
const [leftOpen, setLeftOpen] = useState(true);
|
||||||
const [friendsOpen, setFriendsOpen] = useState(true);
|
const [friendsOpen, setFriendsOpen] = useState(true);
|
||||||
|
const showDebugNav = useDebugNavVisible();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
@@ -119,16 +121,20 @@ function Shell() {
|
|||||||
divider: true,
|
divider: true,
|
||||||
onClick: () => nav.openSettings(),
|
onClick: () => nav.openSettings(),
|
||||||
},
|
},
|
||||||
{
|
...(showDebugNav
|
||||||
id: "debug",
|
? [
|
||||||
label: t("nav:debug"),
|
{
|
||||||
icon: Settings,
|
id: "debug",
|
||||||
external: true,
|
label: t("nav:debug"),
|
||||||
onClick: (e) => {
|
icon: Settings,
|
||||||
if (e.ctrlKey || e.metaKey) void api.debug.cacheClear();
|
external: true,
|
||||||
else void api.debug.openWindow();
|
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);
|
const openProfile = (id: "me" | string) => nav.openUser(id);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Boxes,
|
Boxes,
|
||||||
Check,
|
Check,
|
||||||
|
Bug,
|
||||||
Download,
|
Download,
|
||||||
Droplet,
|
Droplet,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
@@ -26,6 +27,8 @@ import type { AppConfig, PreferredRegion, RegionPing } from "../../../../shared/
|
|||||||
import type { UnityStatus } from "../../../../shared/types/unity";
|
import type { UnityStatus } from "../../../../shared/types/unity";
|
||||||
import { api, errorMessage } from "../../lib/api";
|
import { api, errorMessage } from "../../lib/api";
|
||||||
import { useAsync } from "../../lib/useAsync";
|
import { useAsync } from "../../lib/useAsync";
|
||||||
|
import { useSetDebugNavVisible, useDebugNavVisible } from "../../lib/debugSettings";
|
||||||
|
import { useAppConfig } from "../../lib/AppConfigContext";
|
||||||
import { regionFlag, regionLabel } from "../../lib/vrchat";
|
import { regionFlag, regionLabel } from "../../lib/vrchat";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -34,12 +37,13 @@ import {
|
|||||||
PAGE_TITLE,
|
PAGE_TITLE,
|
||||||
SettingsSection as Section,
|
SettingsSection as Section,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
Toggle,
|
||||||
} from "../../components/ui";
|
} from "../../components/ui";
|
||||||
|
|
||||||
const SHELL = "mx-auto flex w-full max-w-[760px] flex-col gap-[18px] px-12 pb-16 pt-10";
|
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]";
|
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() {
|
export function SettingsView() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -56,6 +60,7 @@ export function SettingsView() {
|
|||||||
{ id: "appearance", label: t("settings:tabs.appearance") },
|
{ id: "appearance", label: t("settings:tabs.appearance") },
|
||||||
{ id: "game", label: t("settings:tabs.game") },
|
{ id: "game", label: t("settings:tabs.game") },
|
||||||
{ id: "creator", label: t("settings:tabs.creator") },
|
{ id: "creator", label: t("settings:tabs.creator") },
|
||||||
|
{ id: "debug", label: t("settings:tabs.debug") },
|
||||||
{ id: "about", label: t("settings:tabs.about") },
|
{ id: "about", label: t("settings:tabs.about") },
|
||||||
]}
|
]}
|
||||||
active={tab}
|
active={tab}
|
||||||
@@ -83,6 +88,12 @@ export function SettingsView() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{tab === "debug" ? (
|
||||||
|
<div className={SECTIONS}>
|
||||||
|
<DebugSection />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{tab === "about" ? (
|
{tab === "about" ? (
|
||||||
<div className={SECTIONS}>
|
<div className={SECTIONS}>
|
||||||
<AboutSection />
|
<AboutSection />
|
||||||
@@ -92,6 +103,27 @@ export function SettingsView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DebugSection() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const showDebugNav = useDebugNavVisible();
|
||||||
|
const setDebugNavVisible = useSetDebugNavVisible();
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
title={t("settings:debug.title")}
|
||||||
|
icon={<Bug size={16} />}
|
||||||
|
description={t("settings:debug.description")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-lg border border-border bg-surface-2 p-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-[13px] font-semibold text-text">{t("settings:debug.sidebar")}</div>
|
||||||
|
<div className="mt-0.5 text-[12px] text-muted">{t("settings:debug.sidebarHint")}</div>
|
||||||
|
</div>
|
||||||
|
<Toggle checked={showDebugNav} onChange={setDebugNavVisible} />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SCHEME_OPTIONS: { mode: SchemeMode; labelKey: string; hintKey: string; icon: ReactNode }[] = [
|
const SCHEME_OPTIONS: { mode: SchemeMode; labelKey: string; hintKey: string; icon: ReactNode }[] = [
|
||||||
{
|
{
|
||||||
mode: "auto",
|
mode: "auto",
|
||||||
@@ -259,18 +291,15 @@ function LanguageSection() {
|
|||||||
|
|
||||||
function GameSection() {
|
function GameSection() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
const { config, setGamePath, pickGamePath } = useAppConfig();
|
||||||
const [path, setPath] = useState("");
|
const [path, setPath] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [ok, setOk] = useState<string | null>(null);
|
const [ok, setOk] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.config.get().then((c) => {
|
setPath(config?.gamePath ?? "");
|
||||||
setConfig(c);
|
}, [config?.gamePath]);
|
||||||
setPath(c.gamePath ?? "");
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function run(p: Promise<AppConfig>, okMsg: string) {
|
async function run(p: Promise<AppConfig>, okMsg: string) {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
@@ -278,7 +307,6 @@ function GameSection() {
|
|||||||
setOk(null);
|
setOk(null);
|
||||||
try {
|
try {
|
||||||
const next = await p;
|
const next = await p;
|
||||||
setConfig(next);
|
|
||||||
setPath(next.gamePath ?? "");
|
setPath(next.gamePath ?? "");
|
||||||
setOk(okMsg);
|
setOk(okMsg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -307,7 +335,7 @@ function GameSection() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => void run(api.config.pickGamePath(), t("settings:game.saved"))}
|
onClick={() => void run(pickGamePath(), t("settings:game.saved"))}
|
||||||
>
|
>
|
||||||
<FolderOpen size={14} />
|
<FolderOpen size={14} />
|
||||||
{t("settings:game.browse")}
|
{t("settings:game.browse")}
|
||||||
@@ -339,7 +367,7 @@ function GameSection() {
|
|||||||
<div className="mt-3 flex flex-wrap items-center gap-2.5">
|
<div className="mt-3 flex flex-wrap items-center gap-2.5">
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void run(api.config.setGamePath(path.trim() || null), t("settings:game.saved"))
|
void run(setGamePath(path.trim() || null), t("settings:game.saved"))
|
||||||
}
|
}
|
||||||
loading={busy}
|
loading={busy}
|
||||||
disabled={!dirty}
|
disabled={!dirty}
|
||||||
@@ -349,7 +377,7 @@ function GameSection() {
|
|||||||
{config?.gamePath ? (
|
{config?.gamePath ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => void run(api.config.setGamePath(null), t("settings:game.resetDone"))}
|
onClick={() => void run(setGamePath(null), t("settings:game.resetDone"))}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
{t("settings:game.reset")}
|
{t("settings:game.reset")}
|
||||||
@@ -365,20 +393,16 @@ const REGION_OPTIONS: PreferredRegion[] = ["auto", "us", "use", "eu", "jp"];
|
|||||||
|
|
||||||
function RegionSection() {
|
function RegionSection() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [region, setRegion] = useState<PreferredRegion>("auto");
|
const { config, setPreferredRegion } = useAppConfig();
|
||||||
const [ok, setOk] = useState<string | null>(null);
|
const [ok, setOk] = useState<string | null>(null);
|
||||||
const [pings, setPings] = useState<RegionPing[] | null>(null);
|
const [pings, setPings] = useState<RegionPing[] | null>(null);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const region = config?.preferredRegion ?? "auto";
|
||||||
api.config.get().then((c) => setRegion(c.preferredRegion));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const choose = async (r: PreferredRegion) => {
|
const choose = async (r: PreferredRegion) => {
|
||||||
setRegion(r);
|
|
||||||
setOk(null);
|
setOk(null);
|
||||||
const next = await api.config.setPreferredRegion(r);
|
await setPreferredRegion(r);
|
||||||
setRegion(next.preferredRegion);
|
|
||||||
setOk(t("settings:region.saved"));
|
setOk(t("settings:region.saved"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
|
import type { AppConfig, AppPreferences, PreferredRegion } from "../../../shared/types/appConfig";
|
||||||
|
import { api } from "./api";
|
||||||
|
|
||||||
|
interface AppConfigContextValue {
|
||||||
|
config: AppConfig | null;
|
||||||
|
setGamePath: (gamePath: string | null) => Promise<AppConfig>;
|
||||||
|
pickGamePath: () => Promise<AppConfig>;
|
||||||
|
setPreferredRegion: (region: PreferredRegion) => Promise<AppConfig>;
|
||||||
|
setPreferences: (patch: Partial<AppPreferences>) => Promise<AppConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppConfigContext = createContext<AppConfigContextValue | null>(null);
|
||||||
|
|
||||||
|
export function AppConfigProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
void api.config.get().then((next) => {
|
||||||
|
if (!cancelled) setConfig(next);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const commit = useCallback(async (next: Promise<AppConfig>) => {
|
||||||
|
const config = await next;
|
||||||
|
setConfig(config);
|
||||||
|
return config;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo<AppConfigContextValue>(
|
||||||
|
() => ({
|
||||||
|
config,
|
||||||
|
setGamePath: (gamePath) => commit(api.config.setGamePath(gamePath)),
|
||||||
|
pickGamePath: () => commit(api.config.pickGamePath()),
|
||||||
|
setPreferredRegion: (region) => commit(api.config.setPreferredRegion(region)),
|
||||||
|
setPreferences: (patch) => commit(api.config.setPreferences(patch)),
|
||||||
|
}),
|
||||||
|
[config, commit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <AppConfigContext.Provider value={value}>{children}</AppConfigContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppConfig(): AppConfigContextValue {
|
||||||
|
const ctx = useContext(AppConfigContext);
|
||||||
|
if (!ctx) throw new Error("useAppConfig must be used within <AppConfigProvider>");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -12,13 +13,11 @@ import {
|
|||||||
applyTheme,
|
applyTheme,
|
||||||
builtInThemes,
|
builtInThemes,
|
||||||
initialTheme,
|
initialTheme,
|
||||||
persistScheme,
|
|
||||||
storedAccent,
|
|
||||||
storedScheme,
|
|
||||||
systemScheme,
|
systemScheme,
|
||||||
type SchemeMode,
|
type SchemeMode,
|
||||||
type Theme,
|
type Theme,
|
||||||
} from "./theme";
|
} from "./theme";
|
||||||
|
import { useAppConfig } from "./AppConfigContext";
|
||||||
|
|
||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
@@ -34,44 +33,64 @@ interface ThemeContextValue {
|
|||||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
|
const { config, setPreferences } = useAppConfig();
|
||||||
const [themes, setThemes] = useState<Theme[]>(builtInThemes);
|
const [themes, setThemes] = useState<Theme[]>(builtInThemes);
|
||||||
const [theme, setActive] = useState<Theme>(() => {
|
const [theme, setActive] = useState<Theme>(() => {
|
||||||
const t = initialTheme(builtInThemes);
|
const t = initialTheme(builtInThemes, "auto");
|
||||||
applyTheme(t);
|
applyTheme(t);
|
||||||
|
applyAccent(null);
|
||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
const [schemeMode, setMode] = useState<SchemeMode>(() => storedScheme());
|
const [schemeMode, setMode] = useState<SchemeMode>("auto");
|
||||||
const [accent, setAccentState] = useState<string | null>(() => storedAccent());
|
const [accent, setAccentState] = useState<string | null>(null);
|
||||||
|
const lastApplied = useRef<string>("");
|
||||||
|
|
||||||
const setAccent = useCallback((hex: string | null) => {
|
const setAccent = useCallback((hex: string | null) => {
|
||||||
applyAccent(hex);
|
applyAccent(hex);
|
||||||
setAccentState(hex);
|
setAccentState(hex);
|
||||||
}, []);
|
void setPreferences({ accent: hex });
|
||||||
|
}, [setPreferences]);
|
||||||
|
|
||||||
const setTheme = useCallback(
|
const setTheme = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const next = themes.find((t) => t.id === id);
|
const next = themes.find((t) => t.id === id);
|
||||||
if (!next) return;
|
if (!next) return;
|
||||||
applyTheme(next);
|
applyTheme(next);
|
||||||
|
applyAccent(accent);
|
||||||
setActive(next);
|
setActive(next);
|
||||||
},
|
},
|
||||||
[themes],
|
[themes, accent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setSchemeMode = useCallback(
|
const setSchemeMode = useCallback(
|
||||||
(mode: SchemeMode) => {
|
(mode: SchemeMode) => {
|
||||||
persistScheme(mode);
|
|
||||||
setMode(mode);
|
setMode(mode);
|
||||||
const target = mode === "auto" ? systemScheme() : mode;
|
const target = mode === "auto" ? systemScheme() : mode;
|
||||||
const next = themes.find((t) => t.scheme === target);
|
const next = themes.find((t) => t.scheme === target);
|
||||||
if (next) {
|
if (next) {
|
||||||
applyTheme(next);
|
applyTheme(next);
|
||||||
|
applyAccent(accent);
|
||||||
setActive(next);
|
setActive(next);
|
||||||
}
|
}
|
||||||
|
void setPreferences({ schemeMode: mode });
|
||||||
},
|
},
|
||||||
[themes],
|
[themes, accent, setPreferences],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prefs = config?.preferences;
|
||||||
|
if (!prefs) return;
|
||||||
|
const key = `${prefs.schemeMode}:${prefs.accent ?? ""}`;
|
||||||
|
if (key === lastApplied.current) return;
|
||||||
|
lastApplied.current = key;
|
||||||
|
setMode(prefs.schemeMode);
|
||||||
|
setAccentState(prefs.accent);
|
||||||
|
const next = initialTheme(themes, prefs.schemeMode);
|
||||||
|
applyTheme(next);
|
||||||
|
applyAccent(prefs.accent);
|
||||||
|
setActive(next);
|
||||||
|
}, [config?.preferences, themes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schemeMode !== "auto") return;
|
if (schemeMode !== "auto") return;
|
||||||
const mq = window.matchMedia("(prefers-color-scheme: light)");
|
const mq = window.matchMedia("(prefers-color-scheme: light)");
|
||||||
@@ -79,12 +98,13 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
const next = themes.find((t) => t.scheme === systemScheme());
|
const next = themes.find((t) => t.scheme === systemScheme());
|
||||||
if (next) {
|
if (next) {
|
||||||
applyTheme(next);
|
applyTheme(next);
|
||||||
|
applyAccent(accent);
|
||||||
setActive(next);
|
setActive(next);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
mq.addEventListener("change", onChange);
|
mq.addEventListener("change", onChange);
|
||||||
return () => mq.removeEventListener("change", onChange);
|
return () => mq.removeEventListener("change", onChange);
|
||||||
}, [schemeMode, themes]);
|
}, [schemeMode, themes, accent]);
|
||||||
|
|
||||||
const registerTheme = useCallback((custom: Theme, activate = false) => {
|
const registerTheme = useCallback((custom: Theme, activate = false) => {
|
||||||
setThemes((prev) => {
|
setThemes((prev) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ContentFilterKey } from "../../../shared/types/settings";
|
|||||||
import type { UserStatus } from "../../../shared/types/user";
|
import type { UserStatus } from "../../../shared/types/user";
|
||||||
import type { EnhancementId } from "../../../shared/types/enhancements";
|
import type { EnhancementId } from "../../../shared/types/enhancements";
|
||||||
import type { CreateInstanceInput } from "../../../shared/types/instance";
|
import type { CreateInstanceInput } from "../../../shared/types/instance";
|
||||||
import type { PreferredRegion } from "../../../shared/types/appConfig";
|
import type { AppPreferences, PreferredRegion } from "../../../shared/types/appConfig";
|
||||||
import type { AvatarEdit, FavoriteGroupEdit, MoveResult } from "../../../shared/types/avatar";
|
import type { AvatarEdit, FavoriteGroupEdit, MoveResult } from "../../../shared/types/avatar";
|
||||||
import type {
|
import type {
|
||||||
FavoriteGroupEdit as WorldFavoriteGroupEdit,
|
FavoriteGroupEdit as WorldFavoriteGroupEdit,
|
||||||
@@ -150,6 +150,7 @@ export const api = {
|
|||||||
setGamePath: (gamePath: string | null) => call("config:setGamePath", { gamePath }),
|
setGamePath: (gamePath: string | null) => call("config:setGamePath", { gamePath }),
|
||||||
pickGamePath: () => call("config:pickGamePath"),
|
pickGamePath: () => call("config:pickGamePath"),
|
||||||
setPreferredRegion: (region: PreferredRegion) => call("config:setPreferredRegion", { region }),
|
setPreferredRegion: (region: PreferredRegion) => call("config:setPreferredRegion", { region }),
|
||||||
|
setPreferences: (p: Partial<AppPreferences>) => call("config:setPreferences", p),
|
||||||
},
|
},
|
||||||
region: {
|
region: {
|
||||||
detect: () => call("region:detect"),
|
detect: () => call("region:detect"),
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useAppConfig } from "./AppConfigContext";
|
||||||
|
|
||||||
|
export function useDebugNavVisible(): boolean {
|
||||||
|
return useAppConfig().config?.preferences.showDebugNav ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetDebugNavVisible(): (visible: boolean) => void {
|
||||||
|
const { setPreferences } = useAppConfig();
|
||||||
|
return useCallback(
|
||||||
|
(visible: boolean) => {
|
||||||
|
void setPreferences({ showDebugNav: visible });
|
||||||
|
},
|
||||||
|
[setPreferences],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useCallback, type ReactNode } from "react";
|
import { useCallback, useEffect, type ReactNode } from "react";
|
||||||
import { I18nextProvider, useTranslation } from "react-i18next";
|
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||||
import type { Locale, LocaleCode } from "./types";
|
import type { Locale, LocaleCode } from "./types";
|
||||||
import { availableLocales, getLocale, i18n, persistLocale } from "./registry";
|
import { availableLocales, getLocale, i18n } from "./registry";
|
||||||
|
import { useAppConfig } from "../AppConfigContext";
|
||||||
|
|
||||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||||
|
const { config } = useAppConfig();
|
||||||
|
useEffect(() => {
|
||||||
|
const locale = config?.preferences.locale;
|
||||||
|
if (locale && i18n.language !== locale) void i18n.changeLanguage(locale);
|
||||||
|
}, [config?.preferences.locale]);
|
||||||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,15 +22,16 @@ interface I18nValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useI18n(): I18nValue {
|
export function useI18n(): I18nValue {
|
||||||
|
const { setPreferences } = useAppConfig();
|
||||||
const { t, i18n: instance } = useTranslation();
|
const { t, i18n: instance } = useTranslation();
|
||||||
const locale = instance.language as LocaleCode;
|
const locale = instance.language as LocaleCode;
|
||||||
|
|
||||||
const setLocale = useCallback(
|
const setLocale = useCallback(
|
||||||
(code: LocaleCode) => {
|
(code: LocaleCode) => {
|
||||||
persistLocale(code);
|
|
||||||
void instance.changeLanguage(code);
|
void instance.changeLanguage(code);
|
||||||
|
void setPreferences({ locale: code });
|
||||||
},
|
},
|
||||||
[instance],
|
[instance, setPreferences],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { locale, current: getLocale(locale), locales: availableLocales(), setLocale, t };
|
return { locale, current: getLocale(locale), locales: availableLocales(), setLocale, t };
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export {
|
|||||||
NAMESPACES,
|
NAMESPACES,
|
||||||
availableLocales,
|
availableLocales,
|
||||||
getLocale,
|
getLocale,
|
||||||
storedLocale,
|
|
||||||
persistLocale,
|
|
||||||
i18n,
|
i18n,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
export type { Locale, LocaleCode, LocaleMeta, Messages } from "./types";
|
export type { Locale, LocaleCode, LocaleMeta, Messages } from "./types";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"game": "Game",
|
"game": "Game",
|
||||||
"creator": "Creator",
|
"creator": "Creator",
|
||||||
|
"debug": "Debug",
|
||||||
"about": "About"
|
"about": "About"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
@@ -83,6 +84,12 @@
|
|||||||
"version": "Version",
|
"version": "Version",
|
||||||
"disclaimer": "Unofficial. Not affiliated with or endorsed by VRChat Inc."
|
"disclaimer": "Unofficial. Not affiliated with or endorsed by VRChat Inc."
|
||||||
},
|
},
|
||||||
|
"debug": {
|
||||||
|
"title": "Debug",
|
||||||
|
"description": "Developer and troubleshooting options.",
|
||||||
|
"sidebar": "Show Debug in the sidebar",
|
||||||
|
"sidebarHint": "Adds the Debug shortcut back to the left navigation."
|
||||||
|
},
|
||||||
"region": {
|
"region": {
|
||||||
"title": "Instance Region",
|
"title": "Instance Region",
|
||||||
"description": "The region new instances you create are hosted in. Auto picks the lowest-latency region for your connection.",
|
"description": "The region new instances you create are hosted in. Auto picks the lowest-latency region for your connection.",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"appearance": "外観",
|
"appearance": "外観",
|
||||||
"game": "ゲーム",
|
"game": "ゲーム",
|
||||||
"creator": "クリエイター",
|
"creator": "クリエイター",
|
||||||
|
"debug": "デバッグ",
|
||||||
"about": "情報"
|
"about": "情報"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
@@ -83,6 +84,12 @@
|
|||||||
"version": "バージョン",
|
"version": "バージョン",
|
||||||
"disclaimer": "非公式です。VRChat Inc. とは提携しておらず、承認も受けていません。"
|
"disclaimer": "非公式です。VRChat Inc. とは提携しておらず、承認も受けていません。"
|
||||||
},
|
},
|
||||||
|
"debug": {
|
||||||
|
"title": "デバッグ",
|
||||||
|
"description": "開発者向けとトラブルシューティング用の設定です。",
|
||||||
|
"sidebar": "サイドバーにデバッグを表示",
|
||||||
|
"sidebarHint": "左のナビゲーションにデバッグのショートカットを表示します。"
|
||||||
|
},
|
||||||
"region": {
|
"region": {
|
||||||
"title": "インスタンスのリージョン",
|
"title": "インスタンスのリージョン",
|
||||||
"description": "作成する新しいインスタンスのホスト地域です。自動を選ぶと、接続に最も遅延の少ないリージョンが選ばれます。",
|
"description": "作成する新しいインスタンスのホスト地域です。自動を選ぶと、接続に最も遅延の少ないリージョンが選ばれます。",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"appearance": "รูปลักษณ์",
|
"appearance": "รูปลักษณ์",
|
||||||
"game": "เกม",
|
"game": "เกม",
|
||||||
"creator": "ครีเอเตอร์",
|
"creator": "ครีเอเตอร์",
|
||||||
|
"debug": "ดีบัก",
|
||||||
"about": "เกี่ยวกับ"
|
"about": "เกี่ยวกับ"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
@@ -83,6 +84,12 @@
|
|||||||
"version": "เวอร์ชัน",
|
"version": "เวอร์ชัน",
|
||||||
"disclaimer": "ไม่เป็นทางการ ไม่ได้มีส่วนเกี่ยวข้องหรือได้รับการรับรองจาก VRChat Inc."
|
"disclaimer": "ไม่เป็นทางการ ไม่ได้มีส่วนเกี่ยวข้องหรือได้รับการรับรองจาก VRChat Inc."
|
||||||
},
|
},
|
||||||
|
"debug": {
|
||||||
|
"title": "ดีบัก",
|
||||||
|
"description": "ตัวเลือกสำหรับนักพัฒนาและการแก้ปัญหา",
|
||||||
|
"sidebar": "แสดงดีบักในแถบด้านข้าง",
|
||||||
|
"sidebarHint": "เพิ่มทางลัดดีบักกลับไปที่เมนูนำทางด้านซ้าย"
|
||||||
|
},
|
||||||
"region": {
|
"region": {
|
||||||
"title": "ภูมิภาคของอินสแตนซ์",
|
"title": "ภูมิภาคของอินสแตนซ์",
|
||||||
"description": "ภูมิภาคที่ใช้โฮสต์อินสแตนซ์ใหม่ที่คุณสร้าง อัตโนมัติจะเลือกภูมิภาคที่มีความหน่วงต่ำที่สุดสำหรับการเชื่อมต่อของคุณ",
|
"description": "ภูมิภาคที่ใช้โฮสต์อินสแตนซ์ใหม่ที่คุณสร้าง อัตโนมัติจะเลือกภูมิภาคที่มีความหน่วงต่ำที่สุดสำหรับการเชื่อมต่อของคุณ",
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import i18n, { type Resource, type ResourceLanguage } from "i18next";
|
import i18n, { type Resource, type ResourceLanguage } from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import type { Locale, LocaleCode, LocaleMeta } from "./types";
|
import { DEFAULT_LOCALE, LOCALES } from "../../../../shared/locales";
|
||||||
|
import type { Locale, LocaleCode } from "./types";
|
||||||
|
|
||||||
export const DEFAULT_LOCALE: LocaleCode = "en";
|
export { DEFAULT_LOCALE } from "../../../../shared/locales";
|
||||||
|
|
||||||
const STORAGE_KEY = "vrc-circle.locale";
|
|
||||||
|
|
||||||
const META: Record<LocaleCode, LocaleMeta> = {
|
|
||||||
en: { code: "en", nativeName: "English", englishName: "English" },
|
|
||||||
ja: { code: "ja", nativeName: "日本語", englishName: "Japanese" },
|
|
||||||
th: { code: "th", nativeName: "ไทย", englishName: "Thai" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const files = import.meta.glob<{ default: Record<string, unknown> }>("./locales/*/*.json", {
|
const files = import.meta.glob<{ default: Record<string, unknown> }>("./locales/*/*.json", {
|
||||||
eager: true,
|
eager: true,
|
||||||
@@ -29,11 +22,11 @@ export const NAMESPACES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function buildLocale(code: LocaleCode): Locale {
|
function buildLocale(code: LocaleCode): Locale {
|
||||||
return { meta: META[code], messages: resources[code] ?? {} };
|
return { meta: LOCALES.find((locale) => locale.code === code)!, messages: resources[code] ?? {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
const registry = new Map<LocaleCode, Locale>(
|
const registry = new Map<LocaleCode, Locale>(
|
||||||
(Object.keys(META) as LocaleCode[]).map((code) => [code, buildLocale(code)]),
|
LOCALES.map((locale) => [locale.code, buildLocale(locale.code)]),
|
||||||
);
|
);
|
||||||
|
|
||||||
export function availableLocales(): Locale[] {
|
export function availableLocales(): Locale[] {
|
||||||
@@ -44,22 +37,8 @@ export function getLocale(code: LocaleCode): Locale {
|
|||||||
return registry.get(code) ?? registry.get(DEFAULT_LOCALE)!;
|
return registry.get(code) ?? registry.get(DEFAULT_LOCALE)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storedLocale(): LocaleCode {
|
|
||||||
try {
|
|
||||||
const v = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (v && registry.has(v as LocaleCode)) return v as LocaleCode;
|
|
||||||
} catch {}
|
|
||||||
return DEFAULT_LOCALE;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function persistLocale(code: LocaleCode): void {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, code);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
void i18n.use(initReactI18next).init({
|
void i18n.use(initReactI18next).init({
|
||||||
lng: storedLocale(),
|
lng: DEFAULT_LOCALE,
|
||||||
fallbackLng: DEFAULT_LOCALE,
|
fallbackLng: DEFAULT_LOCALE,
|
||||||
ns: NAMESPACES,
|
ns: NAMESPACES,
|
||||||
defaultNS: "common",
|
defaultNS: "common",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export type LocaleCode = "en" | "ja" | "th";
|
import type { AppLocale } from "../../../../shared/locales";
|
||||||
|
|
||||||
|
export type LocaleCode = AppLocale;
|
||||||
|
|
||||||
export interface LocaleMeta {
|
export interface LocaleMeta {
|
||||||
code: LocaleCode;
|
code: LocaleCode;
|
||||||
|
|||||||
@@ -88,10 +88,6 @@ export const lightTheme: Theme = {
|
|||||||
|
|
||||||
export const builtInThemes: Theme[] = [darkTheme, lightTheme];
|
export const builtInThemes: Theme[] = [darkTheme, lightTheme];
|
||||||
|
|
||||||
const STORAGE_KEY = "vrc-circle.theme";
|
|
||||||
const ACCENT_KEY = "vrc-circle.accent";
|
|
||||||
const SCHEME_KEY = "vrc-circle.scheme";
|
|
||||||
|
|
||||||
export type SchemeMode = "light" | "dark" | "auto";
|
export type SchemeMode = "light" | "dark" | "auto";
|
||||||
|
|
||||||
export const ACCENT_PRESETS: { key: string; name: string; value: string }[] = [
|
export const ACCENT_PRESETS: { key: string; name: string; value: string }[] = [
|
||||||
@@ -114,10 +110,6 @@ export function applyTheme(theme: Theme): void {
|
|||||||
}
|
}
|
||||||
root.style.colorScheme = theme.scheme;
|
root.style.colorScheme = theme.scheme;
|
||||||
root.dataset.theme = theme.id;
|
root.dataset.theme = theme.id;
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, theme.id);
|
|
||||||
} catch {}
|
|
||||||
applyAccent(storedAccent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyAccent(hex: string | null): void {
|
export function applyAccent(hex: string | null): void {
|
||||||
@@ -126,18 +118,6 @@ export function applyAccent(hex: string | null): void {
|
|||||||
root.style.setProperty("--accent", accent);
|
root.style.setProperty("--accent", accent);
|
||||||
root.style.setProperty("--on-accent", readableOn(accent));
|
root.style.setProperty("--on-accent", readableOn(accent));
|
||||||
root.style.setProperty("--accent-weak", withAlpha(accent, 0.14));
|
root.style.setProperty("--accent-weak", withAlpha(accent, 0.14));
|
||||||
try {
|
|
||||||
if (hex) localStorage.setItem(ACCENT_KEY, hex);
|
|
||||||
else localStorage.removeItem(ACCENT_KEY);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function storedAccent(): string | null {
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(ACCENT_KEY);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHex(hex: string): [number, number, number] | null {
|
function parseHex(hex: string): [number, number, number] | null {
|
||||||
@@ -164,21 +144,11 @@ function readableOn(hex: string): string {
|
|||||||
return luminance > 0.45 ? "#15151b" : "#ffffff";
|
return luminance > 0.45 ? "#15151b" : "#ffffff";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initialTheme(registry: Theme[]): Theme {
|
export function initialTheme(registry: Theme[], mode: SchemeMode): Theme {
|
||||||
const mode = storedScheme();
|
|
||||||
if (mode !== "auto") {
|
if (mode !== "auto") {
|
||||||
const pick = registry.find((t) => t.scheme === mode);
|
const pick = registry.find((t) => t.scheme === mode);
|
||||||
if (pick) return pick;
|
if (pick) return pick;
|
||||||
}
|
}
|
||||||
|
|
||||||
let storedId: string | null = null;
|
|
||||||
try {
|
|
||||||
storedId = localStorage.getItem(STORAGE_KEY);
|
|
||||||
} catch {}
|
|
||||||
if (mode !== "auto") {
|
|
||||||
const stored = registry.find((t) => t.id === storedId);
|
|
||||||
if (stored) return stored;
|
|
||||||
}
|
|
||||||
return systemScheme() === "light" ? lightTheme : darkTheme;
|
return systemScheme() === "light" ? lightTheme : darkTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,17 +157,3 @@ export function systemScheme(): "light" | "dark" {
|
|||||||
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: light)").matches;
|
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
return prefersLight ? "light" : "dark";
|
return prefersLight ? "light" : "dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storedScheme(): SchemeMode {
|
|
||||||
try {
|
|
||||||
const v = localStorage.getItem(SCHEME_KEY);
|
|
||||||
if (v === "light" || v === "dark" || v === "auto") return v;
|
|
||||||
} catch {}
|
|
||||||
return "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function persistScheme(mode: SchemeMode): void {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(SCHEME_KEY, mode);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|||||||
+14
-11
@@ -2,6 +2,7 @@ import { StrictMode } from "react";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { ThemeProvider } from "./lib/ThemeContext";
|
import { ThemeProvider } from "./lib/ThemeContext";
|
||||||
import { I18nProvider } from "./lib/i18n";
|
import { I18nProvider } from "./lib/i18n";
|
||||||
|
import { AppConfigProvider } from "./lib/AppConfigContext";
|
||||||
import { AuthProvider } from "./features/auth/AuthContext";
|
import { AuthProvider } from "./features/auth/AuthContext";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { MAC_CONTENT_INSET } from "../../shared/window";
|
import { MAC_CONTENT_INSET } from "../../shared/window";
|
||||||
@@ -19,16 +20,18 @@ if (window.api.platform === "darwin") {
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ThemeProvider>
|
<AppConfigProvider>
|
||||||
<I18nProvider>
|
<ThemeProvider>
|
||||||
{isDebugWindow ? (
|
<I18nProvider>
|
||||||
<DebugWindow />
|
{isDebugWindow ? (
|
||||||
) : (
|
<DebugWindow />
|
||||||
<AuthProvider>
|
) : (
|
||||||
<App />
|
<AuthProvider>
|
||||||
</AuthProvider>
|
<App />
|
||||||
)}
|
</AuthProvider>
|
||||||
</I18nProvider>
|
)}
|
||||||
</ThemeProvider>
|
</I18nProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AppConfigProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-1
@@ -29,7 +29,7 @@ import type { AccountSettings, ContentFilterKey, Pending2Fa, RecoveryCode } from
|
|||||||
import type { Group, GroupSnapshot } from "./types/group";
|
import type { Group, GroupSnapshot } from "./types/group";
|
||||||
import type { EnhancementId, EnhancementsSnapshot } from "./types/enhancements";
|
import type { EnhancementId, EnhancementsSnapshot } from "./types/enhancements";
|
||||||
import type { GallerySnapshot, Photo, ThumbCacheStats } from "./types/gallery";
|
import type { GallerySnapshot, Photo, ThumbCacheStats } from "./types/gallery";
|
||||||
import type { AppConfig, PreferredRegion, RegionPing } from "./types/appConfig";
|
import type { AppConfig, AppPreferences, PreferredRegion, RegionPing } from "./types/appConfig";
|
||||||
import type { GameStatus } from "./types/game";
|
import type { GameStatus } from "./types/game";
|
||||||
import type { IpcResult } from "./types/result";
|
import type { IpcResult } from "./types/result";
|
||||||
import type { CacheEntryInfo, CacheStats, DebugSnapshot, LogEntry, WsEvent } from "./types/debug";
|
import type { CacheEntryInfo, CacheStats, DebugSnapshot, LogEntry, WsEvent } from "./types/debug";
|
||||||
@@ -150,6 +150,7 @@ export interface IpcRequests {
|
|||||||
"config:setGamePath": (p: { gamePath: string | null }) => IpcResult<AppConfig>;
|
"config:setGamePath": (p: { gamePath: string | null }) => IpcResult<AppConfig>;
|
||||||
"config:pickGamePath": () => IpcResult<AppConfig>;
|
"config:pickGamePath": () => IpcResult<AppConfig>;
|
||||||
"config:setPreferredRegion": (p: { region: PreferredRegion }) => IpcResult<AppConfig>;
|
"config:setPreferredRegion": (p: { region: PreferredRegion }) => IpcResult<AppConfig>;
|
||||||
|
"config:setPreferences": (p: Partial<AppPreferences>) => IpcResult<AppConfig>;
|
||||||
|
|
||||||
"region:detect": () => IpcResult<InstanceRegion>;
|
"region:detect": () => IpcResult<InstanceRegion>;
|
||||||
"region:ping": () => IpcResult<RegionPing[]>;
|
"region:ping": () => IpcResult<RegionPing[]>;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export const DEFAULT_LOCALE = "en" as const;
|
||||||
|
|
||||||
|
export const LOCALES = [
|
||||||
|
{ code: "en", nativeName: "English", englishName: "English" },
|
||||||
|
{ code: "ja", nativeName: "日本語", englishName: "Japanese" },
|
||||||
|
{ code: "th", nativeName: "ไทย", englishName: "Thai" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type AppLocale = (typeof LOCALES)[number]["code"];
|
||||||
|
|
||||||
|
export function isAppLocale(value: unknown): value is AppLocale {
|
||||||
|
return typeof value === "string" && LOCALES.some((locale) => locale.code === value);
|
||||||
|
}
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
import type { InstanceRegion } from "./instance";
|
import type { InstanceRegion } from "./instance";
|
||||||
|
import type { AppLocale } from "../locales";
|
||||||
|
|
||||||
export type PreferredRegion = InstanceRegion | "auto";
|
export type PreferredRegion = InstanceRegion | "auto";
|
||||||
|
export type AppSchemeMode = "light" | "dark" | "auto";
|
||||||
|
|
||||||
|
export interface AppPreferences {
|
||||||
|
schemeMode: AppSchemeMode;
|
||||||
|
accent: string | null;
|
||||||
|
locale: AppLocale;
|
||||||
|
showDebugNav: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
version: string;
|
version: string;
|
||||||
gamePath: string | null;
|
gamePath: string | null;
|
||||||
detectedGamePath: string | null;
|
detectedGamePath: string | null;
|
||||||
preferredRegion: PreferredRegion;
|
preferredRegion: PreferredRegion;
|
||||||
|
preferences: AppPreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegionPing {
|
export interface RegionPing {
|
||||||
|
|||||||
Reference in New Issue
Block a user