import { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { motion, AnimatePresence, MotionConfig } from "framer-motion"; import "../css/App.css"; import HomeView from "../components/views/HomeView"; import SettingsView from "../components/views/SettingsView"; import VersionsView from "../components/views/VersionsView"; import DevtoolsView from "../components/views/DevtoolsView"; import GuidesView from "../components/views/GuidesView"; import SkinsView from "../components/views/SkinsView"; import WorkshopView from "../components/views/WorkshopView"; import SetupView from "../components/views/SetupView"; import PckEditorView from "../components/views/PckEditorView"; import { ArcEditorView } from "../components/views/ArcEditorView"; import LocEditorView from "../components/views/LocEditorView"; import GrfEditorView from "../components/views/GrfEditorView"; import ColEditorView from "../components/views/ColEditorView"; import OptionsEditorView from "../components/views/OptionsEditorView"; import ModelEditorView from "../components/views/ModelEditorView"; import ScreenshotsView from "../components/views/ScreenshotsView"; import SwfView from "../components/views/SwfView"; import LceOnlineView from "../components/views/LceOnlineView"; import CreditsView from "../components/views/CreditsView"; import SkinViewer from "../components/common/SkinViewer"; import PanoramaBackground from "../components/common/PanoramaBackground"; import { ClickParticles } from "../components/common/ClickParticles"; import { CinematicIntro } from "../components/common/CinematicIntro"; import { DownloadOverlay } from "../components/layout/DownloadOverlay"; import { AppHeader } from "../components/layout/AppHeader"; import { AchievementToast } from "../components/common/AchievementToast"; import GameLogModal from "../components/modals/GameLogModal"; import { useUI, useConfig, useAudio, useGame, useSkin, } from "../context/LauncherContext"; import { TauriService } from "../services/TauriService"; import { lceOnlineService } from "../services/LceOnlineService"; import { useLceOnlineNotifications } from "../hooks/useLceOnlineNotifications"; import { usePluginViews } from "../plugins/PluginContext"; import { usePlatform } from "../hooks/usePlatform"; import { PluginManager } from "../plugins/PluginManager"; import { PluginViewContainer } from "../components/plugins/PluginViewContainer"; import type { Edition } from "../types/edition"; import type { ToastOptions } from "../plugins/types"; import pkg from "../../package.json"; import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { listen } from "@tauri-apps/api/event"; export default function App() { const ui = useUI(); const { showIntro, setShowIntro, activeView, setActiveView, isUiHidden, setIsUiHidden, focusSection, onNavigateToMenu, updateMessage, updateUrl, clearUpdateMessage, connected, } = ui; const config = useConfig(); const audio = useAudio(); const game = useGame(); const skin = useSkin(); const { skinUrl, setSkinUrl, capeUrl } = skin; const notifications = useLceOnlineNotifications(); const { friendRequestMessage, InviteMessage, clearFriendRequestMessage, clearInviteMessage, invites, } = notifications; const [showSetup, setShowSetup] = useState(false); const [isSetupChecked, setIsSetupChecked] = useState(false); const pendingDeepLinks = useRef([]); const appReadyRef = useRef(false); const [workshopTarget, setWorkshopTarget] = useState<{ id: string; type?: string; } | null>(null); const [addFriendTarget, setAddFriendTarget] = useState(null); const displayIsDay = config.isDayTime; const clearError = useCallback(() => game.setError(null), [game]); const clearGameUpdate = useCallback( () => game.setGameUpdateMessage(null), [game], ); const clearSteamSuccess = useCallback( () => game.setSteamSuccessMessage(null), [game], ); const pluginViews = usePluginViews(); const [pluginToast, setPluginToast] = useState<{ message: string; options?: ToastOptions; } | null>(null); useEffect(() => { const pm = PluginManager.instance; pm.setNavigateCallback((viewId) => { setActiveView(viewId); }); pm.setToastCallback((_pluginId, message, options) => { setPluginToast({ message, options }); }); pm.setSoundCallback((name) => { audio.playSfx(name); }); }, [setActiveView, audio.playSfx]); useEffect(() => { if (!config.isLoaded) return; PluginManager.instance.updateSnapshots( { ...config } as unknown as Record, { isGameRunning: game.isGameRunning, downloadProgress: game.downloadProgress, downloadingIds: game.downloadingIds, }, game.installs, ); }, [ config, game.isGameRunning, game.downloadProgress, game.downloadingIds, game.installs, config.isLoaded, ]); useEffect(() => { if (showIntro && config.skipIntro) { setShowIntro(false); } }, [showIntro, config.skipIntro, setShowIntro]); const processDeepLink = useCallback( (url: string) => { try { const parsed = new URL(url); const path = parsed.hostname + parsed.pathname; const parts = path.replace(/^\/+/, "").split("/").filter(Boolean); if (parts.length === 0) return; const action = parts[0]; if (action === "launch") { if (parts.length >= 2) { let instanceId = decodeURIComponent(parts[1]); if (instanceId === "neolegacy") instanceId = "legacy_evolved"; //neo: piebot said so TauriService.launchGame(instanceId, []).catch(console.error); } else { setActiveView("main"); game.handleLaunch().catch(console.error); } return; } if (action === "lceonline" && parts.length >= 2) { if (parts[1] === "auth") { const token = parsed.searchParams.get("token"); if (token) { lceOnlineService.loginWithTokenAndFetchAccount(token); setActiveView("lceonline"); return; } } if (parts[1] === "addfriend") { const username = parsed.searchParams.get("username"); if (username) { setActiveView("lceonline"); setAddFriendTarget(username); return; } } } if (action === "workshop" && parts.length >= 2) { const workshopId = decodeURIComponent(parts[1]); const knownTypes = ["normal", "bytebukkit", "plugin", "version"]; let workshopType: string | undefined; if (parts.length >= 3) { workshopType = parts[2]; } else if (parsed.searchParams.get("type")) { workshopType = parsed.searchParams.get("type")!; } else { workshopType = knownTypes.find((t) => parsed.searchParams.has(t)); } setActiveView("workshop"); setWorkshopTarget({ id: workshopId, type: workshopType }); return; } setActiveView(action); //neo: yeah no im not checking if its valid or not. } catch (e) { console.error("failed to parse deep link:", e); } }, [setActiveView, game.handleLaunch, setWorkshopTarget], ); const appReady = config.isLoaded && isSetupChecked && !showSetup && !showIntro; useEffect(() => { if (appReady) { appReadyRef.current = true; if (pendingDeepLinks.current.length > 0) { for (const url of pendingDeepLinks.current) { processDeepLink(url); } pendingDeepLinks.current = []; } } }, [appReady, processDeepLink]); const queueDeepLink = useCallback( (url: string) => { if (appReadyRef.current) { processDeepLink(url); } else { pendingDeepLinks.current.push(url); } }, [processDeepLink], ); useEffect(() => { getCurrent() .then((urls) => { if (urls && urls.length > 0) { queueDeepLink(urls[0]); } }) .catch(() => {}); let unlistenOpenUrl: Function; onOpenUrl((payload) => { for (const url of payload) { queueDeepLink(url); } }).then((unlistenFn) => { unlistenOpenUrl = unlistenFn; }); let unlistenEvent: Function; listen("deep-link", (event) => { for (const url of event.payload) { queueDeepLink(url); } }).then((unlistenFn) => { unlistenEvent = unlistenFn; }); return () => { if (unlistenOpenUrl) unlistenOpenUrl(); if (unlistenEvent) unlistenEvent(); }; }, [queueDeepLink]); const { isMac, isAndroid } = usePlatform(); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const appWindow = getCurrentWindow(); if (!isMac && !isAndroid) appWindow.setDecorations(false); const checkFs = async () => setIsFullscreen(await appWindow.isFullscreen()); checkFs(); const unlisten = appWindow.onResized(checkFs); return () => { unlisten.then((fn: () => void) => fn()); }; }, [isMac, isAndroid]); const showHeader = (!isMac || isFullscreen) && !isAndroid; useEffect(() => { if (config.isLoaded) { const setupCompleted = localStorage.getItem("lce-setup-completed") === "true"; setShowSetup(!setupCompleted); setIsSetupChecked(true); } }, [config.isLoaded]); const selectedEdition = useMemo( () => game.editions.find((e: Edition) => e.instanceId === config.profile), [game.editions, config.profile], ); const selectedVersionName = selectedEdition?.name ?? ""; const hasAnyInstall = game.installs.length > 0; const titleImage = selectedEdition?.titleImage ?? "/images/MenuTitle.png"; const TITLE_HIDDEN_VIEWS = new Set([ //neo: why an entire Set for that? yes. the answer is yes. "workshop", "lceonline", "devtools", "guides", "pck-editor", "arc-editor", "loc-editor", "grf-editor", "col-editor", "options-editor", "model-editor", "swf-editor", ]); useEffect(() => { const handleContextMenu = (e: MouseEvent) => e.preventDefault(); document.addEventListener("contextmenu", handleContextMenu); return () => document.removeEventListener("contextmenu", handleContextMenu); }, []); const animDuration = config.animationsEnabled ? undefined : { duration: 0 }; const uiFade = useMemo( () => ({ initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: animDuration ?? { duration: 0.5 }, }), [animDuration], ); const backgroundFade = useMemo( () => ({ initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: animDuration ?? { duration: 0.8 }, }), [animDuration], ); if (!config.isLoaded || !isSetupChecked) { return
; } if (showSetup) { return (
{showHeader && ( )}
{ setShowSetup(false); setShowIntro(true); }} />
); } if (showIntro && !config.skipIntro) { return (
{ setShowIntro(false); }} startMusic={audio.startMusic} />
); } return (
{config.vfxEnabled && } {showHeader && ( )} TauriService.openUrl( updateUrl || "https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest", ) } title="Update Available!" variant="update" /> { clearGameUpdate(); setActiveView("versions"); }} title="Game Update Available!" variant="update" /> {pluginToast && ( setPluginToast(null)} title={pluginToast.options?.title} variant={pluginToast.options?.variant} /> )} {!config.legacyMode && !isAndroid && ( )} {isAndroid && activeView !== "main" && ( )} {!config.legacyMode && ( {displayIsDay ? "Day" : "Night"} )} {isUiHidden && !displayIsDay && activeView === "devtools" && ( )}
{activeView !== "credits" && !TITLE_HIDDEN_VIEWS.has(activeView) && ( )} {activeView !== "credits" && !TITLE_HIDDEN_VIEWS.has(activeView) && (
{audio.splashIndex === -1 ? `Welcome ${config.username}!` : audio.splashes[audio.splashIndex]}
)} {activeView === "main" && hasAnyInstall && titleImage === "/images/MenuTitle.png" && ( {selectedVersionName} )}
{activeView === "main" && ( )}
{activeView === "main" && } {activeView === "settings" && ( )} {activeView === "versions" && ( )} {activeView === "workshop" && ( setWorkshopTarget(null)} /> )} {activeView === "devtools" && ( )} {activeView === "guides" && } {activeView === "pck-editor" && ( )} {activeView === "arc-editor" && ( )} {activeView === "loc-editor" && ( )} {activeView === "grf-editor" && ( )} {activeView === "col-editor" && ( )} {activeView === "options-editor" && ( )} {activeView === "model-editor" && ( )} {activeView === "swf-editor" && ( )} {activeView === "lceonline" && ( setAddFriendTarget(null)} invites={invites} /> )} {activeView === "skins" && } {activeView === "screenshots" && ( )} {activeView === "credits" && ( )} {pluginViews.map((pv) => { if (activeView === pv.id) { return ; } return null; })}
Version: {pkg.version} ({__BUILD_DATE__})
Not affiliated with Mojang AB or Microsoft. "Minecraft" is a trademark of Mojang Synergies AB.
{connected && "CONTROLLER CONNECTED"}
{ clearFriendRequestMessage(); setActiveView("lceonline"); }} title="Friend Request" variant="update" /> { clearInviteMessage(); setActiveView("lceonline"); }} title="Game Invite" variant="update" />
); }