import { useState, useEffect, memo, useCallback, useRef, useContext } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useUI, useAudio, useConfig, GameContext } from '../../context/LauncherContext'; import { TauriService } from '../../services/TauriService'; const REGISTRY_URL = 'https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/registry.json'; const RAW_BASE = 'https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main'; const CATEGORY_TABS = ['Skin', 'Texture', 'World', 'Mod', 'DLC'] as const; const ALL_TABS = [...CATEGORY_TABS, 'Search'] as const; type TabType = typeof ALL_TABS[number]; interface RegistryPackage { id: string; name: string; author: string; description: string; category: string[]; thumbnail: string; zips: Record; version: string; } const COLS = 4; const WorkshopView = memo(function WorkshopView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); const config = useConfig(); const containerRef = useRef(null); const gridRef = useRef(null); const searchRef = useRef(null); const [activeTab, setActiveTab] = useState('Skin'); const [allPackages, setAllPackages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [focusedIdx, setFocusedIdx] = useState(null); const [search, setSearch] = useState(''); const [selectedPkg, setSelectedPkg] = useState(null); useEffect(() => { containerRef.current?.focus(); }, []); useEffect(() => { setLoading(true); setError(null); fetch(REGISTRY_URL) .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then((data) => { setAllPackages(data.packages ?? []); setLoading(false); }) .catch((e) => { setError(e.message ?? 'Failed to load registry'); setLoading(false); }); }, []); const filteredItems = allPackages.filter((pkg) => { const matchesTab = activeTab === 'Search' ? true : pkg.category.includes(activeTab); if (!matchesTab) return false; if (!search.trim()) return activeTab === 'Search' ? false : true; const q = search.toLowerCase(); return ( pkg.name.toLowerCase().includes(q) || pkg.author.toLowerCase().includes(q) || pkg.description.toLowerCase().includes(q) ); }); useEffect(() => { setFocusedIdx(null); if (activeTab === 'Search') { setTimeout(() => searchRef.current?.focus(), 50); } else { setSearch(''); } }, [activeTab]); useEffect(() => { if (focusedIdx !== null && gridRef.current) { const el = gridRef.current.querySelector(`[data-card="${focusedIdx}"]`) as HTMLElement; el?.scrollIntoView({ block: 'nearest' }); } }, [focusedIdx]); const cycleTab = useCallback((direction: 'next' | 'prev') => { playPressSound(); setActiveTab((prev) => { const idx = ALL_TABS.indexOf(prev); if (direction === 'next') return ALL_TABS[(idx + 1) % ALL_TABS.length]; return ALL_TABS[(idx - 1 + ALL_TABS.length) % ALL_TABS.length]; }); }, [playPressSound]); const selectTab = useCallback((tab: TabType) => { if (tab !== activeTab) { playPressSound(); setActiveTab(tab); } }, [activeTab, playPressSound]); const openModal = useCallback((pkg: RegistryPackage) => { playPressSound(); setSelectedPkg(pkg); }, [playPressSound]); const closeModal = useCallback(() => { playBackSound(); setSelectedPkg(null); }, [playBackSound]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (selectedPkg) return; const isSearchInput = document.activeElement === searchRef.current; if (isSearchInput) { if (e.key === 'Escape') { setSearch(''); containerRef.current?.focus(); } return; } const count = filteredItems.length; if (e.key === 'Escape' || e.key === 'Backspace') { playBackSound(); setActiveView('main'); return; } if (e.key === 'e' || e.key === 'E') { cycleTab('next'); return; } if (e.key === 'q' || e.key === 'Q') { cycleTab('prev'); return; } if (count === 0) return; if (e.key === 'ArrowRight') { e.preventDefault(); setFocusedIdx((p) => Math.min((p ?? -1) + 1, count - 1)); playPressSound(); } else if (e.key === 'ArrowLeft') { e.preventDefault(); setFocusedIdx((p) => Math.max((p ?? 1) - 1, 0)); playPressSound(); } else if (e.key === 'ArrowDown') { e.preventDefault(); setFocusedIdx((p) => Math.min((p ?? -COLS) + COLS, count - 1)); playPressSound(); } else if (e.key === 'ArrowUp') { e.preventDefault(); setFocusedIdx((p) => Math.max((p ?? COLS) - COLS, 0)); playPressSound(); } else if (e.key === 'Enter' && focusedIdx !== null) { const pkg = filteredItems[focusedIdx]; if (pkg) openModal(pkg); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [playBackSound, playPressSound, setActiveView, cycleTab, filteredItems, focusedIdx, selectedPkg, openModal]); const isSearchTab = activeTab === 'Search'; return (
{ALL_TABS.map((tab) => { const isActive = tab === activeTab; return ( ); })}
{!isSearchTab && ( Entries: {loading ? '...' : filteredItems.length} )}
{isSearchTab ? (
{ setSearch(e.target.value); setFocusedIdx(null); }} placeholder="Search all workshop entries..." spellCheck={false} autoFocus className="bg-transparent border-none outline-none text-white text-base mc-text-shadow w-full placeholder-white font-['Mojangles']" /> {search && ( )}
{search.trim() ? `${filteredItems.length} result${filteredItems.length !== 1 ? 's' : ''}` : ''}
{search.trim() && (
{filteredItems.length === 0 ? (
No results
) : (
{filteredItems.map((pkg, i) => ( setFocusedIdx(i)} onClick={() => openModal(pkg)} /> ))}
)}
)}
) : loading ? ( Please wait ) : error ? ( {error} ) : filteredItems.length === 0 ? ( No entries ) : (
{filteredItems.map((pkg, i) => ( setFocusedIdx(i)} onClick={() => openModal(pkg)} /> ))}
)}
{selectedPkg && ( )}
); }); function PackageCard({ pkg, index, focused, onHover, onClick }: { pkg: RegistryPackage; index: number; focused: boolean; onHover: () => void; onClick: () => void; }) { const thumbnailUrl = `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; const [imgError, setImgError] = useState(false); return (
{imgError ? ( No Image ) : ( {pkg.name} setImgError(true)} /> )}
{pkg.name} by {pkg.author} {pkg.description}
v{pkg.version}
{pkg.category.map((c) => ( {c} ))}
); } function PackageModal({ pkg, onClose, playPressSound }: { pkg: RegistryPackage; onClose: () => void; playPressSound: () => void; }) { const thumbnailUrl = `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; const [imgError, setImgError] = useState(false); const [modalFocus, setModalFocus] = useState<'install' | 'close'>('install'); const [showInstall, setShowInstall] = useState(false); useEffect(() => { if (showInstall) return; //neo: let install modal handle keys const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' || e.key === 'Backspace') { onClose(); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Tab') { e.preventDefault(); playPressSound(); setModalFocus((p) => p === 'install' ? 'close' : 'install'); } else if (e.key === 'Enter') { if (modalFocus === 'close') onClose(); else if (modalFocus === 'install') setShowInstall(true); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [modalFocus, showInstall, onClose, playPressSound]); return ( <> e.stopPropagation()} className="flex flex-col w-[560px] max-h-[80vh] overflow-hidden font-['Mojangles']" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated', }} >
{imgError ? (
No Image
) : ( {pkg.name} setImgError(true)} /> )}
{pkg.name} by {pkg.author}

{pkg.description}

Version v{pkg.version}
Categories
{pkg.category.map((c) => ( {c} ))}
{Object.keys(pkg.zips).length > 0 && (
Files {Object.entries(pkg.zips).map(([file, dest]) => (
{file} {dest && {dest}}
))}
)}
{showInstall && ( setShowInstall(false)} playPressSound={playPressSound} /> )} ); } function InstallModal({ pkg, onClose, playPressSound }: { pkg: RegistryPackage; onClose: () => void; playPressSound: () => void; }) { const game = useContext(GameContext); const availableEditions = game?.editions.filter(e => game.installs.includes(e.id)) || []; const [focusedIdx, setFocusedIdx] = useState(0); const [status, setStatus] = useState<'idle' | 'installing' | 'success' | 'error'>('idle'); const [errorMsg, setErrorMsg] = useState(null); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); if (status === 'installing') return; if (status === 'success') { if (e.key === 'Escape' || e.key === 'Backspace' || e.key === 'Enter') onClose(); return; } if (e.key === 'Escape' || e.key === 'Backspace') { onClose(); } else if (e.key === 'ArrowUp') { e.preventDefault(); playPressSound(); setFocusedIdx((p) => Math.max(p - 1, 0)); } else if (e.key === 'ArrowDown') { e.preventDefault(); playPressSound(); setFocusedIdx((p) => Math.min(p + 1, availableEditions.length - 1)); } else if (e.key === 'Enter') { if (availableEditions.length > 0) { installTo(availableEditions[focusedIdx].id); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [availableEditions, focusedIdx, status, onClose, playPressSound]); const installTo = async (instanceId: string) => { setStatus('installing'); setErrorMsg(null); playPressSound(); try { await TauriService.workshopInstall(instanceId, pkg.id, pkg.zips); setStatus('success'); } catch (e: any) { console.error(e); setStatus('error'); setErrorMsg(typeof e === 'string' ? e : e.message); } }; return ( e.stopPropagation()} className="flex flex-col w-[480px] font-['Mojangles'] text-white" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: '100% 100%', imageRendering: 'pixelated', }} >
Install to Edition Select an installed edition for "{pkg.name}"
{status === 'installing' && (
Installing... Downloading and extracting assets
)} {status === 'success' && (
Installed Successfully! Press any key or click to continue
)} {status === 'error' && (
Installation Failed {errorMsg}
)} {status === 'idle' && ( availableEditions.length === 0 ? (
No installed editions found
) : ( availableEditions.map((ed, i) => (
installTo(ed.id)} onMouseEnter={() => setFocusedIdx(i)} className={`flex flex-col p-3 cursor-pointer border-2 transition-none ${focusedIdx === i ? 'border-[#FFFF55] bg-black/40' : 'border-[#444] bg-black/20'}`} > {ed.name}
)) ) )}
); } export default WorkshopView;