import { useState, useEffect, useRef, memo } from "react"; import { useTranslation } from "react-i18next"; import { motion } from "framer-motion"; import { TauriService, PlaytimeResponse } from "../../services/TauriService"; import CustomTUModal from "../modals/CustomTUModal"; import SetUidModal from "../modals/SetUidModal"; import ImportWorldModal from "../modals/ImportWorldModal"; import PlaytimeModal from "../modals/PlaytimeModal"; import CustomizeModal from "../modals/CustomizeModal"; import DownloadDlcModal from "../modals/DownloadDlcModal"; import OptionsModal from "../modals/OptionsModal"; import { parseSchema } from "../../utils/argsSchema"; import { useUI, useConfig, useAudio, useGame, } from "../../context/LauncherContext"; import { ScreenshotImage } from "../common/ScreenshotImage"; import { usePlatform } from "../../hooks/usePlatform"; import type { Edition } from "../../types/edition"; import { HIDDEN_INSTANCE_URL } from "../../types/edition"; interface DeleteConfirmButtonProps { label: string; onClick: () => void; isDanger?: boolean; } const DeleteConfirmButton = memo(function DeleteConfirmButton({ label, onClick, isDanger = false, }: DeleteConfirmButtonProps) { const [isHovered, setIsHovered] = useState(false); return ( ); }); function formatPlaytime(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); if (h > 0) return `${h}h ${m}m`; if (m > 0) return `${m}m`; return seconds > 0 ? `${seconds}s` : ""; } const VersionsView = memo(function VersionsView() { const { t } = useTranslation(); const { setActiveView } = useUI(); const { profile: selectedProfile, setProfile: setSelectedProfile, animationsEnabled, instanceLaunchArgs, setInstanceLaunchArgs, } = useConfig(); const { playPressSound, playBackSound } = useAudio(); const { editions, installs: installedVersions, toggleInstall, handleUninstall, handleCancelDownload, deleteCustomEdition: onDeleteEdition, addCustomEdition: onAddEdition, updateCustomEdition: onUpdateEdition, downloadingIds, downloadProgress, updatesAvailable, addToSteam, cycleBranch, customizations, updateCustomization, saveCustomPath, } = useGame(); const { isDayTime } = useConfig(); const { isAndroid } = usePlatform(); const visibleEditions = editions.filter( (e) => e.url !== HIDDEN_INSTANCE_URL || installedVersions.includes(e.instanceId), ); const [focusIndex, setFocusIndex] = useState(0); const [focusBtn, setFocusBtn] = useState(0); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isSetUidModalOpen, setIsSetUidModalOpen] = useState(false); const [setUidTargetId, setSetUidTargetId] = useState(""); const [editingEdition, setEditingEdition] = useState(null); const [isImportWorldModalOpen, setIsImportWorldModalOpen] = useState(false); const [importWorldTarget, setImportWorldTarget] = useState<{ id: string; name: string; } | null>(null); const [isPlaytimeModalOpen, setIsPlaytimeModalOpen] = useState(false); const [playtimeTarget, setPlaytimeTarget] = useState<{ id: string; name: string; } | null>(null); const [isCustomizeModalOpen, setIsCustomizeModalOpen] = useState(false); const [customizeTarget, setCustomizeTarget] = useState(null); const [playtimeMap, setPlaytimeMap] = useState< Record >({}); const [initialPath, setInitialPath] = useState(""); const [hoveredBtn, setHoveredBtn] = useState<{ row: number; btn: string; } | null>(null); const [openMenuId, setOpenMenuId] = useState(null); const [deleteConfirmEdition, setDeleteConfirmEdition] = useState(null); const [isDlcModalOpen, setIsDlcModalOpen] = useState(false); const [dlcTargetEdition, setDlcTargetEdition] = useState( null, ); const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); const [optionsTarget, setOptionsTarget] = useState<{ id: string; name: string; } | null>(null); const [argsSchemas, setArgsSchemas] = useState>({}); const containerRef = useRef(null); const listRef = useRef(null); const ITEM_COUNT = visibleEditions.length + 3; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (document.activeElement?.tagName === "INPUT") return; if ( isImportModalOpen || isSetUidModalOpen || isImportWorldModalOpen || isPlaytimeModalOpen || isCustomizeModalOpen || isDlcModalOpen || isOptionsModalOpen || deleteConfirmEdition ) { return; } if (e.key === "Escape" || e.key === "Backspace") { playBackSound(); setActiveView("main"); return; } if (e.key === "ArrowDown") { e.preventDefault(); setFocusIndex((prev) => (prev >= ITEM_COUNT - 1 ? 0 : prev + 1)); setFocusBtn(0); } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusIndex((prev) => (prev <= 0 ? ITEM_COUNT - 1 : prev - 1)); setFocusBtn(0); } else if (e.key === "ArrowLeft") { e.preventDefault(); if (focusIndex < visibleEditions.length) { const edition = visibleEditions[focusIndex]; const isInstalled = installedVersions.includes(edition.id); const isCustom = edition.id.startsWith("custom_"); const maxBtn = isInstalled ? (isCustom ? 6 : 4) : 1; setFocusBtn((prev) => (prev <= 0 ? maxBtn : prev - 1)); } } else if (e.key === "ArrowRight") { e.preventDefault(); if (focusIndex < visibleEditions.length) { const edition = visibleEditions[focusIndex]; const isInstalled = installedVersions.includes(edition.id); const isCustom = edition.id.startsWith("custom_"); const maxBtn = isInstalled ? (isCustom ? 6 : 4) : 1; setFocusBtn((prev) => (prev >= maxBtn ? 0 : prev + 1)); } } else if (e.key === "Enter") { e.preventDefault(); if (focusIndex < visibleEditions.length) { const edition = visibleEditions[focusIndex]; const isInstalled = installedVersions.includes(edition.instanceId); const isDownloading = downloadingIds.includes(edition.instanceId); if (focusBtn === 0) { if (isInstalled) { playPressSound(); setOpenMenuId(openMenuId === edition.id ? null : edition.id); } else { if (!isDownloading) { playPressSound(); toggleInstall(edition.instanceId); } else { handleCancelDownload(edition.instanceId); } } } else if (focusBtn === 1 && !isInstalled) { playPressSound(); setOpenMenuId(openMenuId === edition.id ? null : edition.id); } else if (focusBtn === 2) { playPressSound(); cycleBranch(edition.id); } } else if (focusIndex === visibleEditions.length) { playPressSound(); setIsImportModalOpen(true); } else if (focusIndex === visibleEditions.length + 1) { playPressSound(); handleImportFolder(); } else { playBackSound(); setActiveView("main"); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [ focusIndex, focusBtn, editions, installedVersions, downloadingIds, ITEM_COUNT, playPressSound, playBackSound, setSelectedProfile, setActiveView, toggleInstall, handleCancelDownload, addToSteam, isDayTime, isImportModalOpen, isSetUidModalOpen, isImportWorldModalOpen, isPlaytimeModalOpen, isCustomizeModalOpen, isDlcModalOpen, isOptionsModalOpen, deleteConfirmEdition, ]); useEffect(() => { if (focusIndex < visibleEditions.length && listRef.current) { const el = listRef.current.querySelector( `[data-index="${focusIndex}"]`, ) as HTMLElement; if (el) { el.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } }, [focusIndex]); useEffect(() => { const fetchPlaytimes = async () => { const map: Record = {}; await Promise.all( installedVersions.map(async (id) => { try { map[id] = await TauriService.getPlaytime(id); } catch (e) { console.error(e); } }), ); setPlaytimeMap(map); }; fetchPlaytimes(); }, [installedVersions]); useEffect(() => { let cancelled = false; const checkSchemas = async () => { const map: Record = {}; await Promise.all( installedVersions.map(async (id) => { try { const schema = await TauriService.getInstanceArgsSchema(id); map[id] = !!schema && parseSchema(schema) !== null; } catch (e) { console.error(e); map[id] = false; } }), ); if (!cancelled) setArgsSchemas(map); }; checkSchemas(); return () => { cancelled = true; }; }, [installedVersions]); const handleEditionClick = (edition: Edition, index: number) => { const isInstalled = installedVersions.includes(edition.instanceId); if (isInstalled) { playPressSound(); setSelectedProfile(edition.instanceId); } setFocusIndex(index); }; const handleImportFolder = async () => { try { const folder = await TauriService.pickFolder(); if (folder) { setInitialPath(folder); setIsImportModalOpen(true); } } catch (e) { if (e !== "CANCELED") console.error(e); } }; const handleImportWorld = (instanceId: string) => { const edition = visibleEditions.find( (e: Edition) => e.instanceId === instanceId, ); setImportWorldTarget({ id: instanceId, name: edition?.name ?? instanceId }); setIsImportWorldModalOpen(true); }; return (

{t("versions.title")}

{visibleEditions.map((edition: Edition, i: number) => { const isInstalled = installedVersions.includes( edition.instanceId, ); const hasAnyInstall = installedVersions.length > 0; const isSelected = hasAnyInstall && selectedProfile === edition.instanceId; const isFocused = focusIndex === i; const isCustom = edition.id.startsWith("custom_"); const isDownloading = downloadingIds.includes(edition.instanceId); const isComingSoon = edition.comingSoon; return (
!isComingSoon && setFocusIndex(i)} >
{isDownloading ? ( {Math.floor(downloadProgress[edition.instanceId] || 0)}% ) : edition.logo ? ( edition.logo.startsWith("http") || edition.logo.startsWith("/images") ? ( ) : ( ) ) : (
)}
!isComingSoon && handleEditionClick(edition, i) } className={`flex-1 text-left min-w-0 outline-none rounded cursor-pointer ${ focusIndex === i && focusBtn === 0 && !isComingSoon ? "ring-2 ring-white" : "" } ${isComingSoon ? "cursor-not-allowed" : ""}`} >
{edition.name} {isInstalled && ( )} {edition.category && edition.category.map((cat: string) => ( {cat} ))} {isCustom && !edition.category && ( {t("versions.custom")} )}

{edition.desc}

{!isInstalled && ( )} {openMenuId === edition.id && (
{updatesAvailable?.[edition.instanceId] && ( )} {!isAndroid && !isInstalled && (
`; document.body.appendChild(dialog); const close = () => document.body.removeChild(dialog); dialog .querySelector("#empty-dir-ok") ?.addEventListener("click", close); dialog.addEventListener("click", (e) => { if (e.target === dialog) close(); }); return; } await saveCustomPath( edition.instanceId, folder, ); toggleInstall(edition.instanceId); } } catch (err) { if (err !== "CANCELED") console.error(err); } }} className="w-full text-left px-3 py-2 text-xs text-[#dddddd] flex items-center gap-2 mc-text-shadow" > {t("versions.downloadToCustomPath")} )} {edition.officialDLC && isInstalled ? ( ) : null}{" "} {argsSchemas[edition.instanceId] && ( )} {Array.isArray(edition.branches) && edition.branches.length > 0 && ( )}
{!isAndroid && ( )}{" "} {!isAndroid && ( )}{" "} {!isAndroid && ( )}{" "} {!isAndroid && ( )}{" "} {!isAndroid && ( )}{" "} {!isAndroid && ( )}{" "} {" "} {isCustom ? ( ) : null}{" "}
)}
); })}
{!isAndroid && ( )}
{!isAndroid && (
{" "}
)} { setIsImportModalOpen(false); setEditingEdition(null); setInitialPath(""); }} onImport={(ed: { name: string; desc: string; url: string; path?: string; }) => { if (editingEdition) { onUpdateEdition(editingEdition.id, ed); } else { const id = onAddEdition(ed); setSelectedProfile(id); } }} playPressSound={playPressSound} playBackSound={playBackSound} editingEdition={editingEdition} initialPath={initialPath} /> setIsSetUidModalOpen(false)} playPressSound={playPressSound} playBackSound={playBackSound} instances={visibleEditions} installedVersions={installedVersions} targetInstanceId={setUidTargetId} /> { setIsImportWorldModalOpen(false); setImportWorldTarget(null); }} playPressSound={playPressSound} playBackSound={playBackSound} targetInstanceId={importWorldTarget?.id ?? ""} targetInstanceName={importWorldTarget?.name ?? ""} /> { setIsPlaytimeModalOpen(false); setPlaytimeTarget(null); }} playBackSound={playBackSound} instanceId={playtimeTarget?.id ?? ""} instanceName={playtimeTarget?.name ?? ""} /> { setIsCustomizeModalOpen(false); setCustomizeTarget(null); }} playPressSound={playPressSound} playBackSound={playBackSound} editionName={customizeTarget?.name ?? ""} currentTitleImage={ customizeTarget ? customizations[customizeTarget.instanceId]?.titleImage || customizeTarget.titleImage : undefined } currentPanorama={ customizeTarget ? customizations[customizeTarget.instanceId]?.panorama || customizeTarget.panorama : undefined } onSave={(updates) => { if (customizeTarget) { updateCustomization(customizeTarget.instanceId, updates); } }} /> { setIsDlcModalOpen(false); setDlcTargetEdition(null); }} playPressSound={playPressSound} playBackSound={playBackSound} editionName={dlcTargetEdition?.name ?? ""} instanceId={dlcTargetEdition?.instanceId ?? ""} officialDLC={dlcTargetEdition?.officialDLC ?? ""} /> { setIsOptionsModalOpen(false); setOptionsTarget(null); }} playPressSound={playPressSound} playBackSound={playBackSound} instanceId={optionsTarget?.id ?? ""} instanceName={optionsTarget?.name ?? ""} savedValues={ optionsTarget ? instanceLaunchArgs[optionsTarget.id]?.values : undefined } onSave={(instanceId, values, args) => { setInstanceLaunchArgs((prev) => ({ ...prev, [instanceId]: { values, args }, })); }} /> {deleteConfirmEdition && (

{t("versions.deleteTitle", { name: deleteConfirmEdition.name })}

{t("versions.deleteWarning")}

{ playBackSound(); setDeleteConfirmEdition(null); }} /> { playPressSound(); handleUninstall(deleteConfirmEdition.id); setDeleteConfirmEdition(null); }} />
)} ); }); export default VersionsView;