mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-08-01 06:42:28 +00:00
685 lines
24 KiB
TypeScript
685 lines
24 KiB
TypeScript
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 LceLiveView from "../components/views/LceLiveView";
|
|
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 {
|
|
useUI,
|
|
useConfig,
|
|
useAudio,
|
|
useGame,
|
|
useSkin,
|
|
} from "../context/LauncherContext";
|
|
import { TauriService } from "../services/TauriService";
|
|
import { useLceLiveNotifications } from "../hooks/useLceLiveNotifications";
|
|
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 = useLceLiveNotifications();
|
|
const {
|
|
friendRequestMessage,
|
|
gameInviteMessage,
|
|
clearFriendRequestMessage,
|
|
clearGameInviteMessage,
|
|
} = notifications;
|
|
const [showSetup, setShowSetup] = useState(false);
|
|
const [isSetupChecked, setIsSetupChecked] = useState(false);
|
|
const pendingDeepLinks = useRef<string[]>([]);
|
|
const appReadyRef = useRef(false);
|
|
const [workshopTarget, setWorkshopTarget] = useState<{
|
|
id: string;
|
|
type?: string;
|
|
} | null>(null);
|
|
const [addFriendTarget, setAddFriendTarget] = useState<string | null>(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<string, unknown>,
|
|
{
|
|
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 === "lcelive" &&
|
|
parts.length >= 2 &&
|
|
parts[1] === "addfriend"
|
|
) {
|
|
const username = parsed.searchParams.get("username");
|
|
if (username) {
|
|
setActiveView("lcelive");
|
|
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<string[]>("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 } = usePlatform();
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
useEffect(() => {
|
|
const appWindow = getCurrentWindow();
|
|
if (!isMac) appWindow.setDecorations(false);
|
|
const checkFs = async () => setIsFullscreen(await appWindow.isFullscreen());
|
|
checkFs();
|
|
const unlisten = appWindow.onResized(checkFs);
|
|
return () => {
|
|
unlisten.then((fn: () => void) => fn());
|
|
};
|
|
}, [isMac]);
|
|
const showHeader = !isMac || isFullscreen;
|
|
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";
|
|
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 <div className="w-screen h-screen bg-black" />;
|
|
}
|
|
|
|
if (showSetup) {
|
|
return (
|
|
<div
|
|
className={`w-screen h-screen overflow-hidden select-none flex flex-col relative bg-black text-white font-['Mojangles'] outline-none focus:outline-none ${!config.animationsEnabled ? "no-animations" : ""}`}
|
|
>
|
|
<SetupView
|
|
onComplete={() => {
|
|
setShowSetup(false);
|
|
setShowIntro(true);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (showIntro && !config.skipIntro) {
|
|
return (
|
|
<div
|
|
className={`w-screen h-screen overflow-hidden select-none flex flex-col relative bg-black text-white font-['Mojangles'] outline-none focus:outline-none ${!config.animationsEnabled ? "no-animations" : ""}`}
|
|
>
|
|
<CinematicIntro
|
|
onComplete={() => {
|
|
setShowIntro(false);
|
|
}}
|
|
startMusic={audio.startMusic}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<MotionConfig transition={config.animationsEnabled ? {} : { duration: 0 }}>
|
|
<div
|
|
className={`w-screen h-screen overflow-hidden select-none flex flex-col relative bg-black text-white font-['Mojangles'] outline-none focus:outline-none ${!config.animationsEnabled ? "no-animations" : ""}`}
|
|
>
|
|
<div className="absolute inset-0">
|
|
<AnimatePresence>
|
|
<motion.div
|
|
key={displayIsDay ? "day" : "night"}
|
|
className="absolute inset-0"
|
|
{...backgroundFade}
|
|
>
|
|
<PanoramaBackground
|
|
profile={selectedEdition?.panorama ?? "vanilla_tu19"}
|
|
isDay={displayIsDay}
|
|
/>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
{config.vfxEnabled && <ClickParticles />}
|
|
{showHeader && (
|
|
<AppHeader playPressSound={audio.playPressSound} uiFade={uiFade} />
|
|
)}
|
|
|
|
<DownloadOverlay
|
|
downloadProgress={game.downloadProgress}
|
|
downloadingIds={game.downloadingIds}
|
|
editions={game.editions}
|
|
/>
|
|
|
|
<AchievementToast message={game.error} onClose={clearError} />
|
|
|
|
<AchievementToast
|
|
message={updateMessage}
|
|
onClose={clearUpdateMessage}
|
|
onClick={() =>
|
|
TauriService.openUrl(
|
|
updateUrl ||
|
|
"https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest",
|
|
)
|
|
}
|
|
title="Update Available!"
|
|
variant="update"
|
|
/>
|
|
|
|
<AchievementToast
|
|
message={game.gameUpdateMessage}
|
|
onClose={clearGameUpdate}
|
|
onClick={() => {
|
|
clearGameUpdate();
|
|
setActiveView("versions");
|
|
}}
|
|
title="Game Update Available!"
|
|
variant="update"
|
|
/>
|
|
|
|
<AchievementToast
|
|
message={game.steamSuccessMessage}
|
|
onClose={clearSteamSuccess}
|
|
title="Steam Integration"
|
|
variant="steam"
|
|
/>
|
|
|
|
{pluginToast && (
|
|
<AchievementToast
|
|
message={pluginToast.message}
|
|
onClose={() => setPluginToast(null)}
|
|
title={pluginToast.options?.title}
|
|
variant={pluginToast.options?.variant}
|
|
/>
|
|
)}
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className={`flex flex-col h-full z-10 w-full relative ${showHeader ? "pt-12" : ""}`}
|
|
>
|
|
{!config.legacyMode && (
|
|
<motion.div {...uiFade} className="absolute top-10 left-8 z-50">
|
|
<button
|
|
onClick={() => {
|
|
audio.playPressSound();
|
|
setIsUiHidden(!isUiHidden);
|
|
}}
|
|
className="outline-none bg-transparent border-none"
|
|
>
|
|
<img
|
|
src={
|
|
isUiHidden
|
|
? "/images/Unhide_UI_Button.png"
|
|
: "/images/Hide_UI_Button.png"
|
|
}
|
|
className="w-10 h-10 cursor-pointer object-contain"
|
|
style={{ imageRendering: "pixelated" }}
|
|
/>
|
|
</button>
|
|
</motion.div>
|
|
)}
|
|
|
|
{!config.legacyMode && (
|
|
<motion.div
|
|
{...uiFade}
|
|
className="absolute bottom-6 right-8 z-50 flex items-center gap-3"
|
|
>
|
|
<span className="text-[#E0E0E0] text-[10px] mc-text-shadow tracking-widest uppercase opacity-70 mt-1">
|
|
{displayIsDay ? "Day" : "Night"}
|
|
</span>
|
|
<button
|
|
onClick={() => {
|
|
audio.playPressSound();
|
|
config.setIsDayTime(!config.isDayTime);
|
|
}}
|
|
className="outline-none bg-transparent border-none"
|
|
>
|
|
<img
|
|
src={
|
|
displayIsDay
|
|
? "/images/Day_Toggle.png"
|
|
: "/images/Night_Toggle.png"
|
|
}
|
|
alt="Toggle Time"
|
|
className="w-12 h-12 cursor-pointer block object-contain"
|
|
style={{ imageRendering: "pixelated" }}
|
|
/>
|
|
</button>
|
|
</motion.div>
|
|
)}
|
|
|
|
{isUiHidden && !displayIsDay && activeView === "devtools" && (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
className="absolute inset-0 z-[100] flex items-center justify-center pointer-events-none"
|
|
>
|
|
<button
|
|
onClick={() => {
|
|
audio.playPressSound();
|
|
setIsUiHidden(false);
|
|
setActiveView("swf-editor");
|
|
}}
|
|
className="pointer-events-auto outline-none bg-transparent border-none flex flex-col items-center gap-2 group"
|
|
>
|
|
<img
|
|
src="/images/tools/pck.png"
|
|
className="w-16 h-16 cursor-pointer object-contain opacity-50 drop-shadow-[0_4px_4px_rgba(0,0,0,1)] grayscale"
|
|
style={{ imageRendering: "pixelated" }}
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).src =
|
|
"/images/Button_Background.png";
|
|
}}
|
|
/>
|
|
<span className="text-[#FFFF55] text-sm mc-text-shadow">
|
|
SWF Editor
|
|
</span>
|
|
</button>
|
|
</motion.div>
|
|
)}
|
|
|
|
<div className="shrink-0 flex justify-center py-4 relative w-full pt-4">
|
|
<div className="relative w-full max-w-135 flex justify-center">
|
|
{activeView !== "credits" && (
|
|
<motion.img
|
|
layoutId="mainLogo"
|
|
src={titleImage}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 25,
|
|
}}
|
|
className="w-full drop-shadow-[0_8px_6px_rgba(0,0,0,0.8)] pointer-events-none"
|
|
style={{ imageRendering: "pixelated" }}
|
|
/>
|
|
)}
|
|
{activeView !== "credits" && (
|
|
<motion.div
|
|
{...uiFade}
|
|
className="absolute bottom-[20%] right-[5%] w-0 h-0 flex items-center justify-center"
|
|
>
|
|
<div
|
|
onClick={audio.cycleSplash}
|
|
className="mc-splash text-[#FFFF55] text-[28px] z-100 cursor-pointer whitespace-nowrap"
|
|
style={{ textShadow: "2px 2px 0px #3F3F00" }}
|
|
>
|
|
{audio.splashIndex === -1
|
|
? `Welcome ${config.username}!`
|
|
: audio.splashes[audio.splashIndex]}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
{activeView === "main" &&
|
|
hasAnyInstall &&
|
|
titleImage === "/images/MenuTitle.png" && (
|
|
<motion.div
|
|
{...uiFade}
|
|
className="absolute -bottom-6 text-[#A0A0A0] text-sm mc-text-shadow tracking-widest uppercase opacity-80 font-['Mojangles']"
|
|
>
|
|
{selectedVersionName}
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<main className="flex-1 w-full relative">
|
|
<div
|
|
className={`w-full h-full flex flex-col items-center justify-center ${isUiHidden ? "opacity-0 pointer-events-none" : "opacity-100"}`}
|
|
>
|
|
<AnimatePresence mode="wait">
|
|
{activeView === "main" && (
|
|
<SkinViewer
|
|
key="skin-viewer"
|
|
username={config.username}
|
|
setUsername={config.setUsername}
|
|
playPressSound={audio.playPressSound}
|
|
skinUrl={skinUrl}
|
|
capeUrl={config.legacyMode ? null : capeUrl}
|
|
setSkinUrl={setSkinUrl}
|
|
setActiveView={setActiveView}
|
|
setIsUiHidden={setIsUiHidden}
|
|
isFocusedSection={focusSection === "skin"}
|
|
onNavigateRight={onNavigateToMenu}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<div className="w-full h-full max-w-7xl relative flex justify-center items-center overflow-hidden">
|
|
<AnimatePresence mode="wait">
|
|
{activeView === "main" && <HomeView key="main-view" />}
|
|
{activeView === "settings" && (
|
|
<SettingsView key="settings-view" />
|
|
)}
|
|
{activeView === "versions" && (
|
|
<VersionsView key="versions-view" />
|
|
)}
|
|
{activeView === "workshop" && (
|
|
<WorkshopView
|
|
key="workshop-view"
|
|
workshopTarget={workshopTarget}
|
|
onClearWorkshopTarget={() => setWorkshopTarget(null)}
|
|
/>
|
|
)}
|
|
{activeView === "devtools" && (
|
|
<DevtoolsView key="devtools-view" />
|
|
)}
|
|
{activeView === "guides" && (
|
|
<GuidesView key="guides-view" />
|
|
)}
|
|
{activeView === "pck-editor" && (
|
|
<PckEditorView key="pck-editor-view" />
|
|
)}
|
|
{activeView === "arc-editor" && (
|
|
<ArcEditorView key="arc-editor-view" />
|
|
)}
|
|
{activeView === "loc-editor" && (
|
|
<LocEditorView key="loc-editor-view" />
|
|
)}
|
|
{activeView === "grf-editor" && (
|
|
<GrfEditorView key="grf-editor-view" />
|
|
)}
|
|
{activeView === "col-editor" && (
|
|
<ColEditorView key="col-editor-view" />
|
|
)}
|
|
{activeView === "options-editor" && (
|
|
<OptionsEditorView key="options-editor-view" />
|
|
)}
|
|
{activeView === "model-editor" && (
|
|
<ModelEditorView key="model-editor-view" />
|
|
)}
|
|
{activeView === "swf-editor" && (
|
|
<SwfView key="swf-editor-view" />
|
|
)}
|
|
{activeView === "lcelive" && (
|
|
<LceLiveView
|
|
key="lcelive-view"
|
|
addFriendTarget={addFriendTarget}
|
|
onClearAddFriendTarget={() => setAddFriendTarget(null)}
|
|
/>
|
|
)}
|
|
{activeView === "skins" && <SkinsView key="skins-view" />}
|
|
{activeView === "screenshots" && (
|
|
<ScreenshotsView key="screenshots-view" />
|
|
)}
|
|
{activeView === "credits" && (
|
|
<CreditsView key="credits-view" />
|
|
)}
|
|
{pluginViews.map((pv) => {
|
|
if (activeView === pv.id) {
|
|
return <PluginViewContainer key={pv.id} registry={pv} />;
|
|
}
|
|
return null;
|
|
})}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<motion.footer
|
|
{...uiFade}
|
|
className="shrink-0 p-4 flex justify-between items-end text-[10px] text-[#A0A0A0] mc-text-shadow bg-gradient-to-t from-black/80 to-transparent uppercase tracking-widest opacity-60 font-['Mojangles']"
|
|
style={{ fontWeight: "normal" }}
|
|
>
|
|
<div className="flex-1 text-left whitespace-nowrap">
|
|
Version: {pkg.version} ({__BUILD_DATE__})
|
|
</div>
|
|
<div className="flex-[2] text-center whitespace-nowrap">
|
|
Not affiliated with Mojang AB or Microsoft. "Minecraft" is a
|
|
trademark of Mojang Synergies AB.
|
|
</div>
|
|
<div className="flex-1 text-right whitespace-nowrap">
|
|
{connected && "CONTROLLER CONNECTED"}
|
|
</div>
|
|
</motion.footer>
|
|
</motion.div>
|
|
|
|
<AchievementToast
|
|
message={friendRequestMessage}
|
|
onClose={clearFriendRequestMessage}
|
|
onClick={() => {
|
|
clearFriendRequestMessage();
|
|
setActiveView("lcelive");
|
|
}}
|
|
title="Friend Request"
|
|
variant="update"
|
|
/>
|
|
|
|
<AchievementToast
|
|
message={gameInviteMessage}
|
|
onClose={clearGameInviteMessage}
|
|
onClick={() => {
|
|
clearGameInviteMessage();
|
|
setActiveView("lcelive");
|
|
}}
|
|
title="Game Invite"
|
|
variant="update"
|
|
/>
|
|
</div>
|
|
</MotionConfig>
|
|
);
|
|
}
|