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,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,78 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function DirExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1291974398, path));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function FileExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3373618865, path));
return $resultPromise;
}
/**
* @param {string} archivePath
* @param {string} fileInside
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function FileExistsInZip(archivePath, fileInside) {
let $resultPromise = /** @type {any} */($Call.ByID(1737012553, archivePath, fileInside));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function OpenFolder(path) {
let $resultPromise = /** @type {any} */($Call.ByID(2733319263, path));
return $resultPromise;
}
/**
* @returns {Promise<string> & { cancel(): void }}
*/
export function PickFile() {
let $resultPromise = /** @type {any} */($Call.ByID(2181012076));
return $resultPromise;
}
/**
* @returns {Promise<string> & { cancel(): void }}
*/
export function PickFolder() {
let $resultPromise = /** @type {any} */($Call.ByID(3906046322));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function StartApp(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3825990132, path));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function StartWithConsole(path) {
let $resultPromise = /** @type {any} */($Call.ByID(2364569062, path));
return $resultPromise;
}

View File

@@ -0,0 +1,59 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DownloadProxyProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(1951249093, version));
return $resultPromise;
}
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DownloadServerProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(314135954, version));
return $resultPromise;
}
/**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function GetLatestProxyVersion(oldVersion) {
let $resultPromise = /** @type {any} */($Call.ByID(1462362449, oldVersion));
return $resultPromise;
}
/**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function GetLatestServerVersion(oldVersion) {
let $resultPromise = /** @type {any} */($Call.ByID(1447982978, oldVersion));
return $resultPromise;
}
/**
* @returns {Promise<void> & { cancel(): void }}
*/
export function UnzipProxy() {
let $resultPromise = /** @type {any} */($Call.ByID(4071181044));
return $resultPromise;
}
/**
* @returns {Promise<void> & { cancel(): void }}
*/
export function UnzipServer() {
let $resultPromise = /** @type {any} */($Call.ByID(4110296071));
return $resultPromise;
}

View File

@@ -0,0 +1,54 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function CutData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(3671642725, gamePath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DataExtract(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DeleteFiles(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(989019003, gamePath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function PatchData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(3608591627, gamePath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function VersionValidate(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(3916254383, gamePath, patchPath));
return $resultPromise;
}

View File

@@ -0,0 +1,14 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as FSService from "./fsservice.js";
import * as GitService from "./gitservice.js";
import * as HdiffzService from "./hdiffzservice.js";
import * as LanguageService from "./languageservice.js";
export {
FSService,
GitService,
HdiffzService,
LanguageService
};

View File

@@ -0,0 +1,27 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {string} path
* @returns {Promise<[string, string]> & { cancel(): void }}
*/
export function GetLanguage(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path));
return $resultPromise;
}
/**
* @param {string} path
* @param {string} text
* @param {string} voice
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function SetLanguage(path, text, voice) {
let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice));
return $resultPromise;
}

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html data-theme="cupcake" lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/wails.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wails + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5459
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
frontend/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "react-ts-latest",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build:dev": "tsc && vite build --minify false --mode development",
"build": "tsc && vite build --mode production",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-router": "^1.124.0",
"@tanstack/react-router-devtools": "^1.124.0",
"lucide-react": "^0.525.0",
"motion": "^12.23.0",
"path-browserify": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.11",
"zustand": "^5.0.6"
},
"devDependencies": {
"@tanstack/router-plugin": "^1.124.0",
"@types/node": "^24.0.10",
"@types/path-browserify": "^1.0.3",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"@wailsio/runtime": "^3.0.0-alpha.66",
"daisyui": "^5.0.43",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}

Binary file not shown.

BIN
frontend/public/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

1605
frontend/runtime-debug.js Normal file

File diff suppressed because one or more lines are too long

1
frontend/runtime.js Normal file

File diff suppressed because one or more lines are too long

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" />

30
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@bindings/*": ["bindings/*"],
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "bindings"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

27
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,27 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
base: '/',
build: {
outDir: 'dist',
},
plugins: [
tanstackRouter({
target: 'react',
routesDirectory: 'src/routes',
autoCodeSplitting: false,
}),
tailwindcss(),
react(),
],
resolve: {
alias: {
'@bindings': path.resolve(__dirname, 'bindings'),
'@': path.resolve(__dirname, 'src'),
},
},
})