From 1b36112fdea8b8038ac364b86dbe7ecb93150302 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Fri, 24 Apr 2026 18:05:58 +0300 Subject: [PATCH] feat: versions-in-workshop! --- src-tauri/src/lib.rs | 29 +++++- src/components/views/VersionsView.tsx | 10 ++- src/components/views/WorkshopView.tsx | 124 +++++++++++++++++++------- src/hooks/useGameManager.ts | 4 +- src/services/TauriService.ts | 6 ++ 5 files changed, 136 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 877fc6c..3180f86 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,6 +36,8 @@ pub struct CustomEdition { pub desc: String, pub url: String, pub path: Option, + pub category: Option>, + pub logo: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -1503,6 +1505,31 @@ async fn fetch_skin(username: String) -> Result<(String, String), String> { Ok((image_b64.to_string(), name_exact)) } +#[tauri::command] +async fn download_logo(app: AppHandle, id: String, url: String) -> Result { + let logos_dir = get_app_dir(&app).join("logos"); + fs::create_dir_all(&logos_dir).map_err(|e| e.to_string())?; + let file_ext = if url.to_lowercase().ends_with(".png") { + "png" + } else if url.to_lowercase().ends_with(".jpg") || url.to_lowercase().ends_with(".jpeg") { + "jpg" + } else { + "png" + }; + + let filename = format!("{}.{}", id, file_ext); + let dest_path = logos_dir.join(&filename); + let response = reqwest::get(&url).await.map_err(|e| e.to_string())?; + if !response.status().is_success() { + return Err(format!("Failed to download logo: {}", response.status())); + } + + let bytes = response.bytes().await.map_err(|e| e.to_string())?; + fs::write(&dest_path, bytes).map_err(|e| e.to_string())?; + + Ok(dest_path.to_string_lossy().to_string()) +} + #[tauri::command] fn get_screenshots(app: AppHandle) -> Vec { let mut screenshots = Vec::new(); @@ -1589,7 +1616,7 @@ pub fn run() { } } }) - .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast]) + .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast, download_logo]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/views/VersionsView.tsx b/src/components/views/VersionsView.tsx index a8d324f..5e85daa 100644 --- a/src/components/views/VersionsView.tsx +++ b/src/components/views/VersionsView.tsx @@ -3,7 +3,6 @@ import { motion } from "framer-motion"; import { TauriService } from "../../services/TauriService"; import CustomTUModal from "../modals/CustomTUModal"; import { useUI, useConfig, useAudio, useGame } from "../../context/LauncherContext"; - interface DeleteConfirmButtonProps { label: string; onClick: () => void; @@ -277,7 +276,7 @@ const VersionsView = memo(function VersionsView() {
{edition.logo && ( {edition.name} - {isCustom && ( + {edition.category && edition.category.map((cat: string) => ( + + {cat} + + ))} + {isCustom && !edition.category && ( Custom diff --git a/src/components/views/WorkshopView.tsx b/src/components/views/WorkshopView.tsx index 52c3aa1..b158071 100644 --- a/src/components/views/WorkshopView.tsx +++ b/src/components/views/WorkshopView.tsx @@ -2,12 +2,14 @@ import { useState, useEffect, memo, useCallback, useRef, useContext } from 'reac import { motion, AnimatePresence } from 'framer-motion'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import { useUI, useAudio, useConfig, GameContext } from '../../context/LauncherContext'; +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, 'Installed', 'Search'] as const; +const ALL_TABS = [...CATEGORY_TABS, 'Versions', 'Installed', 'Search'] as const; type TabType = typeof ALL_TABS[number]; interface RegistryPackage { id: string; @@ -17,8 +19,10 @@ interface RegistryPackage { extended_description?: string; category: string[]; thumbnail: string; - zips: Record; + zips?: Record; version: string; + logo?: string; + url?: string; } const COLS = 4; @@ -31,6 +35,7 @@ const WorkshopView = memo(function WorkshopView() { const searchRef = useRef(null); 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); @@ -54,13 +59,13 @@ const WorkshopView = memo(function WorkshopView() { 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 ?? []); + Promise.all([ + fetch(REGISTRY_URL).then(r => r.json()), + fetch(VERSIONS_URL).then(r => r.json()) + ]) + .then(([registryData, versionsData]) => { + setAllPackages(registryData.packages ?? []); + setVersionPackages(versionsData.versionlist ?? []); setLoading(false); }) .catch((e) => { @@ -70,17 +75,33 @@ const WorkshopView = memo(function WorkshopView() { }, []); 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 installedPkgs.filter((p) => p.packageId === pkgId); - }, [installedPkgs]); + }, [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]); + }, [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]); + }, [installedPkgs, activeTab]); const installedPackageList = allPackages.filter((pkg) => isInstalled(pkg.id)); const filteredItems = activeTab === 'Installed' @@ -90,8 +111,8 @@ const WorkshopView = memo(function WorkshopView() { return pkg.name.toLowerCase().includes(q) || pkg.author.toLowerCase().includes(q) || pkg.description.toLowerCase().includes(q); }) : installedPackageList) - : allPackages.filter((pkg) => { - const matchesTab = activeTab === 'Search' ? true : pkg.category.includes(activeTab); + : (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(); @@ -190,7 +211,8 @@ const WorkshopView = memo(function WorkshopView() { const isSearchTab = activeTab === 'Search'; const isInstalledTab = activeTab === 'Installed'; - const showSearch = isSearchTab || isInstalledTab; + const isVersionTab = activeTab === 'Versions'; + const showSearch = isSearchTab || isInstalledTab || isVersionTab; return ( { setSearch(e.target.value); setFocusedIdx(null); }} - placeholder={isInstalledTab ? "FILTER INSTALLED..." : "ENTER KEYWORDS..."} + placeholder={isInstalledTab ? "FILTER INSTALLED..." : (isVersionTab ? "FILTER VERSIONS..." : "ENTER KEYWORDS...")} spellCheck={false} autoFocus={isSearchTab} className="bg-transparent border-none outline-none text-white text-lg mc-text-shadow w-full placeholder-white/40 font-['Mojangles'] tracking-widest" @@ -307,6 +329,7 @@ const WorkshopView = memo(function WorkshopView() { onClick={() => openModal(pkg)} installed={isInstalled(pkg.id)} hasUpdate={hasUpdate(pkg)} + isVersionTab={isVersionTab} /> ))}
@@ -348,6 +371,7 @@ const WorkshopView = memo(function WorkshopView() { onClick={() => openModal(pkg)} installed={isInstalled(pkg.id)} hasUpdate={hasUpdate(pkg)} + isVersionTab={isVersionTab} /> ))} @@ -378,6 +402,7 @@ const WorkshopView = memo(function WorkshopView() { installedEntries={getInstalledEntries(selectedPkg.id)} onInstallComplete={refreshInstalled} onUninstallComplete={refreshInstalled} + isVersionTab={activeTab === 'Versions'} /> )} @@ -385,7 +410,7 @@ const WorkshopView = memo(function WorkshopView() { ); }); -function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpdate }: { +function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpdate, isVersionTab }: { pkg: RegistryPackage; index: number; focused: boolean; @@ -393,8 +418,11 @@ function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpda onClick: () => void; installed: boolean; hasUpdate: boolean; + isVersionTab?: boolean; }) { - const thumbnailUrl = `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; + const thumbnailUrl = isVersionTab + ? `${VERSIONS_BASE}/${pkg.id}/${pkg.thumbnail}` + : `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; const [imgError, setImgError] = useState(false); return (
- Installed + {isVersionTab ? 'Added' : 'Installed'}
)} @@ -448,22 +476,26 @@ function PackageCard({ pkg, index, focused, onHover, onClick, installed, hasUpda ); } -function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstallComplete, onUninstallComplete }: { +function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstallComplete, onUninstallComplete, isVersionTab }: { pkg: RegistryPackage; onClose: () => void; playPressSound: () => void; installedEntries: InstalledWorkshopPackage[]; onInstallComplete: () => void; onUninstallComplete: () => void; + isVersionTab?: boolean; }) { - const thumbnailUrl = `${RAW_BASE}/${pkg.id}/${pkg.thumbnail}`; + const { addCustomEdition } = useGame(); + const thumbnailUrl = 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 [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 + const focusOptions: Array<'install' | 'uninstall' | 'close'> = (hasInstalled || isVersionTab) ? ['install', 'uninstall', 'close'] : ['install', 'close']; @@ -481,7 +513,7 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal }); } else if (e.key === 'Enter') { if (modalFocus === 'close') onClose(); - else if (modalFocus === 'install') setShowInstall(true); + else if (modalFocus === 'install') handleAction(); else if (modalFocus === 'uninstall') setShowUninstall(true); } }; @@ -489,7 +521,31 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal return () => window.removeEventListener('keydown', handleKeyDown); }, [modalFocus, showInstall, showUninstall, onClose, playPressSound, focusOptions]); - const installLabel = !hasInstalled ? 'INSTALL' : needsUpdate ? 'UPDATE' : 'REINSTALL'; + const handleAction = async () => { + if (isVersionTab) { + if (hasInstalled) return; + playPressSound(); + try { + const logoUrl = `${VERSIONS_BASE}/${pkg.id}/${pkg.logo}`; + const localLogoPath = await TauriService.downloadLogo(pkg.id, logoUrl); + addCustomEdition({ + id: pkg.id, + name: pkg.name, + desc: pkg.description, + url: pkg.url!, + category: pkg.category, + logo: localLogoPath + }); + onInstallComplete(); + } catch (e) { + console.error(e); + } + } else { + setShowInstall(true); + } + }; + + const installLabel = isVersionTab ? (hasInstalled ? 'ADDED' : 'ADD') : (!hasInstalled ? 'INSTALL' : needsUpdate ? 'UPDATE' : 'REINSTALL'); return ( <> - - {Object.keys(pkg.zips).length > 0 && ( + {pkg.zips && Object.keys(pkg.zips).length > 0 && (
Files
@@ -605,7 +660,7 @@ function PackageModal({ pkg, onClose, playPressSound, installedEntries, onInstal