This commit is contained in:
/home/neo
2026-05-15 20:31:41 +03:00
committed by GitHub
parent bd2bf0ecab
commit f2398aad23
53 changed files with 9584 additions and 3762 deletions
+72 -70
View File
@@ -1,70 +1,72 @@
import React, { useEffect, useRef, useState } from 'react';
import { useUI } from '../../context/LauncherContext';
interface PanoramaProps {
profile: string;
isDay: boolean;
}
const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => {
const { isWindowVisible } = useUI();
const PANORAMA_PROFILES = ['legacy_evolved', 'vanilla_tu19', '360revived', 'vanilla_tu24'];
const baseId = profile;
const profileId = PANORAMA_PROFILES.includes(baseId) ? baseId : 'vanilla_tu19';
const currentPanorama = `/panorama/${profileId}_Panorama_Background_${isDay ? 'Day' : 'Night'}.png`;
const [bgWidth, setBgWidth] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let active = true;
const updateWidth = () => {
if (!containerRef.current) return;
const img = new Image();
img.src = currentPanorama;
img.onload = () => {
if (!active || !containerRef.current) return;
const height = containerRef.current.clientHeight;
const aspectRatio = img.naturalWidth / img.naturalHeight;
setBgWidth(Math.ceil(height * aspectRatio));
};
};
updateWidth();
window.addEventListener('resize', updateWidth);
return () => {
active = false;
window.removeEventListener('resize', updateWidth);
};
}, [currentPanorama]);
return (
<>
{bgWidth && (
<style>{`
@keyframes panoramaLoop {
0% { transform: translate3d(0, 0, 0); }
100% { transform: translate3d(-${bgWidth}px, 0, 0); }
}
`}</style>
)}
<div ref={containerRef} className="absolute inset-0 overflow-hidden pointer-events-none transition-opacity duration-500">
{isWindowVisible && (
<div
className="absolute top-0 left-0 h-full will-change-transform"
style={{
width: bgWidth ? `calc(100vw + ${bgWidth}px)` : '200vw',
backgroundImage: `url("${currentPanorama}")`,
backgroundSize: bgWidth ? `${bgWidth}px 100%` : 'auto 100%',
backgroundRepeat: 'repeat-x',
animation: bgWidth ? 'panoramaLoop 140s linear infinite' : 'none'
}}
/>
)}
</div>
<div className="absolute inset-0 bg-black/35 pointer-events-none" />
</>
);
});
export default PanoramaBackground;
import React, { useEffect, useRef, useState } from "react";
import { useUI } from "../../context/LauncherContext";
interface PanoramaProps {
profile: string;
isDay: boolean;
}
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 [bgWidth, setBgWidth] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let active = true;
const updateWidth = () => {
if (!containerRef.current) return;
const img = new Image();
img.src = currentPanorama;
img.onload = () => {
if (!active || !containerRef.current) return;
const height = containerRef.current.clientHeight;
const aspectRatio = img.naturalWidth / img.naturalHeight;
setBgWidth(Math.ceil(height * aspectRatio));
};
};
updateWidth();
window.addEventListener("resize", updateWidth);
return () => {
active = false;
window.removeEventListener("resize", updateWidth);
};
}, [currentPanorama]);
return (
<>
{bgWidth && (
<style>{`
@keyframes panoramaLoop {
0% { transform: translate3d(0, 0, 0); }
100% { transform: translate3d(-${bgWidth}px, 0, 0); }
}
`}</style>
)}
<div
ref={containerRef}
className="absolute inset-0 overflow-hidden pointer-events-none transition-opacity duration-500"
>
{isWindowVisible && (
<div
className="absolute top-0 left-0 h-full will-change-transform"
style={{
width: bgWidth ? `calc(100vw + ${bgWidth}px)` : "200vw",
backgroundImage: `url("${currentPanorama}")`,
backgroundSize: bgWidth ? `${bgWidth}px 100%` : "auto 100%",
backgroundRepeat: "repeat-x",
animation: bgWidth ? "panoramaLoop 140s linear infinite" : "none",
}}
/>
)}
</div>
<div className="absolute inset-0 bg-black/35 pointer-events-none" />
</>
);
});
export default PanoramaBackground;
+54
View File
@@ -0,0 +1,54 @@
import { useState, useEffect, useRef } from "react";
import { TauriService } from "../../services/TauriService";
interface ScreenshotImageProps {
path: string;
className?: string;
alt?: string;
loading?: "lazy" | "eager";
style?: React.CSSProperties;
fallbackSrc?: string;
}
export function ScreenshotImage({
path,
className,
alt,
loading,
style,
fallbackSrc,
}: ScreenshotImageProps) {
const [src, setSrc] = useState<string | undefined>(fallbackSrc);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
let cancelled = false;
setSrc(fallbackSrc);
TauriService.readScreenshotAsDataUrl(path)
.then((url) => {
if (!cancelled) setSrc(url);
})
.catch(() => {
if (!cancelled && fallbackSrc) setSrc(fallbackSrc);
});
return () => {
cancelled = true;
};
}, [path, fallbackSrc]);
const handleError = () => {
if (fallbackSrc) setSrc(fallbackSrc);
};
return (
<img
ref={imgRef}
src={src}
className={className}
alt={alt}
loading={loading}
style={style}
onError={handleError}
/>
);
}
+13
View File
@@ -256,6 +256,9 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo
} else if (focusIndex === 4) {
playPressSound();
setActiveView('screenshots');
} else if (focusIndex === 5) {
playPressSound();
setActiveView('lcelive');
}
}
};
@@ -346,6 +349,16 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo
>
<img src="/images/Screenshots_Icon.png" alt="Screenshots" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
<button
data-focus="5" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(5)}
onClick={() => { playPressSound(); setActiveView('lcelive'); }}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 5 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 5 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
title="LCELive"
>
<img src="/images/friends.png" alt="LCELive" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
</div>
</motion.div>
);
@@ -0,0 +1,244 @@
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService";
import { lceLiveService, GameInvite } from "../../services/LceLiveService";
export default function ChooseInstanceModal({
isOpen,
onClose,
playPressSound,
playBackSound,
editions,
installs,
invite,
}: {
isOpen: boolean;
onClose: () => void;
playPressSound: (s?: string) => void;
playBackSound: (s?: string) => void;
editions: any[];
installs: string[];
invite: GameInvite | null;
}) {
const [selectedInstance, setSelectedInstance] = useState<string>("");
const [status, setStatus] = useState<string>("");
const [error, setError] = useState<string>("");
const [isJoining, setIsJoining] = useState(false);
const [focusIndex, setFocusIndex] = useState(0);
const validInstances = editions.filter((e: any) =>
installs.includes(e.instanceId)
);
useEffect(() => {
if (!isOpen) {
setSelectedInstance("");
setStatus("");
setError("");
setIsJoining(false);
setFocusIndex(0);
}
}, [isOpen]);
useEffect(() => {
if (isOpen && validInstances.length > 0 && !selectedInstance) {
setSelectedInstance(validInstances[0].instanceId);
}
}, [isOpen, validInstances, selectedInstance]);
const handleJoin = async () => {
if (!invite || !selectedInstance) return;
playPressSound();
setIsJoining(true);
setError("");
setStatus("Accepting invite...");
try {
const inviteData = await lceLiveService.acceptGameInvite(invite.inviteId);
const hostIp = inviteData.hostIp || (typeof invite.from !== 'string' && (invite as any).from?.hostIp);
const hostPort = inviteData.hostPort || invite.hostPort;
const sessionId = inviteData.signalingSessionId || invite.signalingSessionId || "";
if (sessionId) {
setStatus("Connecting via relay...");
const baseUrl = lceLiveService.apiBaseUrl;
const accessToken = lceLiveService.accessToken ?? "";
const port = await TauriService.startRelayProxy(baseUrl, accessToken, sessionId);
setStatus("Launching game...");
await TauriService.launchGame(selectedInstance, [
{ name: invite.hostName || "LCELive Game", ip: "127.0.0.1", port }
]);
} else {
setStatus("Launching game...");
await TauriService.stopProxy();
await TauriService.launchGame(selectedInstance, [
{ name: invite.hostName || "LCELive Game", ip: hostIp, port: hostPort }
]);
}
onClose();
} catch (e: any) {
setError(e.toString());
setStatus("");
setIsJoining(false);
}
};
useEffect(() => {
if (!isOpen) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
playBackSound();
if (isJoining) return;
onClose();
} else if (e.key === "ArrowDown" || e.key === "Tab") {
e.preventDefault();
const max = 1 + (validInstances.length > 0 ? 1 : 0) + 1;
setFocusIndex((prev) => (prev + 1) % max);
} else if (e.key === "ArrowUp") {
e.preventDefault();
const max = 1 + (validInstances.length > 0 ? 1 : 0) + 1;
setFocusIndex((prev) => (prev - 1 + max) % max);
} else if (e.key === "Enter") {
if (focusIndex === 0 && validInstances.length > 0) {
const currentIdx = validInstances.findIndex((i: any) => i.instanceId === selectedInstance);
const next = (currentIdx + 1) % validInstances.length;
setSelectedInstance(validInstances[next].instanceId);
playPressSound();
} else if (focusIndex === 1 && !isJoining) {
handleJoin();
} else if (focusIndex === (validInstances.length > 0 ? 2 : 1) && !isJoining) {
onClose();
}
}
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, focusIndex, selectedInstance, validInstances, isJoining]);
if (!isOpen) return null;
const hostName = invite ? (typeof invite.from === 'string' ? invite.from : invite.from.displayName) : "";
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-[450px] p-6 flex flex-col items-center shadow-2xl"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
{!isJoining ? (
<>
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Join Game
</h2>
<p className="text-white text-sm mc-text-shadow mb-4 text-center">
Joining {hostName}'s game. Choose an instance:
</p>
{validInstances.length > 0 ? (
<div className="w-full mb-4 flex flex-col gap-2 max-h-[300px] overflow-y-auto"
style={{ scrollbarWidth: "thin", scrollbarColor: "#373737 transparent" }}>
{validInstances.map((inst: any) => {
const isSelected = selectedInstance === inst.instanceId;
return (
<div
key={inst.instanceId}
onClick={() => { playPressSound(); setSelectedInstance(inst.instanceId); }}
onMouseEnter={() => setFocusIndex(0)}
className={`w-full px-4 py-3 cursor-pointer flex items-center gap-3 transition-all outline-none border-none ${isSelected ? "bg-white/15 border-l-4 border-[#FFFF55]" : "bg-black/20 hover:bg-black/30 border-l-4 border-transparent"}`}
style={{ imageRendering: "pixelated" }}
>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${isSelected ? "border-[#FFFF55]" : "border-gray-500"}`}>
{isSelected && <div className="w-2 h-2 rounded-full bg-[#FFFF55]" />}
</div>
<div className="flex flex-col">
<span className="text-white text-lg font-bold mc-text-shadow">{inst.name}</span>
{inst.selectedBranch && (
<span className="text-gray-400 text-xs">{inst.selectedBranch}</span>
)}
</div>
</div>
);
})}
</div>
) : (
<p className="text-red-400 text-sm mc-text-shadow mb-4 text-center">
No installed instances available. Install a version first.
</p>
)}
{error && (
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
{error}
</div>
)}
<div className="flex gap-4 w-full justify-center">
<button
onMouseEnter={() => {
const cancelIdx = validInstances.length > 0 ? 2 : 1;
setFocusIndex(cancelIdx);
}}
onClick={() => { playBackSound(); onClose(); }}
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${(() => {
const cancelIdx = validInstances.length > 0 ? 2 : 1;
return focusIndex === cancelIdx ? "text-[#FFFF55]" : "text-white";
})()}`}
style={{
backgroundImage: (() => {
const cancelIdx = validInstances.length > 0 ? 2 : 1;
return focusIndex === cancelIdx
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')";
})(),
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Cancel
</button>
{validInstances.length > 0 && (
<button
onMouseEnter={() => setFocusIndex(1)}
onClick={handleJoin}
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${focusIndex === 1 ? "text-[#FFFF55]" : "text-white"}`}
style={{
backgroundImage: focusIndex === 1
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Join
</button>
)}
</div>
</>
) : (
<>
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Joining Game
</h2>
<div className="flex flex-col items-center gap-4 py-8">
<div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" />
<p className="text-white text-lg mc-text-shadow text-center">{status}</p>
</div>
{error && (
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
{error}
</div>
)}
</>
)}
</div>
</motion.div>
);
}
+276
View File
@@ -0,0 +1,276 @@
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService";
export default function SetUidModal({
isOpen,
onClose,
playPressSound,
playBackSound,
instances,
installedVersions,
targetInstanceId,
}: any) {
const [mode, setMode] = useState<"manual" | "copy">("manual");
const [uid, setUid] = useState("0xFF02F0C87E8AC1F2");
const [selectedInstance, setSelectedInstance] = useState("");
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [error, setError] = useState("");
const [focusIndex, setFocusIndex] = useState(0);
useEffect(() => {
if (!isOpen) {
setMode("manual");
setUid("0xFF02F0C87E8AC1F2");
setSelectedInstance("");
setError("");
setFocusIndex(0);
setIsDropdownOpen(false);
} else if (targetInstanceId) {
(async () => {
try {
const targetPath = await TauriService.getInstancePath(targetInstanceId);
const data = await TauriService.readBinaryFile(`${targetPath}/uid.dat`);
const currentUid = new TextDecoder().decode(data);
if (currentUid.trim()) {
setUid(currentUid);
} else {
setUid("0xFF02F0C87E8AC1F2");
}
} catch (e) {
setUid("0xFF02F0C87E8AC1F2");
}
})();
}
}, [isOpen, targetInstanceId]);
const validInstances = instances.filter((i: any) => installedVersions.includes(i.instanceId) && i.instanceId !== targetInstanceId);
const handleSave = async () => {
playPressSound("save_click.wav");
try {
let finalUid = uid;
if (mode === "copy") {
if (!selectedInstance) {
setError("Select an instance to copy from.");
return;
}
const sourcePath = await TauriService.getInstancePath(selectedInstance);
try {
const sourceData = await TauriService.readBinaryFile(`${sourcePath}/uid.dat`);
finalUid = new TextDecoder().decode(sourceData);
} catch (e) {
setError("Source instance has no uid.dat or it could not be read.");
return;
}
}
if (!finalUid) {
setError("UID cannot be empty.");
return;
}
const encodedUid = new TextEncoder().encode(finalUid);
const targetPath = await TauriService.getInstancePath(targetInstanceId);
await TauriService.writeBinaryFile(`${targetPath}/uid.dat`, encodedUid);
onClose();
} catch (e: any) {
setError(e.toString());
}
};
useEffect(() => {
if (!isOpen) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
playBackSound("close_click.wav");
onClose();
} else if (e.key === "ArrowDown" || e.key === "Tab") {
e.preventDefault();
setFocusIndex((prev) => (prev + 1) % 5);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setFocusIndex((prev) => (prev - 1 + 5) % 5);
} else if (e.key === "Enter") {
if (focusIndex === 0) {
playPressSound();
setMode("manual");
} else if (focusIndex === 1) {
playPressSound();
setMode("copy");
} else if (focusIndex === 2 && mode === "copy") {
playPressSound();
setIsDropdownOpen(!isDropdownOpen);
} else if (focusIndex === 3) {
playBackSound("close_click.wav");
onClose();
} else if (focusIndex === 4) {
handleSave();
}
}
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, focusIndex, mode, uid, selectedInstance, isDropdownOpen]);
if (!isOpen) return null;
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-[400px] 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">
Set UID
</h2>
<div className="flex gap-4 mb-5 w-full justify-center">
<button
onMouseEnter={() => setFocusIndex(0)}
onClick={() => { playPressSound(); setMode("manual"); }}
className={`w-32 h-10 flex items-center justify-center text-sm mc-text-shadow transition-all outline-none border-none bg-transparent ${focusIndex === 0 ? "text-[#FFFF55]" : mode === "manual" ? "text-white" : "text-gray-400"}`}
style={{
backgroundImage: focusIndex === 0 || mode === "manual"
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Manual
</button>
<button
onMouseEnter={() => setFocusIndex(1)}
onClick={() => { playPressSound(); setMode("copy"); }}
className={`w-32 h-10 flex items-center justify-center text-sm mc-text-shadow transition-all outline-none border-none bg-transparent ${focusIndex === 1 ? "text-[#FFFF55]" : mode === "copy" ? "text-white" : "text-gray-400"}`}
style={{
backgroundImage: focusIndex === 1 || mode === "copy"
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Copy
</button>
</div>
<div className="flex flex-col gap-4 w-full min-h-[80px]">
{mode === "manual" ? (
<div className="flex flex-col gap-1 items-center w-full">
<label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]">
Enter UID
</label>
<input
type="text"
autoFocus
value={uid}
onChange={(e) => setUid(e.target.value)}
onFocus={() => setFocusIndex(2)}
placeholder="0xFF02F0C87E8AC1F2"
className={`w-full h-10 px-3 bg-black/40 border-2 ${focusIndex === 2 ? 'border-white' : 'border-[#373737]'} text-white text-base outline-none font-['Mojangles'] text-center`}
style={{ imageRendering: "pixelated", filter: focusIndex === 2 ? 'brightness(1.2)' : 'none' }}
/>
</div>
) : (
<div className="flex flex-col gap-1 items-center w-full relative">
<label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]">
Select Installed Instance
</label>
<div
onClick={() => { playPressSound(); setIsDropdownOpen(!isDropdownOpen); }}
onFocus={() => { setFocusIndex(2); setIsDropdownOpen(false); }}
tabIndex={0}
className={`w-full h-10 px-3 bg-black/40 border-2 ${focusIndex === 2 ? 'border-white' : 'border-[#373737]'} flex items-center justify-between text-white text-base outline-none font-['Mojangles'] cursor-pointer`}
style={{ imageRendering: "pixelated", filter: focusIndex === 2 ? 'brightness(1.2)' : 'none' }}
>
<span className="truncate">
{selectedInstance
? (() => {
const i = validInstances.find((inst: any) => inst.instanceId === selectedInstance);
return i ? `${i.name} ${i.selectedBranch ? `(${i.selectedBranch})` : ""}` : "-- Select an Instance --";
})()
: "-- Select an Instance --"}
</span>
<span className="text-xs">▼</span>
</div>
{isDropdownOpen && validInstances.length > 0 && (
<div className="absolute top-[60px] left-0 w-full max-h-40 overflow-y-auto bg-black/90 border-2 border-[#373737] z-50 flex flex-col custom-scrollbar shadow-xl" style={{ imageRendering: "pixelated" }}>
{validInstances.map((i: any) => (
<div
key={i.instanceId}
onClick={() => {
playPressSound();
setSelectedInstance(i.instanceId);
setIsDropdownOpen(false);
}}
className="px-3 py-2 text-white text-sm cursor-pointer hover:bg-white/20 transition-colors truncate font-['Mojangles']"
>
{i.name} {i.selectedBranch ? `(${i.selectedBranch})` : ""}
</div>
))}
</div>
)}
{validInstances.length === 0 && (
<p className="text-red-400 text-xs text-center mt-1">No other installed instances available.</p>
)}
</div>
)}
{error && (
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mt-2">
{error}
</div>
)}
</div>
<div className="flex gap-4 mt-6 w-full justify-center">
<button
onMouseEnter={() => setFocusIndex(3)}
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 === 3 ? "text-[#FFFF55]" : "text-white"}`}
style={{
backgroundImage: focusIndex === 3
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Cancel
</button>
<button
onMouseEnter={() => setFocusIndex(4)}
onClick={handleSave}
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",
}}
>
Save
</button>
</div>
</div>
</motion.div>
);
}
+42 -15
View File
@@ -48,17 +48,37 @@ const HomeView = memo(function HomeView() {
: isGameRunning
? stopGame
: isDownloading
? () => { }
? () => {}
: isInstalled
? handleLaunch
: () => toggleInstall(profile),
isDanger: isGameRunning,
disabled: isDownloading,
},
{ label: "Help & Options", action: () => setActiveView("settings"), disabled: false, id: "settings" },
{ label: "Versions", action: () => setActiveView("versions"), disabled: false, id: "versions" },
{ label: "Workshop", action: () => setActiveView("workshop"), disabled: false, id: "workshop" },
{ label: "Developer Tools", action: () => setActiveView("devtools"), disabled: false, id: "devtools" },
{
label: "Help & Options",
action: () => setActiveView("settings"),
disabled: false,
id: "settings",
},
{
label: "Versions",
action: () => setActiveView("versions"),
disabled: false,
id: "versions",
},
{
label: "Workshop",
action: () => setActiveView("workshop"),
disabled: false,
id: "workshop",
},
{
label: "Developer Tools",
action: () => setActiveView("devtools"),
disabled: false,
id: "devtools",
},
],
[
isDownloading,
@@ -111,7 +131,9 @@ const HomeView = memo(function HomeView() {
{buttons.map((btn: any, i: number) => (
<div key={i} className="relative w-full group">
<button
onMouseEnter={() => isFocusedSection && !btn.disabled && setMenuFocus(i)}
onMouseEnter={() =>
isFocusedSection && !btn.disabled && setMenuFocus(i)
}
onMouseLeave={() => setMenuFocus(null)}
onClick={() => {
if (isFocusedSection && !btn.disabled) {
@@ -133,14 +155,19 @@ const HomeView = memo(function HomeView() {
}}
>
<div className="w-full h-full flex items-center justify-center relative">
<span>{btn.label}</span>
{btn.id === "versions" && Object.values(updatesAvailable || {}).some((v) => v) && (
<img
src="/images/Update_Icon.png"
className="absolute right-4 w-6 h-6 object-contain"
style={{ imageRendering: "pixelated", filter: "drop-shadow(0 0 2px rgba(255, 255, 0, 0.8)) sepia(100%) saturate(500%) hue-rotate(5deg) brightness(1.2)" }}
/>
)}
<span>{btn.label}</span>
{btn.id === "versions" &&
Object.values(updatesAvailable || {}).some((v) => v) && (
<img
src="/images/Update_Icon.png"
className="absolute right-4 w-6 h-6 object-contain"
style={{
imageRendering: "pixelated",
filter:
"drop-shadow(0 0 2px rgba(255, 255, 0, 0.8)) sepia(100%) saturate(500%) hue-rotate(5deg) brightness(1.2)",
}}
/>
)}
</div>
</button>
</div>
@@ -150,7 +177,7 @@ const HomeView = memo(function HomeView() {
<div className="pt-4 flex flex-col items-center w-full gap-3">
<div className="flex gap-8">
<a
href="https://discord.gg/NPMp9JhGYH"
href="https://discord.gg/cQVKhQXcCx"
target="_blank"
rel="noopener noreferrer"
onClick={() => {
File diff suppressed because it is too large Load Diff
+7 -12
View File
@@ -10,6 +10,7 @@ import {
ScreenshotService,
ScreenshotInfo,
} from "../../services/ScreenshotService";
import { ScreenshotImage } from "../common/ScreenshotImage";
const ScreenshotsView = memo(function ScreenshotsView() {
const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio();
@@ -207,15 +208,12 @@ const ScreenshotsView = memo(function ScreenshotsView() {
}}
>
<div className="w-full h-full relative overflow-hidden bg-black/50">
<img
src={`screenshots://localhost/${ss.path.replace(/\\/g, "/")}`}
<ScreenshotImage
path={ss.path}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
alt={ss.name}
onError={(e) => {
(e.target as HTMLImageElement).src =
"/images/Folder_Icon.png";
}}
fallbackSrc="/images/Folder_Icon.png"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-60" />
@@ -288,13 +286,10 @@ const ScreenshotsView = memo(function ScreenshotsView() {
onClick={(e) => e.stopPropagation()}
>
<div className="relative w-full aspect-video bg-black/60 overflow-hidden border border-[#444] rounded-sm">
<img
src={`screenshots://localhost/${selectedScreenshot.path.replace(/\\/g, "/")}`}
<ScreenshotImage
path={selectedScreenshot.path}
className="w-full h-full object-contain"
onError={(e) => {
(e.target as HTMLImageElement).src =
"/images/Pack_Icon.png";
}}
fallbackSrc="/images/Pack_Icon.png"
/>
<div className="absolute bottom-4 left-6 right-6 flex items-end justify-between pointer-events-none">
<div className="flex flex-col gap-1">
+198 -101
View File
@@ -2,16 +2,56 @@ import { useState, useEffect, useRef, useMemo, memo } from "react";
import { motion } from "framer-motion";
import { TauriService, Runner } from "../../services/TauriService";
import { usePlatform } from "../../hooks/usePlatform";
import { useUI, useConfig, useAudio, useGame } from "../../context/LauncherContext";
import {
useUI,
useConfig,
useAudio,
useGame,
} from "../../context/LauncherContext";
const SettingsView = memo(function SettingsView() {
const { setActiveView } = useUI();
const { vfxEnabled, setVfxEnabled, animationsEnabled, setAnimationsEnabled, musicVol: musicVolume, setMusicVol: setMusicVolume, sfxVol: sfxVolume, setSfxVol: setSfxVolume, layout, setLayout, linuxRunner, setLinuxRunner, perfBoost, setPerfBoost, rpcEnabled, setRpcEnabled, legacyMode, setLegacyMode } = useConfig();
const { currentTrack, setCurrentTrack, tracks, playPressSound, playBackSound } = useAudio();
const { isGameRunning, stopGame, isRunnerDownloading, runnerDownloadProgress, downloadRunner } = useGame();
const {
vfxEnabled,
setVfxEnabled,
animationsEnabled,
setAnimationsEnabled,
musicVol: musicVolume,
setMusicVol: setMusicVolume,
sfxVol: sfxVolume,
setSfxVol: setSfxVolume,
layout,
setLayout,
linuxRunner,
setLinuxRunner,
perfBoost,
setPerfBoost,
rpcEnabled,
setRpcEnabled,
legacyMode,
setLegacyMode,
mangohudEnabled,
setMangohudEnabled,
} = useConfig();
const {
currentTrack,
setCurrentTrack,
tracks,
playPressSound,
playBackSound,
} = useAudio();
const {
isGameRunning,
stopGame,
isRunnerDownloading,
runnerDownloadProgress,
downloadRunner,
} = useGame();
const { isLinux, isMac } = usePlatform();
const [focusIndex, setFocusIndex] = useState<number | null>(null);
const [currentSubMenu, setCurrentSubMenu] = useState<"main" | "audio" | "video" | "controls" | "launcher">("main");
const [currentSubMenu, setCurrentSubMenu] = useState<
"main" | "audio" | "video" | "controls" | "launcher"
>("main");
const [runners, setRunners] = useState<Runner[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
@@ -53,6 +93,11 @@ const SettingsView = memo(function SettingsView() {
setLegacyMode(!legacyMode);
};
const handleMangohudToggle = () => {
playPressSound();
setMangohudEnabled(!mangohudEnabled);
};
const handleRunnerToggle = () => {
playPressSound();
if (runners.length === 0) return;
@@ -69,35 +114,37 @@ const SettingsView = memo(function SettingsView() {
const handleResetSetup = () => {
playPressSound();
// Create styled confirmation dialog
const dialog = document.createElement('div');
dialog.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
const dialog = document.createElement("div");
dialog.className =
"fixed inset-0 bg-black/80 flex items-center justify-center z-50";
dialog.innerHTML = `
<div class="relative p-8 max-w-md mx-4" style="background-image: url('/images/frame_background.png'); background-size: 100% 100%; background-repeat: no-repeat; image-rendering: pixelated;">
<h3 class="text-2xl font-bold text-white mb-4 text-center" style="text-shadow: 2px 2px 0px rgba(0,0,0,0.8)">Reset Setup</h3>
<p class="text-white mb-6 text-center">Are you sure you want to reset launcher setup?</p>
<div class="flex gap-4 justify-center">
<button id="reset-yes" class="mc-sq-btn px-6 py-3 text-white hover:scale-105 active:scale-95 transition-transform">Yes</button>
<button id="reset-no" class="mc-sq-btn px-6 py-3 text-white hover:scale-105 active:scale-95 transition-transform">No</button>
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
<h3 class="text-2xl font-bold text-[#333333] mb-4 text-left w-full px-4 mc-text-shadow">Reset Setup</h3>
<p class="text-[#333333] mb-8 text-left w-full px-4">Are you sure you want to reset launcher setup?</p>
<div class="flex flex-col gap-3 w-full px-4">
<button id="reset-cancel" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">Cancel</button>
<button id="reset-ok" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">OK</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const handleYes = () => {
const handleOk = () => {
document.body.removeChild(dialog);
showSecondConfirmation();
};
const handleNo = () => {
const handleCancel = () => {
document.body.removeChild(dialog);
};
dialog.querySelector('#reset-yes')?.addEventListener('click', handleYes);
dialog.querySelector('#reset-no')?.addEventListener('click', handleNo);
dialog.querySelector("#reset-ok")?.addEventListener("click", handleOk);
dialog
.querySelector("#reset-cancel")
?.addEventListener("click", handleCancel);
dialog.addEventListener('click', (e) => {
dialog.addEventListener("click", (e) => {
if (e.target === dialog) {
document.body.removeChild(dialog);
}
@@ -105,43 +152,48 @@ const SettingsView = memo(function SettingsView() {
};
const showSecondConfirmation = () => {
const dialog = document.createElement('div');
dialog.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
const dialog = document.createElement("div");
dialog.className =
"fixed inset-0 bg-black/80 flex items-center justify-center z-50";
dialog.innerHTML = `
<div class="relative p-8 max-w-md mx-4" style="background-image: url('/images/frame_background.png'); background-size: 100% 100%; background-repeat: no-repeat; image-rendering: pixelated;">
<h3 class="text-2xl font-bold text-yellow-400 mb-4 text-center" style="text-shadow: 2px 2px 0px rgba(0,0,0,0.8)">CONFIRM RESET</h3>
<div class="text-white mb-6 text-left">
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
<h3 class="text-2xl font-bold text-[#333333] mb-2 text-left w-full px-4 mc-text-shadow">CONFIRM RESET</h3>
<div class="text-[#333333] mb-6 text-left w-full px-4">
<p class="mb-2">⚠️ This will:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<ul class="list-none space-y-1 text-sm">
<li>Clear all launcher settings</li>
<li>Reset your username</li>
<li>Show setup screen again</li>
<li>Require reconfiguration</li>
</ul>
<p class="mt-3 text-yellow-400 font-bold">This action cannot be undone!</p>
<p class="mt-3 text-[#333333] font-bold">This action cannot be undone!</p>
</div>
<div class="flex gap-4 justify-center">
<button id="reset-final-yes" class="mc-sq-btn px-6 py-3 text-yellow-400 hover:scale-105 active:scale-95 transition-transform">YES, RESET</button>
<button id="reset-final-no" class="mc-sq-btn px-6 py-3 text-white hover:scale-105 active:scale-95 transition-transform">Cancel</button>
<div class="flex flex-col gap-3 w-full px-4">
<button id="reset-final-cancel" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">Cancel</button>
<button id="reset-final-ok" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">OK</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const handleFinalYes = () => {
const handleFinalOk = () => {
document.body.removeChild(dialog);
performReset();
};
const handleFinalNo = () => {
const handleFinalCancel = () => {
document.body.removeChild(dialog);
};
dialog.querySelector('#reset-final-yes')?.addEventListener('click', handleFinalYes);
dialog.querySelector('#reset-final-no')?.addEventListener('click', handleFinalNo);
dialog
.querySelector("#reset-final-ok")
?.addEventListener("click", handleFinalOk);
dialog
.querySelector("#reset-final-cancel")
?.addEventListener("click", handleFinalCancel);
dialog.addEventListener('click', (e) => {
dialog.addEventListener("click", (e) => {
if (e.target === dialog) {
document.body.removeChild(dialog);
}
@@ -149,13 +201,10 @@ const SettingsView = memo(function SettingsView() {
};
const performReset = () => {
// Clear all localStorage data
localStorage.clear();
// Set setup as not completed
localStorage.setItem('lce-setup-completed', 'false');
localStorage.setItem("lce-setup-completed", "false");
// Force reload to show setup screen
window.location.reload();
};
@@ -163,11 +212,9 @@ const SettingsView = memo(function SettingsView() {
if (tracks && tracks.length > 0) {
const fullPath = tracks[currentTrack];
if (fullPath) {
trackName = fullPath
.split("/")
.pop()
?.replace(".ogg", "")
.replace(".wav", "") || "Unknown";
trackName =
fullPath.split("/").pop()?.replace(".ogg", "").replace(".wav", "") ||
"Unknown";
}
}
@@ -176,20 +223,20 @@ const SettingsView = memo(function SettingsView() {
type SettingsItem =
| {
id: string;
label: string;
type: "slider";
value: number;
onChange: (val: any) => void;
}
id: string;
label: string;
type: "slider";
value: number;
onChange: (val: any) => void;
}
| {
id: string;
label: string;
type: "button";
onClick: () => void;
small?: boolean;
color?: string;
};
id: string;
label: string;
type: "button";
onClick: () => void;
small?: boolean;
color?: string;
};
const settingsItems = useMemo<SettingsItem[]>(() => {
const items: SettingsItem[] = [];
@@ -199,25 +246,41 @@ const SettingsView = memo(function SettingsView() {
id: "audio_menu",
label: "Audio",
type: "button",
onClick: () => { playPressSound(); setCurrentSubMenu("audio"); setFocusIndex(0); },
onClick: () => {
playPressSound();
setCurrentSubMenu("audio");
setFocusIndex(0);
},
});
items.push({
id: "video_menu",
label: "Video",
type: "button",
onClick: () => { playPressSound(); setCurrentSubMenu("video"); setFocusIndex(0); },
onClick: () => {
playPressSound();
setCurrentSubMenu("video");
setFocusIndex(0);
},
});
items.push({
id: "controls_menu",
label: "Controls",
type: "button",
onClick: () => { playPressSound(); setCurrentSubMenu("controls"); setFocusIndex(0); },
onClick: () => {
playPressSound();
setCurrentSubMenu("controls");
setFocusIndex(0);
},
});
items.push({
id: "launcher_menu",
label: "Launcher",
type: "button",
onClick: () => { playPressSound(); setCurrentSubMenu("launcher"); setFocusIndex(0); },
onClick: () => {
playPressSound();
setCurrentSubMenu("launcher");
setFocusIndex(0);
},
});
} else if (currentSubMenu === "audio") {
items.push({
@@ -288,6 +351,12 @@ const SettingsView = memo(function SettingsView() {
type: "button",
onClick: handleRunnerToggle,
});
items.push({
id: "mangohud",
label: `MangoHud: ${mangohudEnabled ? "ON" : "OFF"}`,
type: "button",
onClick: handleMangohudToggle,
});
items.push({
id: "download_runner",
label: isRunnerDownloading
@@ -296,7 +365,10 @@ const SettingsView = memo(function SettingsView() {
type: "button",
onClick: () => {
if (!isRunnerDownloading) {
downloadRunner("GE-Proton9-25", "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-25/GE-Proton9-25.tar.gz");
downloadRunner(
"GE-Proton9-25",
"https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-25/GE-Proton9-25.tar.gz",
);
}
},
small: true,
@@ -349,6 +421,7 @@ const SettingsView = memo(function SettingsView() {
animationsEnabled,
layout,
isLinux,
mangohudEnabled,
selectedRunnerName,
isRunnerDownloading,
runnerDownloadProgress,
@@ -363,6 +436,7 @@ const SettingsView = memo(function SettingsView() {
handleLayoutToggle,
handleRunnerToggle,
handlePerfToggle,
handleMangohudToggle,
handleResetSetup,
stopGame,
downloadRunner,
@@ -435,11 +509,16 @@ const SettingsView = memo(function SettingsView() {
backgroundImage: "url('/images/Button_Background2.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated" as const,
color: focusIndex === index ? "#FFFF55" : "white",
color: focusIndex === index ? "#ffff00" : "white",
});
const isToggleOption = (label: string): boolean => {
return label.includes("ON") || label.includes("OFF") || label.includes("Enabled") || label.includes("Disabled");
return (
label.includes("ON") ||
label.includes("OFF") ||
label.includes("Enabled") ||
label.includes("Disabled")
);
};
const getToggleState = (label: string): boolean => {
@@ -454,10 +533,18 @@ const SettingsView = memo(function SettingsView() {
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
className="flex flex-col items-center w-full max-w-2xl outline-none"
className="flex flex-col items-center w-full max-w-3xl outline-none"
>
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase opacity-80 font-bold whitespace-nowrap px-4">
{currentSubMenu === "main" ? "Settings" : currentSubMenu === "audio" ? "Audio" : currentSubMenu === "video" ? "Video" : currentSubMenu === "controls" ? "Controls" : "Launcher"}
{currentSubMenu === "main"
? "Settings"
: currentSubMenu === "audio"
? "Audio"
: currentSubMenu === "video"
? "Video"
: currentSubMenu === "controls"
? "Controls"
: "Launcher"}
</h2>
{currentSubMenu === "main" ? (
@@ -472,11 +559,11 @@ const SettingsView = memo(function SettingsView() {
data-index={index}
tabIndex={0}
onMouseEnter={() => setFocusIndex(index)}
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#FFFF55] shrink-0"
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#ffff00] shrink-0"
style={getSliderStyle(index)}
>
<span
className={`absolute z-10 text-xl mc-text-shadow pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#FFFF55]" : "text-white"}`}
className={`absolute z-10 text-xl mc-text-shadow pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#ffff00]" : "text-white"}`}
>
{item.label}
</span>
@@ -505,14 +592,15 @@ const SettingsView = memo(function SettingsView() {
data-index={index}
onMouseEnter={() => setFocusIndex(index)}
onClick={item.onClick}
className={`w-[360px] h-10 flex items-center justify-center px-4 relative z-30 transition-colors outline-none border-none shrink-0 ${isRed
? focusIndex === index
? "text-red-400"
: "text-red-200"
: focusIndex === index
? "text-[#FFFF55]"
: "text-white"
} ${isRed ? "hover:text-red-500" : "hover:text-[#FFFF55]"}`}
className={`w-[360px] h-10 flex items-center justify-center px-4 relative z-30 transition-colors outline-none border-none shrink-0 ${
isRed
? focusIndex === index
? "text-red-400"
: "text-red-200"
: focusIndex === index
? "text-[#ffff00]"
: "text-white"
} ${isRed ? "hover:text-red-500" : "hover:text-[#ffff00]"}`}
style={getItemStyle(index)}
>
<span
@@ -525,15 +613,7 @@ const SettingsView = memo(function SettingsView() {
})}
</div>
) : (
<div
className="min-w-[420px] w-fit p-6 flex flex-col items-center"
style={{
backgroundImage: "url('/images/background.png')",
backgroundSize: "100% 100%",
backgroundRepeat: "no-repeat",
imageRendering: "pixelated",
}}
>
<div className="min-w-[420px] w-fit p-4 flex flex-col items-center mc-options-bg">
<div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2">
{settingsItems.map((item, index) => {
if (item.id === "back") return null;
@@ -545,11 +625,11 @@ const SettingsView = memo(function SettingsView() {
data-index={index}
tabIndex={0}
onMouseEnter={() => setFocusIndex(index)}
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#FFFF55] shrink-0"
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#ffff00] shrink-0"
style={getSliderStyle(index)}
>
<span
className={`absolute z-10 text-xl pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#FFFF55]" : item.id === "music" || item.id === "sfx" ? "text-white" : "text-[#2a2a2a]"}`}
className={`absolute z-10 text-xl pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#ffff00]" : item.id === "music" || item.id === "sfx" ? "text-white" : "text-[#2a2a2a]"}`}
>
{item.label}
</span>
@@ -560,7 +640,9 @@ const SettingsView = memo(function SettingsView() {
max="100"
step="1"
value={item.value}
onChange={(e) => item.onChange(parseInt(e.target.value))}
onChange={(e) =>
item.onChange(parseInt(e.target.value))
}
onMouseUp={playPressSound}
className="mc-slider-custom w-[calc(100%+16px)] h-full opacity-100 cursor-pointer z-20 outline-none m-0"
/>
@@ -580,21 +662,35 @@ const SettingsView = memo(function SettingsView() {
data-index={index}
onMouseEnter={() => setFocusIndex(index)}
onClick={item.onClick}
className={`w-[360px] h-10 flex items-center justify-between px-4 relative z-30 transition-all outline-none border-none shrink-0 rounded ${focusIndex === index ? "bg-black/10" : "bg-transparent"} ${isRed ? "text-red-600" : "text-[#2a2a2a]"} hover:bg-black/15`}
className={`w-[360px] h-10 flex items-center pl-1.5 pr-4 relative z-30 outline-none border-none shrink-0 rounded ${focusIndex === index ? "text-[#ffff00]" : isRed ? "text-red-600" : "text-[#333333]"}`}
>
{isToggle && (
<div className="relative w-6 h-6 mr-3 shrink-0">
<img
src={
focusIndex === index
? "/images/checkbox_highlighted.png"
: "/images/checkbox.png"
}
alt="checkbox"
className="w-full h-full object-contain"
style={{ imageRendering: "pixelated" }}
/>
{toggleState && (
<img
src="/images/check.png"
alt="checked"
className="absolute inset-0 w-full h-full object-contain"
style={{ imageRendering: "pixelated" }}
/>
)}
</div>
)}
<span
className={`tracking-widest uppercase ${isSmall ? "text-xs" : item.label.length > 25 ? "text-base" : "text-lg"} truncate text-left flex-1`}
className={`tracking-widest text-xl mc-text-shadow ${isSmall ? "text-xs" : item.label.length > 25 ? "text-base" : "text-lg"} truncate text-left`}
>
{isToggle ? item.label.split(":")[0] : item.label}
</span>
{isToggle && (
<img
src={toggleState ? "/images/Toggle_Switch_On.png" : "/images/Toggle_Switch_Off.png"}
alt={toggleState ? "ON" : "OFF"}
className="w-12 h-6 object-contain shrink-0 ml-2"
style={{ imageRendering: "pixelated" }}
/>
)}
</button>
);
})}
@@ -612,11 +708,12 @@ const SettingsView = memo(function SettingsView() {
data-index={backIndex}
onMouseEnter={() => setFocusIndex(backIndex)}
onClick={backItem.onClick}
className={`w-72 h-10 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] mt-4 ${focusIndex === backIndex ? "text-[#FFFF55]" : "text-white"}`}
className={`w-40 h-10 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none border-none hover:text-[#ffff00] mt-4 ${focusIndex === backIndex ? "text-[#ffff00]" : "text-white"}`}
style={{
backgroundImage: focusIndex === backIndex
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundImage:
focusIndex === backIndex
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
+7
View File
@@ -46,7 +46,14 @@ const DEFAULT_SKINS: SavedSkin[] = [
},
{ id: "peter", name: "Peter", url: "/Skins/Peter.png", isSlim: false },
{ id: "piebot", name: "piebot", url: "/Skins/piebot.png", isSlim: false },
{ id: "andipog", name: "Andi_Pog", url: "/Skins/andi.png", isSlim: false },
{ id: "sevenhundred", name: "700", url: "/Skins/700.png", isSlim: false },
{
id: "prismachunk0",
name: "PrismaChunk0",
url: "/Skins/PrismaChunk0.png",
isSlim: false,
},
];
const SkinsView = memo(function SkinsView() {
+203 -75
View File
@@ -2,12 +2,14 @@ import { useState, useEffect, useRef, memo } from "react";
import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService";
import CustomTUModal from "../modals/CustomTUModal";
import SetUidModal from "../modals/SetUidModal";
import {
useUI,
useConfig,
useAudio,
useGame,
} from "../../context/LauncherContext";
import { ScreenshotImage } from "../common/ScreenshotImage";
interface DeleteConfirmButtonProps {
label: string;
onClick: () => void;
@@ -26,8 +28,9 @@ const DeleteConfirmButton = memo(function DeleteConfirmButton({
onClick={onClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${isDanger ? "text-red-500" : "text-white"
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${
isDanger ? "text-red-500" : "text-white"
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
style={{
backgroundImage: isHovered
? "url('/images/button_highlighted.png')"
@@ -68,6 +71,8 @@ const VersionsView = memo(function VersionsView() {
const [focusIndex, setFocusIndex] = useState<number>(0);
const [focusBtn, setFocusBtn] = useState<number>(0);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isSetUidModalOpen, setIsSetUidModalOpen] = useState(false);
const [setUidTargetId, setSetUidTargetId] = useState("");
const [editingEdition, setEditingEdition] = useState<any>(null);
const [initialPath, setInitialPath] = useState<string>("");
const [hoveredBtn, setHoveredBtn] = useState<{
@@ -166,7 +171,7 @@ const VersionsView = memo(function VersionsView() {
toggleInstall,
handleCancelDownload,
addToSteam,
isDayTime
isDayTime,
]);
useEffect(() => {
@@ -208,7 +213,7 @@ const VersionsView = memo(function VersionsView() {
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: animationsEnabled ? 0.25 : 0 }}
className="flex flex-col items-center w-full max-w-2xl outline-none"
className="flex flex-col items-center w-full max-w-5xl outline-none"
>
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase font-bold">
Versions
@@ -229,7 +234,9 @@ const VersionsView = memo(function VersionsView() {
>
<div className="flex flex-col gap-1">
{editions.map((edition: any, i: number) => {
const isInstalled = installedVersions.includes(edition.instanceId);
const isInstalled = installedVersions.includes(
edition.instanceId,
);
const hasAnyInstall = installedVersions.length > 0;
const isSelected =
hasAnyInstall && selectedProfile === edition.instanceId;
@@ -242,9 +249,11 @@ const VersionsView = memo(function VersionsView() {
<div
key={edition.id}
data-index={i}
className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${isSelected && !isComingSoon ? "bg-[#404040]/50" : ""
} ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${isComingSoon ? "opacity-50 cursor-not-allowed" : ""
} relative ${openMenuId === edition.id ? "z-50" : "z-0"}`}
className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${
isSelected && !isComingSoon ? "bg-[#404040]/50" : ""
} ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${
isComingSoon ? "opacity-50 cursor-not-allowed" : ""
} relative ${openMenuId === edition.id ? "z-50" : "z-0"}`}
onMouseEnter={() => !isComingSoon && setFocusIndex(i)}
>
<div className="w-6 flex items-center justify-center flex-shrink-0">
@@ -280,28 +289,34 @@ const VersionsView = memo(function VersionsView() {
onClick={() =>
!isComingSoon && handleEditionClick(edition, i)
}
className={`flex-1 text-left min-w-0 outline-none rounded cursor-pointer ${focusIndex === i && focusBtn === 0 && !isComingSoon
? "ring-2 ring-white"
: ""
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
className={`flex-1 text-left min-w-0 outline-none rounded cursor-pointer ${
focusIndex === i && focusBtn === 0 && !isComingSoon
? "ring-2 ring-white"
: ""
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
>
<div className="flex items-center gap-2">
{edition.logo && (
<img
src={
edition.logo.startsWith("http") ||
edition.logo.startsWith("/images")
? edition.logo
: `screenshots://localhost/${edition.logo.replace(/\\/g, "/")}`
}
alt=""
className="w-5 h-5 object-contain flex-shrink-0"
style={{ imageRendering: "pixelated" }}
/>
)}
{edition.logo &&
(edition.logo.startsWith("http") ||
edition.logo.startsWith("/images") ? (
<img
src={edition.logo}
alt=""
className="w-5 h-5 object-contain flex-shrink-0"
style={{ imageRendering: "pixelated" }}
/>
) : (
<ScreenshotImage
path={edition.logo}
alt=""
className="w-5 h-5 object-contain flex-shrink-0"
style={{ imageRendering: "pixelated" }}
/>
))}
<span
className={`text-xl tracking-wide truncate ${isSelected ? "text-white" : "text-black"
}`}
className={`text-xl tracking-wide truncate ${
isSelected ? "text-white" : "text-black"
}`}
style={{ textShadow: "none" }}
>
{edition.name}
@@ -322,8 +337,9 @@ const VersionsView = memo(function VersionsView() {
)}
</div>
<p
className={`text-base font-medium leading-tight ${isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
}`}
className={`text-base font-medium leading-tight ${
isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
}`}
>
{edition.desc}
</p>
@@ -340,23 +356,40 @@ const VersionsView = memo(function VersionsView() {
handleCancelDownload();
}
}}
onMouseEnter={() => setHoveredBtn({ row: i, btn: "main" })}
onMouseEnter={() =>
setHoveredBtn({ row: i, btn: "main" })
}
onMouseLeave={() => setHoveredBtn(null)}
className={`w-9 h-9 flex items-center justify-center ${(isDownloading || (!!downloadingId && !isInstalled)) ? "opacity-50" : ""
}`}
className={`w-9 h-9 flex items-center justify-center ${
isDownloading || (!!downloadingId && !isInstalled)
? "opacity-50"
: ""
}`}
style={{
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "main") || (focusIndex === i && focusBtn === 0)
? "url('/images/Button_Square_Highlighted.png')"
: "url('/images/Button_Square.png')",
backgroundImage:
(hoveredBtn?.row === i &&
hoveredBtn?.btn === "main") ||
(focusIndex === i && focusBtn === 0)
? "url('/images/Button_Square_Highlighted.png')"
: "url('/images/Button_Square.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<img
src={isDownloading ? "/images/Trash_Bin_Icon.png" : "/images/Download_Icon.png"}
src={
isDownloading
? "/images/Trash_Bin_Icon.png"
: "/images/Download_Icon.png"
}
alt=""
className="w-5 h-5 object-contain"
style={{ imageRendering: "pixelated", filter: isDownloading ? "hue-rotate(300deg)" : "none" }}
style={{
imageRendering: "pixelated",
filter: isDownloading
? "hue-rotate(300deg)"
: "none",
}}
/>
</button>
) : (
@@ -364,23 +397,39 @@ const VersionsView = memo(function VersionsView() {
onClick={(e) => {
e.stopPropagation();
playPressSound();
setOpenMenuId(openMenuId === edition.id ? null : edition.id);
setOpenMenuId(
openMenuId === edition.id ? null : edition.id,
);
}}
onMouseEnter={() => setHoveredBtn({ row: i, btn: "menu" })}
onMouseEnter={() =>
setHoveredBtn({ row: i, btn: "menu" })
}
onMouseLeave={() => setHoveredBtn(null)}
className="w-9 h-9 flex flex-col items-center justify-center gap-1 transition-colors relative"
style={{
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "menu") || (focusIndex === i && (focusBtn === 0 || focusBtn === 1))
? "url('/images/Button_Square_Highlighted.png')"
: "url('/images/Button_Square.png')",
backgroundImage:
(hoveredBtn?.row === i &&
hoveredBtn?.btn === "menu") ||
(focusIndex === i &&
(focusBtn === 0 || focusBtn === 1))
? "url('/images/Button_Square_Highlighted.png')"
: "url('/images/Button_Square.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
filter: updatesAvailable?.[edition.instanceId] ? "drop-shadow(0 0 4px rgba(255,255,0,0.8))" : "none"
filter: updatesAvailable?.[edition.instanceId]
? "drop-shadow(0 0 4px rgba(255,255,0,0.8))"
: "none",
}}
>
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
<div
className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`}
/>
<div
className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`}
/>
<div
className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`}
/>
</button>
)}
@@ -388,7 +437,7 @@ const VersionsView = memo(function VersionsView() {
<div
className="absolute right-0 top-11 w-48 bg-[#1a1a1a] border-2 border-[#555] z-[100] shadow-2xl p-0.5 animate-in fade-in zoom-in duration-75"
style={{
imageRendering: "pixelated"
imageRendering: "pixelated",
}}
>
{updatesAvailable?.[edition.instanceId] && (
@@ -401,27 +450,33 @@ const VersionsView = memo(function VersionsView() {
}}
className="w-full text-left px-3 py-1.5 text-xs text-[#ffff55] hover:text-white hover:bg-[#ffff55]/20 flex items-center gap-2 group transition-colors mc-text-shadow font-bold border-b border-white/5 mb-1"
>
<img src="/images/Download_Icon.png" alt="" className="w-3 h-3 object-contain" style={{ imageRendering: "pixelated" }} />
<img
src="/images/Download_Icon.png"
alt=""
className="w-3 h-3 object-contain"
style={{ imageRendering: "pixelated" }}
/>
Update Available!
</button>
)}
{Array.isArray(edition.branches) && edition.branches.length > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
playPressSound();
cycleBranch(edition.id);
}}
className="w-full text-left px-3 py-1.5 text-[10px] text-white mc-text-shadow hover:bg-white/10 flex items-center justify-between group transition-colors"
>
<span className="text-[#AAAAAA] group-hover:text-white font-bold">
Channel
</span>
<span className="text-[#ffff55] font-bold">
{edition.selectedBranch ?? "Latest"}
</span>
</button>
)}
{Array.isArray(edition.branches) &&
edition.branches.length > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
playPressSound();
cycleBranch(edition.id);
}}
className="w-full text-left px-3 py-1.5 text-[10px] text-white mc-text-shadow hover:bg-white/10 flex items-center justify-between group transition-colors"
>
<span className="text-[#AAAAAA] group-hover:text-white font-bold">
Channel
</span>
<span className="text-[#ffff55] font-bold">
{edition.selectedBranch ?? "Latest"}
</span>
</button>
)}
<div className="h-[1px] bg-white/5 my-0.5 mx-1" />
<button
onClick={(e) => {
@@ -432,24 +487,72 @@ const VersionsView = memo(function VersionsView() {
}}
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"
>
<img src="/images/Folder_Icon.png" alt="" className="w-3.5 h-3.5 object-contain" style={{ imageRendering: "pixelated" }} />
<img
src="/images/Folder_Icon.png"
alt=""
className="w-3.5 h-3.5 object-contain"
style={{ imageRendering: "pixelated" }}
/>
Open Folder
</button>
<button
onClick={(e) => {
e.stopPropagation();
playPressSound();
const PANORAMA_PROFILES = ['legacy_evolved', 'vanilla_tu19', '360revived', 'vanilla_tu24'];
const panoId = PANORAMA_PROFILES.includes(edition.id) ? edition.id : 'vanilla_tu19';
const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? 'Day' : 'Night'}.png`;
addToSteam(edition.instanceId, edition.name, edition.titleImage, panoramaUrl);
const PANORAMA_PROFILES = [
"legacy_evolved",
"vanilla_tu19",
"360revived",
"vanilla_tu24",
];
const panoId = PANORAMA_PROFILES.includes(
edition.id,
)
? edition.id
: "vanilla_tu19";
const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? "Day" : "Night"}.png`;
addToSteam(
edition.instanceId,
edition.name,
edition.titleImage,
panoramaUrl,
);
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"
>
<img src="/images/steam.png" alt="" className="w-3.5 h-3.5 object-contain invert brightness-0" style={{ imageRendering: "pixelated" }} />
<img
src="/images/steam.png"
alt=""
className="w-3.5 h-3.5 object-contain invert brightness-0"
style={{ imageRendering: "pixelated" }}
/>
Add to Steam
</button>
<button
onClick={(e) => {
e.stopPropagation();
playPressSound();
setSetUidTargetId(edition.instanceId);
setIsSetUidModalOpen(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="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
Set UID
</button>
{isCustom ? (
<button
onClick={(e) => {
@@ -461,7 +564,17 @@ const VersionsView = memo(function VersionsView() {
}}
className="w-full text-left px-3 py-2 text-xs text-[#aaaaaa] 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>
<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>
Edit Custom
</button>
) : null}
@@ -479,7 +592,12 @@ const VersionsView = memo(function VersionsView() {
}}
className="w-full text-left px-3 py-2 text-xs text-red-500 hover:text-red-400 hover:bg-red-500/10 flex items-center gap-2 transition-colors mc-text-shadow font-bold"
>
<img src="/images/Trash_Bin_Icon.png" alt="" className="w-3.5 h-3.5 object-contain" style={{ imageRendering: "pixelated" }} />
<img
src="/images/Trash_Bin_Icon.png"
alt=""
className="w-3.5 h-3.5 object-contain"
style={{ imageRendering: "pixelated" }}
/>
{isCustom ? "Remove Custom" : "Uninstall"}
</button>
</div>
@@ -503,7 +621,7 @@ const VersionsView = memo(function VersionsView() {
backgroundImage:
(hoveredBtn?.row === editions.length &&
hoveredBtn?.btn === "add") ||
focusIndex === editions.length
focusIndex === editions.length
? "url('/images/Button_Square_Highlighted.png')"
: "url('/images/Button_Square.png')",
backgroundSize: "100% 100%",
@@ -536,7 +654,7 @@ const VersionsView = memo(function VersionsView() {
backgroundImage:
(hoveredBtn?.row === editions.length &&
hoveredBtn?.btn === "folder_import") ||
focusIndex === editions.length + 1
focusIndex === editions.length + 1
? "url('/images/Button_Square_Highlighted.png')"
: "url('/images/Button_Square.png')",
backgroundSize: "100% 100%",
@@ -598,6 +716,16 @@ const VersionsView = memo(function VersionsView() {
initialPath={initialPath}
/>
<SetUidModal
isOpen={isSetUidModalOpen}
onClose={() => setIsSetUidModalOpen(false)}
playPressSound={playPressSound}
playBackSound={playBackSound}
instances={editions}
installedVersions={installedVersions}
targetInstanceId={setUidTargetId}
/>
{deleteConfirmEdition && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div