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; name: string; url: string; 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: "andipog", name: "Andi_Pog", url: "/Skins/andi.png", isSlim: false }, { id: "sevenhundred", name: "700", url: "/Skins/700.png", isSlim: false }, { id: "prismachunk0", name: "PrismaChunk0", url: "/Skins/PrismaChunk0.png", isSlim: false, }, ]; const SkinsView = memo(function SkinsView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); 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 [storedCapes, setStoredCapes] = useLocalStorage( "lce-custom-capes", [], ); const [activeCapeId, setActiveCapeId] = useState(null); const TOP_BUTTONS_COUNT = viewMode === "skin" ? 3 : 3; const SKINS_START_INDEX = TOP_BUTTONS_COUNT; 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), ); setStoredSkins(customOnes); }; const [activeSkinId, setActiveSkinId] = useState(null); const [showImportModal, setShowImportModal] = useState(false); const [modalFocusIndex, setModalFocusIndex] = useState(0); 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 processSkinImage = (url: string, defaultName: string) => { setPendingSkin({ url, defaultName }); setImportMode("model"); setModalFocusIndex(0); }; const handleFinalizeImport = (isSlim: boolean) => { if (!pendingSkin) return; const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { const cvs = document.createElement("canvas"); cvs.width = img.width; cvs.height = img.height; const ctx = cvs.getContext("2d"); if (ctx) { 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]); setSkinUrl(base64String); setSkinIsSlim(isSlim); setActiveSkinId(newId); } }; img.src = pendingSkin.url; setShowImportModal(false); setImportMode(null); setPendingSkin(null); }; const handleFetchUsername = async () => { if (!importUsername.trim()) return; playPressSound(); setIsImporting(true); setImportError(""); try { 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: unknown) { setImportError( e instanceof Error ? e.message : typeof e === "string" ? e : "Failed to fetch", ); } finally { setIsImporting(false); } }; useEffect(() => { if (!activeSkinId) { 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") { playBackSound(); if (importMode) { setImportMode(null); setImportUsername(""); setImportError(""); setModalFocusIndex(0); } else { setShowImportModal(false); setModalFocusIndex(0); } } else if (e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault(); setModalFocusIndex((prev) => (prev + 1) % 3); } else if (e.key === "ArrowUp") { e.preventDefault(); 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) { playBackSound(); setShowImportModal(false); setModalFocusIndex(0); } } else if (importMode === "username") { if (modalFocusIndex === 0 || modalFocusIndex === 1) handleFetchUsername(); else if (modalFocusIndex === 2) { playBackSound(); setImportMode(null); setImportUsername(""); setImportError(""); setModalFocusIndex(0); } } else if (importMode === "model") { if (modalFocusIndex === 0) handleFinalizeImport(false); else if (modalFocusIndex === 1) handleFinalizeImport(true); else if (modalFocusIndex === 2) { playBackSound(); setImportMode(null); setPendingSkin(null); setModalFocusIndex(0); } } } return; } if (document.activeElement?.tagName === "INPUT") return; if (e.key === "Escape") { playBackSound(); 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 (focusIndex === null || focusIndex < TOP_BUTTONS_COUNT) { setFocusIndex(SKINS_START_INDEX); } else if (focusIndex < BACK_BUTTON_INDEX) { const rowCount = viewMode === "cape" ? 3 : 4; const next = focusIndex + rowCount; setFocusIndex(next >= BACK_BUTTON_INDEX ? BACK_BUTTON_INDEX : next); } } else if (e.key === "ArrowUp") { if (focusIndex === null) { setFocusIndex(0); } else if (focusIndex === BACK_BUTTON_INDEX) { const itemCount = viewMode === "cape" ? storedCapes.length + 1 : savedSkins.length; setFocusIndex(SKINS_START_INDEX + itemCount - 1); } else if (focusIndex >= SKINS_START_INDEX) { 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) { 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"); } } }; 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; if (el) el.focus(); } }, [focusIndex]); const handleImportClick = () => { playPressSound(); setShowImportModal(true); setModalFocusIndex(0); }; const handleFileChange = (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; processSkinImage(url, defaultName); }; reader.readAsDataURL(file); e.target.value = ""; }; const handleSkinSelect = (skin: SavedSkin) => { playPressSound(); setActiveSkinId(skin.id); setSkinUrl(skin.url); setSkinIsSlim(skin.isSlim || false); }; 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); setSavedSkins(updatedSkins); setSkinUrl("/images/Default.png"); setSkinIsSlim(false); setActiveSkinId("default"); }; const handleNameChange = (id: string, newName: string) => { const updatedSkins = savedSkins.map((s) => s.id === id ? { ...s, name: newName } : s, ); setSavedSkins(updatedSkins); }; 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 (

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

{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 relative z-10 ${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" >
{isActiveCapeDefault && ( Active )}
{ 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]"}`} > X
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 relative z-10 ${isActive || isFocused ? "text-[#FFFF55]" : "text-white"}`} onClick={(e) => e.stopPropagation()} spellCheck={false} />
); })} )}
{showImportModal && viewMode === "skin" && (

Import Skin

{!importMode ? (
) : importMode === "username" ? (
setImportUsername(e.target.value)} onFocus={() => 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 relative z-10 ${modalFocusIndex === 0 ? "border-[#FFFF55]" : "border-[#373737]"}`} /> {importError && ( {importError} )}
) : importMode === "model" ? (
Choose the player model type for this skin:
) : null}
)}
); }); export default SkinsView;