mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-23 08:32:51 +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 { isWindowVisible } = useUI();
|
||||||
const baseId = profile;
|
const baseId = profile;
|
||||||
const profileId = baseId ? baseId : "vanilla_tu19";
|
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 [bgWidth, setBgWidth] = useState<number | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -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 SetUidModal from "../modals/SetUidModal";
|
||||||
import ImportWorldModal from "../modals/ImportWorldModal";
|
import ImportWorldModal from "../modals/ImportWorldModal";
|
||||||
import PlaytimeModal from "../modals/PlaytimeModal";
|
import PlaytimeModal from "../modals/PlaytimeModal";
|
||||||
|
import CustomizeModal from "../modals/CustomizeModal";
|
||||||
import {
|
import {
|
||||||
useUI,
|
useUI,
|
||||||
useConfig,
|
useConfig,
|
||||||
@@ -77,6 +78,8 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
updatesAvailable,
|
updatesAvailable,
|
||||||
addToSteam,
|
addToSteam,
|
||||||
cycleBranch,
|
cycleBranch,
|
||||||
|
customizations,
|
||||||
|
updateCustomization,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
const { isDayTime } = useConfig();
|
const { isDayTime } = useConfig();
|
||||||
const [focusIndex, setFocusIndex] = useState<number>(0);
|
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 [importWorldTarget, setImportWorldTarget] = useState<{ id: string; name: string } | null>(null);
|
||||||
const [isPlaytimeModalOpen, setIsPlaytimeModalOpen] = useState(false);
|
const [isPlaytimeModalOpen, setIsPlaytimeModalOpen] = useState(false);
|
||||||
const [playtimeTarget, setPlaytimeTarget] = useState<{ id: string; name: string } | null>(null);
|
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 [playtimeMap, setPlaytimeMap] = useState<Record<string, PlaytimeResponse>>({});
|
||||||
const [initialPath, setInitialPath] = useState<string>("");
|
const [initialPath, setInitialPath] = useState<string>("");
|
||||||
const [hoveredBtn, setHoveredBtn] = useState<{
|
const [hoveredBtn, setHoveredBtn] = useState<{
|
||||||
@@ -673,6 +678,29 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
</svg>
|
</svg>
|
||||||
Restore
|
Restore
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -887,6 +915,21 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
instanceName={playtimeTarget?.name ?? ""}
|
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 && (
|
{deleteConfirmEdition && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setProfile: configRaw.setProfile,
|
setProfile: configRaw.setProfile,
|
||||||
customEditions: configRaw.customEditions,
|
customEditions: configRaw.customEditions,
|
||||||
setCustomEditions: configRaw.setCustomEditions,
|
setCustomEditions: configRaw.setCustomEditions,
|
||||||
|
customizations: configRaw.customizations,
|
||||||
|
setCustomizations: configRaw.setCustomizations,
|
||||||
extraLaunchArgs: configRaw.extraLaunchArgs,
|
extraLaunchArgs: configRaw.extraLaunchArgs,
|
||||||
});
|
});
|
||||||
const skinSync = useSkinSync({ username: configRaw.username, profile: configRaw.profile, editions: gameRaw.editions });
|
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.updatesAvailable, gameRaw.addToSteam, gameRaw.steamSuccessMessage,
|
||||||
gameRaw.cycleBranch, gameRaw.toggleInstall, gameRaw.checkInstalls,
|
gameRaw.cycleBranch, gameRaw.toggleInstall, gameRaw.checkInstalls,
|
||||||
gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.addCustomEdition,
|
gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.addCustomEdition,
|
||||||
gameRaw.deleteCustomEdition, gameRaw.downloadRunner
|
gameRaw.deleteCustomEdition, gameRaw.downloadRunner,
|
||||||
|
gameRaw.customizations, gameRaw.updateCustomization,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const audio = useMemo(() => audioRaw, [
|
const audio = useMemo(() => audioRaw, [
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function useAppConfig() {
|
|||||||
const [linuxRunner, setLinuxRunner] = useState<string | undefined>();
|
const [linuxRunner, setLinuxRunner] = useState<string | undefined>();
|
||||||
const [perfBoost, setPerfBoost] = useState(false);
|
const [perfBoost, setPerfBoost] = useState(false);
|
||||||
const [customEditions, setCustomEditions] = useState<CustomEdition[]>([]);
|
const [customEditions, setCustomEditions] = useState<CustomEdition[]>([]);
|
||||||
|
const [customizations, setCustomizations] = useState<Record<string, { titleImage?: string; panorama?: string }>>({});
|
||||||
const [mangohudEnabled, setMangohudEnabled] = useState(false);
|
const [mangohudEnabled, setMangohudEnabled] = useState(false);
|
||||||
const [extraLaunchArgs, setExtraLaunchArgs] = useState<string[] | undefined>();
|
const [extraLaunchArgs, setExtraLaunchArgs] = useState<string[] | undefined>();
|
||||||
const [launchPrefix, setLaunchPrefix] = useState<string | undefined>();
|
const [launchPrefix, setLaunchPrefix] = useState<string | undefined>();
|
||||||
@@ -30,6 +31,7 @@ export function useAppConfig() {
|
|||||||
if (config.appleSiliconPerformanceBoost !== undefined)
|
if (config.appleSiliconPerformanceBoost !== undefined)
|
||||||
setPerfBoost(config.appleSiliconPerformanceBoost);
|
setPerfBoost(config.appleSiliconPerformanceBoost);
|
||||||
if (config.customEditions) setCustomEditions(config.customEditions);
|
if (config.customEditions) setCustomEditions(config.customEditions);
|
||||||
|
if (config.customizations) setCustomizations(config.customizations);
|
||||||
if (config.profile) setProfile(config.profile);
|
if (config.profile) setProfile(config.profile);
|
||||||
if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
|
if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
|
||||||
if (config.animationsEnabled !== undefined) setAnimationsEnabled(config.animationsEnabled);
|
if (config.animationsEnabled !== undefined) setAnimationsEnabled(config.animationsEnabled);
|
||||||
@@ -54,6 +56,7 @@ export function useAppConfig() {
|
|||||||
appleSiliconPerformanceBoost: perfBoost,
|
appleSiliconPerformanceBoost: perfBoost,
|
||||||
profile,
|
profile,
|
||||||
customEditions,
|
customEditions,
|
||||||
|
customizations,
|
||||||
animationsEnabled,
|
animationsEnabled,
|
||||||
vfxEnabled,
|
vfxEnabled,
|
||||||
rpcEnabled,
|
rpcEnabled,
|
||||||
@@ -66,7 +69,7 @@ export function useAppConfig() {
|
|||||||
launchEnvVars,
|
launchEnvVars,
|
||||||
}).catch(console.error);
|
}).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 {
|
return {
|
||||||
username,
|
username,
|
||||||
@@ -97,6 +100,8 @@ export function useAppConfig() {
|
|||||||
setPerfBoost,
|
setPerfBoost,
|
||||||
customEditions,
|
customEditions,
|
||||||
setCustomEditions,
|
setCustomEditions,
|
||||||
|
customizations,
|
||||||
|
setCustomizations,
|
||||||
isLoaded,
|
isLoaded,
|
||||||
hasCompletedSetup,
|
hasCompletedSetup,
|
||||||
setHasCompletedSetup,
|
setHasCompletedSetup,
|
||||||
|
|||||||
+58
-10
@@ -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 { TauriService, type CustomEdition } from "../services/TauriService";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import type { Edition } from "../types/edition";
|
import type { Edition } from "../types/edition";
|
||||||
@@ -77,6 +77,8 @@ interface GameManagerProps {
|
|||||||
setProfile: (id: string) => void;
|
setProfile: (id: string) => void;
|
||||||
customEditions: CustomEdition[];
|
customEditions: CustomEdition[];
|
||||||
setCustomEditions: (editions: CustomEdition[]) => void;
|
setCustomEditions: (editions: CustomEdition[]) => void;
|
||||||
|
customizations: Record<string, { titleImage?: string; panorama?: string }>;
|
||||||
|
setCustomizations: Dispatch<SetStateAction<Record<string, { titleImage?: string; panorama?: string }>>>;
|
||||||
extraLaunchArgs?: string[];
|
extraLaunchArgs?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,8 @@ export function useGameManager({
|
|||||||
setProfile,
|
setProfile,
|
||||||
customEditions,
|
customEditions,
|
||||||
setCustomEditions,
|
setCustomEditions,
|
||||||
|
customizations,
|
||||||
|
setCustomizations,
|
||||||
extraLaunchArgs,
|
extraLaunchArgs,
|
||||||
}: GameManagerProps) {
|
}: GameManagerProps) {
|
||||||
const [installs, setInstalls] = useState<string[]>([]);
|
const [installs, setInstalls] = useState<string[]>([]);
|
||||||
@@ -126,7 +130,6 @@ export function useGameManager({
|
|||||||
>({});
|
>({});
|
||||||
const branchesFetched = useRef<Set<string>>(new Set());
|
const branchesFetched = useRef<Set<string>>(new Set());
|
||||||
const initialBranchesSet = useRef(false);
|
const initialBranchesSet = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialBranchesSet.current || !profile) return;
|
if (initialBranchesSet.current || !profile) return;
|
||||||
BASE_EDITIONS.forEach((e) => {
|
BASE_EDITIONS.forEach((e) => {
|
||||||
@@ -253,7 +256,7 @@ export function useGameManager({
|
|||||||
url = `${baseUrl}/releases/download/${branchToUse}/${filename}`;
|
url = `${baseUrl}/releases/download/${branchToUse}/${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const edition = {
|
||||||
...e,
|
...e,
|
||||||
url,
|
url,
|
||||||
branches: availableBranches,
|
branches: availableBranches,
|
||||||
@@ -261,10 +264,20 @@ export function useGameManager({
|
|||||||
instanceId:
|
instanceId:
|
||||||
selectedBranch === "Stable" ? e.id : `${e.id}_${selectedBranch}`,
|
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 checkInstalls = useCallback(async () => {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
@@ -349,7 +362,11 @@ export function useGameManager({
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(
|
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 {
|
} finally {
|
||||||
setIsRunnerDownloading(false);
|
setIsRunnerDownloading(false);
|
||||||
@@ -376,7 +393,11 @@ export function useGameManager({
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(
|
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);
|
setDownloadProgress(null);
|
||||||
setDownloadingId(null);
|
setDownloadingId(null);
|
||||||
@@ -412,11 +433,19 @@ export function useGameManager({
|
|||||||
setIsGameRunning(true);
|
setIsGameRunning(true);
|
||||||
try {
|
try {
|
||||||
getCurrentWindow().minimize();
|
getCurrentWindow().minimize();
|
||||||
await TauriService.launchGame(profile, PARTNERSHIP_SERVERS, extraLaunchArgs);
|
await TauriService.launchGame(
|
||||||
|
profile,
|
||||||
|
PARTNERSHIP_SERVERS,
|
||||||
|
extraLaunchArgs,
|
||||||
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(
|
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 {
|
} finally {
|
||||||
setIsGameRunning(false);
|
setIsGameRunning(false);
|
||||||
@@ -474,6 +503,19 @@ export function useGameManager({
|
|||||||
[customEditions, setCustomEditions],
|
[customEditions, setCustomEditions],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateCustomization = useCallback(
|
||||||
|
(
|
||||||
|
instanceId: string,
|
||||||
|
updates: { titleImage?: string; panorama?: string },
|
||||||
|
) => {
|
||||||
|
setCustomizations((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[instanceId]: { ...prev[instanceId], ...updates },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const addToSteam = useCallback(
|
const addToSteam = useCallback(
|
||||||
async (
|
async (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -491,7 +533,11 @@ export function useGameManager({
|
|||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(
|
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,
|
updatesAvailable,
|
||||||
addToSteam,
|
addToSteam,
|
||||||
cycleBranch,
|
cycleBranch,
|
||||||
|
customizations,
|
||||||
|
updateCustomization,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface AppConfig {
|
|||||||
extraLaunchArgs?: string[];
|
extraLaunchArgs?: string[];
|
||||||
launchPrefix?: string;
|
launchPrefix?: string;
|
||||||
launchEnvVars?: Record<string, string>;
|
launchEnvVars?: Record<string, string>;
|
||||||
|
customizations?: Record<string, { titleImage?: string; panorama?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemePalette {
|
export interface ThemePalette {
|
||||||
|
|||||||
Reference in New Issue
Block a user