Merge branch 'feature/big-cool-ui-redesign'

This commit is contained in:
KayJann 2026-06-02 16:01:31 +02:00
commit 8bf7502c44
26 changed files with 739 additions and 641 deletions

BIN
public/images/LCE Team.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

BIN
public/sounds/in.ogg Normal file

Binary file not shown.

Binary file not shown.

BIN
public/sounds/out.ogg Normal file

Binary file not shown.

View file

@ -25,6 +25,7 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
animations_enabled: Some(true),
vfx_enabled: Some(true),
rpc_enabled: Some(true),
start_fullscreen: Some(true),
music_vol: Some(50),
sfx_vol: Some(100),
legacy_mode: Some(false),

View file

@ -97,6 +97,14 @@ pub fn run() {
relay::join_game,
])
.setup(|app| {
let app_handle = app.handle().clone();
let config = config::load_config_raw(app_handle.clone());
if config.start_fullscreen.unwrap_or(true) {
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.set_fullscreen(true);
}
}
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 && !args[1].starts_with('-') {
let instance_id = args[1].clone();

View file

@ -40,6 +40,7 @@ pub struct AppConfig {
pub animations_enabled: Option<bool>,
pub vfx_enabled: Option<bool>,
pub rpc_enabled: Option<bool>,
pub start_fullscreen: Option<bool>,
pub music_vol: Option<u32>,
pub sfx_vol: Option<u32>,
pub legacy_mode: Option<bool>,

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useAudio } from "../../context/LauncherContext";
interface AchievementToastProps {
message: string | null;
@ -16,14 +17,24 @@ export function AchievementToast({
title = "Error!",
variant = "error",
}: AchievementToastProps) {
const { playSfx } = useAudio();
useEffect(() => {
if (message) {
if (variant === "update") {
playSfx("notification.ogg");
} else {
playSfx("in.ogg");
}
const timer = setTimeout(() => {
onClose();
}, 8000);
return () => clearTimeout(timer);
return () => {
clearTimeout(timer);
playSfx("out.ogg");
};
}
}, [message, onClose]);
}, [message, onClose, variant, playSfx]);
const getIcon = () => {
if (variant === "update") {

View file

@ -0,0 +1,93 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
interface CinematicIntroProps {
onComplete: () => void;
startMusic: () => void;
}
type Phase = "black" | "white-lceteam" | "white-esrb" | "out";
const TIMINGS = {
black: 800,
"white-lceteam": 3000,
"white-esrb": 3000,
out: 800,
};
export function CinematicIntro({ onComplete, startMusic }: CinematicIntroProps) {
const [phase, setPhase] = useState<Phase>("black");
useEffect(() => {
startMusic();
const run = async () => {
await new Promise((r) => setTimeout(r, TIMINGS.black));
setPhase("white-lceteam");
await new Promise((r) => setTimeout(r, TIMINGS["white-lceteam"]));
setPhase("white-esrb");
await new Promise((r) => setTimeout(r, TIMINGS["white-esrb"]));
setPhase("out");
await new Promise((r) => setTimeout(r, TIMINGS.out));
onComplete();
};
run();
}, [onComplete, startMusic]);
return (
<div className="absolute inset-0 z-50 bg-black">
<AnimatePresence initial={false}>
{phase === "black" && (
<motion.div
key="black"
className="absolute inset-0 bg-black"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8 }}
/>
)}
{(phase === "white-lceteam" || phase === "white-esrb" || phase === "out") && (
<motion.div
key="white-bg"
className="absolute inset-0 bg-white flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: phase === "out" ? 0 : 1 }}
exit={{ opacity: 0 }}
transition={{ duration: phase === "out" ? 0.8 : 0.6 }}
>
<AnimatePresence mode="wait" initial={false}>
{phase === "white-lceteam" && (
<motion.img
key="lceteam"
src="/images/LCE Team.png"
className="max-w-3xl object-contain"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8 }}
/>
)}
{(phase === "white-esrb" || phase === "out") && (
<motion.img
key="esrb"
src="/images/esrb_warning.png"
className="max-w-3xl object-contain"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8 }}
/>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View file

@ -803,16 +803,7 @@ const LceLiveView = memo(function LceLiveView() {
</div>
)}
<div
className="flex-1 flex flex-col p-8 z-10 relative overflow-hidden rounded-b shadow-[0_0_30px_rgba(0,0,0,0.6)] border-4 border-[#222] border-t-0"
style={{
backgroundImage: "url('/images/background.png')",
backgroundSize: "100% auto",
backgroundRepeat: "no-repeat",
backgroundPosition: "top",
imageRendering: "pixelated",
}}
>
<div className="flex-1 flex flex-col p-8 z-10 relative overflow-hidden mc-options-bg">
{renderContent()}
</div>
<div className="flex justify-center pt-4 pb-2">

View file

@ -28,6 +28,8 @@ const SettingsView = memo(function SettingsView() {
setPerfBoost,
rpcEnabled,
setRpcEnabled,
startFullscreen,
setStartFullscreen,
legacyMode,
setLegacyMode,
mangohudEnabled,
@ -100,6 +102,11 @@ const SettingsView = memo(function SettingsView() {
setRpcEnabled(!rpcEnabled);
};
const handleFullscreenToggle = () => {
playPressSound();
setStartFullscreen(!startFullscreen);
};
const handleLegacyToggle = () => {
playPressSound();
setLegacyMode(!legacyMode);
@ -396,6 +403,12 @@ const SettingsView = memo(function SettingsView() {
},
});
} else if (currentSubMenu === "launcher") {
items.push({
id: "fullscreen",
label: `Start in Fullscreen: ${startFullscreen ? "ON" : "OFF"}`,
type: "button",
onClick: handleFullscreenToggle,
});
items.push({
id: "rpc",
label: `Discord RPC: ${rpcEnabled ? "ON" : "OFF"}`,

View file

@ -299,31 +299,29 @@ const VersionsView = memo(function VersionsView() {
onMouseEnter={() => !isComingSoon && setFocusIndex(i)}
>
<div className="w-6 flex items-center justify-center flex-shrink-0">
{isComingSoon ? (
<img
src="/images/wool_8.png"
alt="Coming Soon"
className="w-4 h-4 object-contain"
style={{ imageRendering: "pixelated" }}
/>
) : isDownloading ? (
{isDownloading ? (
<span className="text-xs text-gray-400 font-bold">
{Math.floor(downloadProgress || 0)}%
</span>
) : isInstalled ? (
<img
src="/images/wool_5.png"
alt="Installed"
className="w-4 h-4 object-contain"
style={{ imageRendering: "pixelated" }}
/>
) : edition.logo ? (
edition.logo.startsWith("http") ||
edition.logo.startsWith("/images") ? (
<img
src={edition.logo}
alt=""
className={`w-6 h-6 object-contain ${isComingSoon ? "opacity-40 grayscale" : ""}`}
style={{ imageRendering: "pixelated" }}
/>
) : (
<ScreenshotImage
path={edition.logo}
alt=""
className={`w-6 h-6 object-contain ${isComingSoon ? "opacity-40 grayscale" : ""}`}
style={{ imageRendering: "pixelated" }}
/>
)
) : (
<img
src="/images/wool_14.png"
alt="Not installed"
className="w-4 h-4 object-contain"
style={{ imageRendering: "pixelated" }}
/>
<div className="w-6 h-6 bg-gray-600/30 rounded-sm" />
)}
</div>
@ -338,23 +336,6 @@ const VersionsView = memo(function VersionsView() {
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
>
<div className="flex items-center gap-2">
{edition.logo &&
(edition.logo.startsWith("http") ||
edition.logo.startsWith("/images") ? (
<img
src={edition.logo}
alt=""
className="w-5 h-5 object-contain flex-shrink-0"
style={{ imageRendering: "pixelated" }}
/>
) : (
<ScreenshotImage
path={edition.logo}
alt=""
className="w-5 h-5 object-contain flex-shrink-0"
style={{ imageRendering: "pixelated" }}
/>
))}
<span
className={`text-xl tracking-wide truncate ${
isSelected ? "text-white" : "text-black"

View file

@ -36,14 +36,9 @@ const SERVERS_URL =
const SERVERS_BASE =
"https://raw.githubusercontent.com/bytebukkit/servers/refs/heads/main";
const CATEGORY_TABS = ["Skin", "Texture", "World", "Mod", "DLC"] as const;
const ALL_TABS = [
...CATEGORY_TABS,
"Versions",
"Installed",
"Server",
"Plugins",
"Search",
] as const;
const UTILITY_TABS = ["Versions", "Installed", "Search"] as const;
const SERVER_TABS = ["Server", "Plugins"] as const;
const ALL_TABS = [...CATEGORY_TABS, ...UTILITY_TABS, ...SERVER_TABS] as const;
type TabType = (typeof ALL_TABS)[number];
interface RegistryPackage {
id: string;
@ -506,8 +501,49 @@ const WorkshopView = memo(function WorkshopView() {
Workshop
</h2>
<div className="flex items-center justify-center gap-2 mb-6 w-full flex-wrap px-4">
{ALL_TABS.map((tab) => {
<div className="flex items-center justify-center gap-2 mb-4 w-full flex-wrap px-4">
{CATEGORY_TABS.map((tab) => {
const isActive = tab === activeTab;
return (
<button
key={tab}
onClick={() => selectTab(tab)}
className={`
relative h-10 px-5 text-sm mc-text-shadow tracking-widest border-none outline-none cursor-pointer transition-all
${isActive ? "text-[#FFFF55] scale-105" : "text-white hover:text-[#FFFF55] hover:scale-105"}
`}
style={{
backgroundImage: isActive
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
{tab.toUpperCase()}
</button>
);
})}
</div>
<div className="flex items-center justify-center gap-2 mb-4 w-full flex-wrap px-4">
{SERVER_TABS.map((tab) => {
const isActive = tab === activeTab;
return (
<button
key={tab}
onClick={() => selectTab(tab)}
className={`
relative h-8 px-4 text-xs mc-text-shadow tracking-widest border outline-none cursor-pointer transition-all
${isActive ? "text-[#FFFF55] border-[#FFFF55] bg-black/40" : "text-[#A0A0A0] border-[#444] hover:text-white hover:border-[#888]"}
`}
>
{tab.toUpperCase()}
</button>
);
})}
<div className="w-px h-6 bg-[#444] mx-2" />
{UTILITY_TABS.map((tab) => {
const isActive = tab === activeTab;
const updateCount =
tab === "Installed"
@ -518,16 +554,9 @@ const WorkshopView = memo(function WorkshopView() {
key={tab}
onClick={() => selectTab(tab)}
className={`
relative h-10 px-6 text-lg mc-text-shadow tracking-widest border-none outline-none cursor-pointer transition-all
${isActive ? "text-[#FFFF55] scale-105" : "text-white hover:text-[#FFFF55] hover:scale-105"}
relative h-8 px-4 text-xs mc-text-shadow tracking-widest border outline-none cursor-pointer transition-all
${isActive ? "text-[#FFFF55] border-[#FFFF55] bg-black/40" : "text-[#A0A0A0] border-[#444] hover:text-white hover:border-[#888]"}
`}
style={{
backgroundImage: isActive
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
{tab.toUpperCase()}
{updateCount > 0 && (
@ -540,16 +569,9 @@ const WorkshopView = memo(function WorkshopView() {
})}
</div>
<div className="w-[98%] flex-1 relative overflow-hidden">
<AnimatePresence mode="wait">
{showSearch ? (
<motion.div
key={isInstalledTab ? "installed-tab" : "search-tab"}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute inset-0 flex flex-col pt-2"
>
<div className="w-[98%] flex-1 relative overflow-hidden mc-options-bg">
{showSearch ? (
<div className="absolute inset-0 flex flex-col pt-2">
<div className="flex items-center gap-3 px-6 pb-4">
<div
className="flex items-center flex-1 h-12 px-4 border-2 border-[#444] bg-black/40 rounded shadow-inner"
@ -745,40 +767,21 @@ const WorkshopView = memo(function WorkshopView() {
</div>
)}
</div>
</motion.div>
) : loading ? (
<motion.div
key="loading"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center"
>
</div>
) : loading ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase">
Searching Archives...
</span>
</motion.div>
) : error ? (
<motion.div
key="error"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center"
>
</div>
) : error ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl text-red-500 mc-text-shadow uppercase tracking-widest">
{error}
</span>
</motion.div>
) : (
<motion.div
key={activeTab}
ref={gridRef}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="absolute inset-0 overflow-y-auto p-6 scroll-smooth"
>
</div>
) : (
<div ref={gridRef} className="absolute inset-0 overflow-y-auto p-6 scroll-smooth">
{filteredItems.length === 0 ? (
<div className="flex items-center justify-center h-full">
<span className="text-2xl text-[#E0E0E0] mc-text-shadow uppercase tracking-widest opacity-40">
@ -805,9 +808,8 @@ const WorkshopView = memo(function WorkshopView() {
))}
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
<div className="w-full mt-6 mb-4 flex justify-center">
@ -1104,26 +1106,8 @@ function PackageModal({
: "REINSTALL";
return (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/85 backdrop-blur-sm"
onClick={onClose}
>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 20, opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={(e) => e.stopPropagation()}
className="flex flex-col w-[640px] max-h-[85vh] overflow-hidden font-['Mojangles'] border-2 border-[#555] rounded-sm"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/85" onClick={onClose}>
<div onClick={(e) => e.stopPropagation()} className="flex flex-col w-[640px] max-h-[85vh] overflow-hidden font-['Mojangles'] mc-options-bg">
<div className="w-full h-[240px] flex-shrink-0 bg-black/60 overflow-hidden relative border-b border-[#444]">
{imgError ? (
<div className="absolute inset-0 flex items-center justify-center opacity-20">
@ -1176,11 +1160,11 @@ function PackageModal({
<div className="space-y-4">
{pkg.extended_description &&
pkg.extended_description.trim() !== "" && (
<div className="space-y-2 bg-black/20 p-4 border border-[#333] rounded-sm">
<span className="text-[10px] text-[#555] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Plugin Description
<div className="space-y-2 p-4 border border-[#444] bg-black/40">
<span className="text-[10px] text-[#AAAAAA] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Description
</span>
<div className="text-sm text-[#A0A0A0] mc-text-shadow leading-relaxed workshop-markdown">
<div className="text-sm text-white mc-text-shadow leading-relaxed workshop-markdown">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{pkg.extended_description}
</ReactMarkdown>
@ -1189,9 +1173,9 @@ function PackageModal({
)}
</div>
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-[#333]">
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-[#444]">
<div className="flex flex-col gap-1.5">
<span className="text-[10px] text-[#666] mc-text-shadow uppercase tracking-[0.2em] font-bold">
<span className="text-[10px] text-[#888] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Metadata
</span>
<div className="flex flex-col gap-1">
@ -1435,8 +1419,8 @@ function PackageModal({
</button>
</div>
</div>
</motion.div>
</motion.div>
</div>
</div>
<AnimatePresence>
{showInstall && (

View file

@ -59,7 +59,6 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
const audioRaw = useAudioController({
musicVol: configRaw.musicVol,
sfxVol: configRaw.sfxVol,
showIntro,
isGameRunning: gameRaw.isGameRunning,
isWindowVisible,
});

View file

@ -1 +1,55 @@
/* empty :P */
@keyframes splashPulse {
0% { transform: scale(0.95) rotate(-20deg); }
100% { transform: scale(1.08) rotate(-20deg); }
}
.mc-splash {
animation: splashPulse 0.45s ease-in-out infinite alternate;
transform-origin: center;
}
.mc-slider-custom {
-webkit-appearance: none;
appearance: none;
background: transparent;
height: 100%;
outline: none;
border: none;
margin: 0;
padding: 0;
}
.mc-slider-custom::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 44px;
background: url('/images/Slider_Handle.png') no-repeat center;
background-size: 100% 100%;
cursor: pointer;
position: relative;
z-index: 30;
}
*:focus {
outline: none !important;
box-shadow: none !important;
}
button, input {
border-radius: 0 !important;
border: none !important;
outline: none !important;
box-shadow: none !important;
}
.mc-sq-btn {
background: url('/images/Button_Square.png') no-repeat center;
background-size: 100% 100%;
image-rendering: pixelated;
}
.mc-sq-btn:hover {
background: url('/images/Button_Square_Highlighted.png') no-repeat center;
background-size: 100% 100%;
}

156
src/data/splashes.ts Normal file
View file

@ -0,0 +1,156 @@
export 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!",
"is smartcmd dead ?",
"NO BUILT IN MS AUTH !",
"Mr_Anilex wasn't here!",
"Who's Jack ?",
"This text is blue!",
"Bonjour!",
"Salam!",
"Reverse engineering Wii U version",
"Don't try Valorant!",
"This could never be a sad place!",
"Made without microslop",
"Thank you C418",
"Bread is pain",
"From the star!",
"Never gonna give you up!",
"9+10=21",
".party() was successful",
"Not Kogama",
"You can be proud of you!",
"Let's drink Orange Joe",
"Kirater is a great singer!",
"Mirkette My beloved",
"Started in Bordeaux",
"Oui Oui Baguette",
"Milk In The Microwave",
"8-3: DISINTEGRATION LOOP",
"Turn the light OFF",
"Not written by Mr_Anilex",
"The One Who's Running the Show!",
"Playing Forever",
"The World looks cubic!",
"huh?",
"Sybau",
"Available on Toaster",
"Try ArchLinux",
"69% Accurate",
"A molecule of meow",
"http://localhost:3000",
"uuhhhh...",
"Oyasumi",
"I don't want to set the world on fire",
"Directed by Michael Bay",
"We see you, Opal!",
"A Cool Cat in Town",
"Not BrainRotted!",
"Farting is Natural -Leon",
"93/100 on metacritic",
"Not (anymore) on Steam",
"Sudo apt install EmeraldLauncher",
"Sudo pacman -S EmeraldLauncher",
"Kay-Chan my beloved! <3",
"Peak!",
"OpenSource!",
"made by human with bone and flesh",
"Made with hate against microslop",
"Steelorse :fire:",
"It's Minecraft but i'm not sure",
"Look at you!",
"You're beautiful",
"Mr_Anilex has a big ego",
"Traduis-moi !",
"May contains Mr_Anilex",
"Neoapps didn't write this splash",
"Where's Kinger?",
"KayJann, Breakcore and code",
"Hey Goku!",
"Vegeta is a DZ mashallah",
"Bogos Binted? Vorp",
"YOU SHALL NOT PASS !",
"Bready, Steady, GO !",
"Not-so-Empty-house",
"We'll Meet Again",
"idk",
"wdym",
"Not making sense",
"Dw!",
"i forgor",
"Remember to be patient!",
"NOW'S YOUR CHANCE TO BE A.",
"BIG SHOT",
"A burning memory",
"FREE MONEY!",
"Can You Really Call This A Hotel. I didn't Reveive A Mint On My Pillow Or Anything",
"Try Indie Game",
"SHARK WITH LEGS!",
"it's a seal!",
"Shrimp.",
"Limited edition!",
"Fat free!",
"GOTY!",
"Water proof!",
"LALALA-LAVA",
"CHICHICHI-CHICKEN",
"Tasty ah hell",
"1% sugar!",
"150% hyperbole!",
"Hotter than the sun!",
"Woo, reddit!",
"piebot was here!",
"Legacy in an evolved manner.",
"neoapps is cool!",
"neoapps has put a self insert into this program!",
"Also try neoLegacy!",
"$20 is $20",
"no RenderDragon!",
"Iggy Jiggy",
"Arch, btw!",
"Bedrock bad!",
"Bedrock Linux not bad",
"Also try Terraria!",
"Also try LC Launcher!",
"Exclusively abandonware!",
"100% legal in all 3 states (of matter)",
"Herobrine has been confirmed we are fighting him please help",
"LGTM",
"git revert",
"We do not crypto mine on your computer.",
"100% legal!",
"Definitely not pirated",
"Long Live LCE!",
"For the community, by the community!",
"Not affiliated with Microslop",
"What is nether update",
"To the good old days",
"Also try : Minecraft Pocket Edition",
];

View file

@ -8,6 +8,7 @@ export function useAppConfig() {
const [vfxEnabled, setVfxEnabled] = useLocalStorage("lce-vfx", true);
const [animationsEnabled, setAnimationsEnabled] = useLocalStorage("lce-animations", true);
const [rpcEnabled, setRpcEnabled] = useLocalStorage("discord-rpc", true);
const [startFullscreen, setStartFullscreen] = useLocalStorage("lce-fullscreen", true);
const [musicVol, setMusicVol] = useLocalStorage("lce-music", 50);
const [sfxVol, setSfxVol] = useLocalStorage("lce-sfx", 100);
const [isDayTime, setIsDayTime] = useLocalStorage("lce-daytime", true);
@ -36,6 +37,7 @@ export function useAppConfig() {
if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
if (config.animationsEnabled !== undefined) setAnimationsEnabled(config.animationsEnabled);
if (config.rpcEnabled !== undefined) setRpcEnabled(config.rpcEnabled);
if (config.startFullscreen !== undefined) setStartFullscreen(config.startFullscreen);
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);
@ -60,6 +62,7 @@ export function useAppConfig() {
animationsEnabled,
vfxEnabled,
rpcEnabled,
startFullscreen,
musicVol,
sfxVol,
legacyMode,
@ -69,7 +72,7 @@ export function useAppConfig() {
launchEnvVars,
}).catch(console.error);
}
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customizations, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, isLoaded]);
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, isLoaded]);
return {
username,
@ -84,6 +87,8 @@ export function useAppConfig() {
setAnimationsEnabled,
rpcEnabled,
setRpcEnabled,
startFullscreen,
setStartFullscreen,
musicVol: musicVol ?? 50,
setMusicVol,
sfxVol: sfxVol ?? 100,

View file

@ -1,6 +1,8 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { SPLASHES } from "../data/splashes";
const TRACKS = [
"music/Moog City 2.opus",
"music/Blind Spots.ogg",
"music/Key.ogg",
"music/Living Mice.ogg",
@ -13,167 +15,10 @@ const resolveAudioUrl = (path: string) => {
return new URL(relativePath, window.location.origin).href;
};
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!",
"is smartcmd dead ?",
"NO BUILT IN MS AUTH !",
"Mr_Anilex wasn't here!",
"Who's Jack ?",
"This text is blue!",
"Bonjour!",
"Salam!",
"Reverse engineering Wii U version",
"Don't try Valorant!",
"This could never be a sad place!",
"Made without microslop",
"Thank you C418",
"Bread is pain",
"From the star!",
"Never gonna give you up!",
"9+10=21",
".party() was successful",
"Not Kogama",
"You can be proud of you!",
"Let's drink Orange Joe",
"Kirater is a great singer!",
"Mirkette My beloved",
"Started in Bordeaux",
"Oui Oui Baguette",
"Milk In The Microwave",
"8-3: DISINTEGRATION LOOP",
"Turn the light OFF",
"Not written by Mr_Anilex",
"The One Who's Running the Show!",
"Playing Forever",
"The World looks cubic!",
"huh?",
"Sybau",
"Available on Toaster",
"Try ArchLinux",
"69% Accurate",
"A molecule of meow",
"http://localhost:3000",
"uuhhhh...",
"Oyasumi",
"I don't want to set the world on fire",
"Directed by Michael Bay",
"We see you, Opal!",
"A Cool Cat in Town",
"Not BrainRotted!",
"Farting is Natural -Leon",
"93/100 on metacritic",
"Not (anymore) on Steam",
"Sudo apt install EmeraldLauncher",
"Sudo pacman -S EmeraldLauncher",
"Kay-Chan my beloved! <3",
"Peak!",
"OpenSource!",
"made by human with bone and flesh",
"Made with hate against microslop",
"Steelorse :fire:",
"It's Minecraft but i'm not sure",
"Look at you!",
"You're beautiful",
"Mr_Anilex has a big ego",
"Traduis-moi !",
"May contains Mr_Anilex",
"Neoapps didn't write this splash",
"Where's Kinger?",
"KayJann, Breakcore and code",
"Hey Goku!",
"Vegeta is a DZ mashallah",
"Bogos Binted? Vorp",
"YOU SHALL NOT PASS !",
"Bready, Steady, GO !",
"Not-so-Empty-house",
"We'll Meet Again",
"idk",
"wdym",
"Not making sense",
"Dw!",
"i forgor",
"Remember to be patient!",
"NOW'S YOUR CHANCE TO BE A.",
"BIG SHOT",
"A burning memory",
"FREE MONEY!",
"Can You Really Call This A Hotel. I didn't Reveive A Mint On My Pillow Or Anything",
"Try Indie Game",
"SHARK WITH LEGS!",
"it's a seal!",
"Shrimp.",
"Limited edition!",
"Fat free!",
"GOTY!",
"Water proof!",
"LALALA-LAVA",
"CHICHICHI-CHICKEN",
"Tasty ah hell",
"1% sugar!",
"150% hyperbole!",
"Hotter than the sun!",
"Woo, reddit!",
"piebot was here!",
"Legacy in an evolved manner.",
"neoapps is cool!",
"neoapps has put a self insert into this program!",
"Also try neoLegacy!",
"$20 is $20",
"no RenderDragon!",
"Iggy Jiggy",
"Arch, btw!",
"Bedrock bad!",
"Bedrock Linux not bad",
"Also try Terraria!",
"Also try LC Launcher!",
"Exclusively abandonware!",
"100% legal in all 3 states (of matter)",
"Herobrine has been confirmed we are fighting him please help",
"LGTM",
"git revert",
"We do not crypto mine on your computer.",
"100% legal!",
"Definitely not pirated",
"Long Live LCE!",
"For the community, by the community!",
"Not affiliated with Microslop",
"What is nether update",
"To the good old days",
"Also try : Minecraft Pocket Edition",
];
interface AudioControllerProps {
musicVol: number;
sfxVol: number;
showIntro: boolean;
isGameRunning: boolean;
isWindowVisible: boolean;
}
@ -181,7 +26,6 @@ interface AudioControllerProps {
export function useAudioController({
musicVol,
sfxVol,
showIntro,
isGameRunning,
isWindowVisible,
}: AudioControllerProps) {
@ -404,10 +248,18 @@ export function useAudioController({
setSplashIndex(newIndex);
}, [playSplashSound, splashIndex]);
const [isMusicStarted, setIsMusicStarted] = useState(false);
const startMusic = useCallback(() => {
setIsMusicStarted(true);
}, []);
useEffect(() => {
if (showIntro) return;
if (!isMusicStarted) return;
const loadAndPlay = async () => {
await ensureAudioContextReady();
let buffer = trackBuffersRef.current.get(currentTrack);
if (!buffer) {
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
@ -416,35 +268,16 @@ export function useAudioController({
}
}
if (buffer) {
await playMusicBuffer(buffer);
}
};
loadAndPlay();
return () => {
stopMusic();
};
}, [showIntro, currentTrack, loadAudioBuffer, playMusicBuffer, stopMusic]);
useEffect(() => {
if (!audioContextRef.current || showIntro) return;
const loadAndPlay = async () => {
let buffer = trackBuffersRef.current.get(currentTrack);
if (!buffer) {
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
if (buffer) {
trackBuffersRef.current.set(currentTrack, buffer);
if (currentTrack === 0) {
await playMusicBuffer(buffer);
} else {
await fadeInMusic(buffer, targetVolumeRef.current, 500);
}
}
if (buffer) {
await fadeInMusic(buffer, musicVol / 100, 500);
}
};
loadAndPlay();
}, [currentTrack, showIntro, musicVol, loadAudioBuffer, fadeInMusic]);
}, [isMusicStarted, currentTrack, loadAudioBuffer, playMusicBuffer, fadeInMusic, ensureAudioContextReady]);
useEffect(() => {
const shouldPause = isGameRunning || !isWindowVisible;
@ -517,5 +350,6 @@ export function useAudioController({
playSfx,
tracks: TRACKS,
splashes: SPLASHES,
startMusic,
};
}

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo, useCallback } 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";
@ -20,6 +21,7 @@ import SkinViewer from "../components/common/SkinViewer";
import TeamModal from "../components/modals/TeamModal";
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 { AchievementToast } from "../components/common/AchievementToast";
import {
@ -34,11 +36,10 @@ import { useLceLiveNotifications } from "../hooks/useLceLiveNotifications";
import type { Edition } from "../types/edition";
import pkg from "../../package.json";
export default function App() {
const ui = useUI();
const {
showIntro,
setShowIntro,
logoAnimDone,
setLogoAnimDone,
activeView,
setActiveView,
isUiHidden,
@ -50,44 +51,44 @@ export default function App() {
updateMessage,
updateUrl,
clearUpdateMessage,
} = useUI();
connected,
} = ui;
const config = useConfig();
const audio = useAudio();
const game = useGame();
const { skinUrl, setSkinUrl, capeUrl } = useSkin();
const skin = useSkin();
const { skinUrl, setSkinUrl, capeUrl } = skin;
const notifications = useLceLiveNotifications();
const {
friendRequestMessage,
gameInviteMessage,
clearFriendRequestMessage,
clearGameInviteMessage,
} = useLceLiveNotifications();
const [showSetup, setShowSetup] = useState(true);
const [displayIsDay, setDisplayIsDay] = useState(config.isDayTime);
useEffect(() => {
setDisplayIsDay(config.isDayTime);
}, [config.isDayTime]);
} = notifications;
const [showSetup, setShowSetup] = useState(false);
const [isSetupChecked, setIsSetupChecked] = useState(false);
const displayIsDay = config.isDayTime;
const selectedEdition = game.editions.find(
(e: Edition) => e.instanceId === config.profile,
);
const selectedVersionName = selectedEdition?.name || "";
const hasAnyInstall = game.installs.length > 0;
const titleImage = hasAnyInstall
? selectedEdition?.titleImage || "/images/MenuTitle.png"
: "/images/MenuTitle.png";
const clearError = useCallback(() => game.setError(null), [game]);
const clearGameUpdate = useCallback(() => game.setGameUpdateMessage(null), [game]);
const clearSteamSuccess = useCallback(() => game.setSteamSuccessMessage(null), [game]);
useEffect(() => {
if (config.isLoaded) {
const setupCompleted =
localStorage.getItem("lce-setup-completed") === "true";
const setupCompleted = localStorage.getItem("lce-setup-completed") === "true";
setShowSetup(!setupCompleted);
setIsSetupChecked(true);
}
}, [config.isLoaded]);
useEffect(() => {
setTimeout(() => setShowIntro(false), 2400);
setTimeout(() => setLogoAnimDone(true), 3400);
}, [showSetup]);
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();
@ -95,35 +96,56 @@ export default function App() {
return () => document.removeEventListener("contextmenu", handleContextMenu);
}, []);
const uiFade = {
const animDuration = config.animationsEnabled ? undefined : { duration: 0 };
const uiFade = useMemo(() => ({
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: config.animationsEnabled ? 0.5 : 0 },
};
transition: animDuration ?? { duration: 0.5 },
}), [animDuration]);
const backgroundFade = {
const backgroundFade = useMemo(() => ({
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: config.animationsEnabled ? 0.8 : 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) {
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" : ""}`}
>
<style>{`
@keyframes splashPulse { 0% { transform: scale(0.95) rotate(-20deg); } 100% { transform: scale(1.08) rotate(-20deg); } }
.mc-splash { animation: splashPulse 0.45s ease-in-out infinite alternate; transform-origin: center; }
.mc-slider-custom { -webkit-appearance: none; appearance: none; background: transparent; height: 100%; outline: none; border: none; margin: 0; padding: 0; }
.mc-slider-custom::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 44px; background: url('/images/Slider_Handle.png') no-repeat center; background-size: 100% 100%; cursor: pointer; position: relative; z-index: 30; }
*:focus { outline: none !important; box-shadow: none !important; }
button, input { border-radius: 0 !important; border: none !important; outline: none !important; box-shadow: none !important; }
.mc-sq-btn { background: url('/images/Button_Square.png') no-repeat center; background-size: 100% 100%; image-rendering: pixelated; }
.mc-sq-btn:hover { background: url('/images/Button_Square_Highlighted.png') no-repeat center; background-size: 100% 100%; }
`}</style>
<div className="absolute inset-0">
<AnimatePresence>
@ -139,6 +161,7 @@ export default function App() {
</motion.div>
</AnimatePresence>
</div>
{config.vfxEnabled && <ClickParticles />}
<AnimatePresence>
@ -152,17 +175,15 @@ export default function App() {
)}
</AnimatePresence>
<AnimatePresence>
<DownloadOverlay
downloadProgress={game.downloadProgress}
downloadingId={game.downloadingId}
editions={game.editions}
/>
</AnimatePresence>
<DownloadOverlay
downloadProgress={game.downloadProgress}
downloadingId={game.downloadingId}
editions={game.editions}
/>
<AchievementToast
message={game.error}
onClose={() => game.setError(null)}
onClose={clearError}
/>
<AchievementToast
@ -180,9 +201,9 @@ export default function App() {
<AchievementToast
message={game.gameUpdateMessage}
onClose={() => game.setGameUpdateMessage(null)}
onClose={clearGameUpdate}
onClick={() => {
game.setGameUpdateMessage(null);
clearGameUpdate();
setActiveView("versions");
}}
title="Game Update Available!"
@ -191,280 +212,225 @@ export default function App() {
<AchievementToast
message={game.steamSuccessMessage}
onClose={() => game.setSteamSuccessMessage(null)}
onClose={clearSteamSuccess}
title="Steam Integration"
variant="steam"
/>
<AnimatePresence>
{showSetup ? (
<SetupView
key="setup"
onComplete={() => {
setShowSetup(false);
setShowIntro(true);
}}
/>
) : showIntro ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col h-full z-10 w-full relative"
>
{!config.legacyMode && (
<motion.div
key="intro"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-1 items-center justify-center z-10 pointer-events-none"
{...uiFade}
className="absolute top-4 left-8 z-50"
>
<motion.img
layoutId="mainLogo"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
src={titleImage}
className="w-3/4 max-w-3xl"
style={{ imageRendering: "pixelated" }}
/>
</motion.div>
) : (
<motion.div
key="dashboard"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col h-full z-10 w-full relative"
>
<AnimatePresence>
{logoAnimDone && (
<>
{!config.legacyMode && (
<motion.div
key="hideBtn"
{...uiFade}
className="absolute top-4 left-8 z-50"
>
<button
onClick={() => {
audio.playPressSound();
setIsUiHidden(!isUiHidden);
}}
className="hover:scale-110 active:scale-95 transition-transform 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
key="dayToggle"
{...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="hover:scale-110 active:scale-95 transition-transform 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
key="secret-swf-btn"
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 hover:scale-110 active:scale-95 transition-transform 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 group-hover:opacity-100 drop-shadow-[0_4px_4px_rgba(0,0,0,1)] grayscale group-hover:grayscale-0"
style={{ imageRendering: "pixelated" }}
onError={(e) => {
(e.target as HTMLImageElement).src =
"/images/Button_Background.png";
}}
/>
<span className="text-[#FFFF55] text-sm mc-text-shadow opacity-0 group-hover:opacity-100 transition-opacity">
SWF Editor
</span>
</button>
</motion.div>
)}
</>
)}
</AnimatePresence>
<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">
<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" }}
/>
<AnimatePresence>
{logoAnimDone && (
<>
<motion.div
key="splash"
{...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
key="tu-subtitle"
{...uiFade}
className="absolute -bottom-6 text-[#A0A0A0] text-sm mc-text-shadow tracking-widest uppercase opacity-80 font-['Mojangles']"
>
{selectedVersionName}
</motion.div>
)}
</>
)}
</AnimatePresence>
</div>
</div>
<main className="flex-1 w-full relative">
<div
className={`w-full h-full flex flex-col items-center justify-center ${!logoAnimDone || 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}
isFocusedSection={focusSection === "skin"}
onNavigateRight={onNavigateToMenu}
/>
)}
</AnimatePresence>
<div className="w-full h-full max-w-4xl 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" />
)}
{activeView === "devtools" && (
<DevtoolsView key="devtools-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 === "swf-editor" && (
<SwfView key="swf-editor-view" />
)}
{activeView === "lcelive" && (
<LceLiveView key="lcelive-view" />
)}
{activeView === "skins" && <SkinsView key="skins-view" />}
{activeView === "screenshots" && (
<ScreenshotsView key="screenshots-view" />
)}
</AnimatePresence>
</div>
</div>
</main>
<AnimatePresence>
{logoAnimDone && (
<motion.footer
key="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">
{useUI().connected && "CONTROLLER CONNECTED"}
</div>
</motion.footer>
)}
</AnimatePresence>
<button
onClick={() => {
audio.playPressSound();
setIsUiHidden(!isUiHidden);
}}
className="hover:scale-110 active:scale-95 transition-transform 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>
)}
</AnimatePresence>
{!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="hover:scale-110 active:scale-95 transition-transform 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 hover:scale-110 active:scale-95 transition-transform 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 group-hover:opacity-100 drop-shadow-[0_4px_4px_rgba(0,0,0,1)] grayscale group-hover:grayscale-0"
style={{ imageRendering: "pixelated" }}
onError={(e) => {
(e.target as HTMLImageElement).src =
"/images/Button_Background.png";
}}
/>
<span className="text-[#FFFF55] text-sm mc-text-shadow opacity-0 group-hover:opacity-100 transition-opacity">
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">
<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" }}
/>
<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}
isFocusedSection={focusSection === "skin"}
onNavigateRight={onNavigateToMenu}
/>
)}
</AnimatePresence>
<div className="w-full h-full max-w-4xl 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" />
)}
{activeView === "devtools" && (
<DevtoolsView key="devtools-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 === "swf-editor" && (
<SwfView key="swf-editor-view" />
)}
{activeView === "lcelive" && (
<LceLiveView key="lcelive-view" />
)}
{activeView === "skins" && <SkinsView key="skins-view" />}
{activeView === "screenshots" && (
<ScreenshotsView key="screenshots-view" />
)}
</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}

View file

@ -36,6 +36,7 @@ export interface AppConfig {
animationsEnabled?: boolean;
vfxEnabled?: boolean;
rpcEnabled?: boolean;
startFullscreen?: boolean;
musicVol?: number;
sfxVol?: number;
legacyMode?: boolean;