From d85c9b2a2c39e87155dfbf12d8ef2405bfa0b82b Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Sun, 17 May 2026 21:57:20 +0300 Subject: [PATCH] feat(Workshop): collab with ByteBukkit to bring plugins and servers --- public/images/bytebukkit.png | Bin 0 -> 9846 bytes src-tauri/src/lib.rs | 9 + src/components/views/WorkshopView.tsx | 1387 +++++++++++++++++++------ src/services/TauriService.ts | 1 + 4 files changed, 1087 insertions(+), 310 deletions(-) create mode 100644 public/images/bytebukkit.png diff --git a/public/images/bytebukkit.png b/public/images/bytebukkit.png new file mode 100644 index 0000000000000000000000000000000000000000..e500d8b815cde97e991cb326bec51b985e48413f GIT binary patch literal 9846 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 literal 0 HcmV?d00001 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 {