feat: devtools menu placeholder (#35)

This commit is contained in:
/home/neo
2026-04-03 21:59:57 +03:00
committed by GitHub
parent 53e652b3b1
commit 5afc3c0418
7 changed files with 251 additions and 259 deletions

View File

@@ -13,8 +13,8 @@ export function AchievementToast({
message,
onClose,
onClick,
title = "Error Get!",
variant = "error"
title = "Error!",
variant = "error",
}: AchievementToastProps) {
useEffect(() => {
if (message) {
@@ -69,10 +69,14 @@ export function AchievementToast({
animate={{ x: 0, opacity: 1 }}
exit={{ x: 400, opacity: 0 }}
transition={{ type: "spring", damping: 20, stiffness: 100 }}
onClick={onClick ? () => {
onClick();
onClose();
} : undefined}
onClick={
onClick
? () => {
onClick();
onClose();
}
: undefined
}
className={`fixed top-6 right-6 z-[9999] ${onClick ? "cursor-pointer" : ""}`}
>
<div

View File

@@ -0,0 +1,86 @@
import { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
export default function DevtoolsView() {
const { setActiveView } = useUI();
const [backHover, setBackHover] = useState(false);
const { playBackSound, playClickSound } = useAudio();
const [focusIndex, setFocusIndex] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
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]);
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 (
<motion.div
tabIndex={0}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
className="flex flex-col items-center w-full max-w-4xl outline-none"
>
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-[300px] text-center tracking-widest uppercase opacity-80">
Developer Tools
</h2>
<div
className="w-full max-w-135 h-48 mb-6 p-8 shadow-2xl flex items-center justify-center"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<span className="text-[#E0E0E0] text-xl mc-text-shadow tracking-wide">
Developer Tools coming soon...
</span>
</div>
<button
onMouseEnter={() => 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
</button>
</motion.div>
);
}

View File

@@ -1,12 +1,26 @@
import { useState, useEffect, useMemo, memo } from "react";
import { motion } from "framer-motion";
import { useUI, useConfig, useAudio, useGame } from "../../context/LauncherContext";
import {
useUI,
useConfig,
useAudio,
useGame,
} from "../../context/LauncherContext";
const HomeView = memo(function HomeView() {
const { setActiveView, setShowCredits, focusSection, onNavigateToSkin } = useUI();
const { setActiveView, setShowCredits, focusSection, onNavigateToSkin } =
useUI();
const { profile, legacyMode } = useConfig();
const { playClickSound, playSfx } = useAudio();
const { handleLaunch, isGameRunning, editions, installs, toggleInstall, downloadProgress, downloadingId } = useGame();
const {
handleLaunch,
isGameRunning,
editions,
installs,
toggleInstall,
downloadProgress,
downloadingId,
} = useGame();
const isFocusedSection = focusSection === "menu";
const selectedEdition = editions.find((e: any) => e.id === profile);
@@ -32,7 +46,7 @@ const HomeView = memo(function HomeView() {
{ label: "Help & Options", action: () => setActiveView("settings") },
{ label: "Versions", action: () => setActiveView("versions") },
{ label: "Workshop", action: () => setActiveView("workshop") },
{ label: "Themes & Tools", action: () => setActiveView("themes") },
{ label: "Developer Tools", action: () => setActiveView("devtools") },
],
[
isGameRunning,

View File

@@ -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<number | null>(null);
const [externalPalettes, setExternalPalettes] = useState<ThemePalette[]>([]);
const containerRef = useRef<HTMLDivElement>(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 (
<motion.div
ref={containerRef}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
className="flex flex-col items-center w-full max-w-2xl outline-none"
>
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-[300px] text-center tracking-widest uppercase opacity-80">
Themes & Styles
</h2>
<div className="w-full max-w-[540px] flex flex-col items-center gap-4 mt-4 mb-8">
<button
data-index="0"
onMouseEnter={() => 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)}
>
<span className="text-2xl mc-text-shadow tracking-widest uppercase">
{currentTheme}
</span>
</button>
<button
data-index="1"
onMouseEnter={() => 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)}
>
<span className="text-xl mc-text-shadow tracking-widest uppercase">
Import Theme
</span>
</button>
</div>
<button
data-index="2"
onMouseEnter={() => 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
</button>
</motion.div>
);
});
export default ThemesView;

View File

@@ -30,24 +30,28 @@ export function useDiscordRPC({
const updateRPC = async () => {
if (!rpcEnabled || showIntro || !username) return;
if (!isWindowVisible && !isGameRunning && downloadProgress === null) 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}`;
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";
const downloadingName =
editions.find((e) => e.id === downloadingId)?.name || "Game Files";
details = `Downloading ${downloadingName} (${downloadProgress.toFixed(0)}%)`;
} else {
const tabNames: Record<string, string> = {
main: "Main Menu",
versions: "Selecting Version",
settings: "In Settings",
themes: "Browsing Themes",
devtools: "Developing for LCE",
skins: "Browsing Skins",
workshop: "Browsing Workshop",
};
@@ -58,5 +62,16 @@ export function useDiscordRPC({
};
updateRPC();
}, [rpcEnabled, showIntro, username, profile, activeView, isGameRunning, isWindowVisible, Math.floor(downloadProgress || 0), downloadingId, editions]);
}, [
rpcEnabled,
showIntro,
username,
profile,
activeView,
isGameRunning,
isWindowVisible,
Math.floor(downloadProgress || 0),
downloadingId,
editions,
]);
}

View File

@@ -3,7 +3,7 @@ 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 DevtoolsView from "../components/views/DevtoolsView";
import SkinsView from "../components/views/SkinsView";
import WorkshopView from "../components/views/WorkshopView";
import SetupView from "../components/views/SetupView";
@@ -14,7 +14,13 @@ 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 {
useUI,
useConfig,
useAudio,
useGame,
useSkin,
} from "../context/LauncherContext";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { TauriService } from "../services/TauriService";
import pkg from "../../package.json";
@@ -22,10 +28,20 @@ const appWindow = getCurrentWindow();
export default function App() {
const {
showIntro, setShowIntro, logoAnimDone, setLogoAnimDone,
activeView, setActiveView, isUiHidden, setIsUiHidden,
showCredits, setShowCredits, focusSection,
onNavigateToMenu, updateMessage, clearUpdateMessage
showIntro,
setShowIntro,
logoAnimDone,
setLogoAnimDone,
activeView,
setActiveView,
isUiHidden,
setIsUiHidden,
showCredits,
setShowCredits,
focusSection,
onNavigateToMenu,
updateMessage,
clearUpdateMessage,
} = useUI();
const config = useConfig();
@@ -40,30 +56,25 @@ export default function App() {
setDisplayIsDay(config.isDayTime);
}, [config.isDayTime]);
const selectedEdition = game.editions.find((e: any) => e.id === config.profile);
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';
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);
}
setTimeout(() => setShowIntro(false), 2400);
setTimeout(() => setLogoAnimDone(true), 3400);
}, [showSetup]);
useEffect(() => {
@@ -76,21 +87,21 @@ export default function App() {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: config.animationsEnabled ? 0.5 : 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 }
transition: { duration: config.animationsEnabled ? 0.8 : 0 },
};
return (
<MotionConfig transition={config.animationsEnabled ? {} : { duration: 0 }}>
<div
data-tauri-drag-region
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' : ''}`}
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); } }
@@ -106,11 +117,14 @@ export default function App() {
<div className="absolute inset-0">
<AnimatePresence>
<motion.div
key={displayIsDay ? 'day' : 'night'}
key={displayIsDay ? "day" : "night"}
className="absolute inset-0"
{...backgroundFade}
>
<PanoramaBackground profile={config.profile} isDay={displayIsDay} />
<PanoramaBackground
profile={config.profile}
isDay={displayIsDay}
/>
</motion.div>
</AnimatePresence>
</div>
@@ -143,7 +157,9 @@ export default function App() {
<AchievementToast
message={updateMessage}
onClose={clearUpdateMessage}
onClick={() => TauriService.openUrl("https://emerald-legacy-launcher.github.io/")}
onClick={() =>
TauriService.openUrl("https://emerald-legacy-launcher.github.io/")
}
title="Update Available!"
variant="update"
/>
@@ -181,7 +197,12 @@ export default function App() {
className="flex flex-col h-full z-10 w-full relative"
>
<AnimatePresence>
{logoAnimDone && <AppHeader playClickSound={audio.playClickSound} uiFade={uiFade} />}
{logoAnimDone && (
<AppHeader
playClickSound={audio.playClickSound}
uiFade={uiFade}
/>
)}
</AnimatePresence>
<AnimatePresence>
@@ -201,7 +222,11 @@ export default function App() {
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"}
src={
isUiHidden
? "/images/Unhide_UI_Button.png"
: "/images/Hide_UI_Button.png"
}
className="w-10 h-10 cursor-pointer object-contain"
style={{ imageRendering: "pixelated" }}
/>
@@ -226,7 +251,11 @@ export default function App() {
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"}
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" }}
@@ -273,15 +302,16 @@ export default function App() {
: audio.splashes[audio.splashIndex]}
</div>
</motion.div>
{activeView === "main" && 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>
)}
{activeView === "main" &&
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>
@@ -310,9 +340,7 @@ export default function App() {
<div className="w-full max-w-4xl relative flex justify-center items-center">
<AnimatePresence mode="wait">
{activeView === "main" && (
<HomeView key="main-view" />
)}
{activeView === "main" && <HomeView key="main-view" />}
{activeView === "settings" && (
<SettingsView key="settings-view" />
)}
@@ -322,12 +350,10 @@ export default function App() {
{activeView === "workshop" && (
<WorkshopView key="workshop-view" />
)}
{activeView === "themes" && (
<ThemesView key="themes-view" />
)}
{activeView === "skins" && (
<SkinsView key="skins-view" />
{activeView === "devtools" && (
<DevtoolsView key="devtools-view" />
)}
{activeView === "skins" && <SkinsView key="skins-view" />}
</AnimatePresence>
</div>
</div>
@@ -341,9 +367,12 @@ export default function App() {
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}</div>
<div className="flex-1 text-left whitespace-nowrap">
Version: {pkg.version}
</div>
<div className="flex-[2] text-center whitespace-nowrap">
Not affiliated with Mojang AB or Microsoft. "Minecraft" is a trademark of Mojang Synergies AB.
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"}

View File

@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
export interface McServer {
name: string;
@@ -50,7 +50,7 @@ export interface Runner {
id: string;
name: string;
path: string;
type: 'wine' | 'proton';
type: "wine" | "proton";
}
export interface MacOSSetupProgress {
@@ -61,113 +61,114 @@ export interface MacOSSetupProgress {
export class TauriService {
static async saveConfig(config: AppConfig): Promise<void> {
return invoke('save_config', { config });
return invoke("save_config", { config });
}
static async loadConfig(): Promise<AppConfig> {
return invoke('load_config');
return invoke("load_config");
}
static async getExternalPalettes(): Promise<ThemePalette[]> {
return invoke('get_external_palettes');
return invoke("get_external_palettes");
}
static async importTheme(): Promise<string> {
return invoke('import_theme');
return invoke("import_theme");
}
static async getAvailableRunners(): Promise<Runner[]> {
return invoke('get_available_runners');
return invoke("get_available_runners");
}
static async downloadRunner(name: string, url: string): Promise<string> {
return invoke('download_runner', { name, url });
return invoke("download_runner", { name, url });
}
static async checkGameInstalled(instanceId: string): Promise<boolean> {
return invoke('check_game_installed', { instanceId });
return invoke("check_game_installed", { instanceId });
}
static async openInstanceFolder(instanceId: string): Promise<void> {
return invoke('open_instance_folder', { instanceId });
return invoke("open_instance_folder", { instanceId });
}
static async deleteInstance(instanceId: string): Promise<void> {
return invoke('delete_instance', { instanceId });
return invoke("delete_instance", { instanceId });
}
static async cancelDownload(): Promise<void> {
return invoke('cancel_download');
return invoke("cancel_download");
}
static async setupMacosRuntime(): Promise<void> {
return invoke('setup_macos_runtime');
return invoke("setup_macos_runtime");
}
static async downloadAndInstall(url: string, instanceId: string): Promise<string> {
return invoke('download_and_install', { url, instanceId });
static async downloadAndInstall(
url: string,
instanceId: string,
): Promise<string> {
return invoke("download_and_install", { url, instanceId });
}
static async launchGame(instanceId: string, servers: McServer[]): Promise<void> {
return invoke('launch_game', { instanceId, servers });
static async launchGame(
instanceId: string,
servers: McServer[],
): Promise<void> {
return invoke("launch_game", { instanceId, servers });
}
static async stopGame(instanceId: string): Promise<void> {
return invoke('stop_game', { instanceId });
return invoke("stop_game", { instanceId });
}
static async syncDlc(instanceId: string): Promise<void> {
return invoke('sync_dlc', { instanceId });
return invoke("sync_dlc", { instanceId });
}
static async updateTrayIcon(visible: boolean): Promise<void> {
return invoke('update_tray_icon', { visible });
return invoke("update_tray_icon", { visible });
}
static onDownloadProgress(callback: (percent: number) => void) {
return listen<number>('download-progress', (event) => callback(event.payload));
return listen<number>("download-progress", (event) =>
callback(event.payload),
);
}
static onRunnerDownloadProgress(callback: (percent: number) => void) {
return listen<number>('runner-download-progress', (event) => callback(event.payload));
return listen<number>("runner-download-progress", (event) =>
callback(event.payload),
);
}
static onMacosProgress(callback: (payload: MacOSSetupProgress) => void) {
return listen<MacOSSetupProgress>('macos-setup-progress', (event) => callback(event.payload));
return listen<MacOSSetupProgress>("macos-setup-progress", (event) =>
callback(event.payload),
);
}
static async openUrl(url: string): Promise<void> {
return invoke('plugin:opener|open_url', { url });
return invoke("plugin:opener|open_url", { url });
}
static async restartLauncher(): Promise<void> {
return invoke('restart_launcher');
return invoke("restart_launcher");
}
static async checkMacOSRuntimeInstalled(): Promise<boolean> {
return invoke('check_macos_runtime_installed');
return invoke("check_macos_runtime_installed");
}
static async checkMacOSRuntimeInstalledFast(): Promise<boolean> {
return invoke('check_macos_runtime_installed_fast');
return invoke("check_macos_runtime_installed_fast");
}
static async setupMacOSRuntimeOptimized(): Promise<void> {
return invoke('setup_macos_runtime_optimized');
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<boolean> {
// 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;
}
return invoke("fetch_skin", { username });
}
}