diff --git a/src/components/views/VersionsView.tsx b/src/components/views/VersionsView.tsx index f5a0b1e..7fdb73d 100644 --- a/src/components/views/VersionsView.tsx +++ b/src/components/views/VersionsView.tsx @@ -62,6 +62,7 @@ const VersionsView = memo(function VersionsView() { downloadProgress, updatesAvailable, addToSteam, + cycleBranch, } = useGame(); const { isDayTime } = useConfig(); const [focusIndex, setFocusIndex] = useState(0); @@ -73,6 +74,7 @@ const VersionsView = memo(function VersionsView() { row: number; btn: string; } | null>(null); + const [openMenuId, setOpenMenuId] = useState(null); const [deleteConfirmEdition, setDeleteConfirmEdition] = useState(null); const containerRef = useRef(null); const listRef = useRef(null); @@ -117,62 +119,23 @@ const VersionsView = memo(function VersionsView() { e.preventDefault(); if (focusIndex < editions.length) { const edition = editions[focusIndex]; - const isInstalled = installedVersions.includes(edition.id); - const isCustom = edition.id.startsWith("custom_"); - const isDownloading = downloadingId === edition.id; - + const isInstalled = installedVersions.includes(edition.instanceId); + const isDownloading = downloadingId === edition.instanceId; if (focusBtn === 0) { if (isInstalled) { playPressSound(); - setSelectedProfile(edition.id); - } - } else if (isInstalled) { - if (focusBtn === 1) { - if (!downloadingId) { + setOpenMenuId(openMenuId === edition.id ? null : edition.id); + } else { + if (!isDownloading && !downloadingId) { playPressSound(); - toggleInstall(edition.id); - } - } else if (focusBtn === 2) { - playPressSound(); - TauriService.openInstanceFolder(edition.id); - } else if (focusBtn === 3) { - if (isCustom) { - playBackSound(); - onDeleteEdition(edition.id); - } else { - playPressSound(); - setDeleteConfirmEdition(edition); - } - } else if (focusBtn === 4) { - playPressSound(); - const PANORAMA_PROFILES = ["legacy_evolved", "360revived"]; - const panoId = PANORAMA_PROFILES.includes(edition.id) - ? edition.id - : "legacy_evolved"; - const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? "Day" : "Night"}.png`; - addToSteam( - edition.id, - edition.name, - edition.titleImage, - panoramaUrl, - ); - } else if (focusBtn === 5 && isCustom) { - playPressSound(); - setEditingEdition(edition); - setIsImportModalOpen(true); - } else if (focusBtn === 6 && isCustom) { - playBackSound(); - onDeleteEdition(edition.id); - } - } else { - if (focusBtn === 1) { - if (isDownloading) { + toggleInstall(edition.instanceId); + } else if (isDownloading) { handleCancelDownload(); - } else if (!downloadingId) { - playPressSound(); - toggleInstall(edition.id); } } + } else if (focusBtn === 2) { + playPressSound(); + cycleBranch(edition.id); } } else if (focusIndex === editions.length) { playPressSound(); @@ -218,11 +181,10 @@ const VersionsView = memo(function VersionsView() { }, [focusIndex]); const handleEditionClick = (edition: any, index: number) => { - const isInstalled = installedVersions.includes(edition.id); - + const isInstalled = installedVersions.includes(edition.instanceId); if (isInstalled) { playPressSound(); - setSelectedProfile(edition.id); + setSelectedProfile(edition.instanceId); } setFocusIndex(index); }; @@ -267,13 +229,13 @@ const VersionsView = memo(function VersionsView() { >
{editions.map((edition: any, i: number) => { - const isInstalled = installedVersions.includes(edition.id); + const isInstalled = installedVersions.includes(edition.instanceId); const hasAnyInstall = installedVersions.length > 0; const isSelected = - hasAnyInstall && selectedProfile === edition.id; + hasAnyInstall && selectedProfile === edition.instanceId; const isFocused = focusIndex === i; const isCustom = edition.id.startsWith("custom_"); - const isDownloading = downloadingId === edition.id; + const isDownloading = downloadingId === edition.instanceId; const isComingSoon = edition.comingSoon; return ( @@ -282,7 +244,7 @@ const VersionsView = memo(function VersionsView() { data-index={i} className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${isSelected && !isComingSoon ? "bg-[#404040]/50" : "" } ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${isComingSoon ? "opacity-50 cursor-not-allowed" : "" - }`} + } relative ${openMenuId === edition.id ? "z-50" : "z-0"}`} onMouseEnter={() => !isComingSoon && setFocusIndex(i)} >
@@ -314,12 +276,11 @@ const VersionsView = memo(function VersionsView() { )}
- +
-
+
{!isInstalled ? ( - isDownloading ? ( - - ) : edition.comingSoon ? ( -
- ) : ( - - ) + }} + onMouseEnter={() => setHoveredBtn({ row: i, btn: "main" })} + onMouseLeave={() => setHoveredBtn(null)} + className={`w-9 h-9 flex items-center justify-center ${(isDownloading || (!!downloadingId && !isInstalled)) ? "opacity-50" : "" + }`} + style={{ + backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "main") || (focusIndex === i && focusBtn === 0) + ? "url('/images/Button_Square_Highlighted.png')" + : "url('/images/Button_Square.png')", + backgroundSize: "100% 100%", + imageRendering: "pixelated", + }} + > + + ) : ( - <> - {isDownloading ? ( + + )} + + {openMenuId === edition.id && ( +
+ {updatesAvailable?.[edition.instanceId] && ( - ) : ( - <> - - - - - {isCustom && ( - <> - - - - )} - )} - + {Array.isArray(edition.branches) && edition.branches.length > 0 && ( + + )} +
+ + + {isCustom ? ( + + ) : null} +
+ +
)}
diff --git a/src/context/LauncherContext.tsx b/src/context/LauncherContext.tsx index 71dadab..b49bb11 100644 --- a/src/context/LauncherContext.tsx +++ b/src/context/LauncherContext.tsx @@ -73,7 +73,10 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { gameRaw.downloadingId, gameRaw.editions, gameRaw.isRunnerDownloading, gameRaw.runnerDownloadProgress, gameRaw.error, gameRaw.updateCustomEdition, gameRaw.handleUninstall, gameRaw.handleCancelDownload, gameRaw.gameUpdateMessage, configRaw.profile, - gameRaw.updatesAvailable, gameRaw.addToSteam, gameRaw.steamSuccessMessage + gameRaw.updatesAvailable, gameRaw.addToSteam, gameRaw.steamSuccessMessage, + gameRaw.cycleBranch, gameRaw.toggleInstall, gameRaw.checkInstalls, + gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.addCustomEdition, + gameRaw.deleteCustomEdition, gameRaw.downloadRunner ]); const audio = useMemo(() => audioRaw, [ diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts index 9072b2d..bd134bd 100644 --- a/src/hooks/useGameManager.ts +++ b/src/hooks/useGameManager.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { TauriService } from "../services/TauriService"; import { getCurrentWindow } from "@tauri-apps/api/window"; @@ -21,7 +21,7 @@ const BASE_EDITIONS = [ id: "legacy_evolved", name: "neoLegacy", desc: "Backporting newer title updates and Minigames back to LCE", - url: "https://github.com/pieeebot/neoLegacy/releases/download/v1.0.0b/neoLegacyWindows64.zip", + url: "https://github.com/pieeebot/neoLegacy/releases/download/latest/neoLegacyWindows64.zip", titleImage: "/images/minecraft_title_neoLegacy.png", supportsSlimSkins: true, logo: "/images/neoLegacy.png" @@ -69,6 +69,23 @@ interface GameManagerProps { setCustomEditions: (editions: any[]) => void; } +function compareVersions(v1: string, v2: string) { + const parts1 = v1.replace(/^v/, "").split(/[.-]/); + const parts2 = v2.replace(/^v/, "").split(/[.-]/); + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || "0"; + const p2 = parts2[i] || "0"; + const n1 = parseInt(p1); + const n2 = parseInt(p2); + if (!isNaN(n1) && !isNaN(n2)) { + if (n1 !== n2) return n1 - n2; + } else { + if (p1 !== p2) return p1 > p2 ? 1 : -1; + } + } + return 0; +} + export function useGameManager({ profile, setProfile, @@ -87,6 +104,24 @@ export function useGameManager({ const [gameUpdateMessage, setGameUpdateMessage] = useState(null); const [steamSuccessMessage, setSteamSuccessMessage] = useState(null); const [dynamicUrls, setDynamicUrls] = useState>({}); + const [branches, setBranches] = useState>({}); + const [selectedBranches, setSelectedBranches] = useState>({}); + const branchesFetched = useRef>(new Set()); + const initialBranchesSet = useRef(false); + + useEffect(() => { + if (initialBranchesSet.current || !profile) return; + BASE_EDITIONS.forEach(e => { + if (profile.startsWith(e.id + "_")) { + const branch = profile.replace(e.id + "_", ""); + setSelectedBranches(prev => ({ ...prev, [e.id]: branch })); + } else if (profile === e.id) { + setSelectedBranches(prev => ({ ...prev, [e.id]: "Stable" })); + } + }); + initialBranchesSet.current = true; + }, [profile]); + useEffect(() => { async function fetchLatestReleases() { try { @@ -105,16 +140,84 @@ export function useGameManager({ fetchLatestReleases(); }, []); - const editions = useMemo( - () => [...BASE_EDITIONS.map(e => ({ ...e, url: dynamicUrls[e.id] || e.url })), ...customEditions], - [customEditions, dynamicUrls], - ); + const fetchBranchesForEdition = useCallback(async (editionId: string, url: string) => { + if (branchesFetched.current.has(editionId)) return; + if (!url.includes("github.com")) return; + const parts = url.split("github.com/")[1].split("/"); + const owner = parts[0]; + const repo = parts[1]; + try { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`); + if (response.ok) { + const data = await response.json(); + let tags: string[] = data + .map((r: any) => r.tag_name) + .filter((t: string) => !t.toLowerCase().includes("server")); + + const vTags = tags.filter(t => t.startsWith("v")).sort(compareVersions); + const bestVTag = vTags[vTags.length - 1]; + if (!tags.includes("Stable")) tags.unshift("Stable"); + if (bestVTag) { + setDynamicUrls(prev => ({ ...prev, [`${editionId}_Stable`]: bestVTag })); + } + + setBranches(prev => ({ ...prev, [editionId]: tags })); + branchesFetched.current.add(editionId); + } + } catch (e) { + console.error(`Failed to fetch branches for ${editionId}:`, e); + } + }, []); + + useEffect(() => { + BASE_EDITIONS.forEach(e => fetchBranchesForEdition(e.id, e.url)); + }, [fetchBranchesForEdition]); + + const cycleBranch = useCallback((editionId: string) => { + const available = branches[editionId] || ["Stable"]; + if (available.length <= 1) return; + setSelectedBranches(prev => { + const current = prev[editionId] || available[0]; + const currentIndex = available.indexOf(current); + const nextIndex = (currentIndex + 1) % available.length; + const nextBranch = available[nextIndex]; + const oldInstanceId = current === "Stable" ? editionId : `${editionId}_${current}`; + const newInstanceId = nextBranch === "Stable" ? editionId : `${editionId}_${nextBranch}`; + if (profile === oldInstanceId) { + setProfile(newInstanceId); + } + + return { ...prev, [editionId]: nextBranch }; + }); + }, [branches, profile, setProfile]); + + const editions = useMemo(() => { + return [...BASE_EDITIONS.map(e => { + const availableBranches = branches[e.id] || ["Stable"]; + const selectedBranch = selectedBranches[e.id] || availableBranches[0]; + let url = dynamicUrls[e.id] || e.url; + const branchToUse = selectedBranch === "Stable" ? (dynamicUrls[`${e.id}_Stable`] || "latest") : selectedBranch; + if (e.url.includes("github.com")) { + const baseUrl = e.url.split("/releases/download/")[0]; + const filename = e.url.split("/").pop(); + url = `${baseUrl}/releases/download/${branchToUse}/${filename}`; + } + + return { + ...e, + url, + branches: availableBranches, + selectedBranch, + instanceId: selectedBranch === "Stable" ? e.id : `${e.id}_${selectedBranch}` + }; + }), ...customEditions.map(e => ({ ...e, instanceId: e.id }))]; + }, [customEditions, dynamicUrls, branches, selectedBranches]); const checkInstalls = useCallback(async () => { const results = await Promise.all( editions.map(async (e) => { - const isInstalled = await TauriService.checkGameInstalled(e.id); - return isInstalled ? e.id : null; + const isInstalled = await TauriService.checkGameInstalled(e.instanceId); + return isInstalled ? e.instanceId : null; }), ); setInstalls(results.filter((id): id is string => id !== null)); @@ -124,13 +227,13 @@ export function useGameManager({ const checkForGameUpdates = useCallback(async () => { const checks = await Promise.all( editions.map(async (edition) => { - if (!installs.includes(edition.id)) return [edition.id, false] as const; + if (!installs.includes(edition.instanceId)) return [edition.instanceId, false] as const; try { - const isUpdate = await TauriService.checkGameUpdate(edition.id, edition.url); - return [edition.id, isUpdate] as const; + const isUpdate = await TauriService.checkGameUpdate(edition.instanceId, edition.url); + return [edition.instanceId, isUpdate] as const; } catch (e) { console.error(e); - return [edition.id, false] as const; + return [edition.instanceId, false] as const; } }) ); @@ -195,7 +298,7 @@ export function useGameManager({ const toggleInstall = useCallback( async (id: string) => { if (downloadingId) return; - const edition = editions.find((e) => e.id === id); + const edition = editions.find((e) => e.instanceId === id); if (!edition) return; setError(null); try { @@ -338,5 +441,6 @@ export function useGameManager({ setSteamSuccessMessage, updatesAvailable, addToSteam, + cycleBranch, }; }