This commit is contained in:
2025-07-08 14:54:41 +07:00
commit 644a6f9803
86 changed files with 12422 additions and 0 deletions

View File

@@ -0,0 +1,498 @@
import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { FSService, GitService } 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';
export default function LauncherPage() {
const { gamePath,
setGamePath,
setGameDir,
serverPath,
proxyPath,
gameDir,
serverVersion,
proxyVersion,
setServerVersion,
setProxyVersion,
setServerPath,
setProxyPath
} = useSettingStore()
const { isOpenNotification, setIsOpenNotification } = useModalStore()
const {
isLoading,
modelName,
downloadType,
serverReady,
proxyReady,
isDownloading,
serverRunning,
proxyRunning,
gameRunning,
progressDownload,
downloadSpeed,
setIsLoading,
setModelName,
setDownloadType,
setServerReady,
setProxyReady,
setIsDownloading,
setServerRunning,
setProxyRunning,
setGameRunning,
} = useLauncherStore()
useEffect(() => {
const check = async () => {
if (!serverPath || !proxyPath || !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> => {
let isExists = false
if (serverPath && proxyPath && serverVersion && proxyVersion) {
isExists = true
setServerReady(true)
setProxyReady(true)
}
const dataUpdate = await handlerCheckUpdate()
const exitGame = await FSService.FileExists(gamePath)
if (!exitGame) {
setGameRunning(false)
setGamePath("")
setGameDir("")
}
if (!dataUpdate.server.isUpdate && !dataUpdate.proxy.isUpdate) {
setServerReady(true)
setProxyReady(true)
return
}
if (!isExists) {
setServerReady(false)
setProxyReady(false)
}
setModelName(!isExists ? "Download Data" : "Update Data")
setIsOpenNotification(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)
}
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
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 handlerCheckUpdate = async (): Promise<Record<string, { isUpdate: boolean, version: string }>> => {
let isUpdateServer = false
let isUpdateProxy = false
const [serverOk, serverNewVersion, serverError] = await GitService.GetLatestServerVersion(serverVersion)
const serverExists = await FSService.FileExists(serverPath)
if (serverOk) {
if (serverNewVersion !== serverVersion || !serverExists) {
isUpdateServer = true
}
} else {
toast.error("Server error: " + serverError)
}
const [proxyOk, proxyNewVersion, proxyError] = await GitService.GetLatestProxyVersion(proxyVersion)
const proxyExists = await FSService.FileExists(proxyPath)
if (proxyOk) {
if (proxyNewVersion !== proxyVersion || !proxyExists) {
isUpdateProxy = true
}
} else {
toast.error("Proxy error: " + proxyError)
}
return { server: { isUpdate: isUpdateServer, version: serverNewVersion }, proxy: { isUpdate: isUpdateProxy, version: proxyNewVersion } }
}
const handlerUpdateServer = async (updateInfo: Record<string, { isUpdate: boolean, version: string }>) => {
setIsDownloading(true)
for (const [key, value] of Object.entries(updateInfo)) {
if (value.isUpdate) {
if (key === "server") {
setDownloadType("Downloading server...")
const [ok, error] = await GitService.DownloadServerProgress(value.version)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Download server successfully")
setServerVersion(value.version)
setServerPath("./server/firefly-go_win.exe")
} else {
toast.error(error)
setDownloadType("Download server failed")
}
} else if (key === "proxy") {
setDownloadType("Downloading proxy...")
const [ok, error] = await GitService.DownloadProxyProgress(value.version)
if (ok) {
setDownloadType("Unzipping proxy...")
GitService.UnzipProxy()
setDownloadType("Download proxy successfully")
setProxyVersion(value.version)
setProxyPath("./proxy/FireflyProxy.exe")
} else {
toast.error(error)
setDownloadType("Download proxy failed")
}
}
}
}
setDownloadType("")
setIsDownloading(false)
setServerReady(true)
setProxyReady(true)
}
// Handle ESC key to close modal
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpenNotification(false);
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenNotification]);
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://icons.duckduckgo.com/ip3/sranalysis.kain.id.vn.ico"
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://icons.duckduckgo.com/ip3/srtools.kain.id.vn.ico"
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 updateInfo = await handlerCheckUpdate()
if (updateInfo.server.isUpdate || updateInfo.proxy.isUpdate) {
setModelName("Update Data")
setIsOpenNotification(true)
} else {
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 && (
<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>
)}
{/* 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>
</div>
)}
{/* Modal */}
{isOpenNotification && (
<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">
{modelName !== "Download Data" && (
<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={() => setIsOpenNotification(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">
{modelName}
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
{modelName === "Download Data"
? "Download required data!"
: "Do you want to update data?"}
</p>
</div>
<div className="flex justify-end gap-3">
{modelName !== "Download Data" && (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenNotification(false)}
>
Cancel
</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 () => {
setIsOpenNotification(false)
const dataCheck = await handlerCheckUpdate()
await handlerUpdateServer(dataCheck)
}}
>
Yes
</motion.button>
</div>
</div>
</div>
</div>
)}
</div>
)
}