616 lines
28 KiB
TypeScript
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>
|
|
)
|
|
}
|