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,11 @@
export default function Footer() {
return(
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside>
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Wails & DaisyUi)</p>
</aside>
</footer>
)
}

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
export default function ThemeController() {
const [theme, setTheme] = useState(localStorage.getItem("theme") ?? "light");
const handleToggle = (e: any) => {
if (e.target.checked) {
setTheme("cupcake");
} else {
setTheme("night");
}
};
useEffect(() => {
localStorage.setItem('theme', theme!)
const localTheme = localStorage.getItem('theme')
document.querySelector('html')?.setAttribute('data-theme', localTheme!)
}, [theme]);
return (
<label className="toggle text-base-content">
<input type="checkbox" onChange={handleToggle} className="theme-controller" />
<svg aria-label="moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g strokeLinejoin="round" strokeLinecap="round" strokeWidth="2" fill="none" stroke="currentColor"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></g></svg>
<svg aria-label="sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g strokeLinejoin="round" strokeLinecap="round" strokeWidth="2" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></g></svg>
</label>
)
}

View File

@@ -0,0 +1 @@
export * from "./useGlobalEvents";

View File

@@ -0,0 +1,65 @@
// useGlobalEvents.ts
import { useEffect } from "react";
import { Events } from "@wailsio/runtime";
export function useGlobalEvents({
setGameRunning,
setServerRunning,
setProxyRunning,
setProgressUpdate,
setMaxProgressUpdate,
setProgressDownload,
setDownloadSpeed,
setMessageUpdate,
}: {
setGameRunning: (v: boolean) => void;
setServerRunning: (v: boolean) => void;
setProxyRunning: (v: boolean) => void;
setProgressUpdate: (v: number) => void;
setMaxProgressUpdate: (v: number) => void;
setProgressDownload: (v: number) => void;
setDownloadSpeed: (v: number) => void;
setMessageUpdate: (v: string) => void;
}) {
useEffect(() => {
const onGameExit = () => setGameRunning(false);
const onServerExit = () => setServerRunning(false);
const onProxyExit = () => setProxyRunning(false);
const onDownload = (event: any) => {
const { percent, speed } = event.data[0];
setProgressDownload(Number(percent));
setDownloadSpeed(Number(speed));
};
const onUpdateProgress = (event: any) => {
const { progress, maxProgress } = event.data[0];
setProgressUpdate(Number(progress));
setMaxProgressUpdate(Number(maxProgress));
};
const onMessageUpdate = (event: any) => {
const { message } = event.data[0];
setMessageUpdate(message);
};
Events.On("download:server", onDownload);
Events.On("download:proxy", onDownload);
Events.On("game:exit", onGameExit);
Events.On("server:exit", onServerExit);
Events.On("proxy:exit", onProxyExit);
Events.On("hdiffz:progress", onUpdateProgress);
Events.On("hdiffz:message", onMessageUpdate);
return () => {
Events.Off("download:server");
Events.Off("download:proxy");
Events.Off("game:exit");
Events.Off("server:exit");
Events.Off("proxy:exit");
Events.Off("hdiffz:progress");
Events.Off("hdiffz:message");
};
}, []);
}

29
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import './styles/index.css'
import "../runtime.js"
import "@wailsio/runtime";
// Import the generated route tree
import { routeTree } from './routeTree.gen.js'
// Create a new router instance
const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
}

View File

