Files
Firefly_Launcher/frontend/src/pages/launcher/index.tsx

616 lines
28 KiB
TypeScript

import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { FSService, AppService } from '@bindings/firefly-launcher/internal';
import { toast } from 'react-toastify';
import path from 'path-browserify'
import useSettingStore from '@/stores/settingStore';
import useModalStore from '@/stores/modalStore';
import useLauncherStore from '@/stores/launcherStore';
import { motion } from 'motion/react';
import { Link } from '@tanstack/react-router';
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
export default function LauncherPage() {
const { gamePath,
setGamePath,
setGameDir,
serverPath,
proxyPath,
gameDir,
serverVersion,
proxyVersion,
} = useSettingStore()
const {
isOpenDownloadDataModal,
isOpenUpdateDataModal,
isOpenSelfUpdateModal,
setIsOpenDownloadDataModal,
setIsOpenUpdateDataModal,
setIsOpenSelfUpdateModal
} = useModalStore()
const {
isLoading,
downloadType,
serverReady,
proxyReady,
isDownloading,
serverRunning,
proxyRunning,
gameRunning,
progressDownload,
downloadSpeed,
updateData,
launcherVersion,
setLauncherVersion,
setIsLoading,
setDownloadType,
setServerReady,
setProxyReady,
setIsDownloading,
setServerRunning,
setProxyRunning,
setGameRunning,
setUpdateData,
} = useLauncherStore()
useEffect(() => {
const check = async () => {
if (!serverVersion || !proxyVersion) {
setServerReady(false)
setProxyReady(false)
return
}
const serverExists = await FSService.FileExists(serverPath)
const proxyExists = await FSService.FileExists(proxyPath)
setServerReady(serverExists)
setProxyReady(proxyExists)
}
check()
}, [serverPath, proxyPath, serverVersion, proxyVersion])
useEffect(() => {
const checkStartUp = async (): Promise<void> => {
const [_, version] = await AppService.GetCurrentLauncherVersion()
setLauncherVersion(version)
const launcherData = await CheckUpdateLauncher()
if (launcherData.isUpdate) {
setUpdateData({
server: { isUpdate: false, isExists: false, version: "" },
proxy: { isUpdate: false, isExists: false, version: "" },
launcher: launcherData
})
setIsOpenSelfUpdateModal(true)
return
}
const serverData = await CheckUpdateServer(serverPath, serverVersion)
const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
setUpdateData({
server: serverData,
proxy: proxyData,
launcher: launcherData
})
const exitGame = await FSService.FileExists(gamePath)
if (!exitGame) {
setGameRunning(false)
setGamePath("")
setGameDir("")
}
if (!serverData.isExists || !proxyData.isExists) {
setServerReady(false)
setProxyReady(false)
setIsOpenDownloadDataModal(true)
return
}
if (serverData.isUpdate || proxyData.isUpdate) {
setServerReady(true)
setProxyReady(true)
setIsOpenUpdateDataModal(true)
return
}
setServerReady(true)
setProxyReady(true)
}
checkStartUp()
}, []);
const handlePickFile = async () => {
try {
setIsLoading(true)
const basePath = await FSService.PickFile()
if (basePath.endsWith("StarRail.exe") || basePath.endsWith("launcher.exe")) {
const normalized = basePath.replace(/\\/g, '/')
const folderPath = path.dirname(normalized)
const fullPath = `${folderPath}/StarRail_Data/StreamingAssets/DesignData/Windows`
const exists = await FSService.DirExists(fullPath)
if (!exists) {
toast.error('Game directory not found. Please select the correct folder.')
} else {
setGamePath(basePath)
setGameDir(folderPath)
toast.success('Game path set successfully')
}
} else {
toast.error('Not valid file type')
}
} catch (err: any) {
toast.error('PickFolder error:', err)
} finally {
setIsLoading(false)
}
}
const handleStartGame = async () => {
if (!gamePath) {
return
}
if (gameRunning) {
return
}
try {
setIsLoading(true)
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
const resultProxy = await FSService.StartWithConsole(proxyPath)
if (!resultProxy) {
toast.error('Failed to start proxy')
return
}
setProxyRunning(true)
}
await sleep(500)
if (!serverRunning) {
const resultServer = await FSService.StartWithConsole(serverPath)
if (!resultServer) {
toast.error('Failed to start server')
return
}
setServerRunning(true)
}
await sleep(2000)
if (gamePath.endsWith("launcher.exe")) {
const resultGame = await FSService.StartWithConsole(gamePath)
if (!resultGame) {
toast.error('Failed to start game')
return
}
} else {
const resultGame = await FSService.StartApp(gamePath)
if (!resultGame) {
toast.error('Failed to start game')
return
}
}
setGameRunning(true)
} catch (err: any) {
toast.error('StartGame error:', err)
} finally {
setIsLoading(false)
}
}
const handlerUpdateData = async () => {
setIsDownloading(true)
if (updateData.launcher.isUpdate) {
await UpdateLauncher(updateData.launcher.version)
setUpdateData({...updateData, launcher: { isUpdate: false, isExists: true, version: updateData.launcher.version }})
setIsOpenSelfUpdateModal(true)
}
if (updateData.server.isUpdate || !updateData.server.isExists) {
await UpdateServer(updateData.server.version)
setServerReady(true)
setUpdateData({...updateData, server: { isUpdate: false, isExists: true, version: updateData.server.version }})
}
if (updateData.proxy.isUpdate || !updateData.proxy.isExists) {
await UpdateProxy(updateData.proxy.version)
setProxyReady(true)
setUpdateData({...updateData, proxy: { isUpdate: false, isExists: true, version: updateData.proxy.version }})
}
setDownloadType("")
setIsDownloading(false)
}
// Handle ESC key to close modal
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpenDownloadDataModal(false);
setIsOpenUpdateDataModal(false);
setIsOpenSelfUpdateModal(false);
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenDownloadDataModal, isOpenUpdateDataModal, isOpenSelfUpdateModal]);
return (
<div className="relative min-h-fit overflow-hidden">
<div
className="fixed inset-0 z-0 w-full h-full"
style={{
backgroundImage: "url('/bg.jpg')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat"
}}
></div>
{/* Header */}
<header className="hidden sm:flex fixed z-10 items-center justify-between p-6">
<div className="text-2xl font-bold text-white">Firefly GO</div>
</header>
<div className="hidden sm:flex fixed top-1/4 left-4 z-10 flex-col space-y-3 bg-black/30 backdrop-blur-md rounded-xl p-3 shadow-lg">
<div className="tooltip tooltip-right" data-tip="Firefly SRAnalysis">
<a
className="btn btn-circle btn-primary"
target="_blank"
href="https://sranalysis.kain.id.vn/"
>
<img
src="https://sranalysis.kain.id.vn/ff-sranalysis.png"
alt="SRAnalysis Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="Firefly SRTools">
<a
className="btn btn-circle btn-secondary"
target="_blank"
href="https://srtools.kain.id.vn/"
>
<img
src="https://srtools.kain.id.vn/ff-srtool.png"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="Amazing's SRTools (Original UI)">
<a
className="btn btn-circle btn-primary"
target="_blank"
href="https://srtools.pages.dev/"
>
<img
src="https://icons.duckduckgo.com/ip3/srtools.pages.dev.ico"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="Amazing's SRTools (Modern UI)">
<a
className="btn btn-circle btn-secondary"
target="_blank"
href="https://srtools.neonteam.dev/"
>
<img
src="https://icons.duckduckgo.com/ip3/srtools.neonteam.dev.ico"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="How to use all tools & commands">
<Link
to="/howto"
className="btn btn-warning btn-circle"
>
<MessageCircleQuestionMark className="w-6 h-6 font-bold rounded-full" />
</Link>
</div>
</div>
{/* Bottom Panel */}
{serverReady && proxyReady && !isDownloading && (
<div className="fixed bottom-0 right-0 p-8 z-10">
<div className="flex flex-wrap items-center justify-center gap-2">
{gamePath === "" ? (
<button
className="btn btn-accent btn-xl font-bold"
onClick={handlePickFile}
>
<FolderOpen className="w-6 h-6" />
{isLoading ? 'Selecting...' : 'Select Game file'}
</button>
) : (
<button
className="btn btn-warning btn-xl font-bold"
onClick={handleStartGame}
>
<Play className="w-6 h-6" />
{isLoading ? 'Selecting...' : gameRunning ? 'Game is running' : 'Start Game'}
</button>
)}
<div className="dropdown dropdown-top dropdown-end">
<div tabIndex={0} role="button" className="btn btn-black btn-circle btn-xl m-1">
<Menu className="w-6 h-6" />
</div>
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<li><button onClick={handlePickFile}>Change Game Path</button></li>
<li><button
onClick={async () => {
const serverData = await CheckUpdateServer(serverPath, serverVersion)
const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
const launcherData = await CheckUpdateLauncher()
setUpdateData({
server: serverData,
proxy: proxyData,
launcher: launcherData
})
if (launcherData.isUpdate) {
setIsOpenSelfUpdateModal(true)
return
}
if (!serverData.isExists || !proxyData.isExists) {
setIsOpenDownloadDataModal(true)
return
}
if (serverData.isUpdate || proxyData.isUpdate) {
setIsOpenUpdateDataModal(true)
return
}
toast.success("No updates available")
}}>
Check for Updates
</button></li>
<li><button disabled={!serverPath} onClick={() => {
if (serverPath) {
FSService.OpenFolder("./server")
}
}}>Open server folder</button></li>
<li><button disabled={!proxyPath} onClick={() => {
if (proxyPath) {
FSService.OpenFolder("./proxy")
}
}}>Open proxy folder</button></li>
<li><button disabled={!gameDir} onClick={() => {
if (gameDir) {
FSService.OpenFolder(gameDir + "/StarRail_Data/Persistent/Audio/AudioPackage/Windows")
}
}}>Open voice folder</button></li>
</ul>
</div>
</div>
</div>
)}
{/* Downloading */}
{isDownloading && (
updateData.proxy.isUpdate
|| updateData.server.isUpdate
|| !updateData.proxy.isExists
|| !updateData.server.isExists
) && (
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
<div className="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80">
<span>{downloadType}</span>
<div className="flex items-center gap-4 ml-4">
<span className="text-cyan-400 font-semibold">{downloadSpeed.toFixed(1)} MB/s</span>
<span className="text-white font-bold">{progressDownload.toFixed(1)}%</span>
</div>
</div>
<div className="w-full bg-white/20 rounded-full h-2 overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progressDownload}%` }}
transition={{ duration: 0.3 }}
/>
</div>
<div className="text-center text-xs text-white/60">
{progressDownload < 100 ? 'Please wait...' : 'Complete!'}
</div>
</div>
</div>
)}
{isDownloading && updateData.launcher.isUpdate && (
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
<div className="space-y-3 text-sm text-white/80 text-center">
{["update:launcher:downloading", "update:launcher:success", "update:launcher:failed"].includes(downloadType) && (
<div className="flex justify-center items-center gap-4 ml-4">
<span
className={`font-bold ${downloadType === "update:launcher:downloading"
? "text-yellow-200 text-2xl"
: downloadType === "update:launcher:success"
? "text-emerald-200 text-xl"
: "text-red-200 text-xl"
}`}
>
{downloadType === "update:launcher:downloading" && "Updating launcher"}
{downloadType === "update:launcher:success" && "Launcher updated successfully, auto closing after 5s"}
{downloadType === "update:launcher:failed" && "Launcher update failed, auto closing after 5s"}
<span className="dot-animation ml-1"></span>
</span>
</div>
)}
<div className="text-xs text-white/60">
{progressDownload < 100 ? "Please wait..." : "Complete!"}
</div>
</div>
<style>{`
.dot-animation::after {
content: '';
animation: dots 1.2s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
`}
</style>
</div>
)}
{/* Version Info */}
{serverReady && proxyReady && !isDownloading && (
<div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-black/20 backdrop-blur-sm rounded-lg p-2 shadow">
<p className="text-primary">Version server: {serverVersion}</p>
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
<p className="mt-2 text-success">Version launcher: {launcherVersion}</p>
</div>
)}
{/* Modal */}
{isOpenUpdateDataModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md btn-error absolute right-3 top-3"
onClick={() => setIsOpenUpdateDataModal(false)}
>
</motion.button>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Update Data
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Do you want to update data server and proxy?
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenUpdateDataModal(false)}
>
No
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenUpdateDataModal(false)
await handlerUpdateData()
}}
>
Yes
</motion.button>
</div>
</div>
</div>
</div>
)}
{isOpenDownloadDataModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Download Data
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Data server and proxy download required
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenDownloadDataModal(false)
await handlerUpdateData()
}}
>
Download
</motion.button>
</div>
</div>
</div>
</div>
)}
{isOpenSelfUpdateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md btn-error absolute right-3 top-3"
onClick={() => setIsOpenSelfUpdateModal(false)}
>
</motion.button>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Update Launcher
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Do you want to update launcher?
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenSelfUpdateModal(false)}
>
No
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenSelfUpdateModal(false)
await handlerUpdateData()
}}
>
Yes
</motion.button>
</div>
</div>
</div>
</div>
)}
</div>
)
}