feat: slim skins support

This commit is contained in:
neoapps-dev 2026-04-03 20:40:35 +03:00
parent 60fd9485ea
commit 53e652b3b1
3 changed files with 144 additions and 79 deletions

View file

@ -47,7 +47,6 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
const { updateMessage, clearUpdateMessage } = useUpdateCheck();
const configRaw = useAppConfig();
const skinSync = useSkinSync();
const gameRaw = useGameManager({
profile: configRaw.profile,
setProfile: configRaw.setProfile,
@ -55,6 +54,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
setCustomEditions: configRaw.setCustomEditions,
keepLauncherOpen: configRaw.keepLauncherOpen,
});
const skinSync = useSkinSync({ profile: configRaw.profile, editions: gameRaw.editions });
const audioRaw = useAudioController({
musicVol: configRaw.musicVol,
sfxVol: configRaw.sfxVol,

View file

@ -11,13 +11,15 @@ const BASE_EDITIONS = [
desc: "Backporting the newer title updates back to the LCE",
url: "https://codeberg.org/piebot/LegacyEvolved/releases/download/nightly/LCEWindows64.zip",
titleImage: "/images/minecraft_title_LegacyEvolved.png",
supportsSlimSkins: true,
},
{
id: "revelations",
name: "Legacy Revelations",
desc: "QoL, performance, hardcore mode, & security features for LCE.",
url: "https://github.com/itsRevela/MinecraftConsoles/releases/download/Nightly/LCREWindows64.zip",
titleImage: "/images/minecraft_title_revelations.png"
titleImage: "/images/minecraft_title_revelations.png",
supportsSlimSkins: false,
},
{
id: "vanilla_tu19",
@ -25,6 +27,7 @@ const BASE_EDITIONS = [
desc: "Leaked 4J Studios build. (smartcmd)",
url: "https://github.com/smartcmd/MinecraftConsoles/releases/download/nightly/LCEWindows64.zip",
titleImage: "/images/minecraft_title_tu19.png",
supportsSlimSkins: false,
},
{
id: "360revived",
@ -32,6 +35,7 @@ const BASE_EDITIONS = [
desc: "PC port of Xbox 360 Edition TU19",
url: "https://github.com/BluTac10/360Revived/releases/download/nightly/LCEWindows64.zip",
titleImage: "/images/minecraft_title_360revived.png",
supportsSlimSkins: false,
},
];
@ -51,16 +55,27 @@ interface GameManagerProps {
keepLauncherOpen: boolean;
}
export function useGameManager({ profile, setProfile, customEditions, setCustomEditions, keepLauncherOpen }: GameManagerProps) {
export function useGameManager({
profile,
setProfile,
customEditions,
setCustomEditions,
keepLauncherOpen,
}: GameManagerProps) {
const [installs, setInstalls] = useState<string[]>([]);
const [isGameRunning, setIsGameRunning] = useState(false);
const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
const [downloadingId, setDownloadingId] = useState<string | null>(null);
const [isRunnerDownloading, setIsRunnerDownloading] = useState(false);
const [runnerDownloadProgress, setRunnerDownloadProgress] = useState<number | null>(null);
const [runnerDownloadProgress, setRunnerDownloadProgress] = useState<
number | null
>(null);
const [error, setError] = useState<string | null>(null);
const editions = useMemo(() => [...BASE_EDITIONS, ...customEditions], [customEditions]);
const editions = useMemo(
() => [...BASE_EDITIONS, ...customEditions],
[customEditions],
);
const checkInstalls = useCallback(async () => {
const results = await Promise.all(
@ -74,56 +89,73 @@ export function useGameManager({ profile, setProfile, customEditions, setCustomE
useEffect(() => {
checkInstalls();
const unlistenDownload = TauriService.onDownloadProgress((p) => setDownloadProgress(p));
const unlistenRunner = TauriService.onRunnerDownloadProgress((p) => setRunnerDownloadProgress(p));
const unlistenDownload = TauriService.onDownloadProgress((p) =>
setDownloadProgress(p),
);
const unlistenRunner = TauriService.onRunnerDownloadProgress((p) =>
setRunnerDownloadProgress(p),
);
return () => {
unlistenDownload.then((u) => u());
unlistenRunner.then((u) => u());
};
}, [customEditions, checkInstalls]);
const downloadRunner = useCallback(async (name: string, url: string) => {
if (isRunnerDownloading) return;
setIsRunnerDownloading(true);
setRunnerDownloadProgress(0);
setError(null);
try {
await TauriService.downloadRunner(name, url);
setRunnerDownloadProgress(null);
} catch (e: any) {
console.error(e);
setError(typeof e === 'string' ? e : e.message || "Failed to download runner");
} finally {
setIsRunnerDownloading(false);
}
}, [isRunnerDownloading]);
const downloadRunner = useCallback(
async (name: string, url: string) => {
if (isRunnerDownloading) return;
setIsRunnerDownloading(true);
setRunnerDownloadProgress(0);
setError(null);
try {
await TauriService.downloadRunner(name, url);
setRunnerDownloadProgress(null);
} catch (e: any) {
console.error(e);
setError(
typeof e === "string" ? e : e.message || "Failed to download runner",
);
} finally {
setIsRunnerDownloading(false);
}
},
[isRunnerDownloading],
);
const toggleInstall = useCallback(async (id: string) => {
if (downloadingId) return;
const edition = editions.find((e) => e.id === id);
if (!edition) return;
setError(null);
try {
setDownloadingId(id);
setDownloadProgress(0);
await TauriService.downloadAndInstall(edition.url, id);
await TauriService.syncDlc(id);
const toggleInstall = useCallback(
async (id: string) => {
if (downloadingId) return;
const edition = editions.find((e) => e.id === id);
if (!edition) return;
setError(null);
try {
setDownloadingId(id);
setDownloadProgress(0);
await TauriService.downloadAndInstall(edition.url, id);
await TauriService.syncDlc(id);
await checkInstalls();
setProfile(id);
setDownloadProgress(null);
setDownloadingId(null);
} catch (e: any) {
console.error(e);
setError(
typeof e === "string" ? e : e.message || "Failed to install version",
);
setDownloadProgress(null);
setDownloadingId(null);
}
},
[downloadingId, editions, checkInstalls, setProfile],
);
const handleUninstall = useCallback(
async (id: string) => {
await TauriService.deleteInstance(id);
await checkInstalls();
setProfile(id);
setDownloadProgress(null);
setDownloadingId(null);
} catch (e: any) {
console.error(e);
setError(typeof e === 'string' ? e : e.message || "Failed to install version");
setDownloadProgress(null);
setDownloadingId(null);
}
}, [downloadingId, editions, checkInstalls, setProfile]);
const handleUninstall = useCallback(async (id: string) => {
await TauriService.deleteInstance(id);
await checkInstalls();
}, [checkInstalls]);
},
[checkInstalls],
);
const handleLaunch = useCallback(async () => {
if (isGameRunning) return;
@ -136,7 +168,9 @@ export function useGameManager({ profile, setProfile, customEditions, setCustomE
await TauriService.launchGame(profile, PARTNERSHIP_SERVERS);
} catch (e: any) {
console.error(e);
setError(typeof e === 'string' ? e : e.message || "Failed to launch game");
setError(
typeof e === "string" ? e : e.message || "Failed to launch game",
);
} finally {
setIsGameRunning(false);
await appWindow.show();
@ -154,21 +188,36 @@ export function useGameManager({ profile, setProfile, customEditions, setCustomE
}
}, [profile]);
const addCustomEdition = useCallback((edition: { name: string; desc: string; url: string }) => {
const id = `custom_${Date.now()}`;
const newEdition = { ...edition, id, titleImage: "/images/minecraft_title_tucustom.png" };
setCustomEditions([...customEditions, newEdition]);
return id;
}, [customEditions, setCustomEditions]);
const addCustomEdition = useCallback(
(edition: { name: string; desc: string; url: string }) => {
const id = `custom_${Date.now()}`;
const newEdition = {
...edition,
id,
titleImage: "/images/minecraft_title_tucustom.png",
};
setCustomEditions([...customEditions, newEdition]);
return id;
},
[customEditions, setCustomEditions],
);
const deleteCustomEdition = useCallback((id: string) => {
setCustomEditions(customEditions.filter((e) => e.id !== id));
TauriService.deleteInstance(id).catch(console.error);
}, [customEditions, setCustomEditions]);
const deleteCustomEdition = useCallback(
(id: string) => {
setCustomEditions(customEditions.filter((e) => e.id !== id));
TauriService.deleteInstance(id).catch(console.error);
},
[customEditions, setCustomEditions],
);
const updateCustomEdition = useCallback((id: string, updated: { name: string; desc: string; url: string }) => {
setCustomEditions(customEditions.map((e) => e.id === id ? { ...e, ...updated } : e));
}, [customEditions, setCustomEditions]);
const updateCustomEdition = useCallback(
(id: string, updated: { name: string; desc: string; url: string }) => {
setCustomEditions(
customEditions.map((e) => (e.id === id ? { ...e, ...updated } : e)),
);
},
[customEditions, setCustomEditions],
);
return {
installs,

View file

@ -1,33 +1,49 @@
import { useState, useEffect } from "react";
import { useLocalStorage } from "./useLocalStorage";
export function useSkinSync() {
interface Edition {
id: string;
supportsSlimSkins?: boolean;
}
interface UseSkinSyncProps {
profile: string;
editions: Edition[];
}
export function useSkinSync({ profile, editions }: UseSkinSyncProps) {
const [skinUrl, setSkinUrl] = useLocalStorage("lce-skin", "/images/Default.png");
const [skinBase64, setSkinBase64] = useState<string | null>(null);
useEffect(() => {
const syncSkin = async () => {
if (!skinUrl) return;
try {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const cvs = document.createElement("canvas");
cvs.width = 64;
cvs.height = 32;
const ctx = cvs.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
setSkinBase64(cvs.toDataURL("image/png"));
}
};
img.src = skinUrl;
} catch (e) {
console.error("Skin conversion failed:", e);
const edition = editions.find((e) => e.id === profile);
const supportsSlim = edition?.supportsSlimSkins ?? false;
if (!supportsSlim) {
try {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const cvs = document.createElement("canvas");
cvs.width = 64;
cvs.height = 32;
const ctx = cvs.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
setSkinBase64(cvs.toDataURL("image/png"));
}
};
img.src = skinUrl;
} catch (e) {
console.error("Skin conversion failed:", e);
}
} else {
setSkinBase64(null);
}
};
syncSkin();
}, [skinUrl]);
}, [skinUrl, profile, editions]);
return {
skinUrl,