From b13260ed05bacf43938dba1ea9213d4cf0887d23 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Thu, 16 Apr 2026 18:42:28 +0300 Subject: [PATCH] feat: initial CAPE support! --- src/components/views/SkinsView.tsx | 734 +++++++++++++++++++++++------ src/hooks/useSkinSync.ts | 155 ++++-- src/services/TauriService.ts | 10 +- 3 files changed, 708 insertions(+), 191 deletions(-) diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index a014cfc..9d32022 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -1,8 +1,13 @@ -import { useState, useEffect, useRef, memo } from 'react'; -import { motion } from 'framer-motion'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; -import { TauriService } from '../../services/TauriService'; -import { useUI, useAudio, useSkin, useConfig } from '../../context/LauncherContext'; +import { useState, useEffect, useRef, memo } from "react"; +import { motion } from "framer-motion"; +import { useLocalStorage } from "../../hooks/useLocalStorage"; +import { TauriService } from "../../services/TauriService"; +import { + useUI, + useAudio, + useSkin, + useConfig, +} from "../../context/LauncherContext"; interface SavedSkin { id: string; @@ -11,37 +16,78 @@ interface SavedSkin { isSlim?: boolean; } +interface SavedCape { + id: string; + name: string; + url: string; +} + const DEFAULT_SKINS: SavedSkin[] = [ - { id: 'default', name: 'Default Steve', url: '/images/Default.png', isSlim: false }, - { id: 'neoapps', name: 'neoapps', url: '/Skins/neoapps.png', isSlim: false }, - { id: 'justneki', name: 'JustNeki', url: '/Skins/JustNeki.png', isSlim: false }, - { id: 'kayjann', name: 'KayJann', url: '/Skins/KayJann.png', isSlim: false }, - { id: 'leon', name: 'Leon', url: '/Skins/Leon.png', isSlim: false }, - { id: 'mr_anilex', name: 'mr_anilex', url: '/Skins/mr_anilex.png', isSlim: false }, - { id: 'peter', name: 'Peter', url: '/Skins/Peter.png', isSlim: false }, - { id: 'piebot', name: 'piebot', url: '/Skins/piebot.png', isSlim: false } + { + id: "default", + name: "Default Steve", + url: "/images/Default.png", + isSlim: false, + }, + { id: "neoapps", name: "neoapps", url: "/Skins/neoapps.png", isSlim: false }, + { + id: "justneki", + name: "JustNeki", + url: "/Skins/JustNeki.png", + isSlim: false, + }, + { id: "kayjann", name: "KayJann", url: "/Skins/KayJann.png", isSlim: false }, + { id: "leon", name: "Leon", url: "/Skins/Leon.png", isSlim: false }, + { + id: "mr_anilex", + name: "mr_anilex", + url: "/Skins/mr_anilex.png", + isSlim: false, + }, + { id: "peter", name: "Peter", url: "/Skins/Peter.png", isSlim: false }, + { id: "piebot", name: "piebot", url: "/Skins/piebot.png", isSlim: false }, ]; const SkinsView = memo(function SkinsView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); - const { skinUrl, setSkinUrl, setSkinIsSlim } = useSkin(); + const { skinUrl, setSkinUrl, setSkinIsSlim, capeUrl, setCapeUrl } = useSkin(); const [focusIndex, setFocusIndex] = useState(null); + const [viewMode, setViewMode] = useState<"skin" | "cape">("skin"); const containerRef = useRef(null); const fileInputRef = useRef(null); + const capeFileInputRef = useRef(null); - const [storedSkins, setStoredSkins] = useLocalStorage('lce-custom-skins', []); - const savedSkins = [...DEFAULT_SKINS, ...storedSkins.filter(s => !DEFAULT_SKINS.some(d => d.id === s.id))]; + const [storedSkins, setStoredSkins] = useLocalStorage( + "lce-custom-skins", + [], + ); + const savedSkins = [ + ...DEFAULT_SKINS, + ...storedSkins.filter((s) => !DEFAULT_SKINS.some((d) => d.id === s.id)), + ]; + const [storedCapes, setStoredCapes] = useLocalStorage( + "lce-custom-capes", + [], + ); + const [activeCapeId, setActiveCapeId] = useState(null); - const TOP_BUTTONS_COUNT = 3; // Import, Delete, Folder + const TOP_BUTTONS_COUNT = viewMode === "skin" ? 3 : 3; const SKINS_START_INDEX = TOP_BUTTONS_COUNT; - const BACK_BUTTON_INDEX = SKINS_START_INDEX + savedSkins.length; + const BACK_BUTTON_INDEX = + SKINS_START_INDEX + + (viewMode === "skin" ? savedSkins.length : storedCapes.length); const ITEM_COUNT = BACK_BUTTON_INDEX + 1; - const setSavedSkins = (newSkins: SavedSkin[] | ((val: SavedSkin[]) => SavedSkin[])) => { - const updatedSkins = typeof newSkins === 'function' ? newSkins(savedSkins) : newSkins; - const customOnes = updatedSkins.filter(s => !DEFAULT_SKINS.some(d => d.id === s.id)); + const setSavedSkins = ( + newSkins: SavedSkin[] | ((val: SavedSkin[]) => SavedSkin[]), + ) => { + const updatedSkins = + typeof newSkins === "function" ? newSkins(savedSkins) : newSkins; + const customOnes = updatedSkins.filter( + (s) => !DEFAULT_SKINS.some((d) => d.id === s.id), + ); setStoredSkins(customOnes); }; @@ -49,15 +95,20 @@ const SkinsView = memo(function SkinsView() { const [showImportModal, setShowImportModal] = useState(false); const [modalFocusIndex, setModalFocusIndex] = useState(0); - const [importMode, setImportMode] = useState<'file' | 'username' | 'model' | null>(null); - const [importUsername, setImportUsername] = useState(''); + const [importMode, setImportMode] = useState< + "file" | "username" | "model" | "cape" | null + >(null); + const [importUsername, setImportUsername] = useState(""); const [isImporting, setIsImporting] = useState(false); - const [importError, setImportError] = useState(''); - const [pendingSkin, setPendingSkin] = useState<{ url: string, defaultName: string } | null>(null); + const [importError, setImportError] = useState(""); + const [pendingSkin, setPendingSkin] = useState<{ + url: string; + defaultName: string; + } | null>(null); const processSkinImage = (url: string, defaultName: string) => { setPendingSkin({ url, defaultName }); - setImportMode('model'); + setImportMode("model"); setModalFocusIndex(0); }; @@ -74,8 +125,13 @@ const SkinsView = memo(function SkinsView() { ctx.drawImage(img, 0, 0); const base64String = cvs.toDataURL("image/png"); const newId = Date.now().toString(); - const newSkin = { id: newId, name: pendingSkin.defaultName, url: base64String, isSlim }; - setSavedSkins(prev => [...prev, newSkin]); + const newSkin = { + id: newId, + name: pendingSkin.defaultName, + url: base64String, + isSlim, + }; + setSavedSkins((prev) => [...prev, newSkin]); setSkinUrl(base64String); setSkinIsSlim(isSlim); setActiveSkinId(newId); @@ -92,13 +148,19 @@ const SkinsView = memo(function SkinsView() { if (!importUsername.trim()) return; playPressSound(); setIsImporting(true); - setImportError(''); + setImportError(""); try { - const [base64Raw, exactName] = await TauriService.fetchSkin(importUsername.trim()); - const skinBase64 = `data:image/png;base64,${base64Raw}`; - processSkinImage(skinBase64, exactName.substring(0, 16)); + if (viewMode === "skin") { + const [base64Raw, exactName] = await TauriService.fetchSkin( + importUsername.trim(), + ); + const skinBase64 = `data:image/png;base64,${base64Raw}`; + processSkinImage(skinBase64, exactName.substring(0, 16)); + } } catch (e: any) { - setImportError(typeof e === 'string' ? e : (e.message || 'Failed to fetch skin')); + setImportError( + typeof e === "string" ? e : e.message || "Failed to fetch", + ); } finally { setIsImporting(false); } @@ -106,50 +168,63 @@ const SkinsView = memo(function SkinsView() { useEffect(() => { if (!activeSkinId) { - const match = savedSkins.find(s => s.url === skinUrl); + const match = savedSkins.find((s) => s.url === skinUrl); if (match) setActiveSkinId(match.id); } }, [activeSkinId, savedSkins, skinUrl]); + useEffect(() => { + if (!activeCapeId) { + const match = storedCapes.find((c) => c.url === capeUrl); + if (match) setActiveCapeId(match.id); + } + }, [activeCapeId, storedCapes, capeUrl]); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (showImportModal) { - if (e.key === 'Escape') { + if (e.key === "Escape") { playBackSound(); if (importMode) { setImportMode(null); - setImportUsername(''); - setImportError(''); + setImportUsername(""); + setImportError(""); setModalFocusIndex(0); } else { setShowImportModal(false); setModalFocusIndex(0); } - } else if (e.key === 'ArrowDown' || e.key === 'Tab') { + } else if (e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault(); - setModalFocusIndex(prev => (prev + 1) % 3); - } else if (e.key === 'ArrowUp') { + setModalFocusIndex((prev) => (prev + 1) % 3); + } else if (e.key === "ArrowUp") { e.preventDefault(); - setModalFocusIndex(prev => (prev - 1 + 3) % 3); - } else if (e.key === 'Enter') { + setModalFocusIndex((prev) => (prev - 1 + 3) % 3); + } else if (e.key === "Enter") { if (!importMode) { - if (modalFocusIndex === 0) { playPressSound(); fileInputRef.current?.click(); } - else if (modalFocusIndex === 1) { playPressSound(); setImportMode('username'); setModalFocusIndex(0); } - else if (modalFocusIndex === 2) { + if (modalFocusIndex === 0) { + playPressSound(); + fileInputRef.current?.click(); + } else if (modalFocusIndex === 1) { + playPressSound(); + setImportMode("username"); + setModalFocusIndex(0); + } else if (modalFocusIndex === 2) { playBackSound(); setShowImportModal(false); setModalFocusIndex(0); } - } else if (importMode === 'username') { - if (modalFocusIndex === 0 || modalFocusIndex === 1) handleFetchUsername(); + } else if (importMode === "username") { + if (modalFocusIndex === 0 || modalFocusIndex === 1) + handleFetchUsername(); else if (modalFocusIndex === 2) { playBackSound(); setImportMode(null); - setImportUsername(''); - setImportError(''); + setImportUsername(""); + setImportError(""); setModalFocusIndex(0); } - } else if (importMode === 'model') { + } else if (importMode === "model") { if (modalFocusIndex === 0) handleFinalizeImport(false); else if (modalFocusIndex === 1) handleFinalizeImport(true); else if (modalFocusIndex === 2) { @@ -162,52 +237,80 @@ const SkinsView = memo(function SkinsView() { } return; } - if (document.activeElement?.tagName === 'INPUT') return; - if (e.key === 'Escape') { + if (document.activeElement?.tagName === "INPUT") return; + if (e.key === "Escape") { playBackSound(); - setActiveView('main'); + setActiveView("main"); return; } - if (e.key === 'ArrowRight') { - setFocusIndex(prev => (prev === null || prev >= ITEM_COUNT - 1) ? 0 : prev + 1); - } else if (e.key === 'ArrowLeft') { - setFocusIndex(prev => (prev === null || prev <= 0) ? ITEM_COUNT - 1 : prev - 1); - } else if (e.key === 'ArrowDown') { + if (e.key === "ArrowRight") { + setFocusIndex((prev) => + prev === null || prev >= ITEM_COUNT - 1 ? 0 : prev + 1, + ); + } else if (e.key === "ArrowLeft") { + setFocusIndex((prev) => + prev === null || prev <= 0 ? ITEM_COUNT - 1 : prev - 1, + ); + } else if (e.key === "ArrowDown") { if (focusIndex === null || focusIndex < TOP_BUTTONS_COUNT) { setFocusIndex(SKINS_START_INDEX); } else if (focusIndex < BACK_BUTTON_INDEX) { - const next = focusIndex + 4; + const rowCount = viewMode === "cape" ? 3 : 4; + const next = focusIndex + rowCount; setFocusIndex(next >= BACK_BUTTON_INDEX ? BACK_BUTTON_INDEX : next); } - } else if (e.key === 'ArrowUp') { + } else if (e.key === "ArrowUp") { if (focusIndex === null) { setFocusIndex(0); } else if (focusIndex === BACK_BUTTON_INDEX) { - setFocusIndex(SKINS_START_INDEX + savedSkins.length - 1); + const itemCount = + viewMode === "cape" ? storedCapes.length + 1 : savedSkins.length; + setFocusIndex(SKINS_START_INDEX + itemCount - 1); } else if (focusIndex >= SKINS_START_INDEX) { - const next = focusIndex - 4; + const rowCount = viewMode === "cape" ? 3 : 4; + const next = focusIndex - rowCount; setFocusIndex(next < SKINS_START_INDEX ? 0 : next); } - } else if (e.key === 'Enter' && focusIndex !== null) { - if (focusIndex === 0) handleImportClick(); - else if (focusIndex === 1) handleDeleteActive(); - else if (focusIndex === 2) { playPressSound(); TauriService.openInstanceFolder('Skins').catch(() => { }); } - else if (focusIndex < BACK_BUTTON_INDEX) { + } else if (e.key === "Enter" && focusIndex !== null) { + if (focusIndex === 0) { + if (viewMode === "skin") handleImportClick(); + else capeFileInputRef.current?.click(); + } else if (focusIndex === 1) { + if (viewMode === "skin") handleDeleteActive(); + else handleDeleteActiveCape(); + } else if (focusIndex === 2) { + playPressSound(); + setViewMode(viewMode === "skin" ? "cape" : "skin"); + } else if (focusIndex < BACK_BUTTON_INDEX) { handleSkinSelect(savedSkins[focusIndex - SKINS_START_INDEX]); } else { playBackSound(); - setActiveView('main'); + setActiveView("main"); } } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [focusIndex, savedSkins.length, playBackSound, setActiveView, playPressSound, showImportModal, importMode, modalFocusIndex, importUsername]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + focusIndex, + savedSkins.length, + storedCapes.length, + playBackSound, + setActiveView, + playPressSound, + showImportModal, + importMode, + modalFocusIndex, + importUsername, + viewMode, + ]); useEffect(() => { if (focusIndex !== null) { - const el = containerRef.current?.querySelector(`[data-index="${focusIndex}"]`) as HTMLElement; + const el = containerRef.current?.querySelector( + `[data-index="${focusIndex}"]`, + ) as HTMLElement; if (el) el.focus(); } }, [focusIndex]); @@ -221,16 +324,16 @@ const SkinsView = memo(function SkinsView() { const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; - if (file.type !== 'image/png') return; + if (file.type !== "image/png") return; - const defaultName = file.name.replace('.png', '').substring(0, 16); + const defaultName = file.name.replace(".png", "").substring(0, 16); const reader = new FileReader(); reader.onload = (event) => { const url = event.target?.result as string; processSkinImage(url, defaultName); }; reader.readAsDataURL(file); - e.target.value = ''; + e.target.value = ""; }; const handleSkinSelect = (skin: SavedSkin) => { @@ -240,55 +343,150 @@ const SkinsView = memo(function SkinsView() { setSkinIsSlim(skin.isSlim || false); }; - const isDefaultSkin = (id: string | null) => DEFAULT_SKINS.some(d => d.id === id); + const isDefaultSkin = (id: string | null) => + DEFAULT_SKINS.some((d) => d.id === id); const handleDeleteActive = () => { if (!activeSkinId || isDefaultSkin(activeSkinId)) return; playPressSound(); - const updatedSkins = savedSkins.filter(s => s.id !== activeSkinId); + const updatedSkins = savedSkins.filter((s) => s.id !== activeSkinId); setSavedSkins(updatedSkins); - setSkinUrl('/images/Default.png'); + setSkinUrl("/images/Default.png"); setSkinIsSlim(false); - setActiveSkinId('default'); + setActiveSkinId("default"); }; const handleNameChange = (id: string, newName: string) => { - const updatedSkins = savedSkins.map(s => s.id === id ? { ...s, name: newName } : s); + const updatedSkins = savedSkins.map((s) => + s.id === id ? { ...s, name: newName } : s, + ); setSavedSkins(updatedSkins); }; - const isActiveDefault = isDefaultSkin(activeSkinId) || (!activeSkinId && skinUrl === '/images/Default.png'); + const handleCapeFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + if (file.type !== "image/png") return; + + const defaultName = file.name.replace(".png", "").substring(0, 16); + const reader = new FileReader(); + reader.onload = (event) => { + const url = event.target?.result as string; + const newId = Date.now().toString(); + const newCape: SavedCape = { id: newId, name: defaultName, url }; + setStoredCapes((prev) => [...prev, newCape]); + setCapeUrl(url); + setActiveCapeId(newId); + }; + reader.readAsDataURL(file); + e.target.value = ""; + }; + + const handleCapeSelect = (cape: SavedCape) => { + playPressSound(); + setActiveCapeId(cape.id); + setCapeUrl(cape.url); + }; + + const handleDeleteActiveCape = () => { + if (!activeCapeId) return; + playPressSound(); + const updatedCapes = storedCapes.filter((c) => c.id !== activeCapeId); + setStoredCapes(updatedCapes); + setCapeUrl(null); + setActiveCapeId(null); + }; + + const handleCapeNameChange = (id: string, newName: string) => { + const updatedCapes = storedCapes.map((c) => + c.id === id ? { ...c, name: newName } : c, + ); + setStoredCapes(updatedCapes); + }; + + const isActiveDefault = + isDefaultSkin(activeSkinId) || + (!activeSkinId && skinUrl === "/images/Default.png"); + const isActiveCapeDefault = !activeCapeId && !capeUrl; return ( - -

Skin Library

- -
+ +

+ {viewMode === "skin" ? "Skin Library" : "Cape Library"} +

+
@@ -297,82 +495,276 @@ const SkinsView = memo(function SkinsView() {
- + +
- {savedSkins.map((skin, i) => { - const idx = SKINS_START_INDEX + i; - const isActive = activeSkinId ? activeSkinId === skin.id : skinUrl === skin.url; - const isFocused = focusIndex === idx; - return ( -
setFocusIndex(idx)} className="flex flex-col items-center gap-1 w-32 outline-none"> + {viewMode === "skin" ? ( + savedSkins.map((skin, i) => { + const idx = SKINS_START_INDEX + i; + const isActive = activeSkinId + ? activeSkinId === skin.id + : skinUrl === skin.url; + const isFocused = focusIndex === idx; + return ( +
setFocusIndex(idx)} + className="flex flex-col items-center gap-1 w-32 outline-none" + > +
+ {isActive && ( + + Active + + )} + {skin.isSlim && ( + + Slim + + )} +
+
handleSkinSelect(skin)} + className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${isActive || isFocused ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} + > + {skin.name} +
+ handleNameChange(skin.id, e.target.value)} + className={`bg-transparent text-center outline-none border-none text-base mc-text-shadow w-full truncate transition-colors ${isActive || isFocused ? "text-[#FFFF55]" : "text-white"} ${isDefaultSkin(skin.id) ? "pointer-events-none" : ""}`} + onClick={(e) => e.stopPropagation()} + spellCheck={false} + readOnly={isDefaultSkin(skin.id)} + /> +
+ ); + }) + ) : ( + <> +
setFocusIndex(SKINS_START_INDEX)} + className="flex flex-col items-center gap-1 w-32 outline-none" + >
- {isActive && Active} - {skin.isSlim && Slim} + {isActiveCapeDefault && ( + + Active + + )}
handleSkinSelect(skin)} - className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${(isActive || isFocused) ? 'border-[#FFFF55]' : 'border-[#373737] hover:border-[#A0A0A0]'}`} + onClick={() => { + playPressSound(); + setCapeUrl(null); + setActiveCapeId(null); + }} + className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none flex items-center justify-center ${isActiveCapeDefault || focusIndex === SKINS_START_INDEX ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} > - {skin.name} + X
- handleNameChange(skin.id, e.target.value)} - className={`bg-transparent text-center outline-none border-none text-base mc-text-shadow w-full truncate transition-colors ${(isActive || isFocused) ? 'text-[#FFFF55]' : 'text-white'} ${isDefaultSkin(skin.id) ? 'pointer-events-none' : ''}`} - onClick={(e) => e.stopPropagation()} spellCheck={false} - readOnly={isDefaultSkin(skin.id)} - /> + + No Cape +
- ); - })} + {storedCapes.map((cape, i) => { + const idx = SKINS_START_INDEX + 1 + i; + const isActive = activeCapeId + ? activeCapeId === cape.id + : capeUrl === cape.url; + const isFocused = focusIndex === idx; + return ( +
setFocusIndex(idx)} + className="flex flex-col items-center gap-1 w-32 outline-none" + > +
+ {isActive && ( + + Active + + )} +
+
handleCapeSelect(cape)} + className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${isActive || isFocused ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} + > + {cape.name} +
+ + handleCapeNameChange(cape.id, e.target.value) + } + className={`bg-transparent text-center outline-none border-none text-base mc-text-shadow w-full truncate transition-colors ${isActive || isFocused ? "text-[#FFFF55]" : "text-white"}`} + onClick={(e) => e.stopPropagation()} + spellCheck={false} + /> +
+ ); + })} + + )}
- {showImportModal && ( + {showImportModal && viewMode === "skin" && (
-
-

Import Skin

+
+

+ Import Skin +

{!importMode ? (
- ) : importMode === 'username' ? ( + ) : importMode === "username" ? (
setModalFocusIndex(0)} autoFocus spellCheck={false} - className={`w-full h-12 bg-black/50 border-2 text-white px-4 text-xl outline-none transition-colors ${modalFocusIndex === 0 ? 'border-[#FFFF55]' : 'border-[#373737]'}`} + className={`w-full h-12 bg-black/50 border-2 text-white px-4 text-xl outline-none transition-colors ${modalFocusIndex === 0 ? "border-[#FFFF55]" : "border-[#373737]"}`} /> - {importError && {importError}} + {importError && ( + + {importError} + + )}
- ) : importMode === 'model' ? ( + ) : importMode === "model" ? (
- Choose the player model type for this skin: + + Choose the player model type for this skin: + @@ -425,12 +852,19 @@ const SkinsView = memo(function SkinsView() { playBackSound(); setShowImportModal(false); setImportMode(null); - setImportUsername(''); - setImportError(''); + setImportUsername(""); + setImportError(""); setModalFocusIndex(0); }} - className={`w-40 h-10 flex items-center justify-center transition-colors text-lg mc-text-shadow mt-6 outline-none ${modalFocusIndex === 2 ? 'text-[#FFFF55]' : 'text-white'}`} - style={{ backgroundImage: modalFocusIndex === 2 ? "url('/images/button_highlighted.png')" : "url('/images/Button_Background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated' }} + className={`w-40 h-10 flex items-center justify-center transition-colors text-lg mc-text-shadow mt-6 outline-none ${modalFocusIndex === 2 ? "text-[#FFFF55]" : "text-white"}`} + style={{ + backgroundImage: + modalFocusIndex === 2 + ? "url('/images/button_highlighted.png')" + : "url('/images/Button_Background.png')", + backgroundSize: "100% 100%", + imageRendering: "pixelated", + }} > Cancel diff --git a/src/hooks/useSkinSync.ts b/src/hooks/useSkinSync.ts index 51db0d6..4f5a053 100644 --- a/src/hooks/useSkinSync.ts +++ b/src/hooks/useSkinSync.ts @@ -14,9 +14,17 @@ interface UseSkinSyncProps { } export function useSkinSync({ profile, editions }: UseSkinSyncProps) { - const [skinUrl, setSkinUrl] = useLocalStorage("lce-skin", "/images/Default.png"); + const [skinUrl, setSkinUrl] = useLocalStorage( + "lce-skin", + "/images/Default.png", + ); const [skinIsSlim, setSkinIsSlim] = useLocalStorage("lce-skin-slim", false); const [skinBase64, setSkinBase64] = useState(null); + const [capeUrl, setCapeUrl] = useLocalStorage( + "lce-cape", + null, + ); + const [capeBase64, setCapeBase64] = useState(null); useEffect(() => { let cancelled = false; if (!skinUrl) return; @@ -39,59 +47,125 @@ export function useSkinSync({ profile, editions }: UseSkinSyncProps) { setSkinBase64(b64); try { const res = await fetch(b64); - const buf = await res.arrayBuffer(); + const skinBuf = await res.arrayBuffer(); const isModernHeight = img.height === 64; - const animValue = skinIsSlim ? "0x00080000" : (isModernHeight ? "0x00040000" : "0x00000000"); + const animValue = skinIsSlim + ? "0x00080000" + : isModernHeight + ? "0x00040000" + : "0x00000000"; let boxes: PCKProperty[] = []; - /*if (skinIsSlim) { + if (skinIsSlim) { boxes.push({ key: "BOX", - value: "ARM0 -2 1 -1 3 10 3 40 16 0 0 0" + value: "ARM0 -2 1 -1 3 10 3 40 16 0 0 0", }); boxes.push({ key: "BOX", - value: "ARM1 -1 1 -1 3 10 3 52 16 0 0 0" + value: "ARM1 -1 1 -1 3 10 3 52 16 0 0 0", }); - }*/ - const pckBuf = PckService.serializePCK({ - version: 3, - endianness: "little", - xmlSupport: false, - properties: ["ANIM", "DISPLAYNAME", "THEMENAME", "GAME_FLAGS", "FREE", "BOX"], - files: [{ + } + + let capeBuf: ArrayBuffer | null = null; + if (capeUrl) { + const capeImg = new Image(); + capeImg.crossOrigin = "anonymous"; + await new Promise((resolve) => { + capeImg.onload = () => resolve(); + capeImg.onerror = () => resolve(); + capeImg.src = capeUrl; + }); + if (!cancelled && capeImg.complete && capeImg.naturalWidth > 0) { + const capeCanvas = document.createElement("canvas"); + capeCanvas.width = capeImg.naturalWidth; + capeCanvas.height = capeImg.naturalHeight; + const capeCtx = capeCanvas.getContext("2d"); + if (capeCtx) { + capeCtx.drawImage(capeImg, 0, 0); + const capeDataUrl = capeCanvas.toDataURL("image/png"); + const capeRes = await fetch(capeDataUrl); + capeBuf = await capeRes.arrayBuffer(); + setCapeBase64(capeDataUrl); + } + } + } else { + setCapeBase64(null); + } + + const files: any[] = [ + { id: "0", path: "0", type: PCKAssetType.INFO, size: 0, data: new Uint8Array(0), - properties: [{ - key: "PACKID", - value: "9999" - }] - }, { + properties: [ + { + key: "PACKID", + value: "9999", + }, + ], + }, + { id: "dlcskin00000001", path: "dlcskin00000001.png", type: PCKAssetType.SKIN, - size: buf.byteLength, - data: new Uint8Array(buf), - properties: [{ - key: "DISPLAYNAME", - value: "Custom Skin" - }, { - key: "GAME_FLAGS", - value: "0x18" - }, { - key: "FREE", - value: "1" - }, - { - key: "ANIM", - value: animValue - }, ...boxes, { - key: "THEMENAME", - value: "Emerald Launcher" - }] - }] + size: skinBuf.byteLength, + data: new Uint8Array(skinBuf), + properties: [ + { + key: "DISPLAYNAME", + value: "Custom Skin", + }, + { + key: "GAME_FLAGS", + value: "0x1c", + }, + { + key: "FREE", + value: "1", + }, + { + key: "ANIM", + value: animValue, + }, + ...boxes, + { + key: "THEMENAME", + value: "Emerald Launcher", + }, + { + key: "CAPEPATH", + value: "dlccape00000001.png", + }, + ], + }, + ]; + + if (capeBuf) { + files.push({ + id: "dlccape00000001", + path: "dlccape00000001.png", + type: PCKAssetType.CAPE, + size: capeBuf.byteLength, + data: new Uint8Array(capeBuf), + properties: [], + }); + } + + const pckBuf = PckService.serializePCK({ + version: 3, + endianness: "little", + xmlSupport: false, + properties: [ + "ANIM", + "DISPLAYNAME", + "THEMENAME", + "GAME_FLAGS", + "FREE", + "BOX", + ], + files, }); await TauriService.saveGlobalSkinPck(new Uint8Array(pckBuf)); } catch (e) { @@ -103,7 +177,7 @@ export function useSkinSync({ profile, editions }: UseSkinSyncProps) { return () => { cancelled = true; }; - }, [skinUrl, profile, editions]); + }, [skinUrl, capeUrl, profile, editions, skinIsSlim]); return { skinUrl, @@ -111,5 +185,8 @@ export function useSkinSync({ profile, editions }: UseSkinSyncProps) { skinIsSlim, setSkinIsSlim, skinBase64, + capeUrl, + setCapeUrl, + capeBase64, }; } diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index f8c3be4..02a1b57 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -124,8 +124,14 @@ export class TauriService { return invoke("sync_dlc", { instanceId }); } - static async workshopInstall(instanceId: string, packageId: string, zips: Record): Promise { - return invoke("workshop_install", { request: { instanceId, packageId, zips } }); + static async workshopInstall( + instanceId: string, + packageId: string, + zips: Record, + ): Promise { + return invoke("workshop_install", { + request: { instanceId, packageId, zips }, + }); } static onDownloadProgress(callback: (percent: number) => void) {