mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:07:09 +00:00
feat: branches support, and redesigned a bit the versions menu
This commit is contained in:
parent
f9487ce632
commit
c5a05ca3e2
|
|
@ -62,6 +62,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
downloadProgress,
|
||||
updatesAvailable,
|
||||
addToSteam,
|
||||
cycleBranch,
|
||||
} = useGame();
|
||||
const { isDayTime } = useConfig();
|
||||
const [focusIndex, setFocusIndex] = useState<number>(0);
|
||||
|
|
@ -73,6 +74,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
row: number;
|
||||
btn: string;
|
||||
} | null>(null);
|
||||
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
||||
const [deleteConfirmEdition, setDeleteConfirmEdition] = useState<any>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(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() {
|
|||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{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)}
|
||||
>
|
||||
<div className="w-6 flex items-center justify-center flex-shrink-0">
|
||||
|
|
@ -314,12 +276,11 @@ const VersionsView = memo(function VersionsView() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
<div
|
||||
onClick={() =>
|
||||
!isComingSoon && handleEditionClick(edition, i)
|
||||
}
|
||||
disabled={isComingSoon}
|
||||
className={`flex-1 text-left min-w-0 outline-none rounded ${focusIndex === i && focusBtn === 0 && !isComingSoon
|
||||
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" : ""}`}
|
||||
|
|
@ -366,312 +327,162 @@ const VersionsView = memo(function VersionsView() {
|
|||
>
|
||||
{edition.desc}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 flex-shrink-0 relative">
|
||||
{!isInstalled ? (
|
||||
isDownloading ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isDownloading && !downloadingId) {
|
||||
toggleInstall(edition.instanceId);
|
||||
} else if (isDownloading) {
|
||||
handleCancelDownload();
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "cancel" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "cancel") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="square"
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
) : edition.comingSoon ? (
|
||||
<div className="w-8 h-8" />
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!downloadingId) toggleInstall(edition.id);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "download" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className={`w-8 h-8 flex items-center justify-center ${downloadingId ? "text-gray-400 cursor-not-allowed" : "text-[#3a3a3a]"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "download") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
opacity: downloadingId ? 0.5 : 1,
|
||||
}}
|
||||
disabled={!!downloadingId}
|
||||
>
|
||||
<img
|
||||
src="/images/Download_Icon.png"
|
||||
alt="Download"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={isDownloading ? "/images/Trash_Bin_Icon.png" : "/images/Download_Icon.png"}
|
||||
alt=""
|
||||
className="w-5 h-5 object-contain"
|
||||
style={{ imageRendering: "pixelated", filter: isDownloading ? "hue-rotate(300deg)" : "none" }}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
{isDownloading ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setOpenMenuId(openMenuId === edition.id ? null : edition.id);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: "menu" })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-9 h-9 flex flex-col items-center justify-center gap-1 transition-colors relative"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "menu") || (focusIndex === i && (focusBtn === 0 || focusBtn === 1))
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
filter: updatesAvailable?.[edition.instanceId] ? "drop-shadow(0 0 4px rgba(255,255,0,0.8))" : "none"
|
||||
}}
|
||||
>
|
||||
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
|
||||
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
|
||||
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{openMenuId === edition.id && (
|
||||
<div
|
||||
className="absolute right-0 top-11 w-48 bg-[#1a1a1a] border-2 border-[#555] z-[100] shadow-2xl p-0.5 animate-in fade-in zoom-in duration-75"
|
||||
style={{
|
||||
imageRendering: "pixelated"
|
||||
}}
|
||||
>
|
||||
{updatesAvailable?.[edition.instanceId] && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCancelDownload();
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "cancel" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "cancel") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
playPressSound();
|
||||
toggleInstall(edition.instanceId);
|
||||
setOpenMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-xs text-[#ffff55] hover:text-white hover:bg-[#ffff55]/20 flex items-center gap-2 group transition-colors mc-text-shadow font-bold border-b border-white/5 mb-1"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="square"
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
<img src="/images/Download_Icon.png" alt="" className="w-3 h-3 object-contain" style={{ imageRendering: "pixelated" }} />
|
||||
Update Available!
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!downloadingId) toggleInstall(edition.id);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "update" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className={`w-8 h-8 flex items-center justify-center ${downloadingId ? "text-gray-400 cursor-not-allowed" : "text-[#3a3a3a]"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "update") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
opacity: downloadingId ? 0.5 : 1,
|
||||
}}
|
||||
disabled={!!downloadingId}
|
||||
>
|
||||
<img
|
||||
src="/images/Update_Icon.png"
|
||||
alt="Update"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{
|
||||
imageRendering: "pixelated",
|
||||
filter: updatesAvailable?.[edition.id] ? "brightness(1.5) sepia(1) saturate(5) hue-rotate(15deg) drop-shadow(0 0 4px rgba(255,255,0,0.8))" : "none"
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
TauriService.openInstanceFolder(edition.id);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "folder" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "folder") ||
|
||||
(focusIndex === i && focusBtn === 2)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/Folder_Icon.png"
|
||||
alt="Folder"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playBackSound();
|
||||
setDeleteConfirmEdition(edition);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "delete" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "delete") ||
|
||||
(focusIndex === i && focusBtn === 3)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/Trash_Bin_Icon.png"
|
||||
alt="Delete"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "steam" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "steam") ||
|
||||
(focusIndex === i && focusBtn === 4)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img src="/images/steam.png" alt="Add To Steam" className="w-6 h-6 object-contain" style={{ imageRendering: "pixelated", filter: "brightness(0) invert(1)" }} />
|
||||
</button>
|
||||
{isCustom && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setEditingEdition(edition);
|
||||
setIsImportModalOpen(true);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "edit" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "edit") ||
|
||||
(focusIndex === i && focusBtn === 5)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="square"
|
||||
>
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playBackSound();
|
||||
onDeleteEdition(edition.id);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "delete_custom" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "delete_custom") ||
|
||||
(focusIndex === i && focusBtn === 6)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="square"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{Array.isArray(edition.branches) && edition.branches.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
cycleBranch(edition.id);
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-[10px] text-white mc-text-shadow hover:bg-white/10 flex items-center justify-between group transition-colors"
|
||||
>
|
||||
<span className="text-[#AAAAAA] group-hover:text-white font-bold">
|
||||
Channel
|
||||
</span>
|
||||
<span className="text-[#ffff55] font-bold">
|
||||
{edition.selectedBranch ?? "Latest"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="h-[1px] bg-white/5 my-0.5 mx-1" />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
TauriService.openInstanceFolder(edition.instanceId);
|
||||
setOpenMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||
>
|
||||
<img src="/images/Folder_Icon.png" alt="" className="w-3.5 h-3.5 object-contain" style={{ imageRendering: "pixelated" }} />
|
||||
Open Folder
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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.instanceId, edition.name, edition.titleImage, panoramaUrl);
|
||||
setOpenMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||
>
|
||||
<img src="/images/steam.png" alt="" className="w-3.5 h-3.5 object-contain invert brightness-0" style={{ imageRendering: "pixelated" }} />
|
||||
Add to Steam
|
||||
</button>
|
||||
{isCustom ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setEditingEdition(edition);
|
||||
setIsImportModalOpen(true);
|
||||
setOpenMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-[#aaaaaa] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /></svg>
|
||||
Edit Custom
|
||||
</button>
|
||||
) : null}
|
||||
<div className="h-[2px] bg-[#555] my-0.5 mx-1" />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playBackSound();
|
||||
if (isCustom) {
|
||||
onDeleteEdition(edition.id);
|
||||
} else {
|
||||
setDeleteConfirmEdition(edition);
|
||||
}
|
||||
setOpenMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-xs text-red-500 hover:text-red-400 hover:bg-red-500/10 flex items-center gap-2 transition-colors mc-text-shadow font-bold"
|
||||
>
|
||||
<img src="/images/Trash_Bin_Icon.png" alt="" className="w-3.5 h-3.5 object-contain" style={{ imageRendering: "pixelated" }} />
|
||||
{isCustom ? "Remove Custom" : "Uninstall"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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, [
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
const [steamSuccessMessage, setSteamSuccessMessage] = useState<string | null>(null);
|
||||
const [dynamicUrls, setDynamicUrls] = useState<Record<string, string>>({});
|
||||
const [branches, setBranches] = useState<Record<string, string[]>>({});
|
||||
const [selectedBranches, setSelectedBranches] = useState<Record<string, string>>({});
|
||||
const branchesFetched = useRef<Set<string>>(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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue