diff --git a/public/images/bytebukkit.png b/public/images/bytebukkit.png new file mode 100644 index 0000000..e500d8b Binary files /dev/null and b/public/images/bytebukkit.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 87241fb..a15c581 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -63,6 +63,7 @@ pub struct AppConfig { pub sfx_vol: Option, pub legacy_mode: Option, pub mangohud_enabled: Option, + pub saved_servers: Option>, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -266,6 +267,7 @@ fn load_config(app: AppHandle) -> AppConfig { sfx_vol: Some(100), legacy_mode: Some(false), mangohud_enabled: None, + saved_servers: None, } } @@ -1345,6 +1347,13 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S if !servers.iter().any(|s| s.ip == lce_live.ip && s.port == lce_live.port) { servers.push(lce_live); } + if let Some(ref saved) = config.saved_servers { + for s in saved { + if !servers.iter().any(|existing| existing.ip == s.ip && existing.port == s.port) { + servers.push(s.clone()); + } + } + } ensure_server_list(&working_dir, servers); let game_exe = working_dir.join("Minecraft.Client.exe"); if !game_exe.exists() { diff --git a/src/components/views/WorkshopView.tsx b/src/components/views/WorkshopView.tsx index b158071..68144d2 100644 --- a/src/components/views/WorkshopView.tsx +++ b/src/components/views/WorkshopView.tsx @@ -1,16 +1,49 @@ -import { useState, useEffect, memo, useCallback, useRef, useContext } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import { useUI, useAudio, useConfig, GameContext, useGame } from '../../context/LauncherContext'; -import { TauriService, InstalledWorkshopPackage } from '../../services/TauriService'; -const REGISTRY_URL = 'https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/registry.json'; -const VERSIONS_URL = 'https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/versions.json'; -const RAW_BASE = 'https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main'; -const VERSIONS_BASE = 'https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/.00versions'; -const CATEGORY_TABS = ['Skin', 'Texture', 'World', 'Mod', 'DLC'] as const; -const ALL_TABS = [...CATEGORY_TABS, 'Versions', 'Installed', 'Search'] as const; -type TabType = typeof ALL_TABS[number]; +import { + useState, + useEffect, + memo, + useCallback, + useRef, + useContext, + useMemo, +} from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { + useUI, + useAudio, + useConfig, + GameContext, + useGame, +} from "../../context/LauncherContext"; +import { + TauriService, + InstalledWorkshopPackage, +} from "../../services/TauriService"; +const REGISTRY_URL = + "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/registry.json"; +const VERSIONS_URL = + "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/versions.json"; +const RAW_BASE = + "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main"; +const VERSIONS_BASE = + "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main/.00versions"; +const BYTEBUKKIT_BASE = "https://emerald-bytebukkit.onrender.com"; +const SERVERS_URL = + "https://raw.githubusercontent.com/bytebukkit/servers/refs/heads/main/servers.json"; +const SERVERS_BASE = + "https://raw.githubusercontent.com/bytebukkit/servers/refs/heads/main"; +const CATEGORY_TABS = ["Skin", "Texture", "World", "Mod", "DLC"] as const; +const ALL_TABS = [ + ...CATEGORY_TABS, + "Versions", + "Installed", + "Server", + "Plugins", + "Search", +] as const; +type TabType = (typeof ALL_TABS)[number]; interface RegistryPackage { id: string; name: string; @@ -23,6 +56,44 @@ interface RegistryPackage { version: string; logo?: string; url?: string; + likes?: number; + download_count?: number; + game_version?: string; + github_url?: string; + file_name?: string; + file_size?: number; + server_address?: string; + server_homepage?: string; + server_type?: string; +} + +interface ServerListing { + server_name: string; + server_type: string; + server_address: string; + server_owner: string; + server_homepage?: string; + console_version: string; + server_icon: string; +} + +interface ByteBukkitAddon { + id: string; + name: string; + short_description: string; + description: string; + category: string; + game_version: string; + visibility: string; + github_url?: string; + created_at: string; + likes: number; + downloads: number; + file_name: string; + file_size: number; + has_image: boolean; + username: string; + displayName: string; } const COLS = 4; @@ -33,15 +104,23 @@ const WorkshopView = memo(function WorkshopView() { const containerRef = useRef(null); const gridRef = useRef(null); const searchRef = useRef(null); - const [activeTab, setActiveTab] = useState('Skin'); + const [activeTab, setActiveTab] = useState("Skin"); const [allPackages, setAllPackages] = useState([]); const [versionPackages, setVersionPackages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [focusedIdx, setFocusedIdx] = useState(null); - const [search, setSearch] = useState(''); + const [search, setSearch] = useState(""); const [selectedPkg, setSelectedPkg] = useState(null); - const [installedPkgs, setInstalledPkgs] = useState([]); + const [installedPkgs, setInstalledPkgs] = useState< + InstalledWorkshopPackage[] + >([]); + const [serverPlugins, setServerPlugins] = useState([]); + const [serverCategory, setServerCategory] = useState("all"); + const [serverListings, setServerListings] = useState([]); + const [serverListingCategory, setServerListingCategory] = + useState("all"); + const [savedServers, setSavedServers] = useState>(new Set()); const refreshInstalled = useCallback(async () => { try { const data = await TauriService.workshopListInstalled(); @@ -56,12 +135,20 @@ const WorkshopView = memo(function WorkshopView() { refreshInstalled(); }, [refreshInstalled]); + useEffect(() => { + TauriService.loadConfig() + .then((cfg) => { + setSavedServers(new Set((cfg.savedServers || []).map((s) => s.ip))); + }) + .catch(() => {}); + }, []); + useEffect(() => { setLoading(true); setError(null); Promise.all([ - fetch(REGISTRY_URL).then(r => r.json()), - fetch(VERSIONS_URL).then(r => r.json()) + fetch(REGISTRY_URL).then((r) => r.json()), + fetch(VERSIONS_URL).then((r) => r.json()), ]) .then(([registryData, versionsData]) => { setAllPackages(registryData.packages ?? []); @@ -69,150 +156,341 @@ const WorkshopView = memo(function WorkshopView() { setLoading(false); }) .catch((e) => { - setError(e.message ?? 'Failed to load registry'); + setError(e.message ?? "Failed to load registry"); setLoading(false); }); }, []); - const getInstalledEntries = useCallback((pkgId: string) => { - if (activeTab === 'Versions') { - const isAdded = config.customEditions?.some((e: any) => e.id === pkgId || (e.url === versionPackages.find(p => p.id === pkgId)?.url)); - if (isAdded) { - const vPkg = versionPackages.find(p => p.id === pkgId); - return [{ - packageId: pkgId, - instanceId: pkgId, - version: vPkg?.version || '0.0.0', - }] as InstalledWorkshopPackage[]; + useEffect(() => { + fetch(`${BYTEBUKKIT_BASE}/api/addons?limit=500`) + .then((r) => r.json()) + .then((data: ByteBukkitAddon[]) => { + setServerPlugins( + data.map((a) => ({ + id: a.id, + name: a.name, + author: a.displayName || a.username, + description: a.short_description, + extended_description: a.description, + category: [a.category], + thumbnail: `${BYTEBUKKIT_BASE}/api/addons/${a.id}/icon`, + version: "1.0", + likes: a.likes, + download_count: a.downloads, + game_version: a.game_version, + github_url: a.github_url, + file_name: a.file_name, + file_size: a.file_size, + })), + ); + }) + .catch(() => {}); + }, []); + + useEffect(() => { + fetch(SERVERS_URL) + .then((r) => r.json()) + .then((data: { servers: ServerListing[] }) => { + setServerListings( + data.servers.map((s) => ({ + id: s.server_name.toLowerCase().replace(/\s+/g, "-"), + name: s.server_name, + author: s.server_owner, + description: s.server_address, + extended_description: `**Type:** ${s.server_type}\n**Console:** ${s.console_version}\n**Owner:** ${s.server_owner}`, + category: [s.server_type], + thumbnail: `${SERVERS_BASE}${s.server_icon}`, + version: s.console_version, + server_address: s.server_address, + server_homepage: s.server_homepage, + server_type: s.server_type, + })), + ); + }) + .catch(() => {}); + }, []); + + const serverCategories = useMemo(() => { + const cats = new Set(serverPlugins.flatMap((p) => p.category)); + return ["all", ...cats]; + }, [serverPlugins]); + + const serverListingCategories = useMemo(() => { + const cats = new Set(serverListings.flatMap((p) => p.category)); + return ["all", ...cats]; + }, [serverListings]); + + const getInstalledEntries = useCallback( + (pkgId: string) => { + if (activeTab === "Versions") { + const isAdded = config.customEditions?.some( + (e: any) => + e.id === pkgId || + e.url === versionPackages.find((p) => p.id === pkgId)?.url, + ); + if (isAdded) { + const vPkg = versionPackages.find((p) => p.id === pkgId); + return [ + { + packageId: pkgId, + instanceId: pkgId, + version: vPkg?.version || "0.0.0", + }, + ] as InstalledWorkshopPackage[]; + } + return []; } - return []; - } - return installedPkgs.filter((p) => p.packageId === pkgId); - }, [installedPkgs, activeTab, config.customEditions, versionPackages]); + if (activeTab === "Plugins" || activeTab === "Server") return []; + return installedPkgs.filter((p) => p.packageId === pkgId); + }, + [installedPkgs, activeTab, config.customEditions, versionPackages], + ); - const isInstalled = useCallback((pkgId: string) => { - if (activeTab === 'Versions') { - return config.customEditions?.some((e: any) => e.id === pkgId || (e.url === versionPackages.find(p => p.id === pkgId)?.url)) ?? false; - } - return installedPkgs.some((p) => p.packageId === pkgId); - }, [installedPkgs, activeTab, config.customEditions, versionPackages]); + const isInstalled = useCallback( + (pkgId: string) => { + if (activeTab === "Plugins" || activeTab === "Server") return false; + if (activeTab === "Versions") { + return ( + config.customEditions?.some( + (e: any) => + e.id === pkgId || + e.url === versionPackages.find((p) => p.id === pkgId)?.url, + ) ?? false + ); + } + return installedPkgs.some((p) => p.packageId === pkgId); + }, + [installedPkgs, activeTab, config.customEditions, versionPackages], + ); - const hasUpdate = useCallback((pkg: RegistryPackage) => { - if (activeTab === 'Versions') return false; - const entries = installedPkgs.filter((p) => p.packageId === pkg.id); - return entries.length > 0 && entries.some((e) => e.version !== pkg.version); - }, [installedPkgs, activeTab]); + const hasUpdate = useCallback( + (pkg: RegistryPackage) => { + if ( + activeTab === "Versions" || + activeTab === "Plugins" || + activeTab === "Server" + ) + return false; + const entries = installedPkgs.filter((p) => p.packageId === pkg.id); + return ( + entries.length > 0 && entries.some((e) => e.version !== pkg.version) + ); + }, + [installedPkgs, activeTab], + ); const installedPackageList = allPackages.filter((pkg) => isInstalled(pkg.id)); - const filteredItems = activeTab === 'Installed' - ? (search.trim() - ? installedPackageList.filter((pkg) => { - const q = search.toLowerCase(); - return pkg.name.toLowerCase().includes(q) || pkg.author.toLowerCase().includes(q) || pkg.description.toLowerCase().includes(q); - }) - : installedPackageList) - : (activeTab === 'Versions' ? versionPackages : allPackages).filter((pkg) => { - const matchesTab = (activeTab === 'Search' || activeTab === 'Versions') ? 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) - ); - }); + const filteredItems = + activeTab === "Installed" + ? search.trim() + ? installedPackageList.filter((pkg) => { + const q = search.toLowerCase(); + return ( + pkg.name.toLowerCase().includes(q) || + pkg.author.toLowerCase().includes(q) || + pkg.description.toLowerCase().includes(q) + ); + }) + : installedPackageList + : activeTab === "Plugins" + ? search.trim() + ? serverPlugins.filter((pkg) => { + if ( + serverCategory !== "all" && + !pkg.category.includes(serverCategory) + ) + return false; + const q = search.toLowerCase(); + return ( + pkg.name.toLowerCase().includes(q) || + pkg.author.toLowerCase().includes(q) || + pkg.description.toLowerCase().includes(q) + ); + }) + : serverCategory === "all" + ? serverPlugins + : serverPlugins.filter((pkg) => + pkg.category.includes(serverCategory), + ) + : activeTab === "Server" + ? search.trim() + ? serverListings.filter((pkg) => { + if ( + serverListingCategory !== "all" && + !pkg.category.includes(serverListingCategory) + ) + return false; + const q = search.toLowerCase(); + return ( + pkg.name.toLowerCase().includes(q) || + pkg.author.toLowerCase().includes(q) || + pkg.description.toLowerCase().includes(q) + ); + }) + : serverListingCategory === "all" + ? serverListings + : serverListings.filter((pkg) => + pkg.category.includes(serverListingCategory), + ) + : (activeTab === "Versions" ? versionPackages : allPackages).filter( + (pkg) => { + const matchesTab = + activeTab === "Search" || activeTab === "Versions" + ? 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') { + if (activeTab === "Search") { setTimeout(() => searchRef.current?.focus(), 50); } else { - setSearch(''); + setSearch(""); } }, [activeTab]); useEffect(() => { if (focusedIdx !== null && gridRef.current) { - const el = gridRef.current.querySelector(`[data-card="${focusedIdx}"]`) as HTMLElement; - el?.scrollIntoView({ block: 'nearest' }); + 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) { + const cycleTab = useCallback( + (direction: "next" | "prev") => { playPressSound(); - setActiveTab(tab); - } - }, [activeTab, 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 openModal = useCallback((pkg: RegistryPackage) => { - playPressSound(); - setSelectedPkg(pkg); - }, [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]); + const toggleSavedServer = useCallback(async (serverPkg: RegistryPackage) => { + if (!serverPkg.server_address) return; + const cfg = await TauriService.loadConfig(); + const current = cfg.savedServers || []; + const exists = current.some((s) => s.ip === serverPkg.server_address); + const newSaved = exists + ? current.filter((s) => s.ip !== serverPkg.server_address) + : [ + ...current, + { name: serverPkg.name, ip: serverPkg.server_address, port: 25565 }, + ]; + await TauriService.saveConfig({ ...cfg, savedServers: newSaved }); + setSavedServers(new Set(newSaved.map((s) => s.ip))); + }, []); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (selectedPkg) return; const isSearchInput = document.activeElement === searchRef.current; if (isSearchInput) { - if (e.key === 'Escape') { setSearch(''); containerRef.current?.focus(); } + 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 === "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 (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') { + if (e.key === "ArrowRight") { e.preventDefault(); setFocusedIdx((p) => Math.min((p ?? -1) + 1, count - 1)); playPressSound(); - } else if (e.key === 'ArrowLeft') { + } else if (e.key === "ArrowLeft") { e.preventDefault(); setFocusedIdx((p) => Math.max((p ?? 1) - 1, 0)); playPressSound(); - } else if (e.key === 'ArrowDown') { + } else if (e.key === "ArrowDown") { e.preventDefault(); setFocusedIdx((p) => Math.min((p ?? -COLS) + COLS, count - 1)); playPressSound(); - } else if (e.key === 'ArrowUp') { + } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusedIdx((p) => Math.max((p ?? COLS) - COLS, 0)); playPressSound(); - } else if (e.key === 'Enter' && focusedIdx !== null) { + } 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]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + playBackSound, + playPressSound, + setActiveView, + cycleTab, + filteredItems, + focusedIdx, + selectedPkg, + openModal, + ]); - const isSearchTab = activeTab === 'Search'; - const isInstalledTab = activeTab === 'Installed'; - const isVersionTab = activeTab === 'Versions'; - const showSearch = isSearchTab || isInstalledTab || isVersionTab; + const isSearchTab = activeTab === "Search"; + const isInstalledTab = activeTab === "Installed"; + const isVersionTab = activeTab === "Versions"; + const showSearch = + isSearchTab || + isInstalledTab || + isVersionTab || + activeTab === "Plugins" || + activeTab === "Server"; return ( {ALL_TABS.map((tab) => { const isActive = tab === activeTab; - const updateCount = tab === 'Installed' - ? allPackages.filter((p) => hasUpdate(p)).length - : 0; + const updateCount = + tab === "Installed" + ? allPackages.filter((p) => hasUpdate(p)).length + : 0; return ( + ))} + + )} + {activeTab === "Server" && serverListingCategories.length > 1 && ( +
+ {serverListingCategories.map((cat) => ( + + ))} +
+ )} +
{isSearchTab && !search.trim() ? (
- Start typing to search... + + Start typing to search... +
) : loading ? (
- Searching Archives... + + Searching Archives... +
) : filteredItems.length === 0 ? (
- {isInstalledTab ? 'Nothing Installed' : 'No results'} + {isInstalledTab + ? "Nothing Installed" + : activeTab === "Plugins" + ? "No plugins available" + : activeTab === "Server" + ? "No servers available" + : "No results"}
) : ( -
+
{filteredItems.map((pkg, i) => ( )} + {(activeTab === "Plugins" || activeTab === "Server") && ( +
+ ByteBukkit +
+ )}
) : loading ? ( - - Searching Archives... + + + Searching Archives... + ) : error ? ( - - {error} + + + {error} + ) : ( {filteredItems.length === 0 ? (
- Empty category + + Empty category +
) : ( -
+
{filteredItems.map((pkg, i) => ( @@ -402,7 +785,15 @@ const WorkshopView = memo(function WorkshopView() { installedEntries={getInstalledEntries(selectedPkg.id)} onInstallComplete={refreshInstalled} onUninstallComplete={refreshInstalled} - isVersionTab={activeTab === 'Versions'} + isVersionTab={activeTab === "Versions"} + isServerTab={activeTab === "Plugins"} + isGameServerTab={activeTab === "Server"} + isSaved={ + selectedPkg.server_address + ? savedServers.has(selectedPkg.server_address) + : false + } + onToggleSave={toggleSavedServer} /> )} @@ -410,7 +801,16 @@ const WorkshopView = memo(function WorkshopView() { ); }); -function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpdate, isVersionTab }: { +function PackageCard({ + pkg, + index, + focused, + onHover, + onClick, + installed, + hasUpdate, + isVersionTab, +}: { pkg: RegistryPackage; index: number; focused: boolean; @@ -420,53 +820,79 @@ function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpda hasUpdate: boolean; isVersionTab?: boolean; }) { - const thumbnailUrl = isVersionTab - ? `${VERSIONS_BASE}/${pkg.id}/${pkg.thumbnail}` - : `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; + const thumbnailUrl = pkg.thumbnail.startsWith("http") + ? pkg.thumbnail + : isVersionTab + ? `${VERSIONS_BASE}/${pkg.id}/${pkg.thumbnail}` + : `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; const [imgError, setImgError] = useState(false); return (
-
+
{imgError ? ( - No Image + + No Image + ) : ( - {pkg.name} setImgError(true)} /> + {pkg.name} setImgError(true)} + /> )}
{pkg.category.slice(0, 1).map((c) => ( - {c} + + {c} + ))}
{hasUpdate && (
- Update + + Update +
)} {installed && !hasUpdate && (
- {isVersionTab ? 'Added' : 'Installed'} + + {isVersionTab ? "Added" : "Installed"} +
)}
- + {pkg.name}
- v{pkg.version} - {pkg.author} + + v{pkg.version} + + + {pkg.author} +

{pkg.description} @@ -476,7 +902,19 @@ function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpda ); } -function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstallComplete, onUninstallComplete, isVersionTab }: { +function PackageModal({ + pkg, + onClose, + playPressSound, + installedEntries, + onInstallComplete, + onUninstallComplete, + isVersionTab, + isServerTab, + isGameServerTab, + isSaved, + onToggleSave, +}: { pkg: RegistryPackage; onClose: () => void; playPressSound: () => void; @@ -484,45 +922,90 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal onInstallComplete: () => void; onUninstallComplete: () => void; isVersionTab?: boolean; + isServerTab?: boolean; + isGameServerTab?: boolean; + isSaved?: boolean; + onToggleSave?: (pkg: RegistryPackage) => void; }) { const { addCustomEdition } = useGame(); - const thumbnailUrl = isVersionTab - ? `${VERSIONS_BASE}/${pkg.id}/${pkg.thumbnail}` - : `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; + const thumbnailUrl = pkg.thumbnail.startsWith("http") + ? pkg.thumbnail + : isVersionTab + ? `${VERSIONS_BASE}/${pkg.id}/${pkg.thumbnail}` + : `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; const [imgError, setImgError] = useState(false); - const [modalFocus, setModalFocus] = useState<'install' | 'uninstall' | 'close'>('install'); + const [modalFocus, setModalFocus] = useState< + "install" | "uninstall" | "close" + >("install"); const [showInstall, setShowInstall] = useState(false); const [showUninstall, setShowUninstall] = useState(false); const hasInstalled = installedEntries.length > 0; - const needsUpdate = hasInstalled && installedEntries.some((e) => e.version !== pkg.version); - const focusOptions: Array<'install' | 'uninstall' | 'close'> = (hasInstalled || isVersionTab) - ? ['install', 'uninstall', 'close'] - : ['install', 'close']; + const needsUpdate = + hasInstalled && installedEntries.some((e) => e.version !== pkg.version); + const focusOptions: Array<"install" | "uninstall" | "close"> = isGameServerTab + ? ["install", "close"] + : isServerTab + ? ["install", "close"] + : hasInstalled || isVersionTab + ? ["install", "uninstall", "close"] + : ["install", "close"]; useEffect(() => { if (showInstall || showUninstall) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' || e.key === 'Backspace') { + if (e.key === "Escape" || e.key === "Backspace") { onClose(); - } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Tab') { + } else if ( + e.key === "ArrowLeft" || + e.key === "ArrowRight" || + e.key === "Tab" + ) { e.preventDefault(); playPressSound(); setModalFocus((p) => { const idx = focusOptions.indexOf(p); return focusOptions[(idx + 1) % focusOptions.length]; }); - } else if (e.key === 'Enter') { - if (modalFocus === 'close') onClose(); - else if (modalFocus === 'install') handleAction(); - else if (modalFocus === 'uninstall') setShowUninstall(true); + } else if (e.key === "Enter") { + if (modalFocus === "close") onClose(); + else if (modalFocus === "install") handleAction(); + else if (modalFocus === "uninstall") setShowUninstall(true); } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [modalFocus, showInstall, showUninstall, onClose, playPressSound, focusOptions]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + modalFocus, + showInstall, + showUninstall, + onClose, + playPressSound, + focusOptions, + ]); const handleAction = async () => { - if (isVersionTab) { + if (isGameServerTab) { + playPressSound(); + if (onToggleSave) onToggleSave(pkg); + } else if (isServerTab) { + playPressSound(); + try { + const path = await TauriService.saveFileDialog( + "Save Server Plugin", + pkg.file_name || `${pkg.name}.dll`, + ["*.dll", "*"], + ); + if (!path) return; + const response = await fetch( + `${BYTEBUKKIT_BASE}/api/addons/${pkg.id}/download`, + ); + const blob = await response.blob(); + const buffer = await blob.arrayBuffer(); + await TauriService.writeBinaryFile(path, new Uint8Array(buffer)); + } catch (e) { + console.error(e); + } + } else if (isVersionTab) { if (hasInstalled) return; playPressSound(); try { @@ -534,7 +1017,7 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal desc: pkg.description, url: pkg.url!, category: pkg.category, - logo: localLogoPath + logo: localLogoPath, }); onInstallComplete(); } catch (e) { @@ -545,7 +1028,21 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal } }; - const installLabel = isVersionTab ? (hasInstalled ? 'ADDED' : 'ADD') : (!hasInstalled ? 'INSTALL' : needsUpdate ? 'UPDATE' : 'REINSTALL'); + const installLabel = isGameServerTab + ? isSaved + ? "ADDED" + : "ADD" + : isServerTab + ? "DOWNLOAD" + : isVersionTab + ? hasInstalled + ? "ADDED" + : "ADD" + : !hasInstalled + ? "INSTALL" + : needsUpdate + ? "UPDATE" + : "REINSTALL"; return ( <>

{imgError ? (
- No Image + + No Image +
) : ( - {pkg.name} setImgError(true)} /> + {pkg.name} setImgError(true)} + /> )}
- {pkg.name} - By {pkg.author} + + {pkg.name} + + + By {pkg.author} +
{needsUpdate && (
- Update Available + + Update Available +
)} {hasInstalled && !needsUpdate && (
- Installed + + Installed + +
+ )} + {isGameServerTab && isSaved && ( +
+ + Saved +
)}
@@ -597,60 +1116,219 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal
- Project Description -

{pkg.description}

+ + Project Description + +

+ {pkg.description} +

- {pkg.extended_description && pkg.extended_description.trim() !== "" && ( -
- Additional Resources -
- {pkg.extended_description} + {pkg.extended_description && + pkg.extended_description.trim() !== "" && ( +
+ + Additional Resources + +
+ + {pkg.extended_description} + +
-
- )} + )}
- Metadata + + Metadata +
-
- Version: - v{pkg.version} -
-
- Package ID: - {pkg.id} -
+ {isGameServerTab ? ( + <> +
+ + Address: + + + {pkg.server_address || "N/A"} + +
+
+ + Type: + + + {pkg.server_type || "N/A"} + +
+
+ + Console: + + + {pkg.version} + +
+
+ + Owner: + + + {pkg.author} + +
+ {pkg.server_homepage && ( + + )} + + ) : isServerTab ? ( + <> +
+ + Downloads: + + + {pkg.download_count?.toLocaleString() ?? 0} + +
+
+ + Likes: + + + {pkg.likes?.toLocaleString() ?? 0} + +
+
+ + Game Version: + + + {pkg.game_version || "N/A"} + +
+
+ + File: + + + {pkg.file_name || "N/A"} + +
+ {pkg.file_size && ( +
+ + File Size: + + + {(pkg.file_size / 1024).toFixed(1)} KB + +
+ )} + {pkg.github_url && ( + + )} + + ) : ( + <> +
+ + Version: + + + v{pkg.version} + +
+
+ + Package ID: + + + {pkg.id} + +
+ + )} {hasInstalled && (
- Installed: - + + Installed: + + v{installedEntries[0]?.version} - {needsUpdate ? ' (outdated)' : ''} + {needsUpdate ? " (outdated)" : ""}
)}
- Categories + + Categories +
{pkg.category.map((c) => ( - {c} + + {c} + ))}
{pkg.zips && Object.keys(pkg.zips).length > 0 && (
- Files + + Files +
{Object.entries(pkg.zips).map(([file, dest]) => ( -
- {file} - {dest && {dest}} +
+ + {file} + + {dest && ( + + {dest} + + )}
))}
@@ -659,39 +1337,48 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal
{hasInstalled && ( )}
)} - {status === 'idle' && ( - availableEditions.length === 0 ? ( + {status === "idle" && + (availableEditions.length === 0 ? (
- No installed editions found + + No installed editions found +
) : ( availableEditions.map((ed, i) => ( @@ -842,20 +1570,29 @@ function InstallModal({ pkg, onClose, playPressSound }: { key={ed.id} onClick={() => 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'}`} + 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} + + {ed.name} +
)) - ) - )} + ))}
); } -function UninstallModal({ pkg, installedEntries, onClose, playPressSound, isVersionTab }: { +function UninstallModal({ + pkg, + installedEntries, + onClose, + playPressSound, + isVersionTab, +}: { pkg: RegistryPackage; installedEntries: InstalledWorkshopPackage[]; onClose: () => void; @@ -865,44 +1602,47 @@ function UninstallModal({ pkg, installedEntries, onClose, playPressSound, isVers const { deleteCustomEdition } = useGame(); const game = useContext(GameContext); const [focusedIdx, setFocusedIdx] = useState(0); - const [status, setStatus] = useState<'idle' | 'removing' | 'success' | 'error'>('idle'); + const [status, setStatus] = useState< + "idle" | "removing" | "success" | "error" + >("idle"); const [errorMsg, setErrorMsg] = useState(null); const editionName = (instanceId: string) => { - const ed = game?.editions.find(e => e.id === instanceId); + const ed = game?.editions.find((e) => e.id === instanceId); return ed?.name ?? instanceId; }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); - if (status === 'removing') return; - if (status === 'success') { - if (e.key === 'Escape' || e.key === 'Backspace' || e.key === 'Enter') onClose(); + if (status === "removing") return; + if (status === "success") { + if (e.key === "Escape" || e.key === "Backspace" || e.key === "Enter") + onClose(); return; } - if (e.key === 'Escape' || e.key === 'Backspace') { + if (e.key === "Escape" || e.key === "Backspace") { onClose(); - } else if (e.key === 'ArrowUp') { + } else if (e.key === "ArrowUp") { e.preventDefault(); playPressSound(); setFocusedIdx((p) => Math.max(p - 1, 0)); - } else if (e.key === 'ArrowDown') { + } else if (e.key === "ArrowDown") { e.preventDefault(); playPressSound(); setFocusedIdx((p) => Math.min(p + 1, installedEntries.length - 1)); - } else if (e.key === 'Enter') { + } else if (e.key === "Enter") { if (installedEntries.length > 0) { uninstallFrom(installedEntries[focusedIdx].instanceId); } } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); }, [installedEntries, focusedIdx, status, onClose, playPressSound]); const uninstallFrom = async (instanceId: string) => { - setStatus('removing'); + setStatus("removing"); setErrorMsg(null); playPressSound(); try { @@ -911,11 +1651,11 @@ function UninstallModal({ pkg, installedEntries, onClose, playPressSound, isVers } else { await TauriService.workshopUninstall(instanceId, pkg.id); } - setStatus('success'); + setStatus("success"); } catch (e: any) { console.error(e); - setStatus('error'); - setErrorMsg(typeof e === 'string' ? e : e.message); + setStatus("error"); + setErrorMsg(typeof e === "string" ? e : e.message); } }; @@ -925,7 +1665,7 @@ function UninstallModal({ pkg, installedEntries, onClose, playPressSound, isVers animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-[300] flex items-center justify-center bg-black/80" - onClick={status !== 'removing' ? onClose : undefined} + onClick={status !== "removing" ? onClose : undefined} >
- REMOVE CONTENT - Select edition to remove "{pkg.name}" + + REMOVE CONTENT + + + Select edition to remove "{pkg.name}" +
- {status === 'removing' && ( + {status === "removing" && (
- Removing... - Deleting installed files + + Removing... + + + Deleting installed files +
)} - {status === 'success' && ( + {status === "success" && (
- Removed Successfully! - Press any key or click to continue + + Removed Successfully! + + + Press any key or click to continue +
)} - {status === 'error' && ( + {status === "error" && (
- Removal Failed - {errorMsg} + + Removal Failed + + + {errorMsg} +
)} - {status === 'idle' && installedEntries.map((entry, i) => ( -
uninstallFrom(entry.instanceId)} - onMouseEnter={() => setFocusedIdx(i)} - className={`flex items-center justify-between p-3 cursor-pointer border-2 transition-none ${focusedIdx === i ? 'border-[#FF5555] bg-black/40' : 'border-[#444] bg-black/20'}`} - > - {editionName(entry.instanceId)} - v{entry.version} -
- ))} + {status === "idle" && + installedEntries.map((entry, i) => ( +
uninstallFrom(entry.instanceId)} + onMouseEnter={() => setFocusedIdx(i)} + className={`flex items-center justify-between p-3 cursor-pointer border-2 transition-none ${focusedIdx === i ? "border-[#FF5555] bg-black/40" : "border-[#444] bg-black/20"}`} + > + + {editionName(entry.instanceId)} + + + v{entry.version} + +
+ ))}
diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index e7327ef..ef3539e 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -40,6 +40,7 @@ export interface AppConfig { sfxVol?: number; legacyMode?: boolean; mangohudEnabled?: boolean; + savedServers?: McServer[]; } export interface ThemePalette {