init
This commit is contained in:
93
frontend/Inter Font License.txt
Normal file
93
frontend/Inter Font License.txt
Normal 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.
|
||||
78
frontend/bindings/firefly-launcher/internal/fsservice.js
Normal file
78
frontend/bindings/firefly-launcher/internal/fsservice.js
Normal 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;
|
||||
}
|
||||
59
frontend/bindings/firefly-launcher/internal/gitservice.js
Normal file
59
frontend/bindings/firefly-launcher/internal/gitservice.js
Normal 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;
|
||||
}
|
||||
54
frontend/bindings/firefly-launcher/internal/hdiffzservice.js
Normal file
54
frontend/bindings/firefly-launcher/internal/hdiffzservice.js
Normal 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;
|
||||
}
|
||||
14
frontend/bindings/firefly-launcher/internal/index.js
Normal file
14
frontend/bindings/firefly-launcher/internal/index.js
Normal 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
|
||||
};
|
||||
@@ -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
13
frontend/index.html
Normal 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
5459
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
frontend/package.json
Normal file
43
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/Inter-Medium.ttf
Normal file
BIN
frontend/public/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
frontend/public/bg.jpg
Normal file
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
1605
frontend/runtime-debug.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/runtime.js
Normal file
1
frontend/runtime.js
Normal file
File diff suppressed because one or more lines are too long
11
frontend/src/components/footer/index.tsx
Normal file
11
frontend/src/components/footer/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
frontend/src/components/themeController/index.tsx
Normal file
29
frontend/src/components/themeController/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1
frontend/src/hooks/index.ts
Normal file
1
frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./useGlobalEvents";
|
||||
65
frontend/src/hooks/useGlobalEvents.tsx
Normal file
65
frontend/src/hooks/useGlobalEvents.tsx
Normal 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
29
frontend/src/main.tsx
Normal 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>,
|
||||
)
|
||||
}
|
||||
31
frontend/src/pages/about/index.tsx
Normal file
31
frontend/src/pages/about/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
198
frontend/src/pages/analysis/index.tsx
Normal file
198
frontend/src/pages/analysis/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
345
frontend/src/pages/hdiffz/index.tsx
Normal file
345
frontend/src/pages/hdiffz/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
245
frontend/src/pages/howto/index.tsx
Normal file
245
frontend/src/pages/howto/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
285
frontend/src/pages/language/index.tsx
Normal file
285
frontend/src/pages/language/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
498
frontend/src/pages/launcher/index.tsx
Normal file
498
frontend/src/pages/launcher/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
116
frontend/src/pages/srtools/index.tsx
Normal file
116
frontend/src/pages/srtools/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
189
frontend/src/routeTree.gen.ts
Normal file
189
frontend/src/routeTree.gen.ts
Normal 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>()
|
||||
102
frontend/src/routes/__root.tsx
Normal file
102
frontend/src/routes/__root.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
frontend/src/routes/about.tsx
Normal file
8
frontend/src/routes/about.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import AboutPage from '@/pages/about'
|
||||
|
||||
export const Route = createFileRoute('/about')({
|
||||
component: AboutPage,
|
||||
|
||||
})
|
||||
|
||||
7
frontend/src/routes/analysis.tsx
Normal file
7
frontend/src/routes/analysis.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import AnalysisPage from '@/pages/analysis'
|
||||
|
||||
export const Route = createFileRoute('/analysis')({
|
||||
component: AnalysisPage,
|
||||
})
|
||||
|
||||
7
frontend/src/routes/hdiffz.tsx
Normal file
7
frontend/src/routes/hdiffz.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import HdiffzPage from '@/pages/hdiffz'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/hdiffz')({
|
||||
component: HdiffzPage,
|
||||
})
|
||||
|
||||
6
frontend/src/routes/howto.tsx
Normal file
6
frontend/src/routes/howto.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import HowToPage from '@/pages/howto'
|
||||
|
||||
export const Route = createFileRoute('/howto')({
|
||||
component: HowToPage,
|
||||
})
|
||||
7
frontend/src/routes/index.tsx
Normal file
7
frontend/src/routes/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import LauncherPage from '@/pages/launcher'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: LauncherPage,
|
||||
})
|
||||
|
||||
7
frontend/src/routes/language.tsx
Normal file
7
frontend/src/routes/language.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import LanguagePage from '@/pages/language'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/language')({
|
||||
component: LanguagePage,
|
||||
})
|
||||
|
||||
8
frontend/src/routes/srtools.tsx
Normal file
8
frontend/src/routes/srtools.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import SrToolsPage from '@/pages/srtools'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/srtools')({
|
||||
component: SrToolsPage,
|
||||
})
|
||||
|
||||
|
||||
46
frontend/src/stores/hdiffzStore.ts
Normal file
46
frontend/src/stores/hdiffzStore.ts
Normal 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;
|
||||
54
frontend/src/stores/launcherStore.ts
Normal file
54
frontend/src/stores/launcherStore.ts
Normal 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;
|
||||
14
frontend/src/stores/modalStore.ts
Normal file
14
frontend/src/stores/modalStore.ts
Normal 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;
|
||||
47
frontend/src/stores/settingStore.ts
Normal file
47
frontend/src/stores/settingStore.ts
Normal 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;
|
||||
5
frontend/src/styles/index.css
Normal file
5
frontend/src/styles/index.css
Normal 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
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
30
frontend/tsconfig.json
Normal file
30
frontend/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal 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
27
frontend/vite.config.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user