mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:10:45 +00:00
feat: customize title image and panorama
This commit is contained in:
@@ -10,7 +10,10 @@ const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => {
|
||||
const { isWindowVisible } = useUI();
|
||||
const baseId = profile;
|
||||
const profileId = baseId ? baseId : "vanilla_tu19";
|
||||
const currentPanorama = `/panorama/${profileId}_Panorama_Background_${isDay ? "Day" : "Night"}.png`;
|
||||
const isCustomUrl = profile.startsWith("data:") || profile.startsWith("blob:");
|
||||
const currentPanorama = isCustomUrl
|
||||
? profile
|
||||
: `/panorama/${profileId}_Panorama_Background_${isDay ? "Day" : "Night"}.png`;
|
||||
const [bgWidth, setBgWidth] = useState<number | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
246
src/components/modals/CustomizeModal.tsx
Normal file
246
src/components/modals/CustomizeModal.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
|
||||
function isFilePath(value: string): boolean {
|
||||
return value.startsWith("/") && !value.startsWith("/images/") && !value.startsWith("/panorama/");
|
||||
}
|
||||
|
||||
async function resolvePath(value: string | undefined): Promise<string | undefined> {
|
||||
if (!value) return undefined;
|
||||
if (isFilePath(value)) {
|
||||
try {
|
||||
return await TauriService.readScreenshotAsDataUrl(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export default function CustomizeModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
playPressSound,
|
||||
playBackSound,
|
||||
editionName,
|
||||
currentTitleImage,
|
||||
currentPanorama,
|
||||
onSave,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
playPressSound: (s?: string) => void;
|
||||
playBackSound: (s?: string) => void;
|
||||
editionName: string;
|
||||
currentTitleImage?: string;
|
||||
currentPanorama?: string;
|
||||
onSave: (updates: { titleImage?: string; panorama?: string }) => void;
|
||||
}) {
|
||||
const [titleImage, setTitleImage] = useState(currentTitleImage || "");
|
||||
const [panorama, setPanorama] = useState(currentPanorama || "");
|
||||
const [focusIndex, setFocusIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTitleImage(currentTitleImage || "");
|
||||
setPanorama(currentPanorama || "");
|
||||
setFocusIndex(0);
|
||||
}
|
||||
}, [isOpen, currentTitleImage, currentPanorama]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
playBackSound("close_click.wav");
|
||||
onClose();
|
||||
} else if (e.key === "Enter") {
|
||||
if (focusIndex === 4) {
|
||||
playBackSound("close_click.wav");
|
||||
onClose();
|
||||
} else if (focusIndex === 5) {
|
||||
handleSave();
|
||||
}
|
||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
setFocusIndex((prev) => (prev + 1) % 6);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setFocusIndex((prev) => (prev - 1 + 6) % 6);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKey);
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [isOpen, focusIndex, titleImage, panorama]);
|
||||
|
||||
const handleSave = async () => {
|
||||
playPressSound("save_click.wav");
|
||||
const [resolvedTitle, resolvedPanorama] = await Promise.all([
|
||||
resolvePath(titleImage || undefined),
|
||||
resolvePath(panorama || undefined),
|
||||
]);
|
||||
onSave({
|
||||
titleImage: resolvedTitle,
|
||||
panorama: resolvedPanorama,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handlePickFile = async (field: "titleImage" | "panorama") => {
|
||||
try {
|
||||
const path = await TauriService.pickFile(
|
||||
field === "titleImage" ? "Select Title Image" : "Select Panorama Background",
|
||||
["png", "jpg", "jpeg", "bmp"],
|
||||
);
|
||||
if (path) {
|
||||
if (field === "titleImage") setTitleImage(path);
|
||||
else setPanorama(path);
|
||||
playPressSound();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== "CANCELED") console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = (field: "titleImage" | "panorama") => {
|
||||
playPressSound();
|
||||
if (field === "titleImage") setTitleImage("");
|
||||
else setPanorama("");
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const pickerSection = (
|
||||
field: "titleImage" | "panorama",
|
||||
value: string,
|
||||
idx: number,
|
||||
resetIdx: number,
|
||||
) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[#AAAAAA] text-xs mc-text-shadow uppercase tracking-widest">
|
||||
{field === "titleImage" ? "Title Image" : "Panorama Background"}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handlePickFile(field)}
|
||||
onMouseEnter={() => setFocusIndex(idx)}
|
||||
className="flex-1 h-10 flex items-center justify-center text-xs mc-text-shadow text-white outline-none border-none bg-transparent"
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === idx
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
{value ? "Change Image" : "Pick Image"}
|
||||
</button>
|
||||
{value && (
|
||||
<button
|
||||
onClick={() => handleReset(field)}
|
||||
onMouseEnter={() => setFocusIndex(resetIdx)}
|
||||
className="w-24 h-10 flex items-center justify-center text-xs mc-text-shadow text-red-400 outline-none border-none bg-transparent"
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === resetIdx
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{value && (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<img
|
||||
src={value}
|
||||
alt="Preview"
|
||||
className="w-10 h-6 object-contain border border-[#555]"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<span className="text-[10px] text-white/50 truncate flex-1">
|
||||
{value.split("/").pop() || value}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md outline-none border-none"
|
||||
>
|
||||
<div
|
||||
className="relative w-[420px] p-6 flex flex-col items-center shadow-2xl"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
|
||||
Customize
|
||||
</h2>
|
||||
<p className="text-white/70 text-xs mc-text-shadow mb-4 -mt-2 truncate max-w-full">
|
||||
{editionName}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{pickerSection("titleImage", titleImage, 0, 1)}
|
||||
{pickerSection("panorama", panorama, 2, 3)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mt-6 w-full justify-center">
|
||||
<button
|
||||
onMouseEnter={() => setFocusIndex(4)}
|
||||
onClick={() => {
|
||||
playBackSound("close_click.wav");
|
||||
onClose();
|
||||
}}
|
||||
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${
|
||||
focusIndex === 4 ? "text-[#FFFF55]" : "text-white"
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 4
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onMouseEnter={() => setFocusIndex(5)}
|
||||
onClick={handleSave}
|
||||
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${
|
||||
focusIndex === 5 ? "text-[#FFFF55]" : "text-white"
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 5
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import CustomTUModal from "../modals/CustomTUModal";
|
||||
import SetUidModal from "../modals/SetUidModal";
|
||||
import ImportWorldModal from "../modals/ImportWorldModal";
|
||||
import PlaytimeModal from "../modals/PlaytimeModal";
|
||||
import CustomizeModal from "../modals/CustomizeModal";
|
||||
import {
|
||||
useUI,
|
||||
useConfig,
|
||||
@@ -77,6 +78,8 @@ const VersionsView = memo(function VersionsView() {
|
||||
updatesAvailable,
|
||||
addToSteam,
|
||||
cycleBranch,
|
||||
customizations,
|
||||
updateCustomization,
|
||||
} = useGame();
|
||||
const { isDayTime } = useConfig();
|
||||
const [focusIndex, setFocusIndex] = useState<number>(0);
|
||||
@@ -89,6 +92,8 @@ const VersionsView = memo(function VersionsView() {
|
||||
const [importWorldTarget, setImportWorldTarget] = useState<{ id: string; name: string } | null>(null);
|
||||
const [isPlaytimeModalOpen, setIsPlaytimeModalOpen] = useState(false);
|
||||
const [playtimeTarget, setPlaytimeTarget] = useState<{ id: string; name: string } | null>(null);
|
||||
const [isCustomizeModalOpen, setIsCustomizeModalOpen] = useState(false);
|
||||
const [customizeTarget, setCustomizeTarget] = useState<Edition | null>(null);
|
||||
const [playtimeMap, setPlaytimeMap] = useState<Record<string, PlaytimeResponse>>({});
|
||||
const [initialPath, setInitialPath] = useState<string>("");
|
||||
const [hoveredBtn, setHoveredBtn] = useState<{
|
||||
@@ -673,6 +678,29 @@ const VersionsView = memo(function VersionsView() {
|
||||
</svg>
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setCustomizeTarget(edition);
|
||||
setIsCustomizeModalOpen(true);
|
||||
setOpenMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="w-3.5 h-3.5"
|
||||
>
|
||||
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
Customize
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -887,6 +915,21 @@ const VersionsView = memo(function VersionsView() {
|
||||
instanceName={playtimeTarget?.name ?? ""}
|
||||
/>
|
||||
|
||||
<CustomizeModal
|
||||
isOpen={isCustomizeModalOpen}
|
||||
onClose={() => { setIsCustomizeModalOpen(false); setCustomizeTarget(null); }}
|
||||
playPressSound={playPressSound}
|
||||
playBackSound={playBackSound}
|
||||
editionName={customizeTarget?.name ?? ""}
|
||||
currentTitleImage={customizeTarget ? customizations[customizeTarget.instanceId]?.titleImage || customizeTarget.titleImage : undefined}
|
||||
currentPanorama={customizeTarget ? customizations[customizeTarget.instanceId]?.panorama || customizeTarget.panorama : undefined}
|
||||
onSave={(updates) => {
|
||||
if (customizeTarget) {
|
||||
updateCustomization(customizeTarget.instanceId, updates);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{deleteConfirmEdition && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div
|
||||
|
||||
@@ -51,6 +51,8 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
setProfile: configRaw.setProfile,
|
||||
customEditions: configRaw.customEditions,
|
||||
setCustomEditions: configRaw.setCustomEditions,
|
||||
customizations: configRaw.customizations,
|
||||
setCustomizations: configRaw.setCustomizations,
|
||||
extraLaunchArgs: configRaw.extraLaunchArgs,
|
||||
});
|
||||
const skinSync = useSkinSync({ username: configRaw.username, profile: configRaw.profile, editions: gameRaw.editions });
|
||||
@@ -78,7 +80,8 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
gameRaw.updatesAvailable, gameRaw.addToSteam, gameRaw.steamSuccessMessage,
|
||||
gameRaw.cycleBranch, gameRaw.toggleInstall, gameRaw.checkInstalls,
|
||||
gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.addCustomEdition,
|
||||
gameRaw.deleteCustomEdition, gameRaw.downloadRunner
|
||||
gameRaw.deleteCustomEdition, gameRaw.downloadRunner,
|
||||
gameRaw.customizations, gameRaw.updateCustomization,
|
||||
]);
|
||||
|
||||
const audio = useMemo(() => audioRaw, [
|
||||
|
||||
@@ -18,6 +18,7 @@ export function useAppConfig() {
|
||||
const [linuxRunner, setLinuxRunner] = useState<string | undefined>();
|
||||
const [perfBoost, setPerfBoost] = useState(false);
|
||||
const [customEditions, setCustomEditions] = useState<CustomEdition[]>([]);
|
||||
const [customizations, setCustomizations] = useState<Record<string, { titleImage?: string; panorama?: string }>>({});
|
||||
const [mangohudEnabled, setMangohudEnabled] = useState(false);
|
||||
const [extraLaunchArgs, setExtraLaunchArgs] = useState<string[] | undefined>();
|
||||
const [launchPrefix, setLaunchPrefix] = useState<string | undefined>();
|
||||
@@ -30,6 +31,7 @@ export function useAppConfig() {
|
||||
if (config.appleSiliconPerformanceBoost !== undefined)
|
||||
setPerfBoost(config.appleSiliconPerformanceBoost);
|
||||
if (config.customEditions) setCustomEditions(config.customEditions);
|
||||
if (config.customizations) setCustomizations(config.customizations);
|
||||
if (config.profile) setProfile(config.profile);
|
||||
if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
|
||||
if (config.animationsEnabled !== undefined) setAnimationsEnabled(config.animationsEnabled);
|
||||
@@ -54,6 +56,7 @@ export function useAppConfig() {
|
||||
appleSiliconPerformanceBoost: perfBoost,
|
||||
profile,
|
||||
customEditions,
|
||||
customizations,
|
||||
animationsEnabled,
|
||||
vfxEnabled,
|
||||
rpcEnabled,
|
||||
@@ -66,7 +69,7 @@ export function useAppConfig() {
|
||||
launchEnvVars,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, isLoaded]);
|
||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customizations, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, isLoaded]);
|
||||
|
||||
return {
|
||||
username,
|
||||
@@ -97,6 +100,8 @@ export function useAppConfig() {
|
||||
setPerfBoost,
|
||||
customEditions,
|
||||
setCustomEditions,
|
||||
customizations,
|
||||
setCustomizations,
|
||||
isLoaded,
|
||||
hasCompletedSetup,
|
||||
setHasCompletedSetup,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef, type Dispatch, type SetStateAction } from "react";
|
||||
import { TauriService, type CustomEdition } from "../services/TauriService";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { Edition } from "../types/edition";
|
||||
@@ -77,6 +77,8 @@ interface GameManagerProps {
|
||||
setProfile: (id: string) => void;
|
||||
customEditions: CustomEdition[];
|
||||
setCustomEditions: (editions: CustomEdition[]) => void;
|
||||
customizations: Record<string, { titleImage?: string; panorama?: string }>;
|
||||
setCustomizations: Dispatch<SetStateAction<Record<string, { titleImage?: string; panorama?: string }>>>;
|
||||
extraLaunchArgs?: string[];
|
||||
}
|
||||
|
||||
@@ -102,6 +104,8 @@ export function useGameManager({
|
||||
setProfile,
|
||||
customEditions,
|
||||
setCustomEditions,
|
||||
customizations,
|
||||
setCustomizations,
|
||||
extraLaunchArgs,
|
||||
}: GameManagerProps) {
|
||||
const [installs, setInstalls] = useState<string[]>([]);
|
||||
@@ -126,7 +130,6 @@ export function useGameManager({
|
||||
>({});
|
||||
const branchesFetched = useRef<Set<string>>(new Set());
|
||||
const initialBranchesSet = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialBranchesSet.current || !profile) return;
|
||||
BASE_EDITIONS.forEach((e) => {
|
||||
@@ -253,7 +256,7 @@ export function useGameManager({
|
||||
url = `${baseUrl}/releases/download/${branchToUse}/${filename}`;
|
||||
}
|
||||
|
||||
return {
|
||||
const edition = {
|
||||
...e,
|
||||
url,
|
||||
branches: availableBranches,
|
||||
@@ -261,10 +264,20 @@ export function useGameManager({
|
||||
instanceId:
|
||||
selectedBranch === "Stable" ? e.id : `${e.id}_${selectedBranch}`,
|
||||
};
|
||||
const custom = customizations[e.id];
|
||||
if (custom?.titleImage) edition.titleImage = custom.titleImage;
|
||||
if (custom?.panorama) edition.panorama = custom.panorama;
|
||||
return edition;
|
||||
}),
|
||||
...customEditions.map((e) => {
|
||||
const edition: Edition = { ...e, instanceId: e.id };
|
||||
const custom = customizations[e.id];
|
||||
if (custom?.titleImage) edition.titleImage = custom.titleImage;
|
||||
if (custom?.panorama) edition.panorama = custom.panorama;
|
||||
return edition;
|
||||
}),
|
||||
...customEditions.map((e) => ({ ...e, instanceId: e.id })),
|
||||
];
|
||||
}, [customEditions, dynamicUrls, branches, selectedBranches]);
|
||||
}, [customEditions, dynamicUrls, branches, selectedBranches, customizations]);
|
||||
|
||||
const checkInstalls = useCallback(async () => {
|
||||
const results = await Promise.all(
|
||||
@@ -349,7 +362,11 @@ export function useGameManager({
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
setError(
|
||||
e instanceof Error ? e.message : typeof e === "string" ? e : "Failed to download runner",
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Failed to download runner",
|
||||
);
|
||||
} finally {
|
||||
setIsRunnerDownloading(false);
|
||||
@@ -376,7 +393,11 @@ export function useGameManager({
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
setError(
|
||||
e instanceof Error ? e.message : typeof e === "string" ? e : "Failed to install version",
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Failed to install version",
|
||||
);
|
||||
setDownloadProgress(null);
|
||||
setDownloadingId(null);
|
||||
@@ -412,11 +433,19 @@ export function useGameManager({
|
||||
setIsGameRunning(true);
|
||||
try {
|
||||
getCurrentWindow().minimize();
|
||||
await TauriService.launchGame(profile, PARTNERSHIP_SERVERS, extraLaunchArgs);
|
||||
await TauriService.launchGame(
|
||||
profile,
|
||||
PARTNERSHIP_SERVERS,
|
||||
extraLaunchArgs,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
setError(
|
||||
e instanceof Error ? e.message : typeof e === "string" ? e : "Failed to launch game",
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Failed to launch game",
|
||||
);
|
||||
} finally {
|
||||
setIsGameRunning(false);
|
||||
@@ -474,6 +503,19 @@ export function useGameManager({
|
||||
[customEditions, setCustomEditions],
|
||||
);
|
||||
|
||||
const updateCustomization = useCallback(
|
||||
(
|
||||
instanceId: string,
|
||||
updates: { titleImage?: string; panorama?: string },
|
||||
) => {
|
||||
setCustomizations((prev) => ({
|
||||
...prev,
|
||||
[instanceId]: { ...prev[instanceId], ...updates },
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addToSteam = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
@@ -491,7 +533,11 @@ export function useGameManager({
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
setError(
|
||||
e instanceof Error ? e.message : typeof e === "string" ? e : "Failed to add to Steam",
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Failed to add to Steam",
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -525,5 +571,7 @@ export function useGameManager({
|
||||
updatesAvailable,
|
||||
addToSteam,
|
||||
cycleBranch,
|
||||
customizations,
|
||||
updateCustomization,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface AppConfig {
|
||||
extraLaunchArgs?: string[];
|
||||
launchPrefix?: string;
|
||||
launchEnvVars?: Record<string, string>;
|
||||
customizations?: Record<string, { titleImage?: string; panorama?: string }>;
|
||||
}
|
||||
|
||||
export interface ThemePalette {
|
||||
|
||||
Reference in New Issue
Block a user