-
-
-
{
- if (isFocusedSection) {
- playSfx("orb.ogg");
- setShowCredits(true);
- }
- }}
- className={`text-white hover:text-[#FFFF55] text-xl mc-text-shadow tracking-widest transition-colors mt-1 bg-transparent border-none outline-none ${!isFocusedSection ? "cursor-default pointer-events-none" : ""}`}
- >
- EMERALD TEAM
-
-
{
- if (isFocusedSection) {
- playSfx("orb.ogg");
- setShowSpecialThanks(true);
- }
- }}
- className={`text-white/60 hover:text-[#FFFF55] text-xs mc-text-shadow tracking-[0.2em] transition-colors bg-transparent border-none outline-none ${!isFocusedSection ? "cursor-default pointer-events-none" : ""}`}
- >
- SPECIAL THANKS
-
+
+
+
+
+ Welcome, {username}!
- )}
-
+
+
+ {hasInstalledInstance ? (
+ <>
+
{
+ playSfx('click.wav');
+ setSelectedInstance(e.target.value);
+ }}
+ className="w-full legacy-select p-3 text-2xl outline-none"
+ >
+ {installedStatus.vanilla_tu19 && (
+ Vanilla Nightly (TU19)
+ )}
+ {installedStatus.vanilla_tu24 && (
+ Vanilla TU24
+ )}
+
+
+ {installingInstance ? "WAITING..." : isRunning ? "RUNNING..." : "PLAY"}
+
+ >
+ ) : (
+
+
Game not installed
+
{
+ playSfx('click.wav');
+ setActiveTab("versions");
+ }}
+ className="legacy-btn py-4 px-8 text-3xl w-full"
+ >
+ Go to Versions
+
+
+ )}
+
+
);
-});
-
-export default HomeView;
+};
diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx
index 86e49cf..4cc95d0 100644
--- a/src/components/views/SettingsView.tsx
+++ b/src/components/views/SettingsView.tsx
@@ -1,572 +1,177 @@
-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 React from 'react';
+import { Icons } from '../Icons';
+import { TauriService } from '../../services/tauri';
+import { Runner } from '../../types';
+import { openUrl } from "@tauri-apps/plugin-opener";
-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, keepLauncherOpen, setKeepLauncherOpen, enableTrayIcon, setEnableTrayIcon } = useConfig();
- const { currentTrack, setCurrentTrack, tracks, playClickSound, playBackSound } = useAudio();
- const { isGameRunning, stopGame, isRunnerDownloading, runnerDownloadProgress, downloadRunner } = useGame();
- const { isLinux, isMac } = usePlatform();
- const [focusIndex, setFocusIndex] = useState
(null);
- const [currentSubMenu, setCurrentSubMenu] = useState<"main" | "audio" | "video" | "controls" | "launcher">("main");
- const [runners, setRunners] = useState([]);
- const containerRef = useRef(null);
-
- const layouts = ["KBM", "PLAYSTATION", "XBOX"];
-
- useEffect(() => {
- TauriService.getAvailableRunners().then(setRunners);
- }, [isRunnerDownloading]);
-
- const handleLayoutToggle = () => {
- playClickSound();
- const currentIndex = layouts.indexOf(layout);
- const nextIndex = (currentIndex + 1) % layouts.length;
- setLayout(layouts[nextIndex]);
- };
-
- const handleVfxToggle = () => {
- playClickSound();
- setVfxEnabled(!vfxEnabled);
- };
-
- const handleAnimationsToggle = () => {
- playClickSound();
- setAnimationsEnabled(!animationsEnabled);
- };
-
- const handlePerfToggle = () => {
- playClickSound();
- setPerfBoost(!perfBoost);
- };
-
- const handleRpcToggle = () => {
- playClickSound();
- setRpcEnabled(!rpcEnabled);
- };
-
- const handleLegacyToggle = () => {
- playClickSound();
- setLegacyMode(!legacyMode);
- };
-
- const handleKeepOpenToggle = () => {
- playClickSound();
- setKeepLauncherOpen(!keepLauncherOpen);
- };
-
- const handleTrayToggle = () => {
- playClickSound();
- setEnableTrayIcon(!enableTrayIcon);
- };
-
- const handleRunnerToggle = () => {
- playClickSound();
- if (runners.length === 0) return;
- const currentIndex = runners.findIndex((r) => r.id === linuxRunner);
- const nextIndex = (currentIndex + 1) % runners.length;
- setLinuxRunner(runners[nextIndex].id);
- };
-
- const handleTrackToggle = () => {
- playClickSound();
- setCurrentTrack((currentTrack + 1) % tracks.length);
- };
-
- const handleResetSetup = () => {
- playClickSound();
-
- // Create styled confirmation dialog
- const dialog = document.createElement('div');
- dialog.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
- dialog.innerHTML = `
-
-
Reset Setup
-
Are you sure you want to reset launcher setup?
-
- Yes
- No
-
-
- `;
-
- document.body.appendChild(dialog);
-
- const handleYes = () => {
- document.body.removeChild(dialog);
- showSecondConfirmation();
- };
-
- const handleNo = () => {
- document.body.removeChild(dialog);
- };
-
- dialog.querySelector('#reset-yes')?.addEventListener('click', handleYes);
- dialog.querySelector('#reset-no')?.addEventListener('click', handleNo);
-
- dialog.addEventListener('click', (e) => {
- if (e.target === dialog) {
- document.body.removeChild(dialog);
- }
- });
- };
-
- const showSecondConfirmation = () => {
- const dialog = document.createElement('div');
- dialog.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
- dialog.innerHTML = `
-
-
CONFIRM RESET
-
-
⚠️ This will:
-
- Clear all launcher settings
- Reset your username
- Show setup screen again
- Require reconfiguration
-
-
This action cannot be undone!
-
-
- YES, RESET
- Cancel
-
-
- `;
-
- document.body.appendChild(dialog);
-
- const handleFinalYes = () => {
- document.body.removeChild(dialog);
- performReset();
- };
-
- const handleFinalNo = () => {
- document.body.removeChild(dialog);
- };
-
- dialog.querySelector('#reset-final-yes')?.addEventListener('click', handleFinalYes);
- dialog.querySelector('#reset-final-no')?.addEventListener('click', handleFinalNo);
-
- dialog.addEventListener('click', (e) => {
- if (e.target === dialog) {
- document.body.removeChild(dialog);
- }
- });
- };
-
- const performReset = () => {
- // Clear all localStorage data
- localStorage.clear();
-
- // Set setup as not completed
- localStorage.setItem('lce-setup-completed', 'false');
-
- // Force reload to show setup screen
- window.location.reload();
- };
-
- let trackName = "Unknown";
- if (tracks && tracks.length > 0) {
- const fullPath = tracks[currentTrack];
- if (fullPath) {
- trackName = fullPath
- .split("/")
- .pop()
- ?.replace(".ogg", "")
- .replace(".wav", "") || "Unknown";
- }
- }
-
- const selectedRunnerName =
- runners.find((r) => r.id === linuxRunner)?.name || "Native / Default";
-
- type SettingsItem =
- | {
- id: string;
- label: string;
- type: "slider";
- value: number;
- onChange: (val: any) => void;
- }
- | {
- id: string;
- label: string;
- type: "button";
- onClick: () => void;
- small?: boolean;
- color?: string;
- };
-
- const settingsItems = useMemo(() => {
- const items: SettingsItem[] = [];
-
- if (currentSubMenu === "main") {
- items.push({
- id: "audio_menu",
- label: "Audio",
- type: "button",
- onClick: () => { playClickSound(); setCurrentSubMenu("audio"); setFocusIndex(0); },
- });
- items.push({
- id: "video_menu",
- label: "User Interface",
- type: "button",
- onClick: () => { playClickSound(); setCurrentSubMenu("video"); setFocusIndex(0); },
- });
- items.push({
- id: "controls_menu",
- label: "Controls",
- type: "button",
- onClick: () => { playClickSound(); setCurrentSubMenu("controls"); setFocusIndex(0); },
- });
- items.push({
- id: "launcher_menu",
- label: "Options",
- type: "button",
- onClick: () => { playClickSound(); setCurrentSubMenu("launcher"); setFocusIndex(0); },
- });
- } else if (currentSubMenu === "audio") {
- items.push({
- id: "music",
- label: `Music: ${musicVolume ?? 50}%`,
- type: "slider",
- value: musicVolume ?? 50,
- onChange: setMusicVolume,
- });
- items.push({
- id: "sfx",
- label: `SFX: ${sfxVolume ?? 100}%`,
- type: "slider",
- value: sfxVolume ?? 100,
- onChange: setSfxVolume,
- });
- items.push({
- id: "track",
- label: `${trackName} - C418`,
- type: "button",
- onClick: handleTrackToggle,
- });
- } else if (currentSubMenu === "video") {
- items.push({
- id: "vfx",
- label: `VFX: ${vfxEnabled ? "ON" : "OFF"}`,
- type: "button",
- onClick: handleVfxToggle,
- });
- items.push({
- id: "animations",
- label: `Animations: ${animationsEnabled ? "ON" : "OFF"}`,
- type: "button",
- onClick: handleAnimationsToggle,
- });
- if (isMac) {
- items.push({
- id: "perf",
- label: `M1/M2 Boost: ${perfBoost ? "Enabled" : "Disabled"}`,
- type: "button",
- onClick: handlePerfToggle,
- });
- }
- } else if (currentSubMenu === "controls") {
- items.push({
- id: "layout",
- label: `Layout: ${layout}`,
- type: "button",
- onClick: handleLayoutToggle,
- });
- } else if (currentSubMenu === "launcher") {
- items.push({
- id: "rpc",
- label: `Discord RPC: ${rpcEnabled ? "ON" : "OFF"}`,
- type: "button",
- onClick: handleRpcToggle,
- });
- items.push({
- id: "legacy",
- label: `Legacy Mode: ${legacyMode ? "ON" : "OFF"}`,
- type: "button",
- onClick: handleLegacyToggle,
- });
- items.push({
- id: "keep_open",
- label: `Keep Launcher Open: ${keepLauncherOpen ? "ON" : "OFF"}`,
- type: "button",
- onClick: handleKeepOpenToggle,
- });
- items.push({
- id: "tray_icon",
- label: `Tray Icon: ${enableTrayIcon ? "ON" : "OFF"}`,
- type: "button",
- onClick: handleTrayToggle,
- });
-
- if (isLinux) {
- items.push({
- id: "runner",
- label: `Runner: ${selectedRunnerName}`,
- type: "button",
- onClick: handleRunnerToggle,
- });
-
- if (runners.length === 0 || runners.every(r => r.type !== 'proton')) {
- items.push({
- id: "download_runner",
- label: isRunnerDownloading
- ? `Downloading Runner... ${Math.floor(runnerDownloadProgress || 0)}%`
- : "Download GE-Proton (Recommended)",
- 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");
- }
- },
- small: true,
- });
- }
- }
-
- items.push({
- id: "reset_setup",
- label: "Reset Setup",
- type: "button",
- onClick: handleResetSetup,
- color: "orange",
- small: true,
- });
- }
-
- if (isGameRunning) {
- items.push({
- id: "stop",
- label: "STOP GAME",
- type: "button",
- onClick: stopGame,
- color: "red",
- });
- }
-
- items.push({
- id: "back",
- label: currentSubMenu === "main" ? "Done" : "Back",
- type: "button",
- onClick: () => {
- playBackSound();
- if (currentSubMenu === "main") {
- setActiveView("main");
- } else {
- setCurrentSubMenu("main");
- setFocusIndex(0);
- }
- },
- });
-
- return items;
- }, [
- currentSubMenu,
- musicVolume,
- sfxVolume,
- trackName,
- vfxEnabled,
- rpcEnabled,
- legacyMode,
- animationsEnabled,
- keepLauncherOpen,
- enableTrayIcon,
- layout,
- isLinux,
- selectedRunnerName,
- isRunnerDownloading,
- runnerDownloadProgress,
- isMac,
- perfBoost,
- isGameRunning,
- handleTrackToggle,
- handleVfxToggle,
- handleRpcToggle,
- handleLegacyToggle,
- handleAnimationsToggle,
- handleKeepOpenToggle,
- handleTrayToggle,
- handleLayoutToggle,
- handleRunnerToggle,
- handlePerfToggle,
- handleResetSetup,
- stopGame,
- downloadRunner,
- playClickSound,
- playBackSound,
- setActiveView,
- runners,
- ]);
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape" || e.key === "Backspace") {
- playBackSound();
- if (currentSubMenu !== "main") {
- setCurrentSubMenu("main");
- setFocusIndex(0);
- } else {
- setActiveView("main");
- }
- return;
- }
-
- const itemCount = settingsItems.length;
-
- if (e.key === "ArrowDown") {
- setFocusIndex((prev) =>
- prev === null || prev >= itemCount - 1 ? 0 : prev + 1,
- );
- } else if (e.key === "ArrowUp") {
- setFocusIndex((prev) =>
- prev === null || prev <= 0 ? itemCount - 1 : prev - 1,
- );
- } else if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
- if (focusIndex === null) return;
- const item = settingsItems[focusIndex];
- if (item.type === "slider") {
- const delta = e.key === "ArrowRight" ? 5 : -5;
- item.onChange((v: number) => Math.max(0, Math.min(100, v + delta)));
- }
- } else if (e.key === "Enter" && focusIndex !== null) {
- const item = settingsItems[focusIndex];
- if (item.type === "button") {
- item.onClick();
- }
- }
- };
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [focusIndex, settingsItems, playBackSound, setActiveView, currentSubMenu]);
-
- useEffect(() => {
- if (focusIndex !== null) {
- const el = containerRef.current?.querySelector(
- `[data-index="${focusIndex}"]`,
- ) as HTMLElement;
- if (el) el.focus();
- }
- }, [focusIndex]);
-
- const getItemStyle = (index: number) => ({
- backgroundImage:
- focusIndex === index
- ? "url('/images/button_highlighted.png')"
- : "url('/images/Button_Background.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated" as const,
- });
-
- const getSliderStyle = (index: number) => ({
- backgroundImage: "url('/images/Button_Background2.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated" as const,
- color: focusIndex === index ? "#FFFF55" : "white",
- });
+interface SettingsViewProps {
+ username: string;
+ setUsername: (name: string) => void;
+ isLinux: boolean;
+ selectedRunner: string;
+ setSelectedRunner: (runner: string) => void;
+ availableRunners: Runner[];
+ musicVol: number;
+ setMusicVol: (vol: number) => void;
+ sfxVol: number;
+ setSfxVol: (vol: number) => void;
+ isMuted: boolean;
+ setIsMuted: (muted: boolean) => void;
+ playSfx: (name: string, multiplier?: number) => void;
+}
+export const SettingsView: React.FC = ({
+ username,
+ setUsername,
+ isLinux,
+ selectedRunner,
+ setSelectedRunner,
+ availableRunners,
+ musicVol,
+ setMusicVol,
+ sfxVol,
+ setSfxVol,
+ isMuted,
+ setIsMuted,
+ playSfx,
+}) => {
return (
-
-
- {currentSubMenu === "main" ? "Settings" : currentSubMenu === "audio" ? "Audio" : currentSubMenu === "video" ? "User Interface" : currentSubMenu === "controls" ? "Controls" : "Options"}
-
-
-
- {settingsItems.map((item, index) => {
- if (item.id === "back") return null;
-
- if (item.type === "slider") {
- return (
-
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"
- style={getSliderStyle(index)}
- >
-
- {item.label}
-
-
- item.onChange(parseInt(e.target.value))}
- onMouseUp={playClickSound}
- className="mc-slider-custom w-[calc(100%+16px)] h-full opacity-100 cursor-pointer z-20 outline-none m-0"
- />
-
-
- );
- }
-
- const isRed = (item as any).color === "red";
- const isSmall = (item as any).small;
-
- return (
+
+
Settings
+
+
+
In-game Username
+
+ setUsername(e.target.value)}
+ className="flex-1 bg-black border-4 border-slate-700 p-4 text-3xl outline-none focus:border-emerald-500"
+ />
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]"}`}
- style={getItemStyle(index)}
+ onClick={() => {
+ playSfx('wood click.wav');
+ TauriService.saveConfig({
+ username,
+ linuxRunner: selectedRunner || undefined,
+ });
+ }}
+ className="legacy-btn px-8 text-2xl relative"
>
- 20 ? "text-lg" : "text-xl"} truncate w-full text-center`}
- >
- {item.label}
-
+ Save
- );
- })}
-
+
+
- {(() => {
- const backIndex = settingsItems.findIndex((i) => i.id === "back");
- const backItem = settingsItems[backIndex];
- if (!backItem || backItem.type !== "button") return null;
+ {isLinux && (
+
+
+ Linux Runner
+
+
+
{
+ playSfx('click.wav');
+ setSelectedRunner(e.target.value);
+ }}
+ className="w-full legacy-select p-4 text-2xl outline-none focus:border-emerald-500"
+ >
+ Select a runner...
+ {availableRunners.map((r) => (
+
+ {r.name} ({r.type})
+
+ ))}
+
+ {availableRunners.length === 0 && (
+
+ No Proton or Wine installations found. Please install Steam or Wine.
+
+ )}
+
+
+ )}
- return (
+
+
+ Audio Controls
+
+
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] ${focusIndex === backIndex ? "text-[#FFFF55]" : "text-white"
- }`}
- style={getItemStyle(backIndex)}
+ onClick={() => {
+ setIsMuted(!isMuted);
+ playSfx('pop.wav');
+ }}
+ className="legacy-btn mt-4 py-2"
>
- Back
+ {isMuted ? "UNMUTE ALL" : "MUTE ALL"}
- );
- })()}
-
- );
-});
+
-export default SettingsView;
+
+
+ About the project
+
+
+ I'm KayJann , and I absolutely love this project! It's my very first one,
+ and my goal is to create a central hub for the LCE community to bring us all together.
+
+
Social Links
+
+ openUrl("https://discord.gg/nzbxB8Hxjh")}
+ className="social-btn btn-discord"
+ title="Discord"
+ >
+
+
+ openUrl("https://github.com/KayJannOnGit")}
+ className="social-btn btn-github"
+ title="GitHub"
+ >
+
+
+ openUrl("https://reddit.com/user/KayJann")}
+ className="social-btn btn-reddit"
+ title="Reddit"
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/views/SetupView.tsx b/src/components/views/SetupView.tsx
deleted file mode 100644
index 8e3a201..0000000
--- a/src/components/views/SetupView.tsx
+++ /dev/null
@@ -1,626 +0,0 @@
-import { useState, useEffect } from "react";
-import { motion, AnimatePresence } from "framer-motion";
-import { TauriService, Runner } from "../../services/TauriService";
-import { usePlatform } from "../../hooks/usePlatform";
-import { useConfig, useAudio, useGame } from "../../context/LauncherContext";
-
-interface SetupViewProps {
- onComplete: () => void;
-}
-
-const SetupView: React.FC = ({ onComplete }) => {
- const { isLinux, isMac } = usePlatform();
- const {
- username, setUsername,
- setHasCompletedSetup,
- profile,
- setEnableTrayIcon: setConfigTray,
- setVfxEnabled: setConfigVfx,
- setRpcEnabled: setConfigRpc,
- setKeepLauncherOpen: setConfigKeepOpen,
- setLinuxRunner,
- linuxRunner: configLinuxRunner,
- vfxEnabled: configVfx,
- enableTrayIcon: configTray,
- rpcEnabled: configRpc,
- keepLauncherOpen: configKeepOpen
- } = useConfig();
- const { playClickSound, playSfx } = useAudio();
-
- const { editions } = useGame();
- const titleImage = editions.find(e => e.id === profile)?.titleImage || "/images/MenuTitle.png";
-
- const [currentStep, setCurrentStep] = useState(0);
- const [focusIndex, setFocusIndex] = useState(0);
- const [tempUsername, setTempUsername] = useState(username);
- const [runners, setRunners] = useState([]);
- const [selectedRunner, setSelectedRunner] = useState("");
- const [isSettingUpRuntime, setIsSettingUpRuntime] = useState(false);
- const [setupProgress, setSetupProgress] = useState<{ stage: string; message: string; percent?: number } | null>(null);
- const [runtimeAlreadyInstalled, setRuntimeAlreadyInstalled] = useState(false);
-
- const [enableTrayIcon, setEnableTrayIcon] = useState(configTray);
- const [enableVfx, setEnableVfx] = useState(configVfx);
- const [enableDiscordRPC, setEnableDiscordRPC] = useState(configRpc);
- const [keepLauncherOpen, setKeepLauncherOpen] = useState(configKeepOpen);
-
- const totalSteps = isLinux ? 4 : 4;
-
- useEffect(() => {
- if (isLinux || isMac) {
- TauriService.getAvailableRunners().then(availableRunners => {
- setRunners(availableRunners);
- if (configLinuxRunner && availableRunners.find(r => r.id === configLinuxRunner)) {
- setSelectedRunner(configLinuxRunner);
- }
- });
- }
-
- if (isMac) {
- checkMacOSRuntime();
-
- const unlisten = TauriService.onMacosProgress((progress) => {
- console.log("[macOS Setup Progress]", progress);
- setSetupProgress(progress);
- });
-
- return () => {
- unlisten.then(f => f?.());
- };
- }
- }, [isLinux, isMac]);
-
- const checkMacOSRuntime = async () => {
- try {
- const localStorageInstalled = localStorage.getItem('lce-macos-runtime-installed') === 'true';
-
- if (localStorageInstalled) {
- console.log("[macOS Runtime] Using cached installation status");
- try {
- const runtimeCheck = await TauriService.checkMacOSRuntimeInstalledFast();
- if (runtimeCheck) {
- setRuntimeAlreadyInstalled(true);
- return;
- } else {
- console.log("[macOS Runtime] Cache was wrong, clearing");
- localStorage.removeItem('lce-macos-runtime-installed');
- setRuntimeAlreadyInstalled(false);
- return;
- }
- } catch (error) {
- console.log("[macOS Runtime] Fast check failed, using cache");
- setRuntimeAlreadyInstalled(true);
- return;
- }
- } else {
- console.log("[macOS Runtime] No installation detected");
- setRuntimeAlreadyInstalled(false);
- }
- } catch (error) {
- console.error("[macOS Runtime] Error checking:", error);
- setRuntimeAlreadyInstalled(false);
- }
- };
-
- const handleRunnerSelect = (runnerId: string) => {
- playClickSound();
- setSelectedRunner(runnerId);
- };
-
- const handleNext = async () => {
- playClickSound();
-
- if (currentStep === 0) {
- setUsername(tempUsername);
- setCurrentStep(1);
- setFocusIndex(0);
- } else if (currentStep === 1) {
- if (isLinux && selectedRunner) {
- setLinuxRunner(selectedRunner);
- }
- setCurrentStep(2);
- setFocusIndex(0);
- } else if (currentStep === 2) {
- setConfigTray(enableTrayIcon);
- setConfigVfx(enableVfx);
- setConfigRpc(enableDiscordRPC);
- setConfigKeepOpen(keepLauncherOpen);
-
- setCurrentStep(3);
- setFocusIndex(0);
- } else if (currentStep === 3) {
- playSfx("levelup.ogg");
- setHasCompletedSetup(true);
- onComplete();
- }
- };
-
- const handleBack = () => {
- playClickSound();
- if (currentStep > 0) {
- setCurrentStep(currentStep - 1);
- setFocusIndex(0);
- }
- };
-
- useEffect(() => {
- const handleKey = (e: KeyboardEvent) => {
- // Elements count per step
- let count = 0;
- if (currentStep === 0) count = 2; // Input, Next
- else if (currentStep === 1) {
- if (isLinux) count = runners.length + 2; // Runners, Back, Next
- else if (isMac) count = 3; // Install, Back, Next
- else count = 2; // Back, Next
- } else if (currentStep === 2) count = 6; // 4 Toggles, Back, Next
- else if (currentStep === 3) count = 2; // Back, Finish
-
- if (e.key === "ArrowDown" || e.key === "Tab") {
- e.preventDefault();
- setFocusIndex((prev) => (prev + 1) % count);
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- setFocusIndex((prev) => (prev - 1 + count) % count);
- } else if (e.key === "Enter") {
- // Handle enter based on focusIndex and step
- if (currentStep === 0) {
- if (focusIndex === 0) handleNext(); // For input field
- else if (focusIndex === 1) handleNext(); // Next button
- } else if (currentStep === 1) {
- if (isLinux) {
- if (focusIndex < runners.length) handleRunnerSelect(runners[focusIndex].id);
- else if (focusIndex === runners.length) handleBack();
- else if (focusIndex === runners.length + 1) handleNext();
- } else if (isMac) {
- if (focusIndex === 0) handleMacosSetup();
- else if (focusIndex === 1) handleBack();
- else if (focusIndex === 2) handleNext();
- } else {
- if (focusIndex === 0) handleBack();
- else if (focusIndex === 1) handleNext();
- }
- } else if (currentStep === 2) {
- if (focusIndex === 0) { setEnableTrayIcon(!enableTrayIcon); playClickSound(); }
- else if (focusIndex === 1) { setEnableVfx(!enableVfx); playClickSound(); }
- else if (focusIndex === 2) { setEnableDiscordRPC(!enableDiscordRPC); playClickSound(); }
- else if (focusIndex === 3) { setKeepLauncherOpen(!keepLauncherOpen); playClickSound(); }
- else if (focusIndex === 4) handleBack();
- else if (focusIndex === 5) handleNext();
- } else if (currentStep === 3) {
- if (focusIndex === 0) handleBack();
- else if (focusIndex === 1) handleNext();
- }
- }
- };
- window.addEventListener("keydown", handleKey);
- return () => window.removeEventListener("keydown", handleKey);
- }, [currentStep, focusIndex, runners, enableTrayIcon, enableVfx, enableDiscordRPC, keepLauncherOpen, isLinux, isMac, tempUsername]);
-
- const handleMacosSetup = async () => {
- playClickSound();
- setIsSettingUpRuntime(true);
- setSetupProgress({ stage: "preparing", message: "Preparing macOS runtime setup...", percent: 0 });
-
- try {
- console.log("[macOS Setup] Starting runtime installation...");
- await TauriService.setupMacosRuntime();
- console.log("[macOS Setup] Runtime installation completed successfully!");
- setSetupProgress({ stage: "completed", message: "Setup completed successfully!", percent: 100 });
-
- localStorage.setItem('lce-macos-runtime-installed', 'true');
- setRuntimeAlreadyInstalled(true);
-
- setTimeout(() => {
- setCurrentStep(2);
- setIsSettingUpRuntime(false);
- setSetupProgress(null);
- }, 2000);
- } catch (e) {
- console.error("[macOS Setup] Error:", e);
- setSetupProgress({ stage: "error", message: `Setup failed: ${e}`, percent: 0 });
- setIsSettingUpRuntime(false);
- }
- };
-
- const canProceed = () => {
- if (currentStep === 0) {
- return tempUsername.trim().length > 0;
- }
- if (currentStep === 1 && isMac) {
- return runtimeAlreadyInstalled;
- }
- return true;
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: totalSteps }, (_, i) => (
-
- ))}
-
-
-
-
- {currentStep === 0 && (
-
-
- Welcome to Emerald Legacy
-
-
Let's configure your launcher
-
-
-
- Username
- setTempUsername(e.target.value)}
- onFocus={() => setFocusIndex(0)}
- className={`w-full px-4 py-3 bg-black/50 border-2 font-bold focus:outline-none transition-colors ${focusIndex === 0 ? "border-yellow-400" : "border-white"}`}
- placeholder="Enter your username"
- maxLength={16}
- autoFocus
- />
-
-
-
- )}
-
- {currentStep === 1 && isMac && (
-
-
- macOS Compatibility
-
-
- {runtimeAlreadyInstalled
- ? "Emerald Legacy compatibility runtime is already installed"
- : "Emerald Legacy needs compatibility runtime for macOS"
- }
-
-
- {setupProgress && (
-
-
{setupProgress.stage.toUpperCase()}
-
{setupProgress.message}
- {setupProgress.percent !== undefined && (
-
- )}
-
- )}
-
-
-
-
- {runtimeAlreadyInstalled ? "✓ Runtime Detected" : "⚠ Runtime Not Detected"}
-
-
- {runtimeAlreadyInstalled
- ? "The compatibility runtime is properly installed and ready to use."
- : "You must install the compatibility runtime before proceeding to the next step."
- }
-
-
-
-
setFocusIndex(0)}
- disabled={isSettingUpRuntime}
- className={`px-6 py-3 text-white font-bold bg-green-600 hover:bg-green-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105 active:scale-95 border-4 ${focusIndex === 0 ? "border-yellow-400" : "border-green-400"}`}
- style={{
- fontFamily: "'Mojangles', monospace",
- imageRendering: "pixelated",
- textShadow: "2px 2px 0px rgba(0,0,0,0.8)",
- boxShadow: "4px 4px 0px rgba(0,0,0,0.3)",
- fontSize: "16px",
- letterSpacing: "1px"
- }}
- >
- {isSettingUpRuntime ? "Installing..." : runtimeAlreadyInstalled ? "Reinstall Runtime" : "Install Runtime"}
-
-
- {!runtimeAlreadyInstalled && (
-
- ⚠ Installation required before proceeding to next step
-
- )}
-
-
- )}
-
- {currentStep === 1 && isLinux && (
-
-
- Linux Compatibility
-
-
Choose your preferred compatibility layer
-
- {runners.length === 0 ? (
-
-
No compatible runners found. Please install Wine or Proton.
-
- ) : (
-
- {runners.map((runner, idx) => (
-
handleRunnerSelect(runner.id)}
- onMouseEnter={() => setFocusIndex(idx)}
- className={`w-full p-4 text-left border-2 transition-all duration-200 ${selectedRunner === runner.id
- ? "bg-white/20 border-white shadow-[0_0_15px_rgba(255,255,255,0.2)]"
- : "bg-black/50 border-white/20"
- } ${focusIndex === idx ? "border-yellow-400" : ""}`}
- >
- {runner.name}
- {runner.type}
-
- ))}
-
- )}
-
-
You can change this later in settings
-
- )}
-
- {currentStep === 1 && !isMac && !isLinux && (
-
-
- Windows Setup
-
-
Everything is ready to go!
-
✓ Native compatibility
-
-
-
✓ Windows Native Support
-
Emerald Legacy runs natively on Windows without additional requirements.
-
-
- )}
-
- {currentStep === 2 && (
-
-
- Customize Your Experience
-
-
Choose your preferred launcher settings
-
-
-
-
-
-
System Tray Icon
-
Keep launcher accessible in system tray
-
-
{
- playClickSound();
- setEnableTrayIcon(!enableTrayIcon);
- }}
- onMouseEnter={() => setFocusIndex(0)}
- className={`w-12 h-6 outline-none border-none bg-transparent transition-all duration-200 hover:border-yellow-400 hover:shadow-[0_0_8px_rgba(250,204,21,0.3)] ${focusIndex === 0 ? "scale-110 shadow-[0_0_8px_rgba(250,204,21,0.6)]" : ""}`}
- style={{ imageRendering: "pixelated" }}
- >
-
-
-
-
-
-
-
-
-
Visual Effects
-
Click particles and animations
-
-
{
- playClickSound();
- setEnableVfx(!enableVfx);
- }}
- onMouseEnter={() => setFocusIndex(1)}
- className={`w-12 h-6 outline-none border-none bg-transparent transition-all duration-200 hover:border-yellow-400 hover:shadow-[0_0_8px_rgba(250,204,21,0.3)] ${focusIndex === 1 ? "scale-110 shadow-[0_0_8px_rgba(250,204,21,0.6)]" : ""}`}
- style={{ imageRendering: "pixelated" }}
- >
-
-
-
-
-
-
-
-
-
Discord Rich Presence
-
Show your Emerald Legacy status on Discord
-
-
{
- playClickSound();
- setEnableDiscordRPC(!enableDiscordRPC);
- }}
- onMouseEnter={() => setFocusIndex(2)}
- className={`w-12 h-6 outline-none border-none bg-transparent transition-all duration-200 hover:border-yellow-400 hover:shadow-[0_0_8px_rgba(250,204,21,0.3)] ${focusIndex === 2 ? "scale-110 shadow-[0_0_8px_rgba(250,204,21,0.6)]" : ""}`}
- style={{ imageRendering: "pixelated" }}
- >
-
-
-
-
-
-
-
-
-
Keep Launcher Open
-
Keep launcher running after game launch
-
-
{
- playClickSound();
- setKeepLauncherOpen(!keepLauncherOpen);
- }}
- onMouseEnter={() => setFocusIndex(3)}
- className={`w-12 h-6 outline-none border-none bg-transparent transition-all duration-200 hover:border-yellow-400 hover:shadow-[0_0_8px_rgba(250,204,21,0.3)] ${focusIndex === 3 ? "scale-110 shadow-[0_0_8px_rgba(250,204,21,0.6)]" : ""}`}
- style={{ imageRendering: "pixelated" }}
- >
-
-
-
-
-
-
-
You can change these later in settings
-
- )}
-
- {currentStep === 3 && (
-
-
- Setup Complete!
-
-
-
-
-
Username: {tempUsername}
- {isMac && (
-
- Runtime: Ready
-
- )}
- {isLinux && selectedRunner && (
-
- Runner: {runners.find(r => r.id === selectedRunner)?.name}
-
- )}
-
-
Customization:
-
- {enableTrayIcon && Tray Icon }
- {enableVfx && Visual Effects }
- {enableDiscordRPC && Discord RPC }
- {keepLauncherOpen && Keep Open }
-
-
-
-
-
-
Emerald Legacy is now configured and ready to use!
-
- )}
-
-
-
-
-
- {currentStep > 0 && (
- {
- if (currentStep === 0) setFocusIndex(1);
- else if (currentStep === 1) setFocusIndex(isLinux ? runners.length : (isMac ? 1 : 0));
- else if (currentStep === 2) setFocusIndex(4);
- else if (currentStep === 3) setFocusIndex(0);
- }}
- disabled={currentStep === 0}
- className={`mc-setup-nav-btn px-6 py-3 text-white font-bold disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 hover:border-yellow-400 hover:shadow-[0_0_10px_rgba(250,204,21,0.3)] ${(currentStep === 1 && ((isLinux && focusIndex === runners.length) || (isMac && focusIndex === 1) || (!isLinux && !isMac && focusIndex === 0))) ||
- (currentStep === 2 && focusIndex === 4) ||
- (currentStep === 3 && focusIndex === 0)
- ? "border-yellow-400 shadow-[0_0_10px_rgba(250,204,21,0.3)]" : ""
- }`}
- >
- Back
-
- )}
-
- {
- if (currentStep === 0) setFocusIndex(1);
- else if (currentStep === 1) setFocusIndex(isLinux ? runners.length + 1 : (isMac ? 2 : 1));
- else if (currentStep === 2) setFocusIndex(5);
- else if (currentStep === 3) setFocusIndex(1);
- }}
- disabled={!canProceed()}
- className={`${currentStep > 0 ? '' : 'ml-auto'} mc-setup-nav-btn px-6 py-3 text-white font-bold disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 hover:border-yellow-400 hover:shadow-[0_0_10px_rgba(250,204,21,0.3)] ${(currentStep === 0 && focusIndex === 1) ||
- (currentStep === 1 && ((isLinux && focusIndex === runners.length + 1) || (isMac && focusIndex === 2) || (!isLinux && !isMac && focusIndex === 1))) ||
- (currentStep === 2 && focusIndex === 5) ||
- (currentStep === 3 && focusIndex === 1)
- ? "border-yellow-400 shadow-[0_0_10px_rgba(250,204,21,0.3)]" : ""
- }`}
- >
- {currentStep === totalSteps - 1 ? "Finish" : "Next"}
-
-
-
-
-
-
-
- );
-};
-
-export default SetupView;
diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx
deleted file mode 100644
index 3057285..0000000
--- a/src/components/views/SkinsView.tsx
+++ /dev/null
@@ -1,404 +0,0 @@
-import { useState, useEffect, useRef, memo } from 'react';
-import { motion } from 'framer-motion';
-import { useLocalStorage } from '../../hooks/useLocalStorage';
-import { TauriService } from '../../services/TauriService';
-import { useUI, useAudio, useSkin, useConfig } from '../../context/LauncherContext';
-
-interface SavedSkin {
- id: string;
- name: string;
- url: string;
-}
-
-const DEFAULT_SKINS: SavedSkin[] = [
- { id: 'default', name: 'Default Steve', url: '/images/Default.png' },
- { id: 'journ3ym3n', name: 'Journ3ym3n', url: '/Skins/Journ3ym3n.png' },
- { id: 'justneki', name: 'JustNeki', url: '/Skins/JustNeki.png' },
- { id: 'kayjann', name: 'KayJann', url: '/Skins/KayJann.png' },
- { id: 'leon', name: 'Leon', url: '/Skins/Leon.png' },
- { id: 'mr_anilex', name: 'mr_anilex', url: '/Skins/mr_anilex.png' },
- { id: 'neoapps', name: 'neoapps', url: '/Skins/neoapps.png' },
- { id: 'peter', name: 'Peter', url: '/Skins/Peter.png' },
-];
-
-const SkinsView = memo(function SkinsView() {
- const { setActiveView } = useUI();
- const { playClickSound, playBackSound } = useAudio();
- const { skinUrl, setSkinUrl } = useSkin();
-
- const [focusIndex, setFocusIndex] = useState(null);
- const containerRef = useRef(null);
- const fileInputRef = useRef(null);
-
- const [storedSkins, setStoredSkins] = useLocalStorage('lce-custom-skins', []);
- const savedSkins = [...DEFAULT_SKINS, ...storedSkins.filter(s => !DEFAULT_SKINS.some(d => d.id === s.id))];
-
- const TOP_BUTTONS_COUNT = 3; // Import, Delete, Folder
- const SKINS_START_INDEX = TOP_BUTTONS_COUNT;
- const BACK_BUTTON_INDEX = SKINS_START_INDEX + savedSkins.length;
- const ITEM_COUNT = BACK_BUTTON_INDEX + 1;
-
- const setSavedSkins = (newSkins: SavedSkin[] | ((val: SavedSkin[]) => SavedSkin[])) => {
- const updatedSkins = typeof newSkins === 'function' ? newSkins(savedSkins) : newSkins;
- const customOnes = updatedSkins.filter(s => !DEFAULT_SKINS.some(d => d.id === s.id));
- setStoredSkins(customOnes);
- };
-
- const [activeSkinId, setActiveSkinId] = useState(null);
-
- const [showImportModal, setShowImportModal] = useState(false);
- const [modalFocusIndex, setModalFocusIndex] = useState(0);
- const [importMode, setImportMode] = useState<'file' | 'username' | null>(null);
- const [importUsername, setImportUsername] = useState('');
- const [isImporting, setIsImporting] = useState(false);
- const [importError, setImportError] = useState('');
-
- const processSkinImage = (url: string, defaultName: string) => {
- const img = new Image();
- img.crossOrigin = "Anonymous";
- img.onload = () => {
- const cvs = document.createElement("canvas");
- cvs.width = 64;
- cvs.height = 32;
- const ctx = cvs.getContext("2d");
- if (ctx) {
- ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
- const base64String = cvs.toDataURL("image/png");
- const newId = Date.now().toString();
- const newSkin = { id: newId, name: defaultName, url: base64String };
- setSavedSkins(prev => [...prev, newSkin]);
- setSkinUrl(base64String);
- setActiveSkinId(newId);
- }
- };
- img.src = url;
- };
-
- const handleFetchUsername = async () => {
- if (!importUsername.trim()) return;
- playClickSound();
- setIsImporting(true);
- setImportError('');
- try {
- const [base64Raw, exactName] = await TauriService.fetchSkin(importUsername.trim());
- const skinBase64 = `data:image/png;base64,${base64Raw}`;
- processSkinImage(skinBase64, exactName.substring(0, 16));
-
- setShowImportModal(false);
- setImportMode(null);
- setImportUsername('');
- } catch (e: any) {
- setImportError(typeof e === 'string' ? e : (e.message || 'Failed to fetch skin'));
- } finally {
- setIsImporting(false);
- }
- };
-
- useEffect(() => {
- if (!activeSkinId) {
- const match = savedSkins.find(s => s.url === skinUrl);
- if (match) setActiveSkinId(match.id);
- }
- }, [activeSkinId, savedSkins, skinUrl]);
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (showImportModal) {
- if (e.key === 'Escape') {
- playBackSound();
- if (importMode) {
- setImportMode(null);
- setImportUsername('');
- setImportError('');
- setModalFocusIndex(0);
- } else {
- setShowImportModal(false);
- setModalFocusIndex(0);
- }
- } else if (e.key === 'ArrowDown' || e.key === 'Tab') {
- e.preventDefault();
- setModalFocusIndex(prev => (prev + 1) % 3);
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- setModalFocusIndex(prev => (prev - 1 + 3) % 3);
- } else if (e.key === 'Enter') {
- if (!importMode) {
- if (modalFocusIndex === 0) { playClickSound(); fileInputRef.current?.click(); }
- else if (modalFocusIndex === 1) { playClickSound(); setImportMode('username'); setModalFocusIndex(0); }
- else if (modalFocusIndex === 2) {
- playBackSound();
- setShowImportModal(false);
- setModalFocusIndex(0);
- }
- } else {
- if (modalFocusIndex === 0 || modalFocusIndex === 1) handleFetchUsername();
- else if (modalFocusIndex === 2) {
- playBackSound();
- setImportMode(null);
- setImportUsername('');
- setImportError('');
- setModalFocusIndex(0);
- }
- }
- }
- return;
- }
- if (document.activeElement?.tagName === 'INPUT') return;
- if (e.key === 'Escape') {
- playBackSound();
- setActiveView('main');
- return;
- }
-
- if (e.key === 'ArrowRight') {
- setFocusIndex(prev => (prev === null || prev >= ITEM_COUNT - 1) ? 0 : prev + 1);
- } else if (e.key === 'ArrowLeft') {
- setFocusIndex(prev => (prev === null || prev <= 0) ? ITEM_COUNT - 1 : prev - 1);
- } else if (e.key === 'ArrowDown') {
- if (focusIndex === null || focusIndex < TOP_BUTTONS_COUNT) {
- setFocusIndex(SKINS_START_INDEX);
- } else if (focusIndex < BACK_BUTTON_INDEX) {
- const next = focusIndex + 4;
- setFocusIndex(next >= BACK_BUTTON_INDEX ? BACK_BUTTON_INDEX : next);
- }
- } else if (e.key === 'ArrowUp') {
- if (focusIndex === null) {
- setFocusIndex(0);
- } else if (focusIndex === BACK_BUTTON_INDEX) {
- setFocusIndex(SKINS_START_INDEX + savedSkins.length - 1);
- } else if (focusIndex >= SKINS_START_INDEX) {
- const next = focusIndex - 4;
- setFocusIndex(next < SKINS_START_INDEX ? 0 : next);
- }
- } else if (e.key === 'Enter' && focusIndex !== null) {
- if (focusIndex === 0) handleImportClick();
- else if (focusIndex === 1) handleDeleteActive();
- else if (focusIndex === 2) { playClickSound(); TauriService.openInstanceFolder('Skins').catch(() => { }); }
- else if (focusIndex < BACK_BUTTON_INDEX) {
- handleSkinSelect(savedSkins[focusIndex - SKINS_START_INDEX]);
- } else {
- playBackSound();
- setActiveView('main');
- }
- }
- };
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [focusIndex, savedSkins.length, playBackSound, setActiveView, playClickSound, showImportModal, importMode, modalFocusIndex, importUsername]);
-
- useEffect(() => {
- if (focusIndex !== null) {
- const el = containerRef.current?.querySelector(`[data-index="${focusIndex}"]`) as HTMLElement;
- if (el) el.focus();
- }
- }, [focusIndex]);
-
- const handleImportClick = () => {
- playClickSound();
- setShowImportModal(true);
- setModalFocusIndex(0);
- };
-
- const handleFileChange = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
- if (file.type !== 'image/png') return;
-
- const defaultName = file.name.replace('.png', '').substring(0, 16);
- const reader = new FileReader();
- reader.onload = (event) => {
- const url = event.target?.result as string;
- processSkinImage(url, defaultName);
- };
- reader.readAsDataURL(file);
- e.target.value = '';
- setShowImportModal(false);
- setImportMode(null);
- };
-
- const handleSkinSelect = (skin: SavedSkin) => {
- playClickSound();
- setActiveSkinId(skin.id);
- setSkinUrl(skin.url);
- };
-
- const isDefaultSkin = (id: string | null) => DEFAULT_SKINS.some(d => d.id === id);
-
- const handleDeleteActive = () => {
- if (!activeSkinId || isDefaultSkin(activeSkinId)) return;
- playClickSound();
- const updatedSkins = savedSkins.filter(s => s.id !== activeSkinId);
- setSavedSkins(updatedSkins);
- setSkinUrl('/images/Default.png');
- setActiveSkinId('default');
- };
-
- const handleNameChange = (id: string, newName: string) => {
- const updatedSkins = savedSkins.map(s => s.id === id ? { ...s, name: newName } : s);
- setSavedSkins(updatedSkins);
- };
-
- const isActiveDefault = isDefaultSkin(activeSkinId) || (!activeSkinId && skinUrl === '/images/Default.png');
-
- return (
-
- Skin Library
-
-
-
-
-
- setFocusIndex(0)}
- onClick={handleImportClick}
- className={`w-40 h-10 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] ${focusIndex === 0 ? 'text-[#FFFF55]' : 'text-white'}`}
- style={{ backgroundImage: focusIndex === 0 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
- Import Skin
-
-
- !isActiveDefault && setFocusIndex(1)}
- onClick={handleDeleteActive}
- className={`w-40 h-10 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none ${isActiveDefault ? 'text-gray-400 opacity-80 cursor-not-allowed' : (focusIndex === 1 ? 'text-[#FFFF55]' : 'text-white')}`}
- style={{
- backgroundImage: isActiveDefault ? "url('/images/Button_Background2.png')" : (focusIndex === 1 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')"),
- backgroundSize: '100% 100%',
- imageRendering: 'pixelated'
- }}
- >
- Delete Skin
-
-
-
-
-
-
setFocusIndex(2)}
- onClick={() => { playClickSound(); TauriService.openInstanceFolder('Skins').catch(() => { }); }}
- className={`mc-sq-btn w-10 h-10 flex items-center justify-center outline-none border-none transition-all`}
- style={{ backgroundImage: focusIndex === 2 ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
-
-
-
-
-
-
-
-
- {savedSkins.map((skin, i) => {
- const idx = SKINS_START_INDEX + i;
- const isActive = activeSkinId ? activeSkinId === skin.id : skinUrl === skin.url;
- const isFocused = focusIndex === idx;
- return (
-
setFocusIndex(idx)} className="flex flex-col items-center gap-1 w-32 outline-none">
-
- {isActive && Active }
-
-
handleSkinSelect(skin)}
- className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${(isActive || isFocused) ? 'border-[#FFFF55]' : 'border-[#373737] hover:border-[#A0A0A0]'}`}
- >
-
-
-
handleNameChange(skin.id, e.target.value)}
- className={`bg-transparent text-center outline-none border-none text-base mc-text-shadow w-full truncate transition-colors ${(isActive || isFocused) ? 'text-[#FFFF55]' : 'text-white'} ${isDefaultSkin(skin.id) ? 'pointer-events-none' : ''}`}
- onClick={(e) => e.stopPropagation()} spellCheck={false}
- readOnly={isDefaultSkin(skin.id)}
- />
-
- );
- })}
-
-
-
- setFocusIndex(BACK_BUTTON_INDEX)}
- onClick={() => { playBackSound(); setActiveView('main'); }}
- className={`w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none hover:text-[#FFFF55] ${focusIndex === BACK_BUTTON_INDEX ? 'text-[#FFFF55]' : 'text-white'}`}
- style={{ backgroundImage: focusIndex === BACK_BUTTON_INDEX ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
- Back
-
-
- {showImportModal && (
-
-
-
Import Skin
-
- {!importMode ? (
-
- setModalFocusIndex(0)}
- onClick={() => { playClickSound(); fileInputRef.current?.click(); }}
- className={`w-full h-12 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none ${modalFocusIndex === 0 ? 'text-[#FFFF55]' : 'text-white'}`}
- style={{ backgroundImage: modalFocusIndex === 0 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
- From File
-
- setModalFocusIndex(1)}
- onClick={() => { playClickSound(); setImportMode('username'); setModalFocusIndex(0); }}
- className={`w-full h-12 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none ${modalFocusIndex === 1 ? 'text-[#FFFF55]' : 'text-white'}`}
- style={{ backgroundImage: modalFocusIndex === 1 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
- From Username
-
-
- ) : (
-
- setImportUsername(e.target.value)}
- onFocus={() => setModalFocusIndex(0)}
- autoFocus
- spellCheck={false}
- className={`w-full h-12 bg-black/50 border-2 text-white px-4 text-xl outline-none transition-colors ${modalFocusIndex === 0 ? 'border-[#FFFF55]' : 'border-[#373737]'}`}
- />
-
- {importError && {importError} }
-
- setModalFocusIndex(1)}
- onClick={handleFetchUsername}
- disabled={isImporting}
- className={`w-full h-12 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none ${isImporting ? 'opacity-50' : (modalFocusIndex === 1 ? 'text-[#FFFF55]' : 'text-white')}`}
- style={{ backgroundImage: modalFocusIndex === 1 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
- {isImporting ? 'Fetching...' : 'Fetch Skin'}
-
-
- )}
-
-
setModalFocusIndex(2)}
- onClick={() => {
- playBackSound();
- setShowImportModal(false);
- setImportMode(null);
- setImportUsername('');
- setImportError('');
- setModalFocusIndex(0);
- }}
- className={`w-40 h-10 flex items-center justify-center transition-colors text-lg mc-text-shadow mt-6 outline-none ${modalFocusIndex === 2 ? 'text-[#FFFF55]' : 'text-white'}`}
- style={{ backgroundImage: modalFocusIndex === 2 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }}
- >
- Cancel
-
-
-
- )}
-
- );
-});
-
-export default SkinsView;
diff --git a/src/components/views/ThemesView.tsx b/src/components/views/ThemesView.tsx
deleted file mode 100644
index 993c78a..0000000
--- a/src/components/views/ThemesView.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { useState, useEffect, useRef, memo } from "react";
-import { motion } from "framer-motion";
-import { TauriService, ThemePalette } from "../../services/TauriService";
-import { useUI, useConfig, useAudio } from "../../context/LauncherContext";
-
-const ThemesView = memo(function ThemesView() {
- const { setActiveView } = useUI();
- const { theme: currentTheme, setTheme } = useConfig();
- const { playClickSound, playBackSound } = useAudio();
-
- const [focusIndex, setFocusIndex] = useState(null);
- const [externalPalettes, setExternalPalettes] = useState([]);
- const containerRef = useRef(null);
-
- const baseThemes = ["Default", "Modern"];
-
- useEffect(() => {
- TauriService.getExternalPalettes().then(setExternalPalettes);
- }, []);
-
- const totalPalettes = [...baseThemes, ...externalPalettes.map((p) => p.name)];
- const ITEM_COUNT = 3; // Theme Cycle, Import Theme, Back
-
- const handleImport = async () => {
- playClickSound();
- try {
- const result = await TauriService.importTheme();
- if (result === "success") {
- const updated = await TauriService.getExternalPalettes();
- setExternalPalettes(updated);
- }
- } catch (e) {
- console.error(e);
- }
- };
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape" || e.key === "Backspace") {
- playBackSound();
- setActiveView("main");
- return;
- }
-
- if (e.key === "ArrowDown") {
- setFocusIndex((prev) =>
- prev === null || prev >= ITEM_COUNT - 1 ? 0 : prev + 1,
- );
- } else if (e.key === "ArrowUp") {
- setFocusIndex((prev) =>
- prev === null || prev <= 0 ? ITEM_COUNT - 1 : prev - 1,
- );
- } else if (e.key === "Enter" && focusIndex !== null) {
- if (focusIndex === 0) {
- playClickSound();
- const currentIndex = totalPalettes.indexOf(currentTheme);
- const nextIndex = (currentIndex + 1) % totalPalettes.length;
- setTheme(totalPalettes[nextIndex]);
- } else if (focusIndex === 1) {
- handleImport();
- } else {
- playBackSound();
- setActiveView("main");
- }
- }
- };
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [
- focusIndex,
- currentTheme,
- playClickSound,
- playBackSound,
- setActiveView,
- setTheme,
- totalPalettes,
- ]);
-
- useEffect(() => {
- if (focusIndex !== null) {
- const el = containerRef.current?.querySelector(
- `[data-index="${focusIndex}"]`,
- ) as HTMLElement;
- if (el) el.focus();
- }
- }, [focusIndex]);
-
- const getItemStyle = (index: number) => ({
- backgroundImage:
- focusIndex === index
- ? "url('/images/button_highlighted.png')"
- : "url('/images/Button_Background.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated" as const,
- });
-
- return (
-
-
- Themes & Styles
-
-
-
- setFocusIndex(0)}
- onClick={() => {
- playClickSound();
- const currentIndex = totalPalettes.indexOf(currentTheme);
- const nextIndex = (currentIndex + 1) % totalPalettes.length;
- setTheme(totalPalettes[nextIndex]);
- }}
- className={`w-72 h-12 flex items-center justify-center px-4 relative transition-colors outline-none border-none hover:text-[#FFFF55] ${focusIndex === 0 ? "text-[#FFFF55]" : "text-white"}`}
- style={getItemStyle(0)}
- >
-
- {currentTheme}
-
-
-
- setFocusIndex(1)}
- onClick={handleImport}
- className={`w-72 h-12 flex items-center justify-center px-4 relative transition-colors outline-none border-none hover:text-[#FFFF55] ${focusIndex === 1 ? "text-[#FFFF55]" : "text-white"}`}
- style={getItemStyle(1)}
- >
-
- Import Theme
-
-
-
-
- setFocusIndex(2)}
- onClick={() => {
- playBackSound();
- setActiveView("main");
- }}
- className={`w-72 h-12 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] ${focusIndex === 2 ? "text-[#FFFF55]" : "text-white"}`}
- style={getItemStyle(2)}
- >
- Back
-
-
- );
-});
-
-export default ThemesView;
diff --git a/src/components/views/VersionsView.tsx b/src/components/views/VersionsView.tsx
index 140d0b3..c2eca9e 100644
--- a/src/components/views/VersionsView.tsx
+++ b/src/components/views/VersionsView.tsx
@@ -1,594 +1,96 @@
-import { useState, useEffect, useRef, memo } from "react";
-import { motion } from "framer-motion";
-import { TauriService } from "../../services/TauriService";
-import CustomTUModal from "../modals/CustomTUModal";
-import { useUI, useConfig, useAudio, useGame } from "../../context/LauncherContext";
+import React from 'react';
+import { TauriService } from '../../services/tauri';
+import { ReinstallModalData } from '../../types';
-const VersionsView = memo(function VersionsView() {
- const { setActiveView } = useUI();
- const { profile: selectedProfile, setProfile: setSelectedProfile } = useConfig();
- const { playClickSound, playBackSound, playSfx } = useAudio();
- const { editions, installs: installedVersions, toggleInstall, handleUninstall: onUninstall, deleteCustomEdition: onDeleteEdition, addCustomEdition: onAddEdition, updateCustomEdition: onUpdateEdition, downloadingId } = useGame();
+interface VersionsViewProps {
+ installedStatus: Record;
+ installingInstance: string | null;
+ executeInstall: (id: string, url: string) => void;
+ setReinstallModal: (data: ReinstallModalData | null) => void;
+ playSfx: (name: string, multiplier?: number) => void;
+}
- const [focusRow, setFocusRow] = useState(0);
- const [focusCol, setFocusCol] = useState(0);
- const [isImportModalOpen, setIsImportModalOpen] = useState(false);
- const [editingEdition, setEditingEdition] = useState(null);
- const containerRef = useRef(null);
-
- const ITEM_COUNT = editions.length + 2;
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (document.activeElement?.tagName === "INPUT") return;
- if (e.key === "Escape" || e.key === "Backspace") {
- playBackSound();
- setActiveView("main");
- return;
- }
-
- if (e.key === "ArrowDown") {
- setFocusRow((prev) => (prev >= ITEM_COUNT - 1 ? 0 : prev + 1));
- setFocusCol(0);
- } else if (e.key === "ArrowUp") {
- setFocusRow((prev) => (prev <= 0 ? ITEM_COUNT - 1 : prev - 1));
- setFocusCol(0);
- } else if (e.key === "ArrowRight") {
- if (focusRow < editions.length) {
- const edition = editions[focusRow];
- const isInstalled = installedVersions.includes(edition.id);
- const isCustom = edition.id.startsWith("custom_");
- const hasCredits = !isCustom && edition.credits;
-
- let maxCol = 1;
- if (isInstalled) maxCol = 3;
- if (isCustom) maxCol = isInstalled ? 5 : 3;
- if (hasCredits) maxCol = Math.max(maxCol, 0); // credits button is at col -1
-
- setFocusCol((prev) => (prev < maxCol ? prev + 1 : prev));
- }
- } else if (e.key === "ArrowLeft") {
- if (focusRow < editions.length) {
- const edition = editions[focusRow];
- const isCustom = edition.id.startsWith("custom_");
- const hasCredits = !isCustom && edition.credits;
-
- if (hasCredits && focusCol > -1) {
- setFocusCol(-1);
- } else if (focusCol > 0) {
- setFocusCol((prev) => prev - 1);
- }
- } else {
- setFocusCol((prev) => (prev > 0 ? prev - 1 : prev));
- }
- } else if (e.key === "Enter") {
- if (focusRow < editions.length) {
- const edition = editions[focusRow];
- const isInstalled = installedVersions.includes(edition.id);
- const isCustom = edition.id.startsWith("custom_");
-
- if (focusCol === -1) {
- // Credits button
- if (edition.credits) {
- playClickSound();
- window.open(edition.credits.url, '_blank');
- }
- } else if (focusCol === 1) {
- if (!downloadingId) {
- playClickSound();
- toggleInstall(edition.id);
- }
- } else if (focusCol === 2) {
- if (isInstalled) {
- playClickSound();
- TauriService.openInstanceFolder(edition.id);
- } else if (isCustom) {
- playClickSound();
- setEditingEdition(edition);
- setIsImportModalOpen(true);
- }
- } else if (focusCol === 3) {
- if (isInstalled) {
- playBackSound();
- onUninstall(edition.id);
- } else if (isCustom) {
- playBackSound();
- onDeleteEdition(edition.id);
- }
- } else if (focusCol === 4) {
- if (isCustom) {
- playClickSound();
- setEditingEdition(edition);
- setIsImportModalOpen(true);
- }
- } else if (focusCol === 5) {
- if (isCustom) {
- playBackSound();
- onDeleteEdition(edition.id);
- }
- }
- } else if (focusRow === editions.length) {
- playClickSound();
- setIsImportModalOpen(true);
- } else {
- playBackSound();
- setActiveView("main");
- }
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [editions, focusRow, focusCol, downloadingId, installedVersions, onUninstall, onDeleteEdition, ITEM_COUNT]);
-
- useEffect(() => {
- const el = containerRef.current?.querySelector(
- `[data-row="${focusRow}"][data-col="${focusCol}"]`,
- ) as HTMLElement;
- if (el) el.focus();
- }, [focusRow, focusCol]);
+export const VersionsView: React.FC = ({
+ installedStatus,
+ installingInstance,
+ executeInstall,
+ setReinstallModal,
+ playSfx,
+}) => {
+ const versions = [
+ {
+ id: "vanilla_tu19",
+ name: "Vanilla Nightly (TU19)",
+ desc: "Leaked 4J Studios build.",
+ url: "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu19_vanilla.zip"
+ },
+ {
+ id: "vanilla_tu24",
+ name: "Vanilla TU24",
+ desc: "Horses and Wither update.",
+ url: "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu24_vanilla.zip"
+ }
+ ];
return (
-
-
- Versions
-
-
-
-
-
- {editions.map((edition: any, i: number) => {
- const isInstalled = installedVersions.includes(edition.id);
- const isSelected = selectedProfile === edition.id;
- const isRowFocused = focusRow === i;
- const isCustom = edition.id.startsWith("custom_");
- const isPlaceholder = edition.id === "lmrp_placeholder";
-
- return (
-
{
- if (!isPlaceholder) {
- setFocusRow(i);
- setFocusCol(0);
- }
- }}
+
+
Instances
+
+ {versions.map(v => (
+
+
+
+ {installedStatus[v.id] ? (
+ <>
+
{
+ playSfx('pop.wav');
+ TauriService.openInstanceFolder(v.id);
+ }}
+ className="legacy-btn px-4 py-2 text-xl"
+ >
+ Folder
+
+
{
+ playSfx('click.wav');
+ setReinstallModal({ id: v.id, url: v.url });
+ }}
+ disabled={!!installingInstance}
+ className="legacy-btn px-4 py-2 text-xl reinstall-btn"
+ >
+ Reinstall
+
+ >
+ ) : (
+
{
- if (!isPlaceholder && isInstalled) {
- playClickSound();
- setSelectedProfile(edition.id);
- }
- }}
- style={{
- backdropFilter: 'blur(4px)',
- cursor: isPlaceholder ? 'not-allowed' : isInstalled ? 'pointer' : 'default',
- imageRendering: 'pixelated',
- borderRadius: '0'
+ playSfx('click.wav');
+ executeInstall(v.id, v.url);
}}
+ disabled={!!installingInstance}
+ className="legacy-btn px-6 py-2 text-xl"
>
-
-
-
- {edition.name}
-
- {isCustom && (
-
- Custom
-
- )}
- {edition.id === "revelations_edition" && (
- <>
-
- New
-
-
- Recommended
-
- >
- )}
- {edition.id === "360revived" && (
-
- New
-
- )}
- {edition.id === "lmrp_placeholder" && (
-
- Coming Soon
-
- )}
- {!isCustom && edition.credits && (
-
- by {edition.credits.developer}
-
- )}
-
-
- {edition.desc}
-
-
-
- {!isPlaceholder && (
-
- {!isCustom && edition.credits && (
-
{
- e.stopPropagation();
- setFocusRow(i);
- setFocusCol(-1);
- }}
- onClick={(e) => {
- e.stopPropagation();
- playClickSound();
- window.open(edition.credits.url, '_blank');
- }}
- className={`mc-sq-btn w-8 h-8 flex items-center justify-center outline-none border-none transition-all`}
- style={{
- backgroundImage:
- isRowFocused && focusCol === -1
- ? "url('/images/Button_Square_Highlighted.png')"
- : "url('/images/Button_Square.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- title={`Credits: ${edition.credits.developer} (${edition.credits.platform})`}
- >
- {edition.credits?.platform === "codeberg" ? (
-
-
-
- ) : (
-
-
-
- )}
-
- )}
-
- {!isInstalled ? (
-
{
- e.stopPropagation();
- setFocusRow(i);
- setFocusCol(1);
- }}
- onClick={(e) => {
- e.stopPropagation();
- if (!downloadingId) {
- playClickSound();
- toggleInstall(edition.id);
- }
- }}
- className={`mc-sq-btn w-8 h-8 flex items-center justify-center outline-none border-none transition-all ${downloadingId === edition.id ? "opacity-100" : downloadingId ? "opacity-50" : ""}`}
- style={{
- backgroundImage:
- isRowFocused && focusCol === 1
- ? "url('/images/Button_Square_Highlighted.png')"
- : "url('/images/Button_Square.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
- {downloadingId === edition.id ? (
-
- ) : (
-
- )}
-
- ) : (
- <>
-
{
- e.stopPropagation();
- setFocusRow(i);
- setFocusCol(2);
- }}
- onClick={(e) => {
- e.stopPropagation();
- playClickSound();
- TauriService.openInstanceFolder(edition.id);
- }}
- className="mc-sq-btn w-8 h-8 flex items-center justify-center outline-none border-none transition-all"
- style={{
- backgroundImage:
- isRowFocused && focusCol === 2
- ? "url('/images/Button_Square_Highlighted.png')"
- : "url('/images/Button_Square.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
-
-
-
{
- e.stopPropagation();
- setFocusRow(i);
- setFocusCol(3);
- }}
- onClick={(e) => {
- e.stopPropagation();
- playBackSound();
- onUninstall(edition.id);
- }}
- className="mc-sq-btn w-8 h-8 flex items-center justify-center outline-none border-none transition-all"
- style={{
- backgroundImage:
- isRowFocused && focusCol === 3
- ? "url('/images/Button_Square_Highlighted.png')"
- : "url('/images/Button_Square.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
-
-
-
-
-
-
-
- >
- )}
- {isCustom && (
- <>
-
{
- e.stopPropagation();
- setFocusRow(i);
- setFocusCol(isInstalled ? 4 : 2);
- }}
- onClick={(e) => {
- e.stopPropagation();
- playClickSound();
- setEditingEdition(edition);
- setIsImportModalOpen(true);
- }}
- className="mc-sq-btn w-8 h-8 flex items-center justify-center outline-none border-none transition-all"
- style={{
- backgroundImage:
- isRowFocused && focusCol === (isInstalled ? 4 : 2)
- ? "url('/images/Button_Square_Highlighted.png')"
- : "url('/images/Button_Square.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
-
-
-
-
-
-
{
- e.stopPropagation();
- setFocusRow(i);
- setFocusCol(isInstalled ? 5 : 3);
- }}
- onClick={(e) => {
- e.stopPropagation();
- playBackSound();
- onDeleteEdition(edition.id);
- }}
- className="mc-sq-btn w-8 h-8 flex items-center justify-center outline-none border-none transition-all"
- style={{
- backgroundImage:
- isRowFocused && focusCol === (isInstalled ? 5 : 3)
- ? "url('/images/Button_Square_Highlighted.png')"
- : "url('/images/Button_Square.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
-
-
-
-
-
- >
- )}
-
- )}
-
- );
- })}
+ INSTALL
+
+ )}
+
-
+ ))}
+
+ {['TU75', 'TU9', 'Modded Pack'].map(v => (
+
+
+
Vanilla {v}
+
Legacy version.
+
+
SOON
+
+ ))}
-
-
- {
- setFocusRow(editions.length);
- setFocusCol(0);
- }}
- onClick={() => {
- playClickSound();
- setIsImportModalOpen(true);
- }}
- className={`w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none ${focusRow === editions.length ? "text-[#50C878]" : "text-white"}`}
- style={{
- backgroundImage:
- focusRow === editions.length
- ? "url('/images/button_highlighted.png')"
- : "url('/images/Button_Background.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
- Import Custom TU
-
-
- {
- setFocusRow(editions.length + 1);
- setFocusCol(0);
- }}
- onClick={() => {
- playBackSound();
- setActiveView("main");
- }}
- className={`w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none ${focusRow === editions.length + 1 ? "text-[#50C878]" : "text-white"}`}
- style={{
- backgroundImage:
- focusRow === editions.length + 1
- ? "url('/images/button_highlighted.png')"
- : "url('/images/Button_Background.png')",
- backgroundSize: "100% 100%",
- imageRendering: "pixelated",
- }}
- >
- Back
-
-
-
-
{
- setIsImportModalOpen(false);
- setEditingEdition(null);
- }}
- onImport={(ed: any) => {
- if (editingEdition) {
- onUpdateEdition(editingEdition.id, ed);
- } else {
- const id = onAddEdition(ed);
- setSelectedProfile(id);
- }
- }}
- playSfx={playSfx}
- editingEdition={editingEdition}
- />
-
+
);
-});
-
-export default VersionsView;
+};
diff --git a/src/components/views/WorkshopView.tsx b/src/components/views/WorkshopView.tsx
deleted file mode 100644
index d6b4944..0000000
--- a/src/components/views/WorkshopView.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useState, useEffect, memo } from 'react';
-import { motion } from 'framer-motion';
-import { useUI, useAudio, useConfig } from '../../context/LauncherContext';
-
-const WorkshopView = memo(function WorkshopView() {
- const { setActiveView } = useUI();
- const { playBackSound } = useAudio();
- const [backHover, setBackHover] = useState(false);
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape' || e.key === 'Backspace') {
- playBackSound();
- setActiveView('main');
- }
- };
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [playBackSound, setActiveView]);
-
- return (
-
- Workshop
-
-
- Workshop support coming soon...
-
-
- setBackHover(true)}
- onMouseLeave={() => setBackHover(false)}
- onClick={() => { playBackSound(); setActiveView('main'); }}
- className={`w-72 h-12 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] ${backHover ? 'text-[#FFFF55]' : 'text-white'}`}
- style={{
- backgroundImage: backHover ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')",
- backgroundSize: '100% 100%',
- imageRendering: 'pixelated'
- }}
- >
- Back
-
-
- );
-});
-
-export default WorkshopView;
\ No newline at end of file
diff --git a/src/context/LauncherContext.tsx b/src/context/LauncherContext.tsx
deleted file mode 100644
index da712a0..0000000
--- a/src/context/LauncherContext.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from "react";
-import { useAppConfig } from "../hooks/useAppConfig";
-import { TauriService } from "../services/TauriService";
-import { useAudioController } from "../hooks/useAudioController";
-import { useGameManager } from "../hooks/useGameManager";
-import { useSkinSync } from "../hooks/useSkinSync";
-import { useDiscordRPC } from "../hooks/useDiscordRPC";
-import { useGamepad } from "../hooks/useGamepad";
-import { useUpdateCheck } from "../hooks/useUpdateCheck";
-
-interface UIContextType {
- activeView: string;
- setActiveView: (view: string) => void;
- showIntro: boolean;
- setShowIntro: (show: boolean) => void;
- logoAnimDone: boolean;
- setLogoAnimDone: (done: boolean) => void;
- isUiHidden: boolean;
- setIsUiHidden: (hidden: boolean) => void;
- isWindowVisible: boolean;
- showCredits: boolean;
- setShowCredits: (show: boolean) => void;
- showSpecialThanks: boolean;
- setShowSpecialThanks: (show: boolean) => void;
- focusSection: "menu" | "skin";
- setFocusSection: (section: "menu" | "skin") => void;
- onNavigateToSkin: () => void;
- onNavigateToMenu: () => void;
- connected: boolean;
- updateMessage: string | null;
- clearUpdateMessage: () => void;
-}
-const UIContext = createContext
(undefined);
-
-export const ConfigContext = createContext | undefined>(undefined);
-export const AudioContext = createContext | undefined>(undefined);
-export const GameContext = createContext | undefined>(undefined);
-export const SkinContext = createContext | undefined>(undefined);
-
-export function LauncherProvider({ children }: { children: React.ReactNode }) {
- const [showIntro, setShowIntro] = useState(true);
- const [logoAnimDone, setLogoAnimDone] = useState(false);
- const [activeView, setActiveView] = useState("main");
- const [isUiHidden, setIsUiHidden] = useState(false);
- const [isWindowVisible, setIsWindowVisible] = useState(true);
- const [showCredits, setShowCredits] = useState(false);
- const [showSpecialThanks, setShowSpecialThanks] = useState(false);
- const [focusSection, setFocusSection] = useState<"menu" | "skin">("menu");
-
- const { updateMessage, clearUpdateMessage } = useUpdateCheck();
-
- const configRaw = useAppConfig();
- const skinSync = useSkinSync();
- const gameRaw = useGameManager({
- profile: configRaw.profile,
- setProfile: configRaw.setProfile,
- customEditions: configRaw.customEditions,
- setCustomEditions: configRaw.setCustomEditions,
- keepLauncherOpen: configRaw.keepLauncherOpen,
- });
- const audioRaw = useAudioController({
- musicVol: configRaw.musicVol,
- sfxVol: configRaw.sfxVol,
- showIntro,
- isGameRunning: gameRaw.isGameRunning,
- isWindowVisible,
- });
-
- const config = useMemo(() => configRaw, [
- configRaw.username, configRaw.theme, configRaw.layout, configRaw.vfxEnabled,
- configRaw.rpcEnabled, configRaw.musicVol, configRaw.sfxVol, configRaw.isDayTime,
- configRaw.profile, configRaw.linuxRunner, configRaw.perfBoost, configRaw.customEditions,
- configRaw.legacyMode, configRaw.keepLauncherOpen, configRaw.enableTrayIcon,
- configRaw.animationsEnabled, configRaw.saveConfig, configRaw.setUsername, configRaw.setTheme,
- configRaw.setLayout, configRaw.setVfxEnabled, configRaw.setAnimationsEnabled,
- configRaw.setRpcEnabled, configRaw.setMusicVol, configRaw.setSfxVol, configRaw.setIsDayTime,
- configRaw.setProfile, configRaw.setLinuxRunner, configRaw.setPerfBoost, configRaw.setCustomEditions,
- configRaw.isLoaded, configRaw.hasCompletedSetup, configRaw.setHasCompletedSetup
- ]);
-
- const game = useMemo(() => gameRaw, [
- gameRaw.installs, gameRaw.isGameRunning, gameRaw.downloadProgress,
- gameRaw.downloadingId, gameRaw.editions, gameRaw.isRunnerDownloading,
- gameRaw.runnerDownloadProgress, gameRaw.error, gameRaw.updateCustomEdition, configRaw.profile,
- gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.toggleInstall, gameRaw.handleUninstall,
- gameRaw.addCustomEdition, gameRaw.deleteCustomEdition, gameRaw.downloadRunner, gameRaw.checkInstalls
- ]);
-
- const audio = useMemo(() => audioRaw, [
- audioRaw.currentTrack, audioRaw.splashIndex, audioRaw.tracks, audioRaw.splashes
- ]);
-
- useDiscordRPC({
- rpcEnabled: config.rpcEnabled,
- showIntro,
- username: config.username,
- profile: config.profile,
- activeView,
- isGameRunning: game.isGameRunning,
- downloadProgress: game.downloadProgress,
- downloadingId: game.downloadingId,
- editions: game.editions,
- isWindowVisible,
- });
-
- const { connected } = useGamepad({ playSfx: audio.playSfx, isWindowVisible });
-
- const onNavigateToSkin = useCallback(() => setFocusSection("skin"), []);
- const onNavigateToMenu = useCallback(() => setFocusSection("menu"), []);
-
- useEffect(() => {
- if (activeView === "main") {
- audioRaw.setSplashIndex(-1);
- }
- }, [activeView]);
-
- useEffect(() => {
- if (config.isLoaded && config.profile) {
- TauriService.syncDlc(config.profile).catch(console.error);
- }
- }, [config.profile, config.isLoaded]);
-
- useEffect(() => {
- if (config.isLoaded) {
- config.saveConfig(skinSync.skinBase64);
- }
- }, [
- config.username, skinSync.skinBase64, config.theme, config.linuxRunner,
- config.perfBoost, config.customEditions, config.profile, config.keepLauncherOpen,
- config.enableTrayIcon, config.vfxEnabled, config.animationsEnabled,
- config.rpcEnabled, config.musicVol, config.sfxVol, config.legacyMode, config.isLoaded
- ]);
-
- useEffect(() => {
- const setupVisibilityDetection = async () => {
- try {
- const { listen } = await import("@tauri-apps/api/event");
-
- const unlistenClose = await listen("tauri://close-requested", () => {
- console.log("Window close requested - hiding music");
- setIsWindowVisible(false);
- });
-
- const unlistenShow = await listen("tauri://window-shown", () => {
- console.log("Window shown - resuming music");
- setIsWindowVisible(true);
- });
-
- const unlistenFocus = await listen("tauri://focus", () => {
- console.log("Window focused - resuming music");
- setIsWindowVisible(true);
- });
-
- const unlistenBlur = await listen("tauri://blur", () => {
- console.log("Window blurred - checking visibility");
- });
-
- return () => {
- unlistenClose();
- unlistenShow();
- unlistenFocus();
- unlistenBlur();
- };
- } catch (error) {
- console.error("Failed to setup visibility detection:", error);
- setIsWindowVisible(true);
- }
- };
-
- setupVisibilityDetection();
- }, []);
-
- const uiValue = useMemo(() => ({
- activeView, setActiveView, showIntro, setShowIntro,
- logoAnimDone, setLogoAnimDone, isUiHidden, setIsUiHidden,
- isWindowVisible,
- showCredits, setShowCredits, showSpecialThanks, setShowSpecialThanks, focusSection, setFocusSection,
- onNavigateToSkin, onNavigateToMenu, connected,
- updateMessage, clearUpdateMessage
- }), [activeView, showIntro, logoAnimDone, isUiHidden, isWindowVisible, showCredits, showSpecialThanks, focusSection, onNavigateToSkin, onNavigateToMenu, connected, updateMessage, clearUpdateMessage]);
-
- return (
-
-
-
-
-
- {children}
-
-
-
-
-
- );
-}
-
-export const useUI = () => { const c = useContext(UIContext); if (!c) throw new Error("useUI must be used within LauncherProvider"); return c; };
-export const useConfig = () => { const c = useContext(ConfigContext); if (!c) throw new Error("useConfig must be used within LauncherProvider"); return c; };
-export const useAudio = () => { const c = useContext(AudioContext); if (!c) throw new Error("useAudio must be used within LauncherProvider"); return c; };
-export const useGame = () => { const c = useContext(GameContext); if (!c) throw new Error("useGame must be used within LauncherProvider"); return c; };
-export const useSkin = () => { const c = useContext(SkinContext); if (!c) throw new Error("useSkin must be used within LauncherProvider"); return c; };
diff --git a/src/css/App.css b/src/css/App.css
deleted file mode 100644
index 1d76bf5..0000000
--- a/src/css/App.css
+++ /dev/null
@@ -1 +0,0 @@
-/* empty :P */
\ No newline at end of file
diff --git a/src/css/index.css b/src/css/index.css
deleted file mode 100644
index 433ff46..0000000
--- a/src/css/index.css
+++ /dev/null
@@ -1,114 +0,0 @@
-@import "tailwindcss";
-
-@font-face {
- font-family: 'Mojangles';
- src: url('/fonts/Mojangles.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-
-.gamepad-connected-indicator {
- position: fixed;
- bottom: 10px;
- left: 10px;
- color: #FFFF55;
- font-size: 10px;
- z-index: 1000;
- opacity: 0.5;
-}
-
-.mc-setup-nav-btn {
- border: 4px solid transparent;
- border-image: url('/images/Button_Background.png') 4 stretch;
- image-rendering: pixelated;
- background: rgba(0, 0, 0, 0.6);
- transition: all 0.2s ease;
-}
-
-.mc-setup-nav-btn:hover:not(:disabled) {
- transform: scale(1.05);
-}
-
-.mc-setup-nav-btn:active:not(:disabled) {
- transform: scale(0.95);
-}
-
-.mc-setup-nav-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-body {
- margin: 0;
- background-color: #000;
- font-family: 'Mojangles', monospace;
- -webkit-font-smoothing: none;
-}
-
-.mc-button {
- background-image: url('/images/button.png');
- background-size: 100% 100%;
- image-rendering: pixelated;
- transition: none;
-}
-
-.mc-button:hover:not(:disabled) {
- background-image: url('/images/button_highlighted.png');
-}
-
-.mc-text-shadow {
- text-shadow: 1px 1px 0px rgba(63, 63, 63, 1);
-}
-
-.mc-text-shadow-hover {
- text-shadow: 1px 1px 0px rgba(63, 90, 63, 1);
-}
-
-
-@keyframes sga-burst {
- 0% {
- transform: translate(-50%, -50%) scale(0.5);
- opacity: 1;
- }
-
- 100% {
- transform: translate(calc(-50% + var(--vX)), calc(-50% + var(--vY))) scale(1.2) rotate(var(--rot));
- opacity: 0;
- }
-}
-
-.particle-burst {
- animation: sga-burst 0.8s ease-out forwards;
- filter: drop-shadow(0 0 8px rgba(180, 100, 255, 0.9));
-}
-
-::-webkit-scrollbar {
- width: 14px;
-}
-
-::-webkit-scrollbar-track {
- background: url('/images/backgroundframe.png') center;
- background-size: 100% auto;
- image-rendering: pixelated;
-}
-
-::-webkit-scrollbar-thumb {
- background: url('/images/SliderHandlerBackground.png') no-repeat center;
- background-size: 100% 100%;
- image-rendering: pixelated;
- cursor: pointer;
-}
-
-.no-animations * {
- transition: none !important;
- animation: none !important;
-}
-
-.scrollbar-hide::-webkit-scrollbar {
- display: none;
-}
-
-.scrollbar-hide {
- -ms-overflow-style: none;
- scrollbar-width: none;
-}
\ No newline at end of file
diff --git a/src/hooks/useAppConfig.ts b/src/hooks/useAppConfig.ts
deleted file mode 100644
index 478d2a8..0000000
--- a/src/hooks/useAppConfig.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { useLocalStorage } from "./useLocalStorage";
-import { TauriService } from "../services/TauriService";
-
-export function useAppConfig() {
- const [username, setUsername] = useLocalStorage("lce-username", "Steve");
- const [theme, setTheme] = useLocalStorage("lce-theme", "Modern");
- const [layout, setLayout] = useLocalStorage("lce-layout", "KBM");
- const [vfxEnabled, setVfxEnabled] = useLocalStorage("lce-vfx", true);
- const [animationsEnabled, setAnimationsEnabled] = useLocalStorage("lce-animations", true);
- const [rpcEnabled, setRpcEnabled] = useLocalStorage("discord-rpc", true);
- const [musicVol, setMusicVol] = useLocalStorage("lce-music", 50);
- const [sfxVol, setSfxVol] = useLocalStorage("lce-sfx", 100);
- const [isDayTime, setIsDayTime] = useLocalStorage("lce-daytime", true);
- const [profile, setProfile] = useLocalStorage("lce-profile", "legacy_evolved");
- const [legacyMode, setLegacyMode] = useLocalStorage("lce-legacy-mode", false);
- const [keepLauncherOpen, setKeepLauncherOpen] = useLocalStorage("lce-keep-open", false);
- const [enableTrayIcon, setEnableTrayIcon] = useLocalStorage("lce-tray-icon", true);
- const [hasCompletedSetup, setHasCompletedSetup] = useLocalStorage("lce-setup-completed", false);
-
- const [isLoaded, setIsLoaded] = useState(false);
- const [linuxRunner, setLinuxRunner] = useState();
- const [perfBoost, setPerfBoost] = useState(false);
- const [customEditions, setCustomEditions] = useState([]);
-
- useEffect(() => {
- TauriService.loadConfig().then((config) => {
- if (config.username) setUsername(config.username);
- if (config.themeStyleId) setTheme(config.themeStyleId);
- if (config.linuxRunner) setLinuxRunner(config.linuxRunner);
- if (config.appleSiliconPerformanceBoost !== undefined)
- setPerfBoost(config.appleSiliconPerformanceBoost);
- if (config.customEditions) setCustomEditions(config.customEditions);
- if (config.profile) setProfile(config.profile);
- if (config.keepLauncherOpen !== undefined) setKeepLauncherOpen(config.keepLauncherOpen);
- if (config.enableTrayIcon !== undefined) setEnableTrayIcon(config.enableTrayIcon);
- if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
- if (config.animationsEnabled !== undefined) setAnimationsEnabled(config.animationsEnabled);
- if (config.rpcEnabled !== undefined) setRpcEnabled(config.rpcEnabled);
- if (config.musicVol !== undefined && config.musicVol !== null) setMusicVol(config.musicVol);
- if (config.sfxVol !== undefined && config.sfxVol !== null) setSfxVol(config.sfxVol);
- if (config.legacyMode !== undefined) setLegacyMode(config.legacyMode);
- setIsLoaded(true);
- });
- }, []);
-
- useEffect(() => {
- if (isLoaded) {
- TauriService.updateTrayIcon(enableTrayIcon);
- }
- }, [enableTrayIcon, isLoaded]);
-
- const saveConfig = useCallback((skinBase64?: string | null) => {
- TauriService.saveConfig({
- username,
- skinBase64: skinBase64 || undefined,
- themeStyleId: theme,
- linuxRunner,
- appleSiliconPerformanceBoost: perfBoost,
- profile,
- customEditions,
- keepLauncherOpen,
- enableTrayIcon,
- animationsEnabled,
- vfxEnabled,
- rpcEnabled,
- musicVol,
- sfxVol,
- legacyMode,
- }).catch(console.error);
- }, [username, theme, linuxRunner, perfBoost, profile, customEditions, keepLauncherOpen, enableTrayIcon, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode]);
-
- return {
- username,
- setUsername,
- theme,
- setTheme,
- layout,
- setLayout,
- vfxEnabled,
- setVfxEnabled,
- animationsEnabled,
- setAnimationsEnabled,
- rpcEnabled,
- setRpcEnabled,
- musicVol: musicVol ?? 50,
- setMusicVol,
- sfxVol: sfxVol ?? 100,
- setSfxVol,
- isDayTime,
- setIsDayTime,
- legacyMode,
- setLegacyMode,
- keepLauncherOpen,
- setKeepLauncherOpen,
- enableTrayIcon,
- setEnableTrayIcon,
- profile,
- setProfile,
- linuxRunner,
- setLinuxRunner,
- perfBoost,
- setPerfBoost,
- customEditions,
- setCustomEditions,
- isLoaded,
- hasCompletedSetup,
- setHasCompletedSetup,
- saveConfig,
- };
-}
diff --git a/src/hooks/useAudio.ts b/src/hooks/useAudio.ts
new file mode 100644
index 0000000..6839d6c
--- /dev/null
+++ b/src/hooks/useAudio.ts
@@ -0,0 +1,34 @@
+import { useRef, useEffect } from 'react';
+import { playSfx as playSfxService, ensureAudio } from '../services/audio';
+
+export const useAudio = (musicVol: number, sfxVol: number, isMuted: boolean) => {
+ const musicRef = useRef(null);
+ const lastTrack = useRef(0);
+
+ useEffect(() => {
+ if (musicRef.current) {
+ musicRef.current.volume = isMuted ? 0 : musicVol;
+ }
+ }, [musicVol, isMuted]);
+
+ const playRandomMusic = () => {
+ if (!musicRef.current) return;
+ let track = Math.floor(Math.random() * 5) + 1;
+ if (track === lastTrack.current) track = (track % 5) + 1;
+ lastTrack.current = track;
+ musicRef.current.src = `/music/music${track}.ogg`;
+ musicRef.current.volume = isMuted ? 0 : musicVol;
+ musicRef.current.play().catch(() => {});
+ };
+
+ const playSfx = (name: string, multiplier: number = 1.0) => {
+ playSfxService(name, sfxVol, isMuted, multiplier);
+ };
+
+ return {
+ musicRef,
+ playRandomMusic,
+ playSfx,
+ ensureAudio,
+ };
+};
diff --git a/src/hooks/useAudioController.ts b/src/hooks/useAudioController.ts
deleted file mode 100644
index e29c3c9..0000000
--- a/src/hooks/useAudioController.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { useState, useEffect, useRef, useCallback } from "react";
-
-const TRACKS = [
- "/music/Blind Spots.ogg",
- "/music/Key.ogg",
- "/music/Living Mice.ogg",
- "/music/Oxygene.ogg",
- "/music/Subwoofer Lullaby.ogg",
-];
-
-const SPLASHES = [
- "Legacy is back!", "Pixelated goodness!", "Console Edition vibe!", "100% Not Microsoft!",
- "Symmetry is key!", "Does anyone even read these?", "Task failed successfully.",
- "Hardware accelerated!", "It's a feature, not a bug.", "Look behind you.",
- "Works on my machine.", "Now gluten-free!", "Mom, get the camera!", "Batteries not included.",
- "May contain nuts.", "Press Alt+F4 for diamonds!", "Downloading more RAM...",
- "Reinventing the wheel!", "The cake is a lie.", "Powered by copious amounts of coffee.",
- "I'm running out of ideas.", "That's no moon...", "Now with 100% more nostalgia!",
- "Legacy is the new modern.", "No microtransactions!", "As seen on TV!", "Ironic, isn't it?",
- "Creeper? Aww man.", "Technoblade never dies!",
-];
-
-interface AudioControllerProps {
- musicVol: number;
- sfxVol: number;
- showIntro: boolean;
- isGameRunning: boolean;
- isWindowVisible: boolean;
-}
-
-export function useAudioController({ musicVol, sfxVol, showIntro, isGameRunning, isWindowVisible }: AudioControllerProps) {
- const [currentTrack, setCurrentTrack] = useState(0);
- const [audioElement, setAudioElement] = useState(null);
- const [splashIndex, setSplashIndex] = useState(-1);
- const musicPausedRef = useRef<{ at: number; track: number } | null>(null);
- const fadeIntervalRef = useRef(null);
-
- const playSfx = useCallback((file: string) => {
- const a = new Audio(`/sounds/${file}`);
- a.volume = sfxVol / 100;
- a.play().catch(() => { });
- }, [sfxVol]);
-
- const playClickSound = useCallback(() => playSfx("click.wav"), [playSfx]);
- const playBackSound = useCallback(() => playSfx("back.ogg"), [playSfx]);
- const playSplashSound = useCallback(() => playSfx("orb.ogg"), [playSfx]);
-
- const fadeOut = useCallback((audio: HTMLAudioElement, duration: number = 500) => {
- return new Promise((resolve) => {
- if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
- const initialVolume = audio.volume;
- const steps = 5;
- const stepDuration = duration / steps;
- let currentStep = 0;
- fadeIntervalRef.current = setInterval(() => {
- currentStep++;
- const progress = currentStep / steps;
- audio.volume = initialVolume * (1 - progress);
- if (currentStep >= steps) {
- if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
- fadeIntervalRef.current = null;
- audio.pause();
- audio.volume = initialVolume;
- resolve();
- }
- }, stepDuration);
- });
- }, []);
-
- const fadeIn = useCallback((audio: HTMLAudioElement, targetVolume: number, duration: number = 500) => {
- return new Promise((resolve) => {
- if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
- audio.volume = 0;
- const playPromise = audio.play();
- if (playPromise !== undefined) {
- playPromise.catch(() => { });
- }
-
- const steps = 5;
- const stepDuration = duration / steps;
- let currentStep = 0;
- fadeIntervalRef.current = setInterval(() => {
- currentStep++;
- const progress = currentStep / steps;
- audio.volume = targetVolume * progress;
- if (currentStep >= steps) {
- if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
- fadeIntervalRef.current = null;
- audio.volume = targetVolume;
- resolve();
- }
- }, stepDuration);
- });
- }, []);
-
- const cycleSplash = useCallback(() => {
- playSplashSound();
- let newIndex;
- do {
- newIndex = Math.floor(Math.random() * SPLASHES.length);
- } while (newIndex === splashIndex && SPLASHES.length > 1);
- setSplashIndex(newIndex);
- }, [playSplashSound, splashIndex]);
-
- useEffect(() => {
- if (showIntro) return;
- if (audioElement) return;
-
- const audio = new Audio(TRACKS[currentTrack]);
- audio.volume = musicVol / 100;
- const handleEnded = () => setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
- audio.addEventListener("ended", handleEnded);
-
- const playPromise = audio.play();
- if (playPromise !== undefined) {
- playPromise.catch(() => {
- console.log("Autoplay prevented, waiting for user interaction");
- const startMusic = () => {
- audio.play().catch(() => { });
- document.removeEventListener("click", startMusic);
- document.removeEventListener("keydown", startMusic);
- };
- document.addEventListener("click", startMusic, { once: true });
- document.addEventListener("keydown", startMusic, { once: true });
- });
- }
-
- setAudioElement(audio);
- return () => {
- audio.removeEventListener("ended", handleEnded);
- audio.pause();
- };
- }, [showIntro, audioElement, currentTrack, musicVol]);
-
- useEffect(() => {
- if (!audioElement) return;
- audioElement.src = TRACKS[currentTrack];
- audioElement.play().catch(() => { });
- }, [currentTrack]);
-
- useEffect(() => {
- if (!audioElement) return;
- const shouldPause = isGameRunning || !isWindowVisible;
-
- if (shouldPause) {
- if (!audioElement.paused || fadeIntervalRef.current) {
- if (!musicPausedRef.current) {
- musicPausedRef.current = {
- at: audioElement.currentTime,
- track: currentTrack,
- };
- }
- fadeOut(audioElement, 500);
- }
- } else if (musicPausedRef.current) {
- const { at, track } = musicPausedRef.current;
- musicPausedRef.current = null;
- if (track === currentTrack) {
- audioElement.currentTime = at;
- }
- fadeIn(audioElement, musicVol / 100, 500);
- }
- }, [isGameRunning, isWindowVisible, audioElement, currentTrack, musicVol, fadeOut, fadeIn]);
-
- useEffect(() => {
- if (audioElement) {
- audioElement.volume = musicVol / 100;
- }
- }, [musicVol, audioElement]);
-
- return {
- currentTrack,
- setCurrentTrack,
- splashIndex,
- setSplashIndex,
- cycleSplash,
- playClickSound,
- playBackSound,
- playSfx,
- tracks: TRACKS,
- splashes: SPLASHES,
- };
-}
diff --git a/src/hooks/useDiscordRPC.ts b/src/hooks/useDiscordRPC.ts
deleted file mode 100644
index e5ace3f..0000000
--- a/src/hooks/useDiscordRPC.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { useEffect } from "react";
-import RpcService from "../services/RpcService";
-
-interface DiscordRPCProps {
- rpcEnabled: boolean;
- showIntro: boolean;
- username: string;
- profile: string;
- activeView: string;
- isGameRunning: boolean;
- isWindowVisible: boolean;
- downloadProgress: number | null;
- downloadingId: string | null;
- editions: any[];
-}
-
-export function useDiscordRPC({
- rpcEnabled,
- showIntro,
- username,
- profile,
- activeView,
- isGameRunning,
- isWindowVisible,
- downloadProgress,
- downloadingId,
- editions,
-}: DiscordRPCProps) {
- useEffect(() => {
- const updateRPC = async () => {
- if (!rpcEnabled || showIntro || !username) return;
-
- if (!isWindowVisible && !isGameRunning && downloadProgress === null) return;
-
- const version = editions.find((e) => e.id === profile);
- const versionName = version ? version.name : "Unknown Version";
- let details = "In Menus";
- let state = isGameRunning ? `Playing as ${username}` : `Logged in as ${username}`;
-
- if (isGameRunning) {
- details = `Playing ${versionName}`;
- } else if (downloadProgress !== null) {
- const downloadingName = editions.find((e) => e.id === downloadingId)?.name || "Game Files";
- details = `Downloading ${downloadingName} (${downloadProgress.toFixed(0)}%)`;
- } else {
- const tabNames: Record = {
- main: "Main Menu",
- versions: "Selecting Version",
- settings: "In Settings",
- themes: "Browsing Themes",
- skins: "Browsing Skins",
- workshop: "Browsing Workshop",
- };
- details = tabNames[activeView] || "In Menus";
- }
-
- await RpcService.updateActivity(details, state, isGameRunning);
- };
-
- updateRPC();
- }, [rpcEnabled, showIntro, username, profile, activeView, isGameRunning, isWindowVisible, Math.floor(downloadProgress || 0), downloadingId, editions]);
-}
diff --git a/src/hooks/useGameInstances.ts b/src/hooks/useGameInstances.ts
new file mode 100644
index 0000000..328a6f0
--- /dev/null
+++ b/src/hooks/useGameInstances.ts
@@ -0,0 +1,56 @@
+import { useState, useEffect } from 'react';
+import { listen } from "@tauri-apps/api/event";
+import { TauriService } from '../services/tauri';
+import { InstalledStatus, McNotification } from '../types';
+
+export const useGameInstances = (
+ playSfx: (name: string, multiplier?: number) => void,
+ setMcNotif: (notif: McNotification | null) => void
+) => {
+ const [installingInstance, setInstallingInstance] = useState(null);
+ const [downloadProgress, setDownloadProgress] = useState(0);
+ const [installedStatus, setInstalledStatus] = useState({
+ vanilla_tu19: false,
+ vanilla_tu24: false,
+ });
+
+ const updateAllStatus = async () => {
+ const v19 = await TauriService.checkGameInstalled("vanilla_tu19");
+ const v24 = await TauriService.checkGameInstalled("vanilla_tu24");
+ setInstalledStatus({ vanilla_tu19: v19, vanilla_tu24: v24 });
+ };
+
+ const executeInstall = async (id: string, url: string) => {
+ setInstallingInstance(id);
+ setDownloadProgress(0);
+ try {
+ await TauriService.downloadAndInstall(url, id);
+ setMcNotif({ t: "Success!", m: "Ready to play." });
+ playSfx('orb.ogg');
+ setTimeout(() => setMcNotif(null), 4000);
+ updateAllStatus();
+ } catch (e) {
+ console.error(e);
+ alert("Error during installation: " + e);
+ }
+ setInstallingInstance(null);
+ };
+
+ useEffect(() => {
+ updateAllStatus();
+ const unlisten = listen("download-progress", (e) =>
+ setDownloadProgress(Math.round(e.payload))
+ );
+ return () => {
+ unlisten.then((f) => f());
+ };
+ }, []);
+
+ return {
+ installedStatus,
+ installingInstance,
+ downloadProgress,
+ executeInstall,
+ updateAllStatus,
+ };
+};
diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts
deleted file mode 100644
index 64c20e9..0000000
--- a/src/hooks/useGameManager.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import { useState, useEffect, useCallback, useMemo } from "react";
-import { getCurrentWindow } from "@tauri-apps/api/window";
-import { TauriService } from "../services/TauriService";
-
-const appWindow = getCurrentWindow();
-
-const BASE_EDITIONS = [
- {
- id: "revelations_edition",
- name: "Revelations",
- desc: "Enhanced LCE with uncapped FPS, graphics fixes, hardcore hearts, and dedicated server security. Features LAN multiplayer, split-screen, and keyboard & mouse support.",
- url: "https://github.com/itsRevela/MinecraftConsoles/releases/download/Nightly/LCREWindows64.zip",
- titleImage: "/images/minecraft_title_revelations.png",
- credits: {
- developer: "itsRevela",
- platform: "github",
- url: "https://github.com/itsRevela"
- }
- },
- {
- id: "360revived",
- name: "360 Revived",
- desc: "PC port of Xbox 360 Edition TU19 with desktop optimizations. Features keyboard & mouse, fullscreen, LAN multiplayer, dedicated server, and split-screen support.",
- url: "https://github.com/BluTac10/360Revived/releases/download/nightly/LCEWindows64.zip",
- titleImage: "/images/minecraft_title_360revived.png",
- credits: {
- developer: "BluTac10",
- platform: "github",
- url: "https://github.com/BluTac10"
- }
- },
- {
- id: "legacy_evolved",
- name: "Legacy Evolved",
- desc: "Backports newer title updates to LCE TU19 base. Currently porting TU25 (98% complete) and TU31 (76% complete).",
- url: "https://codeberg.org/piebot/LegacyEvolved/releases/download/nightly/LCEWindows64.zip",
- titleImage: "/images/minecraft_title_LegacyEvolved.png",
- credits: {
- developer: "piebot",
- platform: "codeberg",
- url: "https://codeberg.org/piebot"
- }
- },
- {
- id: "vanilla_tu19",
- name: "Title Update 19",
- desc: "Minecraft LCE v1.6.0560.0 with compilation fixes. Base version for modding with keyboard & mouse, fullscreen, LAN multiplayer, and dedicated server support.",
- url: "https://github.com/smartcmd/MinecraftConsoles/releases/download/nightly/LCEWindows64.zip",
- titleImage: "/images/minecraft_title_tu19.png",
- credits: {
- developer: "smartcmd",
- platform: "github",
- url: "https://github.com/smartcmd"
- }
- },
- {
- id: "lmrp_placeholder",
- name: "LMRP (Coming Soon)",
- desc: "Legacy Minecraft Restoration Project - Classic mini-games and nostalgic gameplay. Stay tuned for updates!",
- url: "",
- titleImage: "/images/minecraft_title_lmrp.png",
- credits: {
- developer: "LMRP Team",
- platform: "github",
- url: "https://github.com/LMRP-Project"
- }
- },
-];
-
-const PARTNERSHIP_SERVERS = [
- {
- name: "Kowhaifans Clubhouse",
- ip: "kowhaifan.ddns.net",
- port: 25565,
- },
-];
-
-interface GameManagerProps {
- profile: string;
- setProfile: (id: string) => void;
- customEditions: any[];
- setCustomEditions: (editions: any[]) => void;
- keepLauncherOpen: boolean;
-}
-
-export function useGameManager({ profile, setProfile, customEditions, setCustomEditions, keepLauncherOpen }: GameManagerProps) {
- const [installs, setInstalls] = useState([]);
- const [isGameRunning, setIsGameRunning] = useState(false);
- const [downloadProgress, setDownloadProgress] = useState(null);
- const [downloadingId, setDownloadingId] = useState(null);
- const [isRunnerDownloading, setIsRunnerDownloading] = useState(false);
- const [runnerDownloadProgress, setRunnerDownloadProgress] = useState(null);
- const [error, setError] = useState(null);
-
- const editions = useMemo(() => [...BASE_EDITIONS, ...customEditions], [customEditions]);
-
- const checkInstalls = useCallback(async () => {
- const results = await Promise.all(
- editions.map(async (e) => {
- const isInstalled = await TauriService.checkGameInstalled(e.id);
- return isInstalled ? e.id : null;
- }),
- );
- setInstalls(results.filter((id): id is string => id !== null));
- }, [editions]);
-
- useEffect(() => {
- checkInstalls();
- const unlistenDownload = TauriService.onDownloadProgress((p) => setDownloadProgress(p));
- const unlistenRunner = TauriService.onRunnerDownloadProgress((p) => setRunnerDownloadProgress(p));
- return () => {
- unlistenDownload.then((u) => u());
- unlistenRunner.then((u) => u());
- };
- }, [customEditions, checkInstalls]);
-
- const downloadRunner = useCallback(async (name: string, url: string) => {
- if (isRunnerDownloading) return;
- setIsRunnerDownloading(true);
- setRunnerDownloadProgress(0);
- setError(null);
- try {
- await TauriService.downloadRunner(name, url);
- setRunnerDownloadProgress(null);
- } catch (e: any) {
- console.error(e);
- setError(typeof e === 'string' ? e : e.message || "Failed to download runner");
- } finally {
- setIsRunnerDownloading(false);
- }
- }, [isRunnerDownloading]);
-
- const toggleInstall = useCallback(async (id: string) => {
- if (downloadingId) return;
- const edition = editions.find((e) => e.id === id);
- if (!edition) return;
- setError(null);
- try {
- setDownloadingId(id);
- setDownloadProgress(0);
- await TauriService.downloadAndInstall(edition.url, id);
- await TauriService.syncDlc(id);
- await checkInstalls();
- setProfile(id);
- setDownloadProgress(null);
- setDownloadingId(null);
- } catch (e: any) {
- console.error(e);
- setError(typeof e === 'string' ? e : e.message || "Failed to install version");
- setDownloadProgress(null);
- setDownloadingId(null);
- }
- }, [downloadingId, editions, checkInstalls, setProfile]);
-
- const handleUninstall = useCallback(async (id: string) => {
- await TauriService.deleteInstance(id);
- await checkInstalls();
- }, [checkInstalls]);
-
- const handleLaunch = useCallback(async () => {
- if (isGameRunning) return;
- setError(null);
- setIsGameRunning(true);
- try {
- if (!keepLauncherOpen) {
- await appWindow.hide();
- }
- await TauriService.launchGame(profile, PARTNERSHIP_SERVERS);
- } catch (e: any) {
- console.error(e);
- setError(typeof e === 'string' ? e : e.message || "Failed to launch game");
- } finally {
- setIsGameRunning(false);
- await appWindow.show();
- await appWindow.unminimize();
- await appWindow.setFocus();
- }
- }, [isGameRunning, profile, keepLauncherOpen]);
-
- const stopGame = useCallback(async () => {
- try {
- await TauriService.stopGame(profile);
- setIsGameRunning(false);
- } catch (e) {
- console.error(e);
- }
- }, [profile]);
-
- const addCustomEdition = useCallback((edition: { name: string; desc: string; url: string }) => {
- const id = `custom_${Date.now()}`;
- const newEdition = { ...edition, id, titleImage: "/images/minecraft_title_tucustom.png" };
- setCustomEditions([...customEditions, newEdition]);
- return id;
- }, [customEditions, setCustomEditions]);
-
- const deleteCustomEdition = useCallback((id: string) => {
- setCustomEditions(customEditions.filter((e) => e.id !== id));
- TauriService.deleteInstance(id).catch(console.error);
- }, [customEditions, setCustomEditions]);
-
- const updateCustomEdition = useCallback((id: string, updated: { name: string; desc: string; url: string }) => {
- setCustomEditions(customEditions.map((e) => e.id === id ? { ...e, ...updated } : e));
- }, [customEditions, setCustomEditions]);
-
- return {
- installs,
- isGameRunning,
- downloadProgress,
- downloadingId,
- isRunnerDownloading,
- runnerDownloadProgress,
- error,
- setError,
- editions,
- toggleInstall,
- handleUninstall,
- handleLaunch,
- stopGame,
- addCustomEdition,
- deleteCustomEdition,
- updateCustomEdition,
- downloadRunner,
- checkInstalls,
- };
-}
diff --git a/src/hooks/useGamepad.ts b/src/hooks/useGamepad.ts
deleted file mode 100644
index 6c6a196..0000000
--- a/src/hooks/useGamepad.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { useEffect, useRef, useState, useCallback } from 'react';
-
-export interface UseGamepadProps {
- playSfx: (file: string) => void;
- isWindowVisible: boolean;
-}
-
-export const useGamepad = ({ playSfx, isWindowVisible }: UseGamepadProps) => {
- const [connected, setConnected] = useState(false);
- const requestRef = useRef(undefined);
- const focusedRef = useRef(document.hasFocus());
- const lastButtons = useRef>({});
- const lastAxes = useRef>({});
- const stateRef = useRef({ playSfx });
-
- useEffect(() => {
- const onFocus = () => { focusedRef.current = true; };
- const onBlur = () => {
- focusedRef.current = false;
- lastButtons.current = {};
- lastAxes.current = {};
- };
- window.addEventListener('focus', onFocus);
- window.addEventListener('blur', onBlur);
- return () => {
- window.removeEventListener('focus', onFocus);
- window.removeEventListener('blur', onBlur);
- };
- }, []);
-
- useEffect(() => {
- stateRef.current = { playSfx };
- }, [playSfx]);
-
- const dispatchKey = (key: string, shiftKey = false) => {
- window.dispatchEvent(new KeyboardEvent('keydown', {
- key, shiftKey, bubbles: true, cancelable: true, view: window
- }));
- window.dispatchEvent(new KeyboardEvent('keyup', {
- key, shiftKey, bubbles: true, cancelable: true, view: window
- }));
- };
-
- const update = useCallback(() => {
- if (!focusedRef.current) {
- requestRef.current = requestAnimationFrame(update);
- return;
- }
- try {
- const gamepads = navigator.getGamepads ? navigator.getGamepads() : null;
- if (gamepads) {
- for (const gp of gamepads) {
- if (!gp) continue;
- const btnVal = (i: number): number => {
- const btn = gp.buttons[i];
- if (!btn) return 0;
- return typeof btn === "object" ? btn.value : (btn as any) ?? 0;
- };
- const justPressed = (i: number) => btnVal(i) > 0.5 && !lastButtons.current[i];
-
- if (justPressed(1)) dispatchKey('Enter');
- if (justPressed(2)) dispatchKey('Escape');
- if (justPressed(4)) dispatchKey('Tab', true);
- if (justPressed(5)) dispatchKey('Tab');
-
- const newButtons: Record = {};
- gp.buttons.forEach((btn, i) => {
- newButtons[i] = (typeof btn === "object" ? btn.value : btn) > 0.5;
- });
- lastButtons.current = newButtons;
-
- const deadzone = 0.5;
- const axisY = gp.axes[2] ?? 0;
- const prevY = lastAxes.current[2] ?? 0;
- if (Math.abs(axisY) > deadzone && Math.abs(prevY) <= deadzone) {
- dispatchKey(axisY < 0 ? 'ArrowDown' : 'ArrowUp');
- }
- lastAxes.current[2] = axisY;
-
- const axisX = gp.axes[1] ?? 0;
- const prevX = lastAxes.current[1] ?? 0;
- if (Math.abs(axisX) > deadzone && Math.abs(prevX) <= deadzone) {
- dispatchKey(axisX > 0 ? 'ArrowRight' : 'ArrowLeft');
- }
- lastAxes.current[1] = axisX;
- }
- }
- } catch (e) {
- console.error(e);
- }
- requestRef.current = requestAnimationFrame(update);
- }, []);
-
- useEffect(() => {
- const handleConnect = () => setConnected(true);
- const handleDisconnect = () => {
- const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
- const hasGamepads = Array.from(gamepads).some(gp => gp !== null);
- setConnected(hasGamepads);
- };
-
- window.addEventListener("gamepadconnected", handleConnect);
- window.addEventListener("gamepaddisconnected", handleDisconnect);
-
- const initialGamepads = navigator.getGamepads ? navigator.getGamepads() : [];
- if (Array.from(initialGamepads).some(gp => gp !== null)) {
- setConnected(true);
- }
-
- return () => {
- window.removeEventListener("gamepadconnected", handleConnect);
- window.removeEventListener("gamepaddisconnected", handleDisconnect);
- };
- }, []);
-
- useEffect(() => {
- if (connected && isWindowVisible) {
- requestRef.current = requestAnimationFrame(update);
- } else if (requestRef.current) {
- cancelAnimationFrame(requestRef.current);
- }
- return () => {
- if (requestRef.current) cancelAnimationFrame(requestRef.current);
- };
- }, [connected, update, isWindowVisible]);
-
- return { connected };
-};
\ No newline at end of file
diff --git a/src/hooks/useLauncher.ts b/src/hooks/useLauncher.ts
new file mode 100644
index 0000000..6367e7a
--- /dev/null
+++ b/src/hooks/useLauncher.ts
@@ -0,0 +1,51 @@
+import { useState } from 'react';
+import { TauriService } from '../services/tauri';
+
+export const useLauncher = (
+ selectedInstance: string,
+ musicRef: React.RefObject,
+ isMuted: boolean,
+ musicVol: number,
+ playRandomMusic: () => void,
+ playSfx: (name: string, multiplier?: number) => void
+) => {
+ const [isRunning, setIsRunning] = useState(false);
+
+ const fadeAndLaunch = async () => {
+ playSfx('levelup.ogg', 0.4);
+ setIsRunning(true);
+ if (musicRef.current && !isMuted) {
+ const startVol = musicRef.current.volume;
+ const steps = 20;
+ let currentStep = 0;
+ const fade = setInterval(() => {
+ currentStep++;
+ if (musicRef.current) {
+ musicRef.current.volume = Math.max(0, startVol * (1 - currentStep / steps));
+ }
+ if (currentStep >= steps) {
+ clearInterval(fade);
+ if (musicRef.current) musicRef.current.pause();
+ }
+ }, 50);
+ }
+ setTimeout(async () => {
+ try {
+ await TauriService.launchGame(selectedInstance);
+ } catch (e) {
+ alert(`Failed to launch game: ${e}`);
+ } finally {
+ setIsRunning(false);
+ if (musicRef.current) {
+ musicRef.current.volume = isMuted ? 0 : musicVol;
+ playRandomMusic();
+ }
+ }
+ }, 1500);
+ };
+
+ return {
+ isRunning,
+ fadeAndLaunch,
+ };
+};
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
deleted file mode 100644
index dfbd1db..0000000
--- a/src/hooks/useLocalStorage.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useState, useCallback } from 'react';
-
-export function useLocalStorage(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
- const [storedValue, setStoredValue] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- return initialValue;
- }
- });
-
- const setValue = useCallback((value: T | ((val: T) => T)) => {
- try {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
- } catch (error) {
- console.log(error);
- }
- }, [key, storedValue]);
-
- return [storedValue, setValue];
-}
\ No newline at end of file
diff --git a/src/hooks/usePlatform.ts b/src/hooks/usePlatform.ts
deleted file mode 100644
index aa1471b..0000000
--- a/src/hooks/usePlatform.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useMemo } from 'react';
-
-
-export function usePlatform() {
- const platform = useMemo(() => {
- if (typeof window === 'undefined') return { isLinux: false, isMac: false, isWindows: false };
-
- const ua = window.navigator.userAgent.toLowerCase();
- const plat = window.navigator.platform.toLowerCase();
-
- const isLinux = plat.includes('linux') || ua.includes('linux');
- const isMac = plat.includes('mac') || ua.includes('mac');
- const isWindows = plat.includes('win') || ua.includes('win');
-
- return { isLinux, isMac, isWindows };
- }, []);
-
- return platform;
-}
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
new file mode 100644
index 0000000..6b0b81b
--- /dev/null
+++ b/src/hooks/useSettings.ts
@@ -0,0 +1,22 @@
+import { useState, useEffect } from 'react';
+
+export const useSettings = () => {
+ const [musicVol, setMusicVol] = useState(parseFloat(localStorage.getItem("musicVol") || "0.4"));
+ const [sfxVol, setSfxVol] = useState(parseFloat(localStorage.getItem("sfxVol") || "0.7"));
+ const [isMuted, setIsMuted] = useState(localStorage.getItem("isMuted") === "true");
+
+ useEffect(() => {
+ localStorage.setItem("musicVol", musicVol.toString());
+ localStorage.setItem("sfxVol", sfxVol.toString());
+ localStorage.setItem("isMuted", isMuted.toString());
+ }, [musicVol, sfxVol, isMuted]);
+
+ return {
+ musicVol,
+ setMusicVol,
+ sfxVol,
+ setSfxVol,
+ isMuted,
+ setIsMuted,
+ };
+};
diff --git a/src/hooks/useSkinSync.ts b/src/hooks/useSkinSync.ts
deleted file mode 100644
index 8f20e91..0000000
--- a/src/hooks/useSkinSync.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useState, useEffect } from "react";
-import { useLocalStorage } from "./useLocalStorage";
-
-export function useSkinSync() {
- const [skinUrl, setSkinUrl] = useLocalStorage("lce-skin", "/images/Default.png");
- const [skinBase64, setSkinBase64] = useState(null);
-
- useEffect(() => {
- const syncSkin = async () => {
- if (!skinUrl) return;
- try {
- const img = new Image();
- img.crossOrigin = "anonymous";
- img.onload = () => {
- const cvs = document.createElement("canvas");
- cvs.width = 64;
- cvs.height = 32;
- const ctx = cvs.getContext("2d");
- if (ctx) {
- ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
- setSkinBase64(cvs.toDataURL("image/png"));
- }
- };
- img.src = skinUrl;
- } catch (e) {
- console.error("Skin conversion failed:", e);
- }
- };
- syncSkin();
- }, [skinUrl]);
-
- return {
- skinUrl,
- setSkinUrl,
- skinBase64,
- };
-}
diff --git a/src/hooks/useUpdateCheck.ts b/src/hooks/useUpdateCheck.ts
deleted file mode 100644
index f6fe239..0000000
--- a/src/hooks/useUpdateCheck.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import pkg from "../../package.json";
-
-const CURRENT_VERSION = pkg.version;
-const REPO_URL = "https://api.github.com/repos/Emerald-Legacy-Launcher/Emerald-Legacy-Launcher/releases/latest";
-
-function isNewerVersion(latest: string, current: string): boolean {
- const latestParts = latest.split('.').map(Number);
- const currentParts = current.split('.').map(Number);
-
- for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
- const latestPart = latestParts[i] || 0;
- const currentPart = currentParts[i] || 0;
-
- if (latestPart > currentPart) return true;
- if (latestPart < currentPart) return false;
- }
-
- return false;
-}
-
-export function useUpdateCheck() {
- const [updateMessage, setUpdateMessage] = useState(null);
-
- const checkUpdates = useCallback(async () => {
- try {
- const response = await fetch(REPO_URL);
- if (!response.ok) return;
-
- const data = await response.json();
- const latestVersion = data.tag_name.replace(/^v/, '');
-
- if (isNewerVersion(latestVersion, CURRENT_VERSION)) {
- setUpdateMessage(`Version ${data.tag_name} is now available!`);
- }
- } catch (e) {
- console.error("Failed to check for updates:", e);
- }
- }, []);
-
- useEffect(() => {
- checkUpdates();
- }, [checkUpdates]);
-
- return {
- updateMessage,
- clearUpdateMessage: () => setUpdateMessage(null),
- };
-}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..85bda6d
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,205 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Fonts and Globals */
+@font-face {
+ font-family: 'Minecraft';
+ src: url('/fonts/Mojangles.ttf') format('truetype');
+}
+
+* {
+ border-radius: 0 !important;
+ font-family: 'Minecraft', sans-serif !important;
+ image-rendering: pixelated;
+ font-weight: normal !important;
+}
+
+body {
+ margin: 0;
+}
+
+main {
+ background: url('/images/dirt-background.png') repeat !important;
+ background-size: 2000px !important;
+ position: relative;
+}
+
+main::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+}
+
+.no-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
+.no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.sidebar-progress {
+ background: #2a2a2a;
+ border-top: 4px solid #000;
+ padding: 20px;
+ position: relative;
+ box-shadow: inset 0 4px #444;
+}
+
+/* Legacy UI Components */
+.legacy-btn {
+ @apply transition-all duration-75 flex items-center justify-center;
+ background: #bebebe;
+ border: 4px solid #000 !important;
+ box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
+ color: #3e3e3e !important;
+ cursor: pointer;
+}
+
+.legacy-btn:hover:not(:disabled) {
+ background: #d0d0d0;
+ outline: 4px solid #fff;
+ outline-offset: -8px;
+}
+
+.legacy-btn:active:not(:disabled) {
+ box-shadow: inset -4px -4px #ffffff, inset 4px 4px #555555 !important;
+}
+
+.legacy-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ filter: grayscale(1);
+}
+
+.legacy-select {
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ background: #bebebe url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%233e3e3e' stroke-width='3' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpath d='M7 10l5 5 5-5'/%3E%3C/svg%3E") no-repeat right 12px center;
+ border: 4px solid #000 !important;
+ box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
+ color: #3e3e3e !important;
+ cursor: pointer;
+ padding-right: 48px;
+}
+
+.legacy-select:focus {
+ outline: 4px solid #fff;
+ outline-offset: -8px;
+}
+
+.mc-progress-container {
+ background: #313131 !important;
+ border: 4px solid #000 !important;
+ height: 32px !important;
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 0 4px;
+}
+
+.mc-progress-bar {
+ background: #55FF55 !important;
+ height: 16px !important;
+ box-shadow: inset -2px -2px #00AA00, inset 2px 2px #AAFFAA !important;
+}
+
+.mc-progress-text {
+ position: absolute;
+ width: 100%;
+ text-align: center;
+ color: #fff;
+ text-shadow: 2px 2px #000;
+ font-size: 14px;
+ z-index: 10;
+ pointer-events: none;
+}
+
+.active-tab {
+ background: #388e3c !important;
+ box-shadow: inset 4px 4px #55ff55, inset -4px -4px #1a4d0a !important;
+ color: #fff !important;
+}
+
+.reinstall-btn,
+.confirm-red-btn,
+.cancel-download-btn {
+ background: #ff5555 !important;
+ box-shadow: inset 4px 4px #ffaaaa, inset -4px -4px #aa0000 !important;
+ color: #fff !important;
+}
+
+.mc-range {
+ appearance: none;
+ -webkit-appearance: none;
+ background: #000;
+ height: 12px;
+ border: 3px solid #555;
+ width: 100%;
+}
+
+.mc-range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 24px;
+ background: #bebebe;
+ border: 3px solid #000;
+ box-shadow: inset 2px 2px #fff, inset -2px -2px #555;
+ cursor: pointer;
+}
+
+.social-btn {
+ background: transparent;
+ border: 4px solid #000;
+ box-shadow: inset 4px 4px #555, inset -4px -4px #111;
+ width: 64px;
+ height: 64px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ transition: transform 0.1s;
+}
+
+.social-btn:hover {
+ transform: scale(1.1);
+}
+
+.btn-discord {
+ background: #5865F2 !important;
+ box-shadow: inset 4px 4px #8ea1e1, inset -4px -4px #313338 !important;
+}
+
+.btn-github {
+ background: #333 !important;
+ box-shadow: inset 4px 4px #666, inset -4px -4px #000 !important;
+}
+
+.btn-reddit {
+ background: #FF4500 !important;
+ box-shadow: inset 4px 4px #ff8b60, inset -4px -4px #942700 !important;
+}
+
+.splash-text {
+ color: #ffff00;
+ text-shadow: 4px 4px #3f3f00;
+ white-space: nowrap;
+ pointer-events: none;
+ z-index: 20;
+ transform: rotate(-20deg);
+ animation: splash-pulse 0.5s infinite alternate;
+ transform-origin: center;
+}
+
+@keyframes splash-pulse {
+ from {
+ transform: rotate(-20deg) scale(1);
+ }
+ to {
+ transform: rotate(-20deg) scale(1.05);
+ }
+}
diff --git a/src/main.tsx b/src/main.tsx
index afe1914..02e3004 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,15 +1,16 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import "tauri-plugin-gamepad-api";
-import App from "./pages/App";
-import "./css/index.css";
-import "./css/App.css";
-import { LauncherProvider } from "./context/LauncherContext";
-// RpcService is now managed by LauncherProvider context
-ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
-
-
-
-);
\ No newline at end of file
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import './index.css';
+
+const rootElement = document.getElementById("root");
+
+if (rootElement) {
+ ReactDOM.createRoot(rootElement).render(
+
+
+
+ );
+} else {
+ console.error("ERREUR FATALE : La div avec l'id 'root' est introuvable dans index.html");
+}
\ No newline at end of file
diff --git a/src/pages/App.tsx b/src/pages/App.tsx
deleted file mode 100644
index 61dbbb3..0000000
--- a/src/pages/App.tsx
+++ /dev/null
@@ -1,369 +0,0 @@
-import { useEffect, useState } from "react";
-import { motion, AnimatePresence, MotionConfig } from "framer-motion";
-import HomeView from "../components/views/HomeView";
-import SettingsView from "../components/views/SettingsView";
-import VersionsView from "../components/views/VersionsView";
-import ThemesView from "../components/views/ThemesView";
-import SkinsView from "../components/views/SkinsView";
-import WorkshopView from "../components/views/WorkshopView";
-import SetupView from "../components/views/SetupView";
-import SkinViewer from "../components/common/SkinViewer";
-import TeamModal from "../components/modals/TeamModal";
-import SpecialThanksModal from "../components/modals/SpecialThanksModal";
-import PanoramaBackground from "../components/common/PanoramaBackground";
-import { ClickParticles } from "../components/common/ClickParticles";
-import { AppHeader } from "../components/layout/AppHeader";
-import { DownloadOverlay } from "../components/layout/DownloadOverlay";
-import { AchievementToast } from "../components/common/AchievementToast";
-import { useUI, useConfig, useAudio, useGame, useSkin } from "../context/LauncherContext";
-import { getCurrentWindow } from "@tauri-apps/api/window";
-import { TauriService } from "../services/TauriService";
-
-const appWindow = getCurrentWindow();
-
-export default function App() {
- const {
- showIntro, setShowIntro, logoAnimDone, setLogoAnimDone,
- activeView, setActiveView, isUiHidden, setIsUiHidden,
- showCredits, setShowCredits, showSpecialThanks, setShowSpecialThanks, focusSection,
- onNavigateToMenu, updateMessage, clearUpdateMessage
- } = useUI();
-
- const config = useConfig();
- const audio = useAudio();
- const game = useGame();
- const { skinUrl, setSkinUrl } = useSkin();
-
- const [showSetup, setShowSetup] = useState(true);
- const [displayIsDay, setDisplayIsDay] = useState(config.isDayTime);
-
- useEffect(() => {
- setDisplayIsDay(config.isDayTime);
- }, [config.isDayTime]);
-
- const selectedEdition = game.editions.find((e: any) => e.id === config.profile);
- const selectedVersionName = selectedEdition?.name || "";
-
- const titleImage = selectedEdition?.titleImage || "/images/MenuTitle.png";
-
- useEffect(() => {
- if (config.isLoaded) {
- // Check localStorage directly to ensure accurate setup state
- const setupCompleted = localStorage.getItem('lce-setup-completed') === 'true';
- setShowSetup(!setupCompleted);
- }
- }, [config.isLoaded]);
-
- useEffect(() => {
- appWindow.show();
- // Only start intro timing if setup is not shown
- if (!showSetup) {
- setTimeout(() => setShowIntro(false), 2400);
- setTimeout(() => setLogoAnimDone(true), 3400);
- } else if (showSetup) {
- // Skip intro entirely if setup is shown
- setShowIntro(false);
- setLogoAnimDone(true);
- }
- }, [showSetup]);
-
- useEffect(() => {
- const handleContextMenu = (e: MouseEvent) => e.preventDefault();
- document.addEventListener("contextmenu", handleContextMenu);
- return () => document.removeEventListener("contextmenu", handleContextMenu);
- }, []);
-
- const uiFade = {
- initial: { opacity: 0 },
- animate: { opacity: 1 },
- exit: { opacity: 0 },
- transition: { duration: config.animationsEnabled ? 0.5 : 0 }
- };
-
- const backgroundFade = {
- initial: { opacity: 0 },
- animate: { opacity: 1 },
- exit: { opacity: 0 },
- transition: { duration: config.animationsEnabled ? 0.8 : 0 }
- };
-
- return (
-
-
-
-
-
- {config.vfxEnabled &&
}
-
-
- {showCredits && (
- setShowCredits(false)}
- playClickSound={audio.playClickSound}
- playSfx={audio.playSfx}
- />
- )}
- {showSpecialThanks && (
- setShowSpecialThanks(false)}
- playClickSound={audio.playClickSound}
- playSfx={audio.playSfx}
- />
- )}
-
-
-
-
-
-
-
game.setError(null)}
- />
-
- TauriService.openUrl("https://emerald-legacy-launcher.github.io/")}
- title="Update Available!"
- variant="update"
- />
-
-
- {showSetup ? (
- {
- setShowSetup(false);
- setShowIntro(true);
- }}
- />
- ) : showIntro ? (
-
-
-
- ) : (
-
-
- {logoAnimDone && }
-
-
-
- {logoAnimDone && (
- <>
- {!config.legacyMode && (
-
- {
- audio.playClickSound();
- setIsUiHidden(!isUiHidden);
- }}
- className="hover:scale-110 active:scale-95 transition-transform outline-none bg-transparent border-none"
- >
-
-
-
- )}
-
- {!config.legacyMode && (
-
-
- {displayIsDay ? "Day" : "Night"}
-
- {
- audio.playClickSound();
- config.setIsDayTime(!config.isDayTime);
- }}
- className="hover:scale-110 active:scale-95 transition-transform outline-none bg-transparent border-none"
- >
-
-
-
- )}
- >
- )}
-
-
-
-
-
-
- {logoAnimDone && (
- <>
-
-
- {audio.splashIndex === -1
- ? `Welcome ${config.username}!`
- : audio.splashes[audio.splashIndex]}
-
-
- {activeView === "main" && titleImage === "/images/MenuTitle.png" && (
-
- {selectedVersionName}
-
- )}
- >
- )}
-
-
-
-
-
-
-
- {activeView === "main" && (
-
- )}
-
-
-
-
- {activeView === "main" && (
-
- )}
- {activeView === "settings" && (
-
- )}
- {activeView === "versions" && (
-
- )}
- {activeView === "workshop" && (
-
- )}
- {activeView === "themes" && (
-
- )}
- {activeView === "skins" && (
-
- )}
-
-
-
-
-
-
- {logoAnimDone && (
-
- Version: 1.0.0
-
- Not affiliated with Mojang AB or Microsoft. "Minecraft" is a trademark of Mojang Synergies AB.
-
-
- {useUI().connected && "CONTROLLER CONNECTED"}
-
-
- )}
-
-
- )}
-
-
-
- );
-}
diff --git a/src/services/RpcService.ts b/src/services/RpcService.ts
deleted file mode 100644
index 7ae7c2c..0000000
--- a/src/services/RpcService.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { setActivity, start } from "tauri-plugin-drpc";
-import { Activity, ActivityType, Assets, Timestamps, Button } from "tauri-plugin-drpc/activity";
-
-class RPC {
- private startTime: number = Date.now();
- private initializationPromise: Promise | null = null;
- private initialized: boolean = false;
-
- public async StartRPC() {
- if (this.initialized) return;
- if (sessionStorage.getItem('lce_rpc_started') === 'true') {
- this.initialized = true;
- return;
- }
-
- if (this.initializationPromise) return this.initializationPromise;
- this.initializationPromise = (async () => {
- try {
- await start("1482504445152460871");
- sessionStorage.setItem('lce_rpc_started', 'true');
- this.initialized = true;
- } catch (e) {
- console.error("Failed to start RPC:", e);
- this.initializationPromise = null;
- }
- })();
-
- return this.initializationPromise;
- }
-
- public async updateActivity(details: string, state: string, isPlaying: boolean = false) {
- if (!this.initialized) {
- await this.StartRPC();
- if (!this.initialized) return;
- }
-
- const activity = new Activity();
- activity.setDetails(details);
- activity.setState(state);
- activity.setActivity(ActivityType.Playing);
-
- const assets = new Assets();
- assets.setLargeImage("logo");
- assets.setLargeText("Emerald Legacy");
- assets.setSmallImage("app-icon");
- assets.setSmallText(isPlaying ? "Playing" : "In Menus");
- activity.setAssets(assets);
-
- activity.setTimestamps(new Timestamps(this.startTime));
-
- activity.setButton([
- new Button("Discord", "https://discord.gg/RHGRUwpmVc"),
- new Button("GitHub", "https://github.com/Emerald-Legacy-Launcher/Emerald-Legacy-Launcher")
- ]);
-
- try {
- await setActivity(activity);
- } catch (e) {
- console.error("Failed to set RPC activity:", e);
- }
- }
-}
-
-export default new RPC();
\ No newline at end of file
diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts
deleted file mode 100644
index fa8dd28..0000000
--- a/src/services/TauriService.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { invoke } from '@tauri-apps/api/core';
-import { listen } from '@tauri-apps/api/event';
-
-export interface McServer {
- name: string;
- ip: string;
- port: number;
-}
-
-export interface SkinLibraryItem {
- id: string;
- name: string;
- skinBase64: string;
-}
-
-export interface CustomEdition {
- id: string;
- name: string;
- desc: string;
- url: string;
-}
-
-export interface AppConfig {
- username: string;
- linuxRunner?: string;
- skinBase64?: string;
- skinLibrary?: SkinLibraryItem[];
- themeStyleId?: string;
- themePaletteId?: string;
- appleSiliconPerformanceBoost?: boolean;
- customEditions?: CustomEdition[];
- profile?: string;
- keepLauncherOpen?: boolean;
- enableTrayIcon?: boolean;
- animationsEnabled?: boolean;
- vfxEnabled?: boolean;
- rpcEnabled?: boolean;
- musicVol?: number;
- sfxVol?: number;
- legacyMode?: boolean;
-}
-
-export interface ThemePalette {
- id: string;
- name: string;
- colors: any;
-}
-
-export interface Runner {
- id: string;
- name: string;
- path: string;
- type: 'wine' | 'proton';
-}
-
-export interface MacOSSetupProgress {
- stage: string;
- message: string;
- percent?: number;
-}
-
-export class TauriService {
- static async saveConfig(config: AppConfig): Promise {
- return invoke('save_config', { config });
- }
-
- static async loadConfig(): Promise {
- return invoke('load_config');
- }
-
- static async getExternalPalettes(): Promise {
- return invoke('get_external_palettes');
- }
-
- static async importTheme(): Promise {
- return invoke('import_theme');
- }
-
- static async getAvailableRunners(): Promise {
- return invoke('get_available_runners');
- }
-
- static async downloadRunner(name: string, url: string): Promise {
- return invoke('download_runner', { name, url });
- }
-
- static async checkGameInstalled(instanceId: string): Promise {
- return invoke('check_game_installed', { instanceId });
- }
-
- static async openInstanceFolder(instanceId: string): Promise {
- return invoke('open_instance_folder', { instanceId });
- }
-
- static async deleteInstance(instanceId: string): Promise {
- return invoke('delete_instance', { instanceId });
- }
-
- static async cancelDownload(): Promise {
- return invoke('cancel_download');
- }
-
- static async setupMacosRuntime(): Promise {
- return invoke('setup_macos_runtime');
- }
-
- static async downloadAndInstall(url: string, instanceId: string): Promise {
- return invoke('download_and_install', { url, instanceId });
- }
-
- static async launchGame(instanceId: string, servers: McServer[]): Promise {
- return invoke('launch_game', { instanceId, servers });
- }
-
- static async stopGame(instanceId: string): Promise {
- return invoke('stop_game', { instanceId });
- }
-
- static async syncDlc(instanceId: string): Promise {
- return invoke('sync_dlc', { instanceId });
- }
-
- static async updateTrayIcon(visible: boolean): Promise {
- return invoke('update_tray_icon', { visible });
- }
-
- static onDownloadProgress(callback: (percent: number) => void) {
- return listen('download-progress', (event) => callback(event.payload));
- }
-
- static onRunnerDownloadProgress(callback: (percent: number) => void) {
- return listen('runner-download-progress', (event) => callback(event.payload));
- }
-
- static onMacosProgress(callback: (payload: MacOSSetupProgress) => void) {
- return listen('macos-setup-progress', (event) => callback(event.payload));
- }
-
- static async openUrl(url: string): Promise {
- return invoke('plugin:opener|open_url', { url });
- }
-
- static async restartLauncher(): Promise {
- return invoke('restart_launcher');
- }
-
- static async checkMacOSRuntimeInstalled(): Promise {
- return invoke('check_macos_runtime_installed');
- }
-
- static async checkMacOSRuntimeInstalledFast(): Promise {
- return invoke('check_macos_runtime_installed_fast');
- }
-
- static async setupMacOSRuntimeOptimized(): Promise {
- return invoke('setup_macos_runtime_optimized');
- }
-
- static async fetchSkin(username: string): Promise<[string, string]> {
- return invoke('fetch_skin', { username });
- }
-
- static async pathExists(_path: string): Promise {
- // Simple web implementation using fetch to check if path exists
- try {
- // This is a simplified check - in a real implementation you'd use the Tauri API
- // For now, we'll just check common paths via heuristics
- return false; // Placeholder - will be implemented properly if needed
- } catch {
- return false;
- }
- }
-}
diff --git a/src/services/audio.ts b/src/services/audio.ts
new file mode 100644
index 0000000..4d93c78
--- /dev/null
+++ b/src/services/audio.ts
@@ -0,0 +1,45 @@
+let audioCtx: AudioContext | null = null;
+let sfxGain: GainNode | null = null;
+const buffers: Record = {};
+
+export const ensureAudio = async () => {
+ let currentCtx = audioCtx;
+ if (!currentCtx) {
+ const AC = (window as any).AudioContext || (window as any).webkitAudioContext;
+ if (!AC) return null;
+ const newCtx = new AC() as AudioContext;
+ const newGain = newCtx.createGain();
+ newGain.connect(newCtx.destination);
+ audioCtx = newCtx;
+ sfxGain = newGain;
+ currentCtx = newCtx;
+
+ const sounds = ['click.wav', 'orb.ogg', 'levelup.ogg', 'back.ogg', 'pop.wav', 'wood click.wav'];
+ sounds.forEach((s) => {
+ fetch(`/sounds/${s}`)
+ .then((r) => r.arrayBuffer())
+ .then((b) => newCtx.decodeAudioData(b))
+ .then((buf) => {
+ if (buf) buffers[s] = buf;
+ });
+ });
+ }
+ if (currentCtx.state === 'suspended') await currentCtx.resume();
+ return { audioCtx, sfxGain, buffers };
+};
+
+export const playSfx = async (n: string, sfxVol: number, isMuted: boolean, multiplier: number = 1.0) => {
+ const audioData = await ensureAudio();
+ if (!audioData) return;
+ const { audioCtx, sfxGain, buffers } = audioData;
+
+ if (!audioCtx || !sfxGain || !buffers[n] || isMuted) return;
+
+ const s = audioCtx.createBufferSource();
+ s.buffer = buffers[n];
+ const g = audioCtx.createGain();
+ g.gain.value = sfxVol * multiplier;
+ s.connect(g);
+ g.connect(audioCtx.destination);
+ s.start(0);
+};
diff --git a/src/services/tauri.ts b/src/services/tauri.ts
new file mode 100644
index 0000000..f40c149
--- /dev/null
+++ b/src/services/tauri.ts
@@ -0,0 +1,13 @@
+import { invoke } from "@tauri-apps/api/core";
+import { AppConfig, Runner } from "../types";
+
+export const TauriService = {
+ loadConfig: () => invoke("load_config"),
+ saveConfig: (config: AppConfig) => invoke("save_config", { config }),
+ launchGame: (instanceId: string) => invoke("launch_game", { instanceId }),
+ downloadAndInstall: (url: string, instanceId: string) => invoke("download_and_install", { url, instanceId }),
+ checkGameInstalled: (instanceId: string) => invoke("check_game_installed", { instanceId }),
+ getAvailableRunners: () => invoke("get_available_runners"),
+ openInstanceFolder: (instanceId: string) => invoke("open_instance_folder", { instanceId }),
+ cancelDownload: () => invoke("cancel_download"),
+};
diff --git a/src/services/unused.ts b/src/services/unused.ts
deleted file mode 100644
index ada22b4..0000000
--- a/src/services/unused.ts
+++ /dev/null
@@ -1 +0,0 @@
-// empty :P
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..5500c77
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,27 @@
+export interface Runner {
+ id: string;
+ name: string;
+ path: string;
+ type: string;
+}
+
+export interface AppConfig {
+ username: string;
+ linuxRunner?: string;
+}
+
+export interface InstalledStatus {
+ vanilla_tu19: boolean;
+ vanilla_tu24: boolean;
+ [key: string]: boolean;
+}
+
+export interface ReinstallModalData {
+ id: string;
+ url: string;
+}
+
+export interface McNotification {
+ t: string;
+ m: string;
+}
diff --git a/src/types/unused.ts b/src/types/unused.ts
deleted file mode 100644
index ada22b4..0000000
--- a/src/types/unused.ts
+++ /dev/null
@@ -1 +0,0 @@
-// empty :P
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 7d0ff9e..11f02fe 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1 @@
-///
+///
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..c1d2abe
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,10 @@
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+ }
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 9bdaa77..f50b75c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,25 +1,23 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
-}
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 165a9ba..42872c5 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -1,10 +1,10 @@
-{
- "compilerOptions": {
- "composite": true,
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true
- },
- "include": ["vite.config.ts"]
-}
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
index 5bfc4ed..4fc4018 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,17 +1,18 @@
-import { defineConfig } from 'vite'
-import tailwindcss from '@tailwindcss/vite'
-import react from '@vitejs/plugin-react'
-
-export default defineConfig({
- plugins: [
- react(),
- tailwindcss(),
- ],
- server: {
- port: 1420,
- strictPort: true,
- watch: {
- ignored: ["**/src-tauri/**"],
- },
- },
-})
\ No newline at end of file
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ clearScreen: false,
+ optimizeDeps: {
+ entries: ['index.html'],
+ exclude: ['bin', 'game', 'src-tauri']
+ },
+ server: {
+ port: 1420,
+ strictPort: true,
+ watch: {
+ ignored: ["**/src-tauri/**", "**/bin/**", "**/game/**", "**/*.app/**"],
+ }
+ },
+});
\ No newline at end of file