@@ -0,0 +1,31 @@
import { Link } from "@tanstack/react-router";
export default function AboutPage() {
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-3xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-6">
<h1 className="text-4xl font-bold text-primary text-center">About</h1>
<div className="space-y-4">
<p className="text-lg leading-relaxed">
Hello! I'm <span className="font-semibold text-error">Kain</span>, a developer passionate about building useful tools and improving user experiences.
</p>
<p className="text-lg leading-relaxed">
I created a lightweight and modern <span className="font-semibold text-success">Game Launcher</span> to help users easily launch and manage their games with better performance and simplicity.
</p>
<p className="text-lg leading-relaxed">
The launcher is built using <span className="font-mono text-info">Go + Wails</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>.
</p>
<p className="text-lg leading-relaxed">
My goal is to make tools that are fast, efficient, and enjoyable to use and this launcher is just the beginning.
</p>
</div>
<div className="text-center pt-4">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,198 @@
import { Link } from '@tanstack/react-router';
export default function AnalysisPage() {
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
<h1 className="text-4xl font-bold text-primary text-center">
Firefly Analysis & Veritas Plugin
</h1>
{/* About Veritas Section */}
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
<span>🔬</span>
<span>About Veritas</span>
</h2>
<div className="space-y-4 text-green-700">
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg"></div>
<div>
<p className="mb-2">
<span className="font-semibold text-success">Veritas</span> is a powerful{" "}
<span className="text-info font-mono bg-blue-100 px-2 py-1 rounded">Damage Logger</span> designed for analyzing damage in real-time during gameplay.
</p>
<p className="text-sm">It's lightweight, fast, and easy to use for comprehensive damage analysis.</p>
</div>
</div>
<div className="bg-white border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-green-600 text-lg">📁</span>
<span className="font-semibold text-green-800">GitHub Repository</span>
</div>
<a
href="https://github.com/hessiser/veritas"
target="_blank"
rel="noopener noreferrer"
className="link link-info font-mono break-all"
>
https://github.com/hessiser/veritas
</a>
</div>
</div>
</div>
{/* Web Analysis Tools Section */}
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
<span>🌐</span>
<span>Web Analysis Tools</span>
</h2>
<div className="space-y-4">
<p className="text-blue-700">
Use these web applications for real-time damage analysis with Veritas:
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 text-lg">🏆</span>
<span className="font-semibold text-blue-800">Master Website</span>
</div>
<a
href="https://sranalysis.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://sranalysis.kain.id.vn/
</a>
</div>
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 text-lg">🔄</span>
<span className="font-semibold text-blue-800">Backup Website</span>
</div>
<a
href="https://sr-analysis.vercel.app/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://sr-analysis.vercel.app/
</a>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<div className="text-yellow-600 text-lg">💡</div>
<p className="text-yellow-800 text-sm">
<strong>Tip:</strong> If your country has issues loading from the master site, please use the backup site instead.
</p>
</div>
</div>
</div>
</div>
{/* Installation Instructions */}
<div className="bg-red-50 border-l-4 border-red-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-red-800 flex items-center gap-2 mb-4">
<span></span>
<span>Installation Instructions</span>
</h2>
<div className="bg-white border border-red-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-red-600 text-xl">📋</div>
<div>
<h3 className="font-semibold text-red-800 mb-2">Important Setup Step</h3>
<p className="text-red-700 mb-2">
After downloading Veritas, you must rename the file for it to work properly:
</p>
<div className="bg-red-100 p-3 rounded-lg">
<p className="text-red-800 font-mono text-sm">
Rename: <code className="bg-red-200 px-1 py-0.5 rounded">veritas.dll</code> <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code>
</p>
<p className="text-red-800 text-sm mt-1">
Then place <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code> into your game directory.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Usage Instructions */}
<div className="bg-purple-50 border-l-4 border-purple-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-purple-800 flex items-center gap-2 mb-4">
<span>🛠</span>
<span>How to Use Web App</span>
</h2>
<div className="space-y-6">
{/* Firefly GO Local */}
<div className="bg-white border border-purple-200 rounded-lg p-4">
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
<span className="text-purple-600">🚀</span>
<span>For Firefly GO Local</span>
</h3>
<div className="space-y-2 text-purple-700">
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">1.</span>
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Firefly Private Server (PS)</span>.</p>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
<p>Open one of the web analysis tools.</p>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">3.</span>
<p>Go to <strong>Connection Settings</strong> select <strong>Connection Type: PS</strong> click <strong>Connect</strong>.</p>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">4.</span>
<p>Once connected, play the game. The tool will automatically analyze in the background.</p>
</div>
</div>
</div>
{/* Other Private Servers */}
<div className="bg-white border border-purple-200 rounded-lg p-4">
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
<span className="text-purple-600">🌐</span>
<span>For Other Private Servers</span>
</h3>
<div className="space-y-2 text-purple-700">
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">1.</span>
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Private Server</span>.</p>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
<p>Open one of the web analysis tools.</p>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">3.</span>
<p>Go to <strong>Connection Settings</strong> select <strong>Connection Type: Native</strong> click <strong>Connect</strong>.</p>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">4.</span>
<p>Once connected, play the game normally.</p>
</div>
</div>
</div>
</div>
</div>
<div className="text-center pt-6">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,345 @@
import useSettingStore from "@/stores/settingStore"
import { Check, Folder, File, X, Settings } from "lucide-react"
import { useEffect } from "react"
import { toast } from "react-toastify"
import { FSService, HdiffzService} from "@bindings/firefly-launcher/internal"
import { motion } from "motion/react"
import useHdiffzStore from "@/stores/hdiffzStore"
export default function HdiffzPage() {
const { gameDir, setGameDir } = useSettingStore()
const {
isLoading,
setIsLoading,
folderCheckResult,
setFolderCheckResult,
diffDir,
setDiffDir,
diffCheckResult,
setDiffCheckResult,
isDiffLoading,
setIsDiffLoading,
progressUpdate,
setProgressUpdate,
maxProgressUpdate,
setMaxProgressUpdate,
stageType,
setStageType,
messageUpdate,
setMessageUpdate
} = useHdiffzStore()
useEffect(() => {
const getLanguage = async () => {
if (gameDir) {
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
const fullPath = `${gameDir}/${subPath}`
const exists = await FSService.DirExists(fullPath)
if (exists) {
setFolderCheckResult('success')
} else {
setFolderCheckResult('error')
setGameDir('')
}
}
}
getLanguage()
}, [gameDir])
const handlePickGameFolder = async () => {
try {
setIsLoading({game: true, diff: false})
const basePath = await FSService.PickFolder()
if (basePath) {
setGameDir(basePath)
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
const fullPath = `${basePath}/${subPath}`
const exists = await FSService.DirExists(fullPath)
setFolderCheckResult(exists ? 'success' : 'error')
setGameDir(exists ? basePath : '')
if (!exists) {
toast.error('Game directory not found. Please select the correct folder.')
}
} else {
toast.error('No folder path selected')
setFolderCheckResult('error')
setGameDir('')
}
} catch (err: any) {
toast.error('PickFolder error:', err)
setFolderCheckResult('error')
} finally {
setIsLoading({game: false, diff: false})
}
}
const handlePickDiffFile = async () => {
try {
setIsLoading({game: false, diff: true})
const basePath = await FSService.PickFile()
if (basePath) {
if (!basePath.endsWith(".7z") && !basePath.endsWith(".zip") && !basePath.endsWith(".rar")) {
toast.error('Not valid file type')
setDiffCheckResult('error')
setDiffDir('')
return
}
const [exists, error] = await FSService.FileExistsInZip(basePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
if (!exists) {
toast.error(error)
setDiffCheckResult('error')
setDiffDir('')
return
}
setDiffDir(basePath)
setDiffCheckResult('success')
} else {
toast.error('No file path selected')
setDiffCheckResult('error')
setDiffDir('')
}
} catch (err: any) {
toast.error('PickFile error:', err)
setDiffCheckResult('error')
} finally {
setIsLoading({game: false, diff: false})
}
}
const handleUpdateGame = async () => {
try {
setIsDiffLoading(true)
if (!gameDir || !diffDir) {
toast.error('Please select game directory and diff file')
setIsDiffLoading(false)
return
}
setStageType('Version Validate')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
if (!validVersion) {
toast.error(errorVersion)
setIsDiffLoading(false)
return
}
setProgressUpdate(1)
setStageType('Data Extract')
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir)
if (!validData) {
toast.error(errorData)
setIsDiffLoading(false)
return
}
setStageType('Cut Data')
setMessageUpdate('')
const [validCut, errorCut] = await HdiffzService.CutData(gameDir)
if (!validCut) {
toast.error(errorCut)
setIsDiffLoading(false)
return
}
setStageType('Patch Data')
const [validPatch, errorPatch] = await HdiffzService.PatchData(gameDir)
if (!validPatch) {
toast.error(errorPatch)
setIsDiffLoading(false)
return
}
setStageType('Delete old files')
const [validDelete, errorDelete] = await HdiffzService.DeleteFiles(gameDir)
if (!validDelete) {
toast.error(errorDelete)
setIsDiffLoading(false)
return
}
toast.success('Update game completed')
} catch (err: any) {
console.error(err)
toast.error('PickFile error:', err)
setIsDiffLoading(false)
} finally {
setIsDiffLoading(false)
}
}
return (
<div className="p-2 mx-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-2">
<h1 className="text-4xl font-bold mb-2">
🎮 Game Update by Hdiffz
</h1>
<p className="">Help you update game with hdiffz</p>
</div>
{/* Main Content */}
<div className="rounded-2xl p-2 space-y-4">
{/* Folder Selection Section */}
<div className="pb-2">
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<Folder className="text-primary" size={24} />
Game Directory
</h2>
<div className="space-y-1">
<div className='grid grid-cols-1 md:grid-cols-2 gap-2 items-center'>
<button
onClick={handlePickGameFolder}
disabled={isLoading.game}
className="btn btn-primary"
>
<Folder size={20} />
{isLoading.game ? 'Selecting...' : 'Select Game Folder'}
</button>
{gameDir && (
<div className="rounded-lg p-2">
<p className="font-mono text-sm px-3 py-2 rounded border truncate max-w-full overflow-hidden whitespace-nowrap">
{gameDir}
</p>
</div>
)}
</div>
{folderCheckResult && (
<div className={`flex items-center gap-2 p-3 rounded-lg ${folderCheckResult === 'success'
? 'bg-success/5 text-success border border-success'
: 'bg-error/5 text-error border border-error'
}`}>
{folderCheckResult === 'success' ? (
<>
<Check size={20} />
<span>Valid game directory found!</span>
</>
) : (
<>
<X size={20} />
<span>Game directory not found. Please select the correct folder.</span>
</>
)}
</div>
)}
</div>
</div>
{/* Folder Selection Section */}
<div className="pb-2">
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<File className="text-primary" size={24} />
Diff file Directory
</h2>
<div className="space-y-1">
<div className='grid grid-cols-1 md:grid-cols-2 gap-2 items-center'>
<button
onClick={handlePickDiffFile}
disabled={isLoading.diff}
className="btn btn-primary"
>
<File size={20} />
{isLoading.diff ? 'Selecting...' : 'Select Diff file Folder'}
</button>
{diffDir && (
<div className="rounded-lg p-2">
<p className="font-mono text-sm px-3 py-2 rounded border truncate max-w-full overflow-hidden whitespace-nowrap">
{diffDir}
</p>
</div>
)}
</div>
{diffCheckResult && (
<div className={`flex items-center gap-2 p-3 mt-2 rounded-lg ${diffCheckResult === 'success'
? 'bg-success/5 text-success border border-success'
: 'bg-error/5 text-error border border-error'
}`}>
{diffCheckResult === 'success' ? (
<>
<Check size={20} />
<span>Valid diff file found!</span>
</>
) : (
<>
<X size={20} />
<span>Diff file not found. Please select the correct file.</span>
</>
)}
</div>
)}
</div>
{/* Apply Button */}
<div className="mt-6 flex justify-center">
<button
onClick={handleUpdateGame}
disabled={!diffDir || !gameDir || isLoading.game || isLoading.diff}
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
>
<Settings size={20} />
{isDiffLoading ? 'Updating...' : 'Update Game'}
</button>
</div>
</div>
{isDiffLoading && (
<div className="fixed inset-0 z-50 h-full 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 text-center">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Update Game
</h3>
</div>
<div className="px-6 pb-6">
<div className="w-full p-4">
<div className="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80">
<span className="font-bold text-lg text-warning">{stageType}:</span>
<div className="flex items-center gap-4 ml-2">
{stageType !== 'Cut Data' && <span className="text-white font-bold">{progressUpdate.toFixed(0)} / {maxProgressUpdate.toFixed(0)}</span>}
{stageType === 'Cut Data' && <span className="text-white font-bold truncate max-w-full overflow-hidden whitespace-nowrap">{messageUpdate}</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: `${(progressUpdate/maxProgressUpdate)*100}%` }}
transition={{ duration: 0.3 }}
/>
</div>
<div className="text-center text-lg text-white/60">
Please wait...
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Instructions */}
<div className="bg-info/5 rounded-lg p-4 border border-info/30 mt-6">
<h3 className="font-medium text-error mb-2">📋 Instructions:</h3>
<ol className="text-sm text-error space-y-1">
<li>1. Click "Select Game Folder" and choose your game's root directory</li>
<li>2. Wait for the system to validate the game directory</li>
<li>3. Click "Select Diff file Folder" and choose your diff file's root directory</li>
<li>4. Wait for the system to validate the diff file directory</li>
<li>5. Click "Update Game" to save your changes</li>
</ol>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,245 @@
import { Link } from '@tanstack/react-router';
export default function HowToPage() {
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
<h1 className="text-4xl font-bold text-primary text-center">How to Use</h1>
{/* Section 1: Launcher Features */}
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
<span>🚀</span>
<span>Using the Launcher Features</span>
</h2>
<div className="space-y-3 text-green-700">
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">🔄</div>
<p>Automatically update <span className="font-semibold text-amber-600">Firefly Go</span> and proxy tools when launching.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">🎮</div>
<p>Launch the game directly through the launcher with correct parameters and runtime environment.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">🌐</div>
<p>Support switching in-game language (e.g., EN, JP, ZH, KR) via{" "}
<a href="/language" className="link link-info font-mono">Language Tools</a>
</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">📦</div>
<p>Patch and update game files using{" "}
<a href="/hdiff" className="link link-info font-mono">Hdiffz Tool</a>
{" "}(HDiffPatch) fast & lightweight incremental updates.
</p>
</div>
</div>
</div>
{/* Section 2: FireflyGo Commands */}
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
<span>📜</span>
<span>FireflyGo Chat Commands</span>
</h2>
<p className="text-blue-700 mb-4">
Below are in-game chat commands you can use. Some commands require you to enable{" "}
<span className="font-semibold text-warning">Theorycraft Mode</span>.
</p>
{/* Theorycraft Mode Warning */}
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<div className="text-red-600 text-xl">🔒</div>
<div>
<h3 className="font-semibold text-red-800 mb-2">Theorycraft Mode Required</h3>
<p className="text-red-700 mb-2">The following commands are only available when <strong>Theorycraft Mode</strong> is enabled:</p>
<div className="flex flex-wrap gap-2">
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/cycle</code>
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/hp</code>
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/log</code>
</div>
</div>
</div>
</div>
{/* Commands List */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
{/* Theorycraft Toggle */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg"></div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Theorycraft Mode</h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 1</code> Enable Theorycraft Mode</p>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 0</code> Disable Theorycraft Mode</p>
</div>
</div>
</div>
</div>
{/* Cycle Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🔄</div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Cycle Control <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle N</code> Set cycle count in battle</p>
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 30</code> sets battle to 30 cycles</p>
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 0</code> disables custom cycle</p>
</div>
</div>
</div>
</div>
{/* HP Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg"></div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp WAVE HP</code> Set monster HP for a specific wave</p>
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000</code> sets Wave 1 monster HP to 2,000,000</p>
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> disables HP override</p>
</div>
</div>
</div>
</div>
{/* Log Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">📝</div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Battle Log <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 1</code> Enable battle log output</p>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 0</code> Disable battle log</p>
<p className="text-sm">Output will be written as <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">.json</code></p>
</div>
</div>
</div>
</div>
{/* Skip Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg"></div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Skip Nodes</h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip N</code> Skip nodes in MOC / AS / Pure Fiction</p>
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 2</code> skips node 2</p>
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 0</code> disables skipping</p>
</div>
</div>
</div>
</div>
{/* ID Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🔄</div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Character Path Switch</h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id CHAR_ID</code> Switch path for multi-form characters</p>
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id 8008</code> to change MC (Trailblazer) form</p>
<p className="text-sm">Works with IDs like <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">8001 8008</code>, <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">1001 1224</code></p>
</div>
</div>
</div>
</div>
{/* Update Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🔄</div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Refresh Data</h4>
<div className="space-y-1 text-blue-700">
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/update</code> Refresh server data from current <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code></p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Section 3: Other Notes */}
<div className="bg-gray-50 border-l-4 border-gray-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-2 mb-4">
<span>📌</span>
<span>Other Notes</span>
</h2>
<div className="space-y-4">
{/* Administrator Note */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-yellow-600 text-xl"></div>
<div>
<h3 className="font-semibold text-yellow-800 mb-1">Administrator Rights</h3>
<p className="text-yellow-700">
Always run the launcher as Administrator for file permission access.
</p>
</div>
</div>
</div>
{/* Backup Note */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-xl">💾</div>
<div>
<h3 className="font-semibold text-blue-800 mb-1">Backup Data</h3>
<p className="text-blue-700">
Backup your <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">config.json</code> and{' '}
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code> regularly.
</p>
</div>
</div>
</div>
{/* Voice Pack Note */}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-green-600 text-xl">🎵</div>
<div className="flex-1">
<h3 className="font-semibold text-green-800 mb-2">Enable Voice Packs in Beta Client</h3>
<div className="space-y-3 text-green-700">
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-green-600">1.</span>
<div>
<p className="mb-1">Copy the desired voice folder (e.g., <code className="bg-green-100 px-1 py-0.5 rounded text-sm">Japanese</code>, <code className="bg-green-100 px-1 py-0.5 rounded text-sm">English</code>) from:</p>
<code className="block bg-green-100 px-2 py-1 rounded text-sm mt-1">
Star Rail\Games\StarRail_Data\Persistent\Audio\AudioPackage\Windows
</code>
<p className="mt-1">to the beta folder by clicking <strong>"Open Voice Folder"</strong> on the Home tab.</p>
</div>
</div>
<div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-green-600">2.</span>
<p>When launching the game for the first time, it may delete the voice folder. If so, repeat step 1 to restore it.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="text-center pt-4">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,285 @@
import { useEffect, useState } from 'react'
import { Folder, Settings, Check, X, Globe, Mic } from 'lucide-react'
import { FSService } from '@bindings/firefly-launcher/internal'
import { LanguageService } from '@bindings/firefly-launcher/internal'
import { toast } from 'react-toastify'
import useSettingStore from '@/stores/settingStore'
export default function LanguagePage() {
const { gameDir, setGameDir } = useSettingStore()
const [folderCheckResult, setFolderCheckResult] = useState<'success' | 'error' | null>(null)
const [textLang, setTextLang] = useState('')
const [voiceLang, setVoiceLang] = useState('')
const [selectedTextLang, setSelectedTextLang] = useState('')
const [selectedVoiceLang, setSelectedVoiceLang] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isSettingLanguage, setIsSettingLanguage] = useState(false)
const languageOptions = [
{ value: 'en', label: 'English', flag: '🇺🇸' },
{ value: 'cn', label: 'Chinese', flag: '🇨🇳' },
{ value: 'jp', label: 'Japanese', flag: '🇯🇵' },
{ value: 'kr', label: 'Korean', flag: '🇰🇷' }
]
useEffect(() => {
const getLanguage = async () => {
if (gameDir) {
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
const fullPath = `${gameDir}/${subPath}`
const exists = await FSService.DirExists(fullPath)
if (exists) {
const [textLang, voiceLang] = await LanguageService.GetLanguage(fullPath)
setTextLang(textLang)
setVoiceLang(voiceLang)
setFolderCheckResult('success')
setSelectedTextLang(textLang)
setSelectedVoiceLang(voiceLang)
} else {
setTextLang('')
setVoiceLang('')
setSelectedTextLang('')
setSelectedVoiceLang('')
setFolderCheckResult('error')
setGameDir('')
}
}
}
getLanguage()
}, [gameDir])
const handlePickFolder = async () => {
try {
setIsLoading(true)
const basePath = await FSService.PickFolder()
if (basePath) {
setGameDir(basePath)
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
const fullPath = `${basePath}/${subPath}`
const exists = await FSService.DirExists(fullPath)
setFolderCheckResult(exists ? 'success' : 'error')
setGameDir(exists ? basePath : "")
if (!exists) {
toast.error('Game directory not found. Please select the correct folder.')
}
} else {
toast.error('No folder path selected')
setFolderCheckResult('error')
setGameDir('')
}
} catch (err: any) {
toast.error('PickFolder error:', err)
setFolderCheckResult('error')
} finally {
setIsLoading(false)
}
}
const handleSetLanguage = async () => {
if (!gameDir) {
toast.error('No folder path selected')
return
}
try {
setIsSettingLanguage(true)
const result = await LanguageService.SetLanguage(
`${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`,
selectedTextLang,
selectedVoiceLang
)
if (result) {
toast.success('Language set successfully')
setTextLang(selectedTextLang)
setVoiceLang(selectedVoiceLang)
}
else {
toast.error('Language set failed')
}
} catch (err: any) {
toast.error('SetLanguage error:', err)
} finally {
setIsSettingLanguage(false)
}
}
const getLanguageLabel = (code: string) => {
const lang = languageOptions.find(l => l.value === code)
return lang ? `${lang.flag} ${lang.label}` : code
}
return (
<div className="p-2 mx-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-2">
<h1 className="text-4xl font-bold mb-2">
🎮 Game Language Manager
</h1>
<p className="">Manage text and voice language settings for your game</p>
</div>
{/* Main Content */}
<div className="rounded-2xl p-2 space-y-4">
{/* Folder Selection Section */}
<div className="pb-2">
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<Folder className="text-primary" size={24} />
Game Directory
</h2>
<div className="space-y-1">
<div className='grid grid-cols-1 md:grid-cols-2 gap-2 items-center'>
<button
onClick={handlePickFolder}
disabled={isLoading}
className="btn btn-primary"
>
<Folder size={20} />
{isLoading ? 'Selecting...' : 'Select Game Folder'}
</button>
{gameDir && (
<div className="rounded-lg p-2">
<p className="font-mono text-sm px-3 py-2 rounded border truncate max-w-full overflow-hidden whitespace-nowrap">
{gameDir}
</p>
</div>
)}
</div>
{folderCheckResult && (
<div className={`flex items-center gap-2 p-3 rounded-lg ${folderCheckResult === 'success'
? 'bg-success/5 text-success border border-success'
: 'bg-error/5 text-error border border-error'
}`}>
{folderCheckResult === 'success' ? (
<>
<Check size={20} />
<span>Valid game directory found!</span>
</>
) : (
<>
<X size={20} />
<span>Game directory not found. Please select the correct folder.</span>
</>
)}
</div>
)}
</div>
</div>
{/* Current Language Display */}
{(textLang && voiceLang) && (
<div className="pb-2">
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<Globe className="text-primary" size={24} />
Current Languages
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-success/5 rounded-lg p-2 border border-success/30">
<div className="flex items-center gap-2 mb-1">
<Globe size={20} className="text-success" />
<span className="font-bold text-success">Text Language</span>
</div>
<p className="text-2xl font-bold text-success">
{getLanguageLabel(textLang)}
</p>
</div>
<div className="bg-warning/5 rounded-lg p-2 border border-warning/30">
<div className="flex items-center gap-2 mb-1">
<Mic size={20} className="text-warning" />
<span className="font-bold text-warning">Voice Language</span>
</div>
<p className="text-2xl font-bold text-warning">
{getLanguageLabel(voiceLang)}
</p>
</div>
</div>
</div>
)}
{/* Language Selection */}
<div className={`transition-opacity duration-300 ${gameDir === "" ? 'opacity-50 pointer-events-none' : ''
}`}>
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<Settings className="text-primary" size={24} />
Language Settings
</h2>
<div className="grid md:grid-cols-2 gap-6">
{/* Text Language */}
<div className="space-y-3">
<label className="flex text-sm font-medium text-success items-center gap-2">
<Globe size={16} />
Text Language
</label>
<select
value={selectedTextLang}
onChange={(e) => setSelectedTextLang(e.target.value)}
className="w-full select select-success"
>
<option value="">Select text language...</option>
{languageOptions.map(lang => (
<option key={lang.value} value={lang.value}>
{lang.flag} {lang.label}
</option>
))}
</select>
</div>
{/* Voice Language */}
<div className="space-y-3">
<label className="flex text-sm font-medium text-warning items-center gap-2">
<Mic size={16} />
Voice Language
</label>
<select
value={selectedVoiceLang}
onChange={(e) => setSelectedVoiceLang(e.target.value)}
className="w-full select select-warning"
>
<option value="">Select voice language...</option>
{languageOptions.map(lang => (
<option key={lang.value} value={lang.value}>
{lang.flag} {lang.label}
</option>
))}
</select>
</div>
</div>
{/* Apply Button */}
<div className="mt-6 flex justify-center">
<button
onClick={handleSetLanguage}
disabled={!selectedTextLang || !selectedVoiceLang || isSettingLanguage}
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
>
<Settings size={20} />
{isSettingLanguage ? 'Applying...' : 'Apply Language Settings'}
</button>
</div>
</div>
{/* Instructions */}
<div className="bg-info/5 rounded-lg p-4 border border-info/30 mt-6">
<h3 className="font-medium text-error mb-2">📋 Instructions:</h3>
<ol className="text-sm text-error space-y-1">
<li>1. Click "Select Game Folder" and choose your game's root directory</li>
<li>2. Wait for the system to validate the game directory</li>
<li>3. Select your preferred text and voice languages</li>
<li>4. Click "Apply Language Settings" to save your changes</li>
</ol>
</div>
</div>
</div>
</div>
)
}

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>
)
}

View File

@@ -0,0 +1,116 @@
import { Link } from "@tanstack/react-router";
export default function SrToolsPage() {
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
<h1 className="text-4xl font-bold text-primary text-center">SR Tools</h1>
{/* Section 1: About SR Tools */}
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
<span></span>
<span>About SR Tools</span>
</h2>
<div className="space-y-3 text-blue-700">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🏠</div>
<p>This site uses a self-hosted version of <span className="font-semibold text-success">SR Tools</span> available at{" "}
<a
href="https://srtools.kain.id.vn/"
className="link link-info"
target="_blank"
rel="noopener noreferrer"
>
srtools.kain.id.vn
</a>
</p>
</div>
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">👨💻</div>
<p>The original tool was created by a third-party developer named <span className="font-semibold text-warning">Amazing</span>. This version is directly based on that work, without modification to core logic.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🔗</div>
<p>There is also a more modern version by the same author available at{" "}
<a
href="https://srtools.neonteam.dev/"
className="link link-warning"
target="_blank"
rel="noopener noreferrer"
>
srtools.neonteam.dev
</a>
{" "}and the original version at{" "}
<a
href="https://srtools.pages.dev/"
className="link link-warning"
target="_blank"
rel="noopener noreferrer"
>
srtools.pages.dev
</a>
</p>
</div>
</div>
</div>
{/* Section 2: Main Features */}
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
<span>🔧</span>
<span>Main Features</span>
</h2>
<div className="space-y-3 text-green-700">
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg"></div>
<p>Configure characters, light cones, relics, traces, and eidolons easily in your browser.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">🔌</div>
<p>Instantly apply setups to <span className="font-semibold text-warning">Firefly Private Server</span> using <span className="font-semibold">Connect PS</span> no manual file uploads required.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">📂</div>
<p>Export and import full builds using <code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg"></div>
<p>Fast testing workflow no sync cooldowns, instant in-game updates.</p>
</div>
</div>
</div>
{/* Section 3: Getting Started */}
<div className="bg-purple-50 border-l-4 border-purple-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-purple-800 flex items-center gap-2 mb-4">
<span>🚀</span>
<span>Getting Started</span>
</h2>
<div className="space-y-3 text-purple-700">
<div className="flex items-start gap-3">
<div className="text-purple-600 text-lg">1</div>
<p>Access the tool through your browser at the self-hosted instance.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-purple-600 text-lg">2</div>
<p>Configure your character builds with the intuitive web interface.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-purple-600 text-lg">3</div>
<p>Use <span className="font-semibold">Connect PS</span> feature to instantly sync with your private server.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-purple-600 text-lg">4</div>
<p>Test your builds in-game with real-time updates and modifications.</p>
</div>
</div>
</div>
<div className="text-center pt-4">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,189 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as SrtoolsRouteImport } from './routes/srtools'
import { Route as LanguageRouteImport } from './routes/language'
import { Route as HowtoRouteImport } from './routes/howto'
import { Route as HdiffzRouteImport } from './routes/hdiffz'
import { Route as AnalysisRouteImport } from './routes/analysis'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
const SrtoolsRoute = SrtoolsRouteImport.update({
id: '/srtools',
path: '/srtools',
getParentRoute: () => rootRouteImport,
} as any)
const LanguageRoute = LanguageRouteImport.update({
id: '/language',
path: '/language',
getParentRoute: () => rootRouteImport,
} as any)
const HowtoRoute = HowtoRouteImport.update({
id: '/howto',
path: '/howto',
getParentRoute: () => rootRouteImport,
} as any)
const HdiffzRoute = HdiffzRouteImport.update({
id: '/hdiffz',
path: '/hdiffz',
getParentRoute: () => rootRouteImport,
} as any)
const AnalysisRoute = AnalysisRouteImport.update({
id: '/analysis',
path: '/analysis',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/analysis': typeof AnalysisRoute
'/hdiffz': typeof HdiffzRoute
'/howto': typeof HowtoRoute
'/language': typeof LanguageRoute
'/srtools': typeof SrtoolsRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/analysis': typeof AnalysisRoute
'/hdiffz': typeof HdiffzRoute
'/howto': typeof HowtoRoute
'/language': typeof LanguageRoute
'/srtools': typeof SrtoolsRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/analysis': typeof AnalysisRoute
'/hdiffz': typeof HdiffzRoute
'/howto': typeof HowtoRoute
'/language': typeof LanguageRoute
'/srtools': typeof SrtoolsRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/about'
| '/analysis'
| '/hdiffz'
| '/howto'
| '/language'
| '/srtools'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/about'
| '/analysis'
| '/hdiffz'
| '/howto'
| '/language'
| '/srtools'
id:
| '__root__'
| '/'
| '/about'
| '/analysis'
| '/hdiffz'
| '/howto'
| '/language'
| '/srtools'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
AnalysisRoute: typeof AnalysisRoute
HdiffzRoute: typeof HdiffzRoute
HowtoRoute: typeof HowtoRoute
LanguageRoute: typeof LanguageRoute
SrtoolsRoute: typeof SrtoolsRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/srtools': {
id: '/srtools'
path: '/srtools'
fullPath: '/srtools'
preLoaderRoute: typeof SrtoolsRouteImport
parentRoute: typeof rootRouteImport
}
'/language': {
id: '/language'
path: '/language'
fullPath: '/language'
preLoaderRoute: typeof LanguageRouteImport
parentRoute: typeof rootRouteImport
}
'/howto': {
id: '/howto'
path: '/howto'
fullPath: '/howto'
preLoaderRoute: typeof HowtoRouteImport
parentRoute: typeof rootRouteImport
}
'/hdiffz': {
id: '/hdiffz'
path: '/hdiffz'
fullPath: '/hdiffz'
preLoaderRoute: typeof HdiffzRouteImport
parentRoute: typeof rootRouteImport
}
'/analysis': {
id: '/analysis'
path: '/analysis'
fullPath: '/analysis'
preLoaderRoute: typeof AnalysisRouteImport
parentRoute: typeof rootRouteImport
}
'/about': {
id: '/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
AnalysisRoute: AnalysisRoute,
HdiffzRoute: HdiffzRoute,
HowtoRoute: HowtoRoute,
LanguageRoute: LanguageRoute,
SrtoolsRoute: SrtoolsRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@@ -0,0 +1,102 @@
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import ThemeController from '../components/themeController'
import { ToastContainer } from 'react-toastify'
import { useGlobalEvents } from '@/hooks';
import useLauncherStore from '@/stores/launcherStore';
import useHdiffzStore from '@/stores/hdiffzStore';
export const Route = createRootRoute({
component: RootLayout
})
function RootLayout() {
const { setGameRunning, setServerRunning, setProxyRunning, setProgressDownload, setDownloadSpeed } = useLauncherStore()
const { setProgressUpdate, setMaxProgressUpdate, setMessageUpdate } = useHdiffzStore()
useGlobalEvents({
setGameRunning,
setServerRunning,
setProxyRunning,
setProgressUpdate,
setMaxProgressUpdate,
setProgressDownload,
setDownloadSpeed,
setMessageUpdate,
});
return (
<>
<div className="navbar bg-base-100 shadow-sm sticky top-0 z-50 px-3">
<div className="navbar-start">
<div className="dropdown">
<div tabIndex={0} role="button" className="btn btn-ghost md:hidden">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" /> </svg>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li><Link to="/">Home</Link></li>
<li>
<a>Tools</a>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/hdiffz">Hdiffz</Link></li>
</ul>
</li>
<li>
<a>Plugins</a>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li>
</ul>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
</div>
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<h1 className="text-lg font-bold">
<span className="text-emerald-500">Firefly Lauc</span>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
her
</span>
</h1>
<p className="text-sm text-gray-500">By Kain</p>
</Link>
</div>
<div className="navbar-center hidden md:flex">
<ul className="menu menu-horizontal px-1">
<li><Link to="/">Home</Link></li>
<li>
<details>
<summary>Tools</summary>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/hdiffz">Hdiffz</Link></li>
</ul>
</details>
</li>
<li>
<details>
<summary>Plugins</summary>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li>
</ul>
</details>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
</div>
<div className="navbar-end">
<ThemeController />
</div>
</div>
<div className="min-h-[78vh]">
<Outlet />
</div>
<ToastContainer />
</>
)
}

View File

@@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router'
import AboutPage from '@/pages/about'
export const Route = createFileRoute('/about')({
component: AboutPage,
})

View File

@@ -0,0 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import AnalysisPage from '@/pages/analysis'
export const Route = createFileRoute('/analysis')({
component: AnalysisPage,
})

View File

@@ -0,0 +1,7 @@
import HdiffzPage from '@/pages/hdiffz'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/hdiffz')({
component: HdiffzPage,
})

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import HowToPage from '@/pages/howto'
export const Route = createFileRoute('/howto')({
component: HowToPage,
})

View File

@@ -0,0 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import LauncherPage from '@/pages/launcher'
export const Route = createFileRoute('/')({
component: LauncherPage,
})

View File

@@ -0,0 +1,7 @@
import LanguagePage from '@/pages/language'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/language')({
component: LanguagePage,
})

View File

@@ -0,0 +1,8 @@
import SrToolsPage from '@/pages/srtools'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/srtools')({
component: SrToolsPage,
})

View File

@@ -0,0 +1,46 @@
import { create } from 'zustand'
interface LauncherState {
folderCheckResult: 'success' | 'error' | null,
isLoading: {game: boolean, diff: boolean},
diffDir: string,
diffCheckResult: 'success' | 'error' | null,
isDiffLoading: boolean,
progressUpdate: number,
maxProgressUpdate: number,
messageUpdate: string,
stageType: string,
setMessageUpdate: (value: string) => void,
setIsLoading: (value: {game: boolean, diff: boolean}) => void,
setFolderCheckResult: (value: 'success' | 'error' | null) => void,
setDiffDir: (value: string) => void,
setDiffCheckResult: (value: 'success' | 'error' | null) => void,
setIsDiffLoading: (value: boolean) => void,
setProgressUpdate: (value: number) => void,
setMaxProgressUpdate: (value: number) => void,
setStageType: (value: string) => void,
}
const useLauncherStore = create<LauncherState>((set, get) => ({
isLoading: {game: false, diff: false},
folderCheckResult: null,
diffDir: "",
diffCheckResult: null,
isDiffLoading: false,
progressUpdate: 0,
maxProgressUpdate: 0,
messageUpdate: "",
stageType: "",
setIsLoading: (value: {game: boolean, diff: boolean}) => set({ isLoading: value }),
setFolderCheckResult: (value: 'success' | 'error' | null) => set({ folderCheckResult: value }),
setDiffDir: (value: string) => set({ diffDir: value }),
setDiffCheckResult: (value: 'success' | 'error' | null) => set({ diffCheckResult: value }),
setIsDiffLoading: (value: boolean) => set({ isDiffLoading: value }),
setProgressUpdate: (value: number) => set({ progressUpdate: value }),
setMaxProgressUpdate: (value: number) => set({ maxProgressUpdate: value }),
setMessageUpdate: (value: string) => set({ messageUpdate: value }),
setStageType: (value: string) => set({ stageType: value }),
}));
export default useLauncherStore;

View File

@@ -0,0 +1,54 @@
import { create } from 'zustand'
interface LauncherState {
modelName: string;
downloadType: string;
serverReady: boolean;
proxyReady: boolean;
isDownloading: boolean;
serverRunning: boolean;
proxyRunning: boolean;
isLoading: boolean;
gameRunning: boolean;
progressDownload: number;
downloadSpeed: number;
setModelName: (value: string) => void;
setDownloadType: (value: string) => void;
setServerReady: (value: boolean) => void;
setProxyReady: (value: boolean) => void;
setIsDownloading: (value: boolean) => void;
setServerRunning: (value: boolean) => void;
setProxyRunning: (value: boolean) => void;
setIsLoading: (value: boolean) => void;
setGameRunning: (value: boolean) => void;
setProgressDownload: (value: number) => void;
setDownloadSpeed: (value: number) => void;
}
const useLauncherStore = create<LauncherState>((set, get) => ({
isLoading: false,
modelName: "Download Data",
downloadType: "",
serverReady: false,
proxyReady: false,
isDownloading: false,
serverRunning: false,
proxyRunning: false,
gameRunning: false,
progressDownload: 0,
downloadSpeed: 0,
setIsLoading: (value: boolean) => set({ isLoading: value }),
setModelName: (value: string) => set({ modelName: value }),
setDownloadType: (value: string) => set({ downloadType: value }),
setServerReady: (value: boolean) => set({ serverReady: value }),
setProxyReady: (value: boolean) => set({ proxyReady: value }),
setIsDownloading: (value: boolean) => set({ isDownloading: value }),
setServerRunning: (value: boolean) => set({ serverRunning: value }),
setProxyRunning: (value: boolean) => set({ proxyRunning: value }),
setGameRunning: (value: boolean) => set({ gameRunning: value }),
setProgressDownload: (value: number) => set({ progressDownload: value }),
setDownloadSpeed: (value: number) => set({ downloadSpeed: value }),
}));
export default useLauncherStore;

View File

@@ -0,0 +1,14 @@
import { create } from 'zustand'
interface ModalState {
isOpenNotification: boolean;
setIsOpenNotification: (modal: boolean) => void;
}
const useModalStore = create<ModalState>((set, get) => ({
isOpenNotification: false,
setIsOpenNotification: (modal: boolean) => set({ isOpenNotification: modal }),
}));
export default useModalStore;

View File

@@ -0,0 +1,47 @@
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware';
interface SettingState {
locale: string;
gamePath: string;
gameDir: string;
serverPath: string;
proxyPath: string;
serverVersion: string;
proxyVersion: string;
setLocale: (newLocale: string) => void;
setGamePath: (newGamePath: string) => void;
setGameDir: (newGameDir: string) => void;
setServerPath: (newServerPath: string) => void;
setProxyPath: (newProxyPath: string) => void;
setServerVersion: (newServerVersion: string) => void;
setProxyVersion: (newProxyVersion: string) => void;
}
const useSettingStore = create<SettingState>()(
persist(
(set) => ({
locale: "en",
gamePath: "",
gameDir: "",
serverPath: "",
proxyPath: "",
serverVersion: "",
proxyVersion: "",
setLocale: (newLocale: string) => set({ locale: newLocale }),
setGamePath: (newGamePath: string) => set({ gamePath: newGamePath }),
setGameDir: (newGameDir: string) => set({ gameDir: newGameDir }),
setServerPath: (newServerPath: string) => set({ serverPath: newServerPath }),
setProxyPath: (newProxyPath: string) => set({ proxyPath: newProxyPath }),
setServerVersion: (newServerVersion: string) => set({ serverVersion: newServerVersion }),
setProxyVersion: (newProxyVersion: string) => set({ proxyVersion: newProxyVersion }),
}),
{
name: 'setting-storage',
storage: createJSONStorage(() => localStorage),
}
)
);
export default useSettingStore;

View File

@@ -0,0 +1,5 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui" {
themes: night --default, night --prefersdark, cupcake;
}

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />