Compare commits

16 Commits
2.1.1 ... 2.5.1

Author SHA1 Message Date
2fafe7813d FIX: change cmd dir to game cmd dir 2026-04-07 21:30:32 +07:00
45d345d7cb UPDATE: Add muti language 2026-04-03 18:37:01 +07:00
2b0c82bd7e UPDATE: go 1.26.1, new data, fix ui 2026-04-03 12:44:41 +07:00
d0427142e3 UPDATE: go 1.26.1, new data, fix ui 2026-04-03 12:35:11 +07:00
2b7ebfb4e6 UPDATE: Handler for 4.0 2026-02-10 21:46:46 +07:00
70b719489b UPDATE: Handler for 4.0 2026-02-10 21:45:45 +07:00
5f1708129a UPDATE: Fix bug 2026-01-15 13:22:19 +07:00
e4f014e3b4 UPDATE: Fix bug 2026-01-14 05:56:18 +07:00
2349d6d360 FIX: Fix some bug 2025-12-12 18:55:46 +07:00
acdd761652 UPDATE: Change proxy 2025-12-12 18:49:30 +07:00
e08b265ae8 FIX: Fail to start game 2025-11-05 23:33:34 +07:00
52134c2200 UPDATE: handler before start game 2025-11-05 20:44:41 +07:00
892ea44c17 UPDATE: Fix ui and add License 2025-10-20 18:11:37 +07:00
ca612797ee FIX: Drag drop, ui/ux 2025-10-20 17:20:16 +07:00
787962c6d0 UPDATE: Backgroud setting, Remake ui/ux 2025-10-19 23:37:04 +07:00
09434fcc5b UPDATE: System tray 2025-10-13 09:09:55 +07:00
65 changed files with 4331 additions and 2005 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Firefly Shelter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -22,6 +22,11 @@ build:
wails3 build
@echo Done!
generate:
@echo Generating bindings...
wails3 generate bindings -ts
@echo Done!
release:
@echo Building release application...
wails3 package

View File

@@ -15,7 +15,7 @@ A lightweight and modern launcher for Anime game — designed to make launching,
## 🧑‍💻 About the Developer
Hi! I'm **Kain**, a developer passionate about building simple and powerful tools.
Hi! We are **Firefly Shelter**, a developer team passionate about building simple and powerful tools.
Firefly Launcher is built with:
- ⚙️ **Go + Wails** for backend/frontend integration

Binary file not shown.

View File

@@ -15,7 +15,7 @@
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>

View File

@@ -28,9 +28,30 @@ export function GetCurrentLauncherVersion() {
return $Call.ByID(3575133982);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function HideApp() {
return $Call.ByID(88003266);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function MaximizeApp() {
return $Call.ByID(1257306588);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function MinimizeApp() {
return $Call.ByID(3434614194);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function RestoreApp() {
return $Call.ByID(3115625834);
}

View File

@@ -31,6 +31,22 @@ export function FileExistsInZip(archivePath, fileInside) {
return $Call.ByID(2509699047, archivePath, fileInside);
}
/**
* @param {string} path
* @returns {$CancellablePromise<string>}
*/
export function GetDir(path) {
return $Call.ByID(1744445742, path);
}
/**
* @param {string[]} paths
* @returns {$CancellablePromise<string>}
*/
export function Join(...paths) {
return $Call.ByID(2460588289, paths);
}
/**
* @param {string} path
* @returns {$CancellablePromise<[boolean, string]>}
@@ -56,7 +72,15 @@ export function PickFolder() {
/**
* @param {string} path
* @returns {$CancellablePromise<boolean>}
* @returns {$CancellablePromise<void>}
*/
export function RemoveFile(path) {
return $Call.ByID(3206735043, path);
}
/**
* @param {string} path
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function StartApp(path) {
return $Call.ByID(1267568402, path);
@@ -64,7 +88,7 @@ export function StartApp(path) {
/**
* @param {string} path
* @returns {$CancellablePromise<boolean>}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function StartWithConsole(path) {
return $Call.ByID(3249271428, path);

View File

@@ -43,13 +43,6 @@ export function GetLatestServerVersion() {
return $Call.ByID(2918980975);
}
/**
* @returns {$CancellablePromise<void>}
*/
export function UnzipProxy() {
return $Call.ByID(2563246729);
}
/**
* @returns {$CancellablePromise<void>}
*/

View File

@@ -0,0 +1,9 @@
//@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 { Create as $Create } from "@wailsio/runtime";
Object.freeze($Create.Events);

View File

@@ -0,0 +1,2 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dracula">
<head>
<meta charset="UTF-8" />
@@ -9,7 +9,7 @@
</head>
<body>
<div id="root"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

File diff suppressed because it is too large Load Diff

View File

@@ -11,33 +11,38 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-router": "^1.131.27",
"@tanstack/react-router-devtools": "^1.131.27",
"lucide-react": "^0.541.0",
"motion": "^12.23.12",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-router": "^1.168.10",
"@tanstack/react-router-devtools": "^1.166.11",
"i18next": "^26.0.3",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.4",
"lucide-react": "^1.7.0",
"motion": "^12.38.0",
"path-browserify": "^1.0.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-easy-crop": "^5.5.7",
"react-i18next": "^17.0.2",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.14",
"zustand": "^5.0.8"
"tailwindcss": "^4.2.2",
"zustand": "^5.0.12"
},
"devDependencies": {
"@tanstack/router-plugin": "^1.131.27",
"@types/node": "^24.3.0",
"@tanstack/router-plugin": "^1.167.12",
"@types/node": "^25.5.0",
"@types/path-browserify": "^1.0.3",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"@vitejs/plugin-react": "^5.0.1",
"@wailsio/runtime": "^3.0.0-alpha.66",
"daisyui": "^5.1.27",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"typescript": "^5.9.2",
"vite": "^7.1.3"
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@vitejs/plugin-react": "^6.0.1",
"@wailsio/runtime": "^3.0.0-alpha.79",
"daisyui": "^5.5.19",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"typescript": "^6.0.2",
"vite": "^8.0.3"
}
}

BIN
frontend/public/bg-1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
frontend/public/bg-10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
frontend/public/bg-11.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

BIN
frontend/public/bg-12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

BIN
frontend/public/bg-13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
frontend/public/bg-16.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

BIN
frontend/public/bg-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
frontend/public/bg-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
frontend/public/bg-5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
frontend/public/bg-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
frontend/public/bg-7.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

BIN
frontend/public/bg-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
frontend/public/bg-9.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,337 @@
{
"header": {
"home": "Home",
"tools": "Tools",
"language": "Language",
"diff": "Diff",
"client_update": "Client Update",
"plugins": "Plugins",
"analysis": "Analysis (Veritas)",
"firefly_tools": "Firefly Tools",
"how_to": "How to?",
"about": "About",
"settings": "Settings",
"minimize": "Minimize",
"close": "Close",
"by": "Firefly Shelter"
},
"background": {
"select_bg": "Select Background",
"choose_bg": "Choose Background",
"paste_url": "Paste image URL (https://...)",
"add": "Add",
"upload": "Upload",
"upload_from_computer": "Upload from computer",
"done": "Done",
"remove": "Remove",
"invalid_url": "Invalid url"
},
"close": {
"title": "Confirm Action",
"description": "Do you want to minimize the application to the system tray or close the application?",
"dont_ask": "Do not ask me again",
"minimize": "Minimize",
"close": "Close"
},
"setting": {
"title": "Settings",
"launcher_update_title": "Launcher Update",
"launcher_update_desc": "Check if your launcher is up to date.",
"launcher_update_btn": "Check for Launcher Updates",
"launcher_update_success": "Launcher is already up to date",
"closing_options_title": "Closing Options",
"set_dont_ask_again": "Set do not ask again",
"closing_auto_desc": "Next time you close the app, it will automatically {{action}} without asking.",
"action_minimize": "minimize to system tray",
"action_quit": "quit the app",
"version_label": "Version",
"server_label": "Server",
"proxy_label": "Proxy",
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "How to use all tools & commands",
"btn_select_game": "Select Game file",
"btn_selecting": "Selecting...",
"btn_start_game": "Start Game",
"btn_game_running": "Game is running",
"menu_change_path": "Change Game Path",
"menu_check_update": "Check for Updates Server & Proxy",
"menu_open_server": "Open server folder",
"menu_open_proxy": "Open proxy folder",
"menu_open_voice": "Open voice folder",
"status_updating_launcher": "Updating launcher",
"status_update_success": "Launcher updated successfully, auto closing after 5s",
"status_update_failed": "Launcher update failed, auto closing after 5s",
"status_wait": "Please wait...",
"status_complete": "Complete!",
"modal_update_title": "Update Data",
"modal_update_msg": "Do you want to update data server and proxy?",
"modal_download_title": "Download Data",
"modal_download_msg": "Data server and proxy download required",
"modal_self_update_title": "Update Launcher",
"modal_self_update_msg": "Do you want to update launcher?",
"btn_yes": "Yes",
"btn_no": "No",
"btn_download": "Download",
"error_game_dir": "Invalid game directory",
"error_file_type": "Incorrect file type",
"game_path_success": "Game path set successfully",
"no_updates": "No updates available",
"toast_pick_folder_error": "Pick folder error: ",
"toast_start_proxy_failed": "Failed to start proxy: ",
"toast_start_server_failed": "Failed to start server: ",
"toast_start_game_failed": "Failed to start game: ",
"toast_start_game_error": "Start game error: "
},
"diff": {
"header_title": "🎮 Game Update by Diff Tool",
"header_desc": "Help you update game with Diff Tool",
"game_dir_title": "Game Directory",
"btn_selecting": "Selecting...",
"btn_select_game": "Select Game Folder",
"game_dir_valid": "Valid game directory found!",
"game_dir_invalid": "Game directory not found. Please select the correct folder.",
"diff_file_title": "Diff file Directory",
"btn_select_diff": "Select Diff file Folder",
"diff_file_valid": "Valid diff file found!",
"diff_file_invalid": "Diff file not found. Please select the correct file.",
"btn_updating": "Updating...",
"btn_update_game": "Update Game",
"modal_update_title": "Update Game",
"status_wait": "Please wait...",
"inst_title": "📋 Instructions:",
"inst_step_1": "1. Click \"Select Game Folder\" and choose your game's root directory",
"inst_step_2": "2. Wait for the system to validate the game directory",
"inst_step_3": "3. Click \"Select Diff file Folder\" and choose your diff file's root directory",
"inst_step_4": "4. Wait for the system to validate the diff file directory",
"inst_step_5": "5. Click \"Update Game\" to save your changes",
"toast_game_dir_not_found": "Game directory not found. Please select the correct folder.",
"toast_no_folder_selected": "No folder path selected",
"toast_pick_folder_error": "Pick folder error: ",
"toast_invalid_file_type": "Not valid file type",
"toast_no_file_selected": "No file path selected",
"toast_pick_file_error": "Pick file error: ",
"toast_select_both": "Please select game directory and diff file",
"toast_update_completed": "Update game completed",
"stage_check_type": "Check Type Diff Tool",
"stage_version_validate": "Version Validate",
"stage_data_extract": "Data Extract",
"stage_cut_data": "Cut Data",
"stage_patch_data": "Patch Data",
"stage_delete_old_files": "Delete old files"
},
"language": {
"header_title": "🎮 Game Language Manager",
"header_desc": "Manage text and voice language settings for your game",
"game_dir_title": "Game Directory",
"btn_select_game": "Select Game Folder",
"btn_selecting": "Selecting...",
"game_dir_valid": "Valid game directory found!",
"game_dir_invalid": "Game directory not found. Please select the correct folder.",
"current_languages_title": "Current Languages",
"text_language": "Text Language",
"voice_language": "Voice Language",
"language_settings_title": "Language Settings",
"select_text_placeholder": "Select text language...",
"select_voice_placeholder": "Select voice language...",
"btn_apply": "Apply Language Settings",
"btn_applying": "Applying...",
"inst_title": "📋 Instructions:",
"inst_step_1": "1. Click \"Select Game Folder\" and choose your game's root directory",
"inst_step_2": "2. Wait for the system to validate the game directory",
"inst_step_3": "3. Select your preferred text and voice languages",
"inst_step_4": "4. Click \"Apply Language Settings\" to save your changes",
"toast_no_folder_selected": "No folder path selected",
"toast_pick_folder_error": "Pick Folder error: ",
"toast_set_language_success": "Language set successfully",
"toast_set_language_error": "Set language error: "
},
"howto": {
"title": "How to Use",
"sect1_title": "Using the Launcher Features",
"sect1_auto_update_pre": "Automatically update",
"sect1_auto_update_post": "and proxy tools when launching.",
"sect1_launch_game": "Launch the game directly through the launcher with correct parameters and runtime environment.",
"sect1_lang_pre": "Support switching in-game language (e.g., EN, JP, ZH, KR) via",
"sect1_lang_link": "Language Tools",
"sect1_patch_title": "Patch & Update Game Files",
"sect1_patch_desc_1_pre": "Use the",
"sect1_patch_desc_1_link": "Diff Tool",
"sect1_patch_desc_1_post": "(DiffPatch) for fast & lightweight incremental updates.",
"sect1_patch_desc_2_pre": "Supports",
"sect1_patch_desc_2_post": ", and custom diff formats.",
"sect2_title": "FireflyGo Chat Commands",
"sect2_desc_pre": "Below are in-game chat commands you can use. Some commands require you to enable",
"sect2_desc_bold": "Theorycraft Mode",
"sect2_desc_post": ".",
"sect2_tc_req_title": "Theorycraft Mode Required",
"sect2_tc_req_desc_pre": "The following commands are only available when",
"sect2_tc_req_desc_bold": "Theorycraft Mode",
"sect2_tc_req_desc_post": "is enabled:",
"sect2_extra_title": "Extra Settings",
"sect2_hidden_ui_title": "Hidden UI",
"sect2_hidden_ui_desc": "Instantly hides the entire game UI — often used in DIM showcase videos.",
"sect2_censor_title": "Disable Censorship",
"sect2_censor_desc": "Remove the Lens Flare censor effect 💀 for a cleaner experience.",
"sect2_tc_title": "Theorycraft Mode",
"sect2_tc_desc": "No need to type chat commands anymore — configure everything through the web: adjust monster HP, set cycles, view logs, and more.",
"sect2_cmd_title": "Available Commands:",
"sect2_cmd_tc": "Theorycraft Mode",
"sect2_cmd_tc_enable": "— Enable Theorycraft Mode",
"sect2_cmd_tc_disable": "— Disable Theorycraft Mode",
"sect2_cmd_cycle": "Cycle Control",
"sect2_cmd_tc_only": "(Theorycraft only)",
"sect2_cmd_cycle_desc": "— Set cycle count in battle",
"sect2_cmd_cycle_ex1_pre": "Example:",
"sect2_cmd_cycle_ex1_post": "sets battle to 30 cycles",
"sect2_cmd_cycle_ex2_post": "disables custom cycle",
"sect2_cmd_hp": "HP Override",
"sect2_cmd_hp_desc1": "— Set monster HP (only available in Theorycraft mode)",
"sect2_cmd_hp_desc2": "— Disable the set HP feature",
"sect2_cmd_hp_desc3": "— Set HP for each monster in a specific wave",
"sect2_cmd_hp_ex_pre": "Example:",
"sect2_cmd_hp_ex_post": "sets wave 1 monster1 HP=2,000,000 and monster2 HP=3,000,000",
"sect2_cmd_log": "Battle Log",
"sect2_cmd_log_desc1": "— Enable battle log output",
"sect2_cmd_log_desc2": "— Disable battle log",
"sect2_cmd_log_out_pre": "Output will be written as",
"sect2_cmd_skip": "Skip Nodes",
"sect2_cmd_skip_desc": "— Skip nodes in MOC / AS / Pure Fiction",
"sect2_cmd_skip_ex1_pre": "Example:",
"sect2_cmd_skip_ex1_post": "skips node 2",
"sect2_cmd_skip_ex2_post": "disables skipping",
"sect2_cmd_id": "Character Path Switch",
"sect2_cmd_id_desc": "— Switch path for multi-form characters",
"sect2_cmd_id_ex1_pre": "Example:",
"sect2_cmd_id_ex1_post": "to change MC (Trailblazer) form",
"sect2_cmd_id_ex2_pre": "Works with IDs like",
"sect2_cmd_update": "Refresh Data",
"sect2_cmd_update_desc_pre": "— Refresh server data from current",
"sect3_title": "Other Notes",
"sect3_admin_title": "Administrator Rights",
"sect3_admin_desc": "Always run the launcher as Administrator for file permission access.",
"sect3_backup_title": "Backup Data",
"sect3_backup_desc_pre": "Backup your",
"sect3_backup_desc_mid": "and",
"sect3_backup_desc_post": "regularly.",
"sect3_voice_title": "Enable Voice Packs in Beta Client",
"sect3_voice_step1_pre": "Copy the desired voice folder (e.g.,",
"sect3_voice_step1_mid": ") from:",
"sect3_voice_step1_post_pre": "to the beta folder by clicking",
"sect3_voice_step1_post_bold": "\"Open Voice Folder\"",
"sect3_voice_step1_post_post": "on the Home tab.",
"sect3_voice_step2": "When launching the game for the first time, it may delete the voice folder. If so, repeat step 1 to restore it.",
"btn_back": "Back to Home"
},
"about": {
"title": "About",
"p1_pre": "Hello! We are ",
"p1_post": ", a developer team passionate about building useful tools and improving user experiences.",
"p2_pre": "I created a lightweight and modern ",
"p2_highlight": "Game Launcher",
"p2_post": " to help users easily launch and manage their games with better performance and simplicity.",
"p3_pre": "The launcher is built using ",
"p3_mid": ", with a clean and responsive interface styled with ",
"p3_and": " and ",
"p3_post": ".",
"p4": "My goal is to make tools that are fast, efficient, and enjoyable to use — and this launcher is just the beginning.",
"btn_back": "Back to Home"
},
"fireflytools": {
"title": "Firefly Tools",
"sect1_title": "About Firefly Tools",
"sect1_p1_pre": "This site is another version of ",
"sect1_p1_tool": "Firefly Tools ",
"sect1_p1_mid": "developed by ",
"sect1_p1_author": "Firefly Shelter",
"sect1_master_site": "Master Website",
"sect1_p2_pre": "The original tool was created by a third-party developer named ",
"sect1_p2_author": "Amazing",
"sect1_p2_post": ". This version is directly based on that work, without modification to core logic.",
"sect1_p3_pre": "There is also a more modern version by the same author available at ",
"sect2_title": "Main Features",
"sect2_feat1": "Configure characters, light cones, relics, traces, and eidolons easily in your browser.",
"sect2_feat2_pre": "Instantly apply setups to ",
"sect2_feat2_server": "Firefly GO Server",
"sect2_feat2_mid": " using ",
"sect2_feat2_conn": "Connect PS",
"sect2_feat2_post": " — no manual file uploads required.",
"sect2_feat3_title": "Extra Settings",
"sect2_feat3_desc_pre": "Enhance your ",
"sect2_feat3_desc_server": "Firefly GO Server",
"sect2_feat3_desc_post": " experience with extra features:",
"sect2_feat3_ui": "Hidden Game UI",
"sect2_feat3_ui_desc": "— remove the entire game interface.",
"sect2_feat3_censor": "Disable Censorship",
"sect2_feat3_censor_desc": "— get rid of Lens Flare censor 💀.",
"sect2_feat3_tc": "Theorycraft Mode",
"sect2_feat3_tc_desc": "— configure HP, cycles, and more via the web.",
"sect2_feat4_pre": "Export and import full builds using ",
"sect2_feat4_post": ".",
"sect2_feat5": "Fast testing workflow — no sync cooldowns, instant in-game updates.",
"sect3_title": "Getting Started",
"sect3_step1": "Access the tool through your browser at the self-hosted instance.",
"sect3_step2": "Configure your character builds with the intuitive web interface.",
"sect3_step3_pre": "Use ",
"sect3_step3_bold": "Connect PS",
"sect3_step3_post": " feature to instantly sync with your private server.",
"sect3_step4": "Test your builds in-game with real-time updates and modifications.",
"btn_back": "Back to Home"
},
"analysis": {
"title": "Firefly Analysis & Veritas Plugin",
"sect1_title": "About Veritas",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": " is a powerful ",
"sect1_p1_highlight": "Damage Logger",
"sect1_p1_post": " designed for analyzing damage in real-time during gameplay.",
"sect1_p2": "It's lightweight, fast, and easy to use for comprehensive damage analysis.",
"sect1_github": "GitHub Repository",
"sect2_title": "Web Analysis Tools",
"sect2_desc": "Use these web applications for real-time damage analysis with Veritas:",
"sect2_master": "Master Website",
"sect2_backup": "Backup Website",
"sect2_tip_pre": "Tip:",
"sect2_tip_post": " If your country has issues loading from the master site, please use the backup site instead.",
"sect3_title": "Installation Instructions",
"sect3_subtitle": "Important Setup Step",
"sect3_desc": "After downloading Veritas, you must rename the file for it to work properly:",
"sect3_rename": "Rename: ",
"sect3_place": "Then place ",
"sect3_place_post": " into your game directory.",
"sect4_title": "How to Use Web App",
"sect4_sub1": "For Firefly GO Local",
"sect4_sub1_step1_pre": "Launch the ",
"sect4_sub1_step1_game": "game",
"sect4_sub1_step1_mid": " and your ",
"sect4_sub1_step1_server": "Firefly GO Server (PS)",
"sect4_sub1_step1_post": ".",
"sect4_sub1_step2": "Open one of the web analysis tools.",
"sect4_sub1_step3_pre": "Go to ",
"sect4_sub1_step3_conn": "Connection Settings",
"sect4_sub1_step3_mid1": " → select ",
"sect4_sub1_step3_type": "Connection Type: PS",
"sect4_sub1_step3_mid2": " → click ",
"sect4_sub1_step3_btn": "Connect",
"sect4_sub1_step3_post": ".",
"sect4_sub1_step4": "Once connected, play the game. The tool will automatically analyze in the background.",
"sect4_sub2": "For Other Private Servers",
"sect4_sub2_step1_pre": "Launch the ",
"sect4_sub2_step1_game": "game",
"sect4_sub2_step1_mid": " and your ",
"sect4_sub2_step1_server": "Private Server",
"sect4_sub2_step1_post": ".",
"sect4_sub2_step2": "Open one of the web analysis tools.",
"sect4_sub2_step3_pre": "Go to ",
"sect4_sub2_step3_conn": "Connection Settings",
"sect4_sub2_step3_mid1": " → select ",
"sect4_sub2_step3_type": "Connection Type: Native",
"sect4_sub2_step3_mid2": " → click ",
"sect4_sub2_step3_btn": "Connect",
"sect4_sub2_step3_post": ".",
"sect4_sub2_step4": "Once connected, play the game normally.",
"btn_back": "Back to Home"
}
}

View File

@@ -0,0 +1,337 @@
{
"header": {
"home": "ホーム",
"tools": "ツール",
"language": "言語",
"diff": "差分",
"client_update": "クライアント更新",
"plugins": "プラグイン",
"analysis": "分析Veritas",
"firefly_tools": "Fireflyツール",
"how_to": "使い方",
"about": "概要",
"settings": "設定",
"minimize": "最小化",
"close": "閉じる",
"by": "Firefly Shelter"
},
"background": {
"select_bg": "背景を選択",
"choose_bg": "背景を選択",
"paste_url": "画像URLを貼り付け (https://...)",
"add": "追加",
"upload": "アップロード",
"upload_from_computer": "パソコンからアップロード",
"done": "完了",
"remove": "削除",
"invalid_url": "無効なURLです"
},
"close": {
"title": "実行の確認",
"description": "最小化してシステムトレイに格納しますか?それとも終了しますか?",
"dont_ask": "次回から表示しない",
"minimize": "最小化",
"close": "終了"
},
"setting": {
"title": "設定",
"launcher_update_title": "ランチャーの更新",
"launcher_update_desc": "最新バージョンかどうかを確認します。",
"launcher_update_btn": "更新を確認",
"launcher_update_success": "ランチャーは最新の状態です",
"closing_options_title": "終了オプション",
"set_dont_ask_again": "次回から表示しない",
"closing_auto_desc": "次回終了時、確認なしで自動的に{{action}}します。",
"action_minimize": "システムトレイに最小化",
"action_quit": "アプリを終了",
"version_label": "バージョン",
"server_label": "Server",
"proxy_label": "Proxy",
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "ツールとコマンドの使い方",
"btn_select_game": "ゲームファイルを選択",
"btn_selecting": "選択中...",
"btn_start_game": "ゲーム開始",
"btn_game_running": "実行中",
"menu_change_path": "ゲームパスを変更",
"menu_check_update": "サーバーとプロキシの更新を確認",
"menu_open_server": "サーバーフォルダを開く",
"menu_open_proxy": "プロキシフォルダを開く",
"menu_open_voice": "ボイスフォルダを開く",
"status_updating_launcher": "ランチャーを更新中",
"status_update_success": "更新完了。5秒後に閉じます",
"status_update_failed": "更新失敗。5秒後に閉じます",
"status_wait": "お待ちください...",
"status_complete": "完了!",
"modal_update_title": "データ更新",
"modal_update_msg": "サーバーとプロキシを更新しますか?",
"modal_download_title": "データダウンロード",
"modal_download_msg": "サーバーとプロキシのダウンロードが必要です",
"modal_self_update_title": "ランチャー更新",
"modal_self_update_msg": "ランチャーを更新しますか?",
"btn_yes": "はい",
"btn_no": "いいえ",
"btn_download": "ダウンロード",
"error_game_dir": "無効なゲームディレクトリ",
"error_file_type": "ファイル形式が正しくありません",
"game_path_success": "ゲームパスが設定されました",
"no_updates": "利用可能なアップデートはありません",
"toast_pick_folder_error": "フォルダ選択エラー: ",
"toast_start_proxy_failed": "プロキシの起動に失敗しました: ",
"toast_start_server_failed": "サーバーの起動に失敗しました: ",
"toast_start_game_failed": "ゲームの起動に失敗しました: ",
"toast_start_game_error": "ゲーム起動エラー: "
},
"diff": {
"header_title": "🎮 Diff Toolによるゲーム更新",
"header_desc": "Diff Toolを使用したゲームの更新をサポートします",
"game_dir_title": "ゲームディレクトリ",
"btn_selecting": "選択中...",
"btn_select_game": "ゲームフォルダを選択",
"game_dir_valid": "有効なゲームディレクトリが見つかりました!",
"game_dir_invalid": "ゲームディレクトリが見つかりません。正しいフォルダを選択してください。",
"diff_file_title": "Diffファイルディレクトリ",
"btn_select_diff": "Diffファイルを選択",
"diff_file_valid": "有効なDiffファイルが見つかりました",
"diff_file_invalid": "Diffファイルが見つかりません。正しいファイルを選択してください。",
"btn_updating": "更新中...",
"btn_update_game": "ゲームを更新",
"modal_update_title": "ゲームを更新",
"status_wait": "お待ちください...",
"inst_title": "📋 手順:",
"inst_step_1": "1. 「ゲームフォルダを選択」をクリックし、ゲームのルートディレクトリを選択します",
"inst_step_2": "2. システムがゲームディレクトリを検証するのを待ちます",
"inst_step_3": "3. 「Diffファイルを選択」をクリックし、Diffファイルを選択します",
"inst_step_4": "4. システムがDiffファイルを検証するのを待ちます",
"inst_step_5": "5. 「ゲームを更新」をクリックして変更を保存します",
"toast_game_dir_not_found": "ゲームディレクトリが見つかりません。正しいフォルダを選択してください。",
"toast_no_folder_selected": "フォルダパスが選択されていません",
"toast_pick_folder_error": "フォルダ選択エラー: ",
"toast_invalid_file_type": "無効なファイル形式です",
"toast_no_file_selected": "ファイルパスが選択されていません",
"toast_pick_file_error": "ファイル選択エラー: ",
"toast_select_both": "ゲームディレクトリとDiffファイルの両方を選択してください",
"toast_update_completed": "ゲームの更新が完了しました",
"stage_check_type": "Diff Toolタイプの確認",
"stage_version_validate": "バージョンの検証",
"stage_data_extract": "データの抽出",
"stage_cut_data": "データの切り取り",
"stage_patch_data": "データのパッチ適用",
"stage_delete_old_files": "古いファイルの削除"
},
"language": {
"header_title": "🎮 ゲーム言語マネージャー",
"header_desc": "ゲームのテキストと音声言語の設定を管理します",
"game_dir_title": "ゲームディレクトリ",
"btn_select_game": "ゲームフォルダを選択",
"btn_selecting": "選択中...",
"game_dir_valid": "有効なゲームディレクトリが見つかりました!",
"game_dir_invalid": "ゲームディレクトリが見つかりません。正しいフォルダを選択してください。",
"current_languages_title": "現在の言語",
"text_language": "テキスト言語",
"voice_language": "音声言語",
"language_settings_title": "言語設定",
"select_text_placeholder": "テキスト言語を選択...",
"select_voice_placeholder": "音声言語を選択...",
"btn_apply": "言語設定を適用",
"btn_applying": "適用中...",
"inst_title": "📋 手順:",
"inst_step_1": "1. 「ゲームフォルダを選択」をクリックし、ゲームのルートディレクトリを選択します",
"inst_step_2": "2. システムがゲームディレクトリを検証するのを待ちます",
"inst_step_3": "3. 希望するテキスト言語と音声言語を選択します",
"inst_step_4": "4. 「言語設定を適用」をクリックして変更を保存します",
"toast_no_folder_selected": "フォルダパスが選択されていません",
"toast_pick_folder_error": "フォルダ選択エラー: ",
"toast_set_language_success": "言語が正常に設定されました",
"toast_set_language_error": "言語設定エラー: "
},
"howto": {
"title": "使い方",
"sect1_title": "ランチャー機能の使用",
"sect1_auto_update_pre": "起動時に",
"sect1_auto_update_post": "およびプロキシツールを自動的に更新します。",
"sect1_launch_game": "正しいパラメータと実行環境で、ランチャーから直接ゲームを起動します。",
"sect1_lang_pre": "ゲーム内言語(例: EN, JP, ZH, KRの切り替えをサポートします",
"sect1_lang_link": "言語ツール",
"sect1_patch_title": "ゲームファイルのパッチと更新",
"sect1_patch_desc_1_pre": "高速で軽量な差分更新には",
"sect1_patch_desc_1_link": "Diff Tool",
"sect1_patch_desc_1_post": "DiffPatchを使用します。",
"sect1_patch_desc_2_pre": "サポート形式:",
"sect1_patch_desc_2_post": "、およびカスタムdiffフォーマット。",
"sect2_title": "FireflyGo チャットコマンド",
"sect2_desc_pre": "ゲーム内で使用できるチャットコマンドは以下の通りです。一部のコマンドは",
"sect2_desc_bold": "Theorycraftモード",
"sect2_desc_post": "を有効にする必要があります。",
"sect2_tc_req_title": "Theorycraftモードが必要",
"sect2_tc_req_desc_pre": "以下のコマンドは",
"sect2_tc_req_desc_bold": "Theorycraftモード",
"sect2_tc_req_desc_post": "が有効な場合のみ使用できます:",
"sect2_extra_title": "追加設定",
"sect2_hidden_ui_title": "UIを隠す",
"sect2_hidden_ui_desc": "ゲームUI全体を即座に非表示にします — DIMショーケースビデオでよく使用されます。",
"sect2_censor_title": "検閲の無効化",
"sect2_censor_desc": "よりクリーンな体験のためにレンズフレア検閲効果 💀 を削除します。",
"sect2_tc_title": "Theorycraftモード",
"sect2_tc_desc": "チャットコマンドを入力する必要はもうありません — ウェブからすべてを設定できますモンスターのHP調整、サイクルの設定、ログの表示など。",
"sect2_cmd_title": "利用可能なコマンド:",
"sect2_cmd_tc": "Theorycraftモード",
"sect2_cmd_tc_enable": "— Theorycraftモードを有効にする",
"sect2_cmd_tc_disable": "— Theorycraftモードを無効にする",
"sect2_cmd_cycle": "サイクル制御",
"sect2_cmd_tc_only": "(Theorycraftのみ)",
"sect2_cmd_cycle_desc": "— 戦闘でのサイクル数を設定する",
"sect2_cmd_cycle_ex1_pre": "例:",
"sect2_cmd_cycle_ex1_post": "は戦闘を30サイクルに設定します",
"sect2_cmd_cycle_ex2_post": "はカスタムサイクルを無効にします",
"sect2_cmd_hp": "HPの上書き",
"sect2_cmd_hp_desc1": "— モンスターのHPを設定する (Theorycraftモードでのみ利用可能)",
"sect2_cmd_hp_desc2": "— HP設定機能を無効にする",
"sect2_cmd_hp_desc3": "— 特定のウェーブの各モンスターにHPを設定する",
"sect2_cmd_hp_ex_pre": "例:",
"sect2_cmd_hp_ex_post": "はウェーブ1のモンスター1のHPを2,000,000に、モンスター2のHPを3,000,000に設定します",
"sect2_cmd_log": "バトルログ",
"sect2_cmd_log_desc1": "— バトルログ出力を有効にする",
"sect2_cmd_log_desc2": "— バトルログを無効にする",
"sect2_cmd_log_out_pre": "出力は以下の形式で書き込まれます:",
"sect2_cmd_skip": "ノードのスキップ",
"sect2_cmd_skip_desc": "— MOC / AS / 虚構叙事でノードをスキップする",
"sect2_cmd_skip_ex1_pre": "例:",
"sect2_cmd_skip_ex1_post": "はード2をスキップします",
"sect2_cmd_skip_ex2_post": "はスキップを無効にします",
"sect2_cmd_id": "キャラクター運命の切り替え",
"sect2_cmd_id_desc": "— 複数形態を持つキャラクターの運命を切り替える",
"sect2_cmd_id_ex1_pre": "例:",
"sect2_cmd_id_ex1_post": "は主人公(開拓者)の形態を変更します",
"sect2_cmd_id_ex2_pre": "対応ID例:",
"sect2_cmd_update": "データのリフレッシュ",
"sect2_cmd_update_desc_pre": "— 現在のファイルからサーバーデータを更新します:",
"sect3_title": "その他の注意事項",
"sect3_admin_title": "管理者権限",
"sect3_admin_desc": "ファイル権限にアクセスするため、常にランチャーを管理者として実行してください。",
"sect3_backup_title": "データのバックアップ",
"sect3_backup_desc_pre": "定期的に",
"sect3_backup_desc_mid": "と",
"sect3_backup_desc_post": "をバックアップしてください。",
"sect3_voice_title": "ベータクライアントでボイスパックを有効にする",
"sect3_voice_step1_pre": "希望するボイスフォルダ (例:",
"sect3_voice_step1_mid": ") を以下からコピーし:",
"sect3_voice_step1_post_pre": "ホームタブの",
"sect3_voice_step1_post_bold": "「ボイスフォルダを開く」",
"sect3_voice_step1_post_post": "をクリックしてベータフォルダに配置します。",
"sect3_voice_step2": "ゲームを初めて起動した際、ボイスフォルダが削除される場合があります。その場合は、手順1を繰り返して復元してください。",
"btn_back": "ホームに戻る"
},
"about": {
"title": "概要",
"p1_pre": "こんにちは!私たちは ",
"p1_post": " です。便利なツールの構築とユーザー体験の向上に情熱を注ぐ開発チームです。",
"p2_pre": "ユーザーがより良いパフォーマンスとシンプルさで簡単にゲームを起動・管理できるように、軽量でモダンな ",
"p2_highlight": "Game Launcher",
"p2_post": " を作成しました。",
"p3_pre": "ランチャーは ",
"p3_mid": " を使用して構築されており、",
"p3_and": " と ",
"p3_post": " でデザインされたクリーンでレスポンシブなインターフェースを備えています。",
"p4": "私の目標は、高速で効率的、そして使って楽しいツールを作ることです。このランチャーはその始まりに過ぎません。",
"btn_back": "ホームに戻る"
},
"fireflytools": {
"title": "Firefly ツール",
"sect1_title": "Firefly ツールについて",
"sect1_p1_pre": "このサイトは ",
"sect1_p1_tool": "Firefly Tools ",
"sect1_p1_mid": "の別バージョンであり、開発元は ",
"sect1_p1_author": "Firefly Shelter",
"sect1_master_site": "マスターウェブサイト",
"sect1_p2_pre": "オリジナルのツールは、",
"sect1_p2_author": "Amazing",
"sect1_p2_post": " というサードパーティの開発者によって作成されました。このバージョンは、コアロジックを変更することなく、その作業に直接基づいています。",
"sect1_p3_pre": "同じ作者によるよりモダンなバージョンも以下で利用可能です:",
"sect2_title": "主な機能",
"sect2_feat1": "ブラウザでキャラクター、光円錐、遺物、軌跡、星魂を簡単に設定できます。",
"sect2_feat2_pre": "",
"sect2_feat2_server": "Firefly GO Server",
"sect2_feat2_mid": " に ",
"sect2_feat2_conn": "Connect PS",
"sect2_feat2_post": " を使用して設定を即座に適用します — 手動でのファイルアップロードは不要です。",
"sect2_feat3_title": "追加設定",
"sect2_feat3_desc_pre": "追加機能で ",
"sect2_feat3_desc_server": "Firefly GO Server",
"sect2_feat3_desc_post": " の体験を向上させます:",
"sect2_feat3_ui": "UIを隠す",
"sect2_feat3_ui_desc": "— ゲームインターフェース全体を削除します。",
"sect2_feat3_censor": "検閲の無効化",
"sect2_feat3_censor_desc": "— レンズフレア検閲 💀 を取り除きます。",
"sect2_feat3_tc": "Theorycraft モード",
"sect2_feat3_tc_desc": "— ウェブ経由でHP、サイクルなどを設定します。",
"sect2_feat4_pre": "を使用して完全なビルドをエクスポートおよびインポートします:",
"sect2_feat4_post": "。",
"sect2_feat5": "高速なテストワークフロー — 同期のクールダウンなし、ゲーム内の即時更新。",
"sect3_title": "はじめに",
"sect3_step1": "セルフホストされたインスタンスのブラウザからツールにアクセスします。",
"sect3_step2": "直感的なウェブインターフェースでキャラクタービルドを設定します。",
"sect3_step3_pre": "",
"sect3_step3_bold": "Connect PS",
"sect3_step3_post": " 機能を使用して、プライベートサーバーと即座に同期します。",
"sect3_step4": "リアルタイムの更新と変更を使用して、ゲーム内でビルドをテストします。",
"btn_back": "ホームに戻る"
},
"analysis": {
"title": "Firefly 分析 & Veritas プラグイン",
"sect1_title": "Veritas について",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": " は、ゲームプレイ中のダメージをリアルタイムで分析するために設計された強力な ",
"sect1_p1_highlight": "ダメージロガー",
"sect1_p1_post": " です。",
"sect1_p2": "軽量で高速、包括的なダメージ分析に使いやすいツールです。",
"sect1_github": "GitHub リポジトリ",
"sect2_title": "Web 分析ツール",
"sect2_desc": "Veritas を使用したリアルタイムダメージ分析には、以下の Web アプリケーションを使用してください:",
"sect2_master": "マスターウェブサイト",
"sect2_backup": "バックアップウェブサイト",
"sect2_tip_pre": "ヒント:",
"sect2_tip_post": " マスターサイトの読み込みに問題がある場合は、バックアップサイトをご利用ください。",
"sect3_title": "インストール手順",
"sect3_subtitle": "重要なセットアップ手順",
"sect3_desc": "Veritas をダウンロードした後、正常に動作させるにはファイル名を変更する必要があります:",
"sect3_rename": "名前の変更: ",
"sect3_place": "その後、",
"sect3_place_post": " をゲームディレクトリに配置します。",
"sect4_title": "Web アプリの使い方",
"sect4_sub1": "Firefly GO ローカル用",
"sect4_sub1_step1_pre": "",
"sect4_sub1_step1_game": "ゲーム",
"sect4_sub1_step1_mid": " と ",
"sect4_sub1_step1_server": "Firefly GO サーバー (PS)",
"sect4_sub1_step1_post": " を起動します。",
"sect4_sub1_step2": "いずれかの Web 分析ツールを開きます。",
"sect4_sub1_step3_pre": "",
"sect4_sub1_step3_conn": "Connection Settings",
"sect4_sub1_step3_mid1": " に移動し、",
"sect4_sub1_step3_type": "Connection Type: PS",
"sect4_sub1_step3_mid2": " を選択して、",
"sect4_sub1_step3_btn": "Connect",
"sect4_sub1_step3_post": " をクリックします。",
"sect4_sub1_step4": "接続後、ゲームをプレイしてください。ツールがバックグラウンドで自動的に分析を行います。",
"sect4_sub2": "その他のプライベートサーバー用",
"sect4_sub2_step1_pre": "",
"sect4_sub2_step1_game": "ゲーム",
"sect4_sub2_step1_mid": " と ",
"sect4_sub2_step1_server": "プライベートサーバー",
"sect4_sub2_step1_post": " を起動します。",
"sect4_sub2_step2": "いずれかの Web 分析ツールを開きます。",
"sect4_sub2_step3_pre": "",
"sect4_sub2_step3_conn": "Connection Settings",
"sect4_sub2_step3_mid1": " に移動し、",
"sect4_sub2_step3_type": "Connection Type: Native",
"sect4_sub2_step3_mid2": " を選択して、",
"sect4_sub2_step3_btn": "Connect",
"sect4_sub2_step3_post": " をクリックします。",
"sect4_sub2_step4": "接続後、通常通りゲームをプレイしてください。",
"btn_back": "ホームに戻る"
}
}

View File

@@ -0,0 +1,337 @@
{
"header": {
"home": "홈",
"tools": "도구",
"language": "언어",
"diff": "비교",
"client_update": "클라이언트 업데이트",
"plugins": "플러그인",
"analysis": "분석 (Veritas)",
"firefly_tools": "Firefly 도구",
"how_to": "사용 방법",
"about": "정보",
"settings": "설정",
"minimize": "최소화",
"close": "닫기",
"by": "Firefly Shelter"
},
"background": {
"select_bg": "배경 선택",
"choose_bg": "배경 선택",
"paste_url": "이미지 URL 붙여넣기 (https://...)",
"add": "추가",
"upload": "업로드",
"upload_from_computer": "컴퓨터에서 업로드",
"done": "완료",
"remove": "삭제",
"invalid_url": "잘못된 URL입니다"
},
"close": {
"title": "작업 확인",
"description": "앱을 트레이로 최소화하시겠습니까, 아니면 종료하시겠습니까?",
"dont_ask": "다시 묻지 않음",
"minimize": "최소화",
"close": "종료"
},
"setting": {
"title": "설정",
"launcher_update_title": "런처 업데이트",
"launcher_update_desc": "런처가 최신 상태인지 확인합니다.",
"launcher_update_btn": "업데이트 확인",
"launcher_update_success": "런처가 이미 최신 버전입니다",
"closing_options_title": "종료 옵션",
"set_dont_ask_again": "다시 묻지 않음 설정",
"closing_auto_desc": "다음 종료 시 묻지 않고 자동으로 {{action}}합니다.",
"action_minimize": "트레이로 최소화",
"action_quit": "앱 종료",
"version_label": "버전",
"server_label": "Server",
"proxy_label": "Proxy",
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "도구 및 명령 사용법",
"btn_select_game": "게임 파일 선택",
"btn_selecting": "선택 중...",
"btn_start_game": "게임 시작",
"btn_game_running": "게임 실행 중",
"menu_change_path": "게임 경로 변경",
"menu_check_update": "서버 및 프록시 업데이트 확인",
"menu_open_server": "서버 폴더 열기",
"menu_open_proxy": "프록시 폴더 열기",
"menu_open_voice": "음성 폴더 열기",
"status_updating_launcher": "런처 업데이트 중",
"status_update_success": "업데이트 완료, 5초 후 종료",
"status_update_failed": "업데이트 실패, 5초 후 종료",
"status_wait": "잠시만 기다려 주세요...",
"status_complete": "완료!",
"modal_update_title": "데이터 업데이트",
"modal_update_msg": "서버 및 프록시 데이터를 업데이트하시겠습니까?",
"modal_download_title": "데이터 다운로드",
"modal_download_msg": "서버 및 프록시 데이터 다운로드가 필요합니다",
"modal_self_update_title": "런처 업데이트",
"modal_self_update_msg": "런처를 업데이트하시겠습니까?",
"btn_yes": "예",
"btn_no": "아니요",
"btn_download": "다운로드",
"error_game_dir": "잘못된 게임 디렉토리",
"error_file_type": "잘못된 파일 형식",
"game_path_success": "게임 경로 설정 완료",
"no_updates": "업데이트가 없습니다",
"toast_pick_folder_error": "폴더 선택 오류: ",
"toast_start_proxy_failed": "프록시 시작 실패: ",
"toast_start_server_failed": "서버 시작 실패: ",
"toast_start_game_failed": "게임 시작 실패: ",
"toast_start_game_error": "게임 시작 오류: "
},
"diff": {
"header_title": "🎮 Diff Tool로 게임 업데이트",
"header_desc": "Diff Tool을 사용하여 게임을 업데이트하도록 도와줍니다",
"game_dir_title": "게임 디렉토리",
"btn_selecting": "선택 중...",
"btn_select_game": "게임 폴더 선택",
"game_dir_valid": "유효한 게임 디렉토리를 찾았습니다!",
"game_dir_invalid": "게임 디렉토리를 찾을 수 없습니다. 올바른 폴더를 선택해주세요.",
"diff_file_title": "Diff 파일 디렉토리",
"btn_select_diff": "Diff 파일 선택",
"diff_file_valid": "유효한 Diff 파일을 찾았습니다!",
"diff_file_invalid": "Diff 파일을 찾을 수 없습니다. 올바른 파일을 선택해주세요.",
"btn_updating": "업데이트 중...",
"btn_update_game": "게임 업데이트",
"modal_update_title": "게임 업데이트",
"status_wait": "잠시만 기다려 주세요...",
"inst_title": "📋 지침:",
"inst_step_1": "1. \"게임 폴더 선택\"을 클릭하고 게임의 루트 디렉토리를 선택합니다",
"inst_step_2": "2. 시스템이 게임 디렉토리를 검증할 때까지 기다립니다",
"inst_step_3": "3. \"Diff 파일 선택\"을 클릭하고 Diff 파일을 선택합니다",
"inst_step_4": "4. 시스템이 Diff 파일을 검증할 때까지 기다립니다",
"inst_step_5": "5. \"게임 업데이트\"를 클릭하여 변경 사항을 저장합니다",
"toast_game_dir_not_found": "게임 디렉토리를 찾을 수 없습니다. 올바른 폴더를 선택해주세요.",
"toast_no_folder_selected": "폴더 경로가 선택되지 않았습니다",
"toast_pick_folder_error": "폴더 선택 오류: ",
"toast_invalid_file_type": "유효하지 않은 파일 형식입니다",
"toast_no_file_selected": "파일 경로가 선택되지 않았습니다",
"toast_pick_file_error": "파일 선택 오류: ",
"toast_select_both": "게임 디렉토리와 Diff 파일을 모두 선택해주세요",
"toast_update_completed": "게임 업데이트가 완료되었습니다",
"stage_check_type": "Diff Tool 유형 확인",
"stage_version_validate": "버전 유효성 검사",
"stage_data_extract": "데이터 추출",
"stage_cut_data": "데이터 자르기",
"stage_patch_data": "데이터 패치",
"stage_delete_old_files": "오래된 파일 삭제"
},
"language": {
"header_title": "🎮 게임 언어 관리자",
"header_desc": "게임의 텍스트 및 음성 언어 설정을 관리합니다",
"game_dir_title": "게임 디렉토리",
"btn_select_game": "게임 폴더 선택",
"btn_selecting": "선택 중...",
"game_dir_valid": "유효한 게임 디렉토리를 찾았습니다!",
"game_dir_invalid": "게임 디렉토리를 찾을 수 없습니다. 올바른 폴더를 선택해주세요.",
"current_languages_title": "현재 언어",
"text_language": "텍스트 언어",
"voice_language": "음성 언어",
"language_settings_title": "언어 설정",
"select_text_placeholder": "텍스트 언어 선택...",
"select_voice_placeholder": "음성 언어 선택...",
"btn_apply": "언어 설정 적용",
"btn_applying": "적용 중...",
"inst_title": "📋 지침:",
"inst_step_1": "1. \"게임 폴더 선택\"을 클릭하고 게임의 루트 디렉토리를 선택합니다",
"inst_step_2": "2. 시스템이 게임 디렉토리를 검증할 때까지 기다립니다",
"inst_step_3": "3. 선호하는 텍스트 및 음성 언어를 선택합니다",
"inst_step_4": "4. \"언어 설정 적용\"을 클릭하여 변경 사항을 저장합니다",
"toast_no_folder_selected": "폴더 경로가 선택되지 않았습니다",
"toast_pick_folder_error": "폴더 선택 오류: ",
"toast_set_language_success": "언어 설정이 완료되었습니다",
"toast_set_language_error": "언어 설정 오류: "
},
"howto": {
"title": "사용 방법",
"sect1_title": "런처 기능 사용",
"sect1_auto_update_pre": "실행 시",
"sect1_auto_update_post": "및 프록시 도구를 자동으로 업데이트합니다.",
"sect1_launch_game": "올바른 매개변수와 실행 환경으로 런처를 통해 게임을 직접 실행합니다.",
"sect1_lang_pre": "게임 내 언어(예: EN, JP, ZH, KR) 전환 지원:",
"sect1_lang_link": "언어 도구",
"sect1_patch_title": "게임 파일 패치 및 업데이트",
"sect1_patch_desc_1_pre": "빠르고 가벼운 증분 업데이트를 위해",
"sect1_patch_desc_1_link": "Diff Tool",
"sect1_patch_desc_1_post": "(DiffPatch)을 사용합니다.",
"sect1_patch_desc_2_pre": "지원 형식:",
"sect1_patch_desc_2_post": "및 사용자 정의 diff 형식.",
"sect2_title": "FireflyGo 채팅 명령",
"sect2_desc_pre": "게임 내에서 사용할 수 있는 채팅 명령어입니다. 일부 명령은",
"sect2_desc_bold": "Theorycraft 모드",
"sect2_desc_post": "를 활성화해야 합니다.",
"sect2_tc_req_title": "Theorycraft 모드 필요",
"sect2_tc_req_desc_pre": "다음 명령은",
"sect2_tc_req_desc_bold": "Theorycraft 모드",
"sect2_tc_req_desc_post": "가 활성화된 경우에만 사용할 수 있습니다:",
"sect2_extra_title": "추가 설정",
"sect2_hidden_ui_title": "UI 숨기기",
"sect2_hidden_ui_desc": "DIM 쇼케이스 비디오에서 자주 사용되는 게임 UI 전체를 즉시 숨깁니다.",
"sect2_censor_title": "검열 비활성화",
"sect2_censor_desc": "더 깔끔한 경험을 위해 렌즈 플레어 검열 효과 💀를 제거합니다.",
"sect2_tc_title": "Theorycraft 모드",
"sect2_tc_desc": "더 이상 채팅 명령어를 입력할 필요가 없습니다 — 몬스터 HP 조정, 주기 설정, 로그 보기 등 모든 것을 웹을 통해 구성하세요.",
"sect2_cmd_title": "사용 가능한 명령:",
"sect2_cmd_tc": "Theorycraft 모드",
"sect2_cmd_tc_enable": "— Theorycraft 모드 활성화",
"sect2_cmd_tc_disable": "— Theorycraft 모드 비활성화",
"sect2_cmd_cycle": "주기 제어",
"sect2_cmd_tc_only": "(Theorycraft 전용)",
"sect2_cmd_cycle_desc": "— 전투에서 주기 수 설정",
"sect2_cmd_cycle_ex1_pre": "예:",
"sect2_cmd_cycle_ex1_post": "는 전투를 30주기로 설정합니다",
"sect2_cmd_cycle_ex2_post": "는 사용자 정의 주기를 비활성화합니다",
"sect2_cmd_hp": "HP 덮어쓰기",
"sect2_cmd_hp_desc1": "— 몬스터 HP 설정 (Theorycraft 모드에서만 사용 가능)",
"sect2_cmd_hp_desc2": "— HP 설정 기능 비활성화",
"sect2_cmd_hp_desc3": "— 특정 웨이브의 각 몬스터에 HP 설정",
"sect2_cmd_hp_ex_pre": "예:",
"sect2_cmd_hp_ex_post": "는 웨이브 1 몬스터1 HP를 2,000,000으로, 몬스터2 HP를 3,000,000으로 설정합니다",
"sect2_cmd_log": "전투 로그",
"sect2_cmd_log_desc1": "— 전투 로그 출력 활성화",
"sect2_cmd_log_desc2": "— 전투 로그 비활성화",
"sect2_cmd_log_out_pre": "출력은 다음 형식으로 작성됩니다:",
"sect2_cmd_skip": "노드 건너뛰기",
"sect2_cmd_skip_desc": "— 혼돈의 기억 / 허구 이야기에서 노드 건너뛰기",
"sect2_cmd_skip_ex1_pre": "예:",
"sect2_cmd_skip_ex1_post": "는 노드 2를 건너뜁니다",
"sect2_cmd_skip_ex2_post": "는 건너뛰기를 비활성화합니다",
"sect2_cmd_id": "캐릭터 운명의 길 전환",
"sect2_cmd_id_desc": "— 다중 형태를 가진 캐릭터의 운명의 길 전환",
"sect2_cmd_id_ex1_pre": "예:",
"sect2_cmd_id_ex1_post": "는 개척자 형태를 변경합니다",
"sect2_cmd_id_ex2_pre": "지원되는 ID 예:",
"sect2_cmd_update": "데이터 새로고침",
"sect2_cmd_update_desc_pre": "— 다음 파일에서 서버 데이터를 새로고침합니다:",
"sect3_title": "기타 참고 사항",
"sect3_admin_title": "관리자 권한",
"sect3_admin_desc": "파일 권한 액세스를 위해 항상 런처를 관리자 권한으로 실행하세요.",
"sect3_backup_title": "데이터 백업",
"sect3_backup_desc_pre": "정기적으로",
"sect3_backup_desc_mid": "및",
"sect3_backup_desc_post": "를 백업하세요.",
"sect3_voice_title": "베타 클라이언트에서 음성 팩 활성화",
"sect3_voice_step1_pre": "원하는 음성 폴더(예:",
"sect3_voice_step1_mid": ")를 다음 경로에서 복사하여:",
"sect3_voice_step1_post_pre": "홈 탭에서",
"sect3_voice_step1_post_bold": "\"음성 폴더 열기\"",
"sect3_voice_step1_post_post": "를 클릭해 베타 폴더에 넣습니다.",
"sect3_voice_step2": "게임을 처음 실행할 때 음성 폴더가 삭제될 수 있습니다. 그런 경우 1단계를 반복하여 복구하세요.",
"btn_back": "홈으로 돌아가기"
},
"about": {
"title": "소개",
"p1_pre": "안녕하세요! 저희는 유용한 도구를 개발하고 사용자 경험을 개선하는 데 열정을 쏟는 개발팀 ",
"p1_post": " 입니다.",
"p2_pre": "사용자가 더 나은 성능과 단순함으로 게임을 쉽게 실행하고 관리할 수 있도록 가볍고 현대적인 ",
"p2_highlight": "Game Launcher",
"p2_post": " 를 만들었습니다.",
"p3_pre": "런처는 ",
"p3_mid": " 을(를) 사용하여 구축되었으며, ",
"p3_and": " 및 ",
"p3_post": " (으)로 디자인된 깔끔하고 반응형인 인터페이스를 특징으로 합니다.",
"p4": "제 목표는 빠르고 효율적이며 사용하기 즐거운 도구를 만드는 것입니다 — 이 런처는 단지 시작일 뿐입니다.",
"btn_back": "홈으로 돌아가기"
},
"fireflytools": {
"title": "Firefly 도구",
"sect1_title": "Firefly 도구 소개",
"sect1_p1_pre": "이 사이트는 ",
"sect1_p1_tool": "Firefly Shelter",
"sect1_p1_mid": "에서 개발한 ",
"sect1_p1_author": "Firefly Tools",
"sect1_master_site": "마스터 웹사이트",
"sect1_p2_pre": "원본 도구는 ",
"sect1_p2_author": "Amazing",
"sect1_p2_post": " 이라는 타사 개발자가 만들었습니다. 이 버전은 핵심 논리를 수정하지 않고 해당 작업을 직접 기반으로 합니다.",
"sect1_p3_pre": "동일한 작성자의 더 현대적인 버전도 다음에서 사용할 수 있습니다: ",
"sect2_title": "주요 기능",
"sect2_feat1": "브라우저에서 캐릭터, 광추, 유물, 행적 및 성혼을 쉽게 구성하세요.",
"sect2_feat2_pre": "",
"sect2_feat2_server": "Firefly GO Server",
"sect2_feat2_mid": "에 ",
"sect2_feat2_conn": "Connect PS",
"sect2_feat2_post": "를 사용하여 설정을 즉시 적용합니다 — 수동 파일 업로드가 필요하지 않습니다.",
"sect2_feat3_title": "추가 설정",
"sect2_feat3_desc_pre": "추가 기능으로 ",
"sect2_feat3_desc_server": "Firefly GO Server",
"sect2_feat3_desc_post": " 환경을 향상시키세요:",
"sect2_feat3_ui": "게임 UI 숨기기",
"sect2_feat3_ui_desc": "— 전체 게임 인터페이스를 제거합니다.",
"sect2_feat3_censor": "검열 비활성화",
"sect2_feat3_censor_desc": "— 렌즈 플레어 검열 💀을 제거합니다.",
"sect2_feat3_tc": "Theorycraft 모드",
"sect2_feat3_tc_desc": "— 웹을 통해 HP, 주기 등을 구성합니다.",
"sect2_feat4_pre": "다음을 사용하여 전체 빌드를 내보내고 가져옵니다: ",
"sect2_feat4_post": ".",
"sect2_feat5": "빠른 테스트 워크플로 — 동기화 재사용 대기시간 없음, 즉각적인 게임 내 업데이트.",
"sect3_title": "시작하기",
"sect3_step1": "자체 호스팅 인스턴스에서 브라우저를 통해 도구에 액세스합니다.",
"sect3_step2": "직관적인 웹 인터페이스로 캐릭터 빌드를 구성합니다.",
"sect3_step3_pre": "",
"sect3_step3_bold": "Connect PS",
"sect3_step3_post": " 기능을 사용하여 사설 서버와 즉시 동기화합니다.",
"sect3_step4": "실시간 업데이트 및 수정 사항을 통해 게임 내에서 빌드를 테스트합니다.",
"btn_back": "홈으로 돌아가기"
},
"analysis": {
"title": "Firefly 분석 & Veritas 플러그인",
"sect1_title": "Veritas 소개",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": "는 게임 플레이 중 실시간으로 피해량을 분석하도록 설계된 강력한 ",
"sect1_p1_highlight": "데미지 로거",
"sect1_p1_post": "입니다.",
"sect1_p2": "포괄적인 피해 분석을 위해 가볍고 빠르며 사용하기 쉽습니다.",
"sect1_github": "GitHub 저장소",
"sect2_title": "웹 분석 도구",
"sect2_desc": "Veritas를 통한 실시간 피해 분석을 위해 다음 웹 애플리케이션을 사용하세요:",
"sect2_master": "마스터 웹사이트",
"sect2_backup": "백업 웹사이트",
"sect2_tip_pre": "팁:",
"sect2_tip_post": " 해당 국가에서 마스터 사이트 로딩에 문제가 있는 경우 백업 사이트를 대신 사용하세요.",
"sect3_title": "설치 지침",
"sect3_subtitle": "중요 설정 단계",
"sect3_desc": "Veritas를 다운로드한 후 정상적으로 작동하려면 파일 이름을 변경해야 합니다:",
"sect3_rename": "이름 변경: ",
"sect3_place": "그런 다음 ",
"sect3_place_post": "를 게임 디렉토리에 배치합니다.",
"sect4_title": "웹 앱 사용 방법",
"sect4_sub1": "Firefly GO 로컬용",
"sect4_sub1_step1_pre": "",
"sect4_sub1_step1_game": "게임",
"sect4_sub1_step1_mid": "과 ",
"sect4_sub1_step1_server": "Firefly GO 서버(PS)",
"sect4_sub1_step1_post": "를 실행합니다.",
"sect4_sub1_step2": "웹 분석 도구 중 하나를 엽니다.",
"sect4_sub1_step3_pre": "",
"sect4_sub1_step3_conn": "Connection Settings",
"sect4_sub1_step3_mid1": "로 이동 → ",
"sect4_sub1_step3_type": "Connection Type: PS",
"sect4_sub1_step3_mid2": " 선택 → ",
"sect4_sub1_step3_btn": "Connect",
"sect4_sub1_step3_post": "를 클릭합니다.",
"sect4_sub1_step4": "연결되면 게임을 플레이합니다. 도구가 백그라운드에서 자동으로 분석합니다.",
"sect4_sub2": "기타 사설 서버용",
"sect4_sub2_step1_pre": "",
"sect4_sub2_step1_game": "게임",
"sect4_sub2_step1_mid": "과 ",
"sect4_sub2_step1_server": "사설 서버",
"sect4_sub2_step1_post": "를 실행합니다.",
"sect4_sub2_step2": "웹 분석 도구 중 하나를 엽니다.",
"sect4_sub2_step3_pre": "",
"sect4_sub2_step3_conn": "Connection Settings",
"sect4_sub2_step3_mid1": "로 이동 → ",
"sect4_sub2_step3_type": "Connection Type: Native",
"sect4_sub2_step3_mid2": " 선택 → ",
"sect4_sub2_step3_btn": "Connect",
"sect4_sub2_step3_post": "를 클릭합니다.",
"sect4_sub2_step4": "연결되면 평소처럼 게임을 플레이합니다.",
"btn_back": "홈으로 돌아가기"
}
}

View File

@@ -0,0 +1,337 @@
{
"header": {
"home": "Trang chủ",
"tools": "Công cụ",
"language": "Ngôn ngữ",
"diff": "So sánh",
"client_update": "Cập nhật client",
"plugins": "Plugin",
"analysis": "Phân tích (Veritas)",
"firefly_tools": "Firefly Tools",
"how_to": "Hướng dẫn",
"about": "Giới thiệu",
"settings": "Cài đặt",
"minimize": "Thu nhỏ",
"close": "Đóng",
"by": "Firefly Shelter"
},
"background": {
"select_bg": "Chọn nền",
"choose_bg": "Chọn ảnh nền",
"paste_url": "Dán URL ảnh (https://...)",
"add": "Thêm",
"upload": "Tải lên",
"upload_from_computer": "Tải từ máy tính",
"done": "Xong",
"remove": "Xóa",
"invalid_url": "URL không hợp lệ"
},
"close": {
"title": "Xác nhận hành động",
"description": "Bạn muốn thu nhỏ ứng dụng xuống khay hệ thống hay đóng ứng dụng?",
"dont_ask": "Không hỏi lại tôi",
"minimize": "Thu nhỏ",
"close": "Đóng"
},
"setting": {
"title": "Cài đặt",
"launcher_update_title": "Cập nhật Launcher",
"launcher_update_desc": "Kiểm tra xem launcher của bạn đã là phiên bản mới nhất chưa.",
"launcher_update_btn": "Kiểm tra cập nhật",
"launcher_update_success": "Launcher đã là phiên bản mới nhất",
"closing_options_title": "Tùy chọn đóng",
"set_dont_ask_again": "Không hỏi lại lần sau",
"closing_auto_desc": "Lần tới khi bạn đóng, ứng dụng sẽ tự động {{action}} mà không cần hỏi.",
"action_minimize": "thu nhỏ xuống khay hệ thống",
"action_quit": "thoát ứng dụng",
"version_label": "Phiên bản",
"server_label": "Server",
"proxy_label": "Proxy",
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "Cách sử dụng công cụ & lệnh",
"btn_select_game": "Chọn tệp Game",
"btn_selecting": "Đang chọn...",
"btn_start_game": "Bắt đầu Game",
"btn_game_running": "Game đang chạy",
"menu_change_path": "Thay đổi đường dẫn Game",
"menu_check_update": "Kiểm tra cập nhật Server & Proxy",
"menu_open_server": "Mở thư mục Server",
"menu_open_proxy": "Mở thư mục Proxy",
"menu_open_voice": "Mở thư mục Voice",
"status_updating_launcher": "Đang cập nhật Launcher",
"status_update_success": "Cập nhật thành công, tự động đóng sau 5s",
"status_update_failed": "Cập nhật thất bại, tự động đóng sau 5s",
"status_wait": "Vui lòng đợi...",
"status_complete": "Hoàn tất!",
"modal_update_title": "Cập nhật dữ liệu",
"modal_update_msg": "Bạn có muốn cập nhật Server và Proxy không?",
"modal_download_title": "Tải dữ liệu",
"modal_download_msg": "Yêu cầu tải xuống dữ liệu Server và Proxy",
"modal_self_update_title": "Cập nhật Launcher",
"modal_self_update_msg": "Bạn có muốn cập nhật Launcher không?",
"btn_yes": "Có",
"btn_no": "Không",
"btn_download": "Tải xuống",
"error_game_dir": "Thư mục Game không hợp lệ",
"error_file_type": "Định dạng tệp không đúng",
"game_path_success": "Đã lưu đường dẫn Game thành công",
"no_updates": "Không có bản cập nhật nào",
"toast_pick_folder_error": "Lỗi chọn thư mục: ",
"toast_start_proxy_failed": "Khởi động proxy thất bại: ",
"toast_start_server_failed": "Khởi động server thất bại: ",
"toast_start_game_failed": "Khởi động game thất bại: ",
"toast_start_game_error": "Lỗi khởi động game: "
},
"diff": {
"header_title": "🎮 Cập nhật Game bằng Diff Tool",
"header_desc": "Hỗ trợ bạn cập nhật game bằng Diff Tool",
"game_dir_title": "Thư mục Game",
"btn_selecting": "Đang chọn...",
"btn_select_game": "Chọn Thư mục Game",
"game_dir_valid": "Đã tìm thấy thư mục game hợp lệ!",
"game_dir_invalid": "Không tìm thấy thư mục game. Vui lòng chọn đúng thư mục.",
"diff_file_title": "Tệp Diff",
"btn_select_diff": "Chọn Tệp Diff",
"diff_file_valid": "Đã tìm thấy tệp diff hợp lệ!",
"diff_file_invalid": "Không tìm thấy tệp diff. Vui lòng chọn đúng tệp.",
"btn_updating": "Đang cập nhật...",
"btn_update_game": "Cập nhật Game",
"modal_update_title": "Cập nhật Game",
"status_wait": "Vui lòng đợi...",
"inst_title": "📋 Hướng dẫn:",
"inst_step_1": "1. Nhấn \"Chọn Thư mục Game\" và chọn thư mục gốc của game",
"inst_step_2": "2. Đợi hệ thống xác thực thư mục game",
"inst_step_3": "3. Nhấn \"Chọn Tệp Diff\" và chọn tệp diff của bạn",
"inst_step_4": "4. Đợi hệ thống xác thực tệp diff",
"inst_step_5": "5. Nhấn \"Cập nhật Game\" để bắt đầu quá trình",
"toast_game_dir_not_found": "Không tìm thấy thư mục game. Vui lòng chọn đúng thư mục.",
"toast_no_folder_selected": "Chưa chọn đường dẫn thư mục",
"toast_pick_folder_error": "Lỗi chọn thư mục: ",
"toast_invalid_file_type": "Định dạng tệp không hợp lệ",
"toast_no_file_selected": "Chưa chọn đường dẫn tệp",
"toast_pick_file_error": "Lỗi chọn tệp: ",
"toast_select_both": "Vui lòng chọn cả thư mục game và tệp diff",
"toast_update_completed": "Cập nhật game hoàn tất",
"stage_check_type": "Kiểm tra loại Diff Tool",
"stage_version_validate": "Xác thực phiên bản",
"stage_data_extract": "Trích xuất dữ liệu",
"stage_cut_data": "Cắt dữ liệu",
"stage_patch_data": "Vá dữ liệu",
"stage_delete_old_files": "Xóa tệp cũ"
},
"language": {
"header_title": "🎮 Quản lý ngôn ngữ Game",
"header_desc": "Quản lý cài đặt ngôn ngữ văn bản và giọng nói cho game của bạn",
"game_dir_title": "Thư mục Game",
"btn_select_game": "Chọn Thư mục Game",
"btn_selecting": "Đang chọn...",
"game_dir_valid": "Đã tìm thấy thư mục game hợp lệ!",
"game_dir_invalid": "Không tìm thấy thư mục game. Vui lòng chọn đúng thư mục.",
"current_languages_title": "Ngôn ngữ hiện tại",
"text_language": "Ngôn ngữ văn bản",
"voice_language": "Ngôn ngữ giọng nói",
"language_settings_title": "Cài đặt ngôn ngữ",
"select_text_placeholder": "Chọn ngôn ngữ văn bản...",
"select_voice_placeholder": "Chọn ngôn ngữ giọng nói...",
"btn_apply": "Áp dụng Cài đặt Ngôn ngữ",
"btn_applying": "Đang áp dụng...",
"inst_title": "📋 Hướng dẫn:",
"inst_step_1": "1. Nhấn \"Chọn Thư mục Game\" và chọn thư mục gốc của game",
"inst_step_2": "2. Đợi hệ thống xác thực thư mục game",
"inst_step_3": "3. Chọn ngôn ngữ văn bản và giọng nói bạn muốn",
"inst_step_4": "4. Nhấn \"Áp dụng Cài đặt Ngôn ngữ\" để lưu thay đổi",
"toast_no_folder_selected": "Chưa chọn đường dẫn thư mục",
"toast_pick_folder_error": "Lỗi chọn thư mục: ",
"toast_set_language_success": "Cài đặt ngôn ngữ thành công",
"toast_set_language_error": "Lỗi cài đặt ngôn ngữ: "
},
"howto": {
"title": "Hướng dẫn sử dụng",
"sect1_title": "Sử dụng các tính năng của Launcher",
"sect1_auto_update_pre": "Tự động cập nhật",
"sect1_auto_update_post": "và các công cụ proxy khi khởi chạy.",
"sect1_launch_game": "Khởi chạy game trực tiếp thông qua launcher với các thông số và môi trường chạy chính xác.",
"sect1_lang_pre": "Hỗ trợ chuyển đổi ngôn ngữ trong game (ví dụ: EN, JP, ZH, KR) qua",
"sect1_lang_link": "Công cụ Ngôn ngữ",
"sect1_patch_title": "Vá & Cập nhật Tệp Game",
"sect1_patch_desc_1_pre": "Sử dụng",
"sect1_patch_desc_1_link": "Công cụ Diff",
"sect1_patch_desc_1_post": "(DiffPatch) để cập nhật gia tăng nhanh chóng & nhẹ nhàng.",
"sect1_patch_desc_2_pre": "Hỗ trợ",
"sect1_patch_desc_2_post": "và các định dạng diff tùy chỉnh.",
"sect2_title": "Lệnh Chat FireflyGo",
"sect2_desc_pre": "Dưới đây là các lệnh chat trong game bạn có thể sử dụng. Một số lệnh yêu cầu bạn phải bật",
"sect2_desc_bold": "Chế độ Theorycraft",
"sect2_desc_post": ".",
"sect2_tc_req_title": "Yêu cầu Chế độ Theorycraft",
"sect2_tc_req_desc_pre": "Các lệnh sau chỉ khả dụng khi",
"sect2_tc_req_desc_bold": "Chế độ Theorycraft",
"sect2_tc_req_desc_post": "được bật:",
"sect2_extra_title": "Cài đặt Bổ sung",
"sect2_hidden_ui_title": "Ẩn UI",
"sect2_hidden_ui_desc": "Ẩn ngay lập tức toàn bộ giao diện game — thường được dùng trong các video showcase DIM.",
"sect2_censor_title": "Tắt Kiểm duyệt",
"sect2_censor_desc": "Loại bỏ hiệu ứng kiểm duyệt Lens Flare 💀 để có trải nghiệm rõ ràng hơn.",
"sect2_tc_title": "Chế độ Theorycraft",
"sect2_tc_desc": "Không cần gõ lệnh chat nữa — cấu hình mọi thứ qua web: điều chỉnh HP quái, đặt chu kỳ, xem log, và hơn thế nữa.",
"sect2_cmd_title": "Các lệnh khả dụng:",
"sect2_cmd_tc": "Chế độ Theorycraft",
"sect2_cmd_tc_enable": "— Bật Chế độ Theorycraft",
"sect2_cmd_tc_disable": "— Tắt Chế độ Theorycraft",
"sect2_cmd_cycle": "Điều khiển Chu kỳ",
"sect2_cmd_tc_only": "(Chỉ Theorycraft)",
"sect2_cmd_cycle_desc": "— Đặt số chu kỳ trong trận đấu",
"sect2_cmd_cycle_ex1_pre": "Ví dụ:",
"sect2_cmd_cycle_ex1_post": "đặt trận đấu thành 30 chu kỳ",
"sect2_cmd_cycle_ex2_post": "tắt chu kỳ tùy chỉnh",
"sect2_cmd_hp": "Ghi đè HP",
"sect2_cmd_hp_desc1": "— Đặt HP quái vật (chỉ khả dụng trong chế độ Theorycraft)",
"sect2_cmd_hp_desc2": "— Tắt tính năng đặt HP",
"sect2_cmd_hp_desc3": "— Đặt HP cho từng quái vật trong một đợt (wave) cụ thể",
"sect2_cmd_hp_ex_pre": "Ví dụ:",
"sect2_cmd_hp_ex_post": "đặt đợt 1 quái 1 HP=2,000,000 và quái 2 HP=3,000,000",
"sect2_cmd_log": "Nhật ký Trận đấu (Log)",
"sect2_cmd_log_desc1": "— Bật xuất nhật ký trận đấu",
"sect2_cmd_log_desc2": "— Tắt nhật ký trận đấu",
"sect2_cmd_log_out_pre": "Đầu ra sẽ được ghi dưới dạng",
"sect2_cmd_skip": "Bỏ qua Node",
"sect2_cmd_skip_desc": "— Bỏ qua các node trong MOC / AS / Pure Fiction",
"sect2_cmd_skip_ex1_pre": "Ví dụ:",
"sect2_cmd_skip_ex1_post": "bỏ qua node 2",
"sect2_cmd_skip_ex2_post": "tắt tính năng bỏ qua",
"sect2_cmd_id": "Chuyển đổi Vận mệnh Nhân vật",
"sect2_cmd_id_desc": "— Chuyển vận mệnh cho các nhân vật có nhiều form",
"sect2_cmd_id_ex1_pre": "Ví dụ:",
"sect2_cmd_id_ex1_post": "để đổi form MC (Nhà Khai Phá)",
"sect2_cmd_id_ex2_pre": "Hoạt động với các ID như",
"sect2_cmd_update": "Làm mới Dữ liệu",
"sect2_cmd_update_desc_pre": "— Làm mới dữ liệu máy chủ từ tệp",
"sect3_title": "Các Lưu ý Khác",
"sect3_admin_title": "Quyền Quản trị viên (Administrator)",
"sect3_admin_desc": "Luôn chạy launcher dưới quyền Administrator để có quyền truy cập tệp.",
"sect3_backup_title": "Sao lưu Dữ liệu",
"sect3_backup_desc_pre": "Sao lưu tệp",
"sect3_backup_desc_mid": "và",
"sect3_backup_desc_post": "của bạn thường xuyên.",
"sect3_voice_title": "Bật Gói Giọng nói (Voice Packs) trong Client Beta",
"sect3_voice_step1_pre": "Sao chép thư mục giọng nói bạn muốn (ví dụ:",
"sect3_voice_step1_mid": "từ:",
"sect3_voice_step1_post_pre": "vào thư mục beta bằng cách nhấn",
"sect3_voice_step1_post_bold": "\"Mở thư mục Voice\"",
"sect3_voice_step1_post_post": "trên tab Trang chủ.",
"sect3_voice_step2": "Khi khởi chạy game lần đầu, thư mục giọng nói có thể bị xóa. Nếu vậy, hãy lặp lại bước 1 để khôi phục.",
"btn_back": "Trở về Trang chủ"
},
"about": {
"title": "Giới thiệu",
"p1_pre": "Xin chào! Chúng tôi là ",
"p1_post": ", một nhóm phát triển đam mê xây dựng các công cụ hữu ích và cải thiện trải nghiệm người dùng.",
"p2_pre": "Tôi đã tạo ra một ",
"p2_highlight": "Game Launcher",
"p2_post": " hiện đại và gọn nhẹ để giúp người dùng dễ dàng khởi chạy và quản lý game với hiệu suất tốt hơn và đơn giản hơn.",
"p3_pre": "Launcher được xây dựng bằng ",
"p3_mid": ", với giao diện gọn gàng và tốc độ phản hồi nhanh được thiết kế bằng ",
"p3_and": " và ",
"p3_post": ".",
"p4": "Mục tiêu của tôi là tạo ra các công cụ nhanh, hiệu quả và mang lại trải nghiệm sử dụng thú vị — và launcher này chỉ là sự khởi đầu.",
"btn_back": "Trở về Trang chủ"
},
"fireflytools": {
"title": "Công cụ Firefly",
"sect1_title": "Về Công cụ Firefly",
"sect1_p1_pre": "Trang web này là một phiên bản khác của ",
"sect1_p1_tool": "Firefly Tools ",
"sect1_p1_mid": "được phát triển bởi ",
"sect1_p1_author": "Firefly Shelter",
"sect1_master_site": "Trang web Gốc",
"sect1_p2_pre": "Công cụ ban đầu được tạo ra bởi một nhà phát triển bên thứ ba tên là ",
"sect1_p2_author": "Amazing",
"sect1_p2_post": ". Phiên bản này dựa trực tiếp trên công việc đó, không sửa đổi logic cốt lõi.",
"sect1_p3_pre": "Ngoài ra còn có một phiên bản hiện đại hơn của cùng tác giả tại ",
"sect2_title": "Tính năng Chính",
"sect2_feat1": "Cấu hình nhân vật, nón ánh sáng, di vật, vết tích và tinh hồn dễ dàng trên trình duyệt của bạn.",
"sect2_feat2_pre": "Áp dụng ngay các thiết lập vào ",
"sect2_feat2_server": "Máy chủ Firefly GO",
"sect2_feat2_mid": " bằng cách sử dụng ",
"sect2_feat2_conn": "Connect PS",
"sect2_feat2_post": " — không cần tải tệp lên thủ công.",
"sect2_feat3_title": "Cài đặt Bổ sung",
"sect2_feat3_desc_pre": "Nâng cao trải nghiệm ",
"sect2_feat3_desc_server": "Máy chủ Firefly GO",
"sect2_feat3_desc_post": " của bạn với các tính năng bổ sung:",
"sect2_feat3_ui": "Ẩn UI Game",
"sect2_feat3_ui_desc": "— xóa toàn bộ giao diện trò chơi.",
"sect2_feat3_censor": "Tắt Kiểm duyệt",
"sect2_feat3_censor_desc": "— loại bỏ hiệu ứng kiểm duyệt Lens Flare 💀.",
"sect2_feat3_tc": "Chế độ Theorycraft",
"sect2_feat3_tc_desc": "— cấu hình HP, chu kỳ và nhiều tính năng khác qua web.",
"sect2_feat4_pre": "Xuất và nhập toàn bộ bản build bằng ",
"sect2_feat4_post": ".",
"sect2_feat5": "Quy trình thử nghiệm nhanh chóng — không có thời gian chờ đồng bộ, cập nhật ngay lập tức trong game.",
"sect3_title": "Bắt đầu Sử dụng",
"sect3_step1": "Truy cập công cụ thông qua trình duyệt của bạn tại phiên bản tự lưu trữ (self-hosted).",
"sect3_step2": "Cấu hình bản build nhân vật của bạn với giao diện web trực quan.",
"sect3_step3_pre": "Sử dụng tính năng ",
"sect3_step3_bold": "Connect PS",
"sect3_step3_post": " để đồng bộ tức thì với máy chủ riêng của bạn.",
"sect3_step4": "Thử nghiệm bản build trong game với các bản cập nhật và sửa đổi theo thời gian thực.",
"btn_back": "Trở về Trang chủ"
},
"analysis": {
"title": "Phân tích Firefly & Plugin Veritas",
"sect1_title": "Về Veritas",
"sect1_p1_pre": "",
"sect1_p1_mid": " là một ",
"sect1_p1_highlight": "Damage Logger",
"sect1_p1_post": " mạnh mẽ được thiết kế để phân tích sát thương theo thời gian thực trong quá trình chơi game.",
"sect1_p2": "Nó gọn nhẹ, nhanh chóng và dễ sử dụng để phân tích sát thương toàn diện.",
"sect1_github": "Kho lưu trữ GitHub",
"sect2_title": "Công cụ Phân tích Web",
"sect2_desc": "Sử dụng các ứng dụng web này để phân tích sát thương thời gian thực với Veritas:",
"sect2_master": "Trang web Gốc",
"sect2_backup": "Trang web Dự phòng",
"sect2_tip_pre": "Mẹo:",
"sect2_tip_post": " Nếu quốc gia của bạn gặp sự cố khi tải trang web gốc, vui lòng sử dụng trang web dự phòng.",
"sect3_title": "Hướng dẫn Cài đặt",
"sect3_subtitle": "Bước Thiết lập Quan trọng",
"sect3_desc": "Sau khi tải xuống Veritas, bạn phải đổi tên tệp để nó hoạt động bình thường:",
"sect3_rename": "Đổi tên: ",
"sect3_place": "Sau đó đặt ",
"sect3_place_post": " vào thư mục game của bạn.",
"sect4_title": "Cách sử dụng Web App",
"sect4_sub1": "Dành cho Firefly GO Local",
"sect4_sub1_step1_pre": "Khởi chạy ",
"sect4_sub1_step1_game": "game",
"sect4_sub1_step1_mid": " và ",
"sect4_sub1_step1_server": "Máy chủ Firefly GO (PS)",
"sect4_sub1_step1_post": " của bạn.",
"sect4_sub1_step2": "Mở một trong các công cụ phân tích web.",
"sect4_sub1_step3_pre": "Đi tới ",
"sect4_sub1_step3_conn": "Cài đặt Kết nối",
"sect4_sub1_step3_mid1": " → chọn ",
"sect4_sub1_step3_type": "Loại Kết nối: PS",
"sect4_sub1_step3_mid2": " → nhấn ",
"sect4_sub1_step3_btn": "Kết nối",
"sect4_sub1_step3_post": ".",
"sect4_sub1_step4": "Sau khi kết nối, hãy chơi game. Công cụ sẽ tự động phân tích ở chế độ nền.",
"sect4_sub2": "Dành cho các Máy chủ riêng Khác",
"sect4_sub2_step1_pre": "Khởi chạy ",
"sect4_sub2_step1_game": "game",
"sect4_sub2_step1_mid": " và ",
"sect4_sub2_step1_server": "Máy chủ riêng",
"sect4_sub2_step1_post": " của bạn.",
"sect4_sub2_step2": "Mở một trong các công cụ phân tích web.",
"sect4_sub2_step3_pre": "Đi tới ",
"sect4_sub2_step3_conn": "Cài đặt Kết nối",
"sect4_sub2_step3_mid1": " → chọn ",
"sect4_sub2_step3_type": "Loại Kết nối: Native",
"sect4_sub2_step3_mid2": " → nhấn ",
"sect4_sub2_step3_btn": "Kết nối",
"sect4_sub2_step3_post": ".",
"sect4_sub2_step4": "Sau khi kết nối, hãy chơi game bình thường.",
"btn_back": "Trở về Trang chủ"
}
}

View File

@@ -0,0 +1,337 @@
{
"header": {
"home": "首页",
"tools": "工具",
"language": "语言",
"diff": "差异",
"client_update": "客户端更新",
"plugins": "插件",
"analysis": "分析Veritas",
"firefly_tools": "Firefly工具",
"how_to": "使用方法",
"about": "关于",
"settings": "设置",
"minimize": "最小化",
"close": "关闭",
"by": "Firefly Shelter"
},
"background": {
"select_bg": "选择背景",
"choose_bg": "选择背景",
"paste_url": "粘贴图片链接 (https://...)",
"add": "添加",
"upload": "上传",
"upload_from_computer": "从电脑上传",
"done": "完成",
"remove": "删除",
"invalid_url": "无效的URL"
},
"close": {
"title": "确认操作",
"description": "您是要最小化到系统托盘还是关闭应用程序?",
"dont_ask": "不再提示",
"minimize": "最小化",
"close": "关闭"
},
"setting": {
"title": "设置",
"launcher_update_title": "启动器更新",
"launcher_update_desc": "检查您的启动器是否为最新版本。",
"launcher_update_btn": "检查更新",
"launcher_update_success": "启动器已是最新版本",
"closing_options_title": "关闭选项",
"set_dont_ask_again": "不再提示",
"closing_auto_desc": "下次关闭时,将自动{{action}}而不进行询问。",
"action_minimize": "最小化到系统托盘",
"action_quit": "退出应用程序",
"version_label": "版本",
"server_label": "Server",
"proxy_label": "Proxy",
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "如何使用工具与命令",
"btn_select_game": "选择游戏文件",
"btn_selecting": "正在选择...",
"btn_start_game": "开始游戏",
"btn_game_running": "游戏正在运行",
"menu_change_path": "更改游戏路径",
"menu_check_update": "检查服务器与代理更新",
"menu_open_server": "打开服务器文件夹",
"menu_open_proxy": "打开代理文件夹",
"menu_open_voice": "打开语音文件夹",
"status_updating_launcher": "正在更新启动器",
"status_update_success": "更新成功5秒后自动关闭",
"status_update_failed": "更新失败5秒后自动关闭",
"status_wait": "请稍候...",
"status_complete": "完成!",
"modal_update_title": "更新数据",
"modal_update_msg": "是否要更新服务器和代理数据?",
"modal_download_title": "下载数据",
"modal_download_msg": "需要下载服务器和代理数据",
"modal_self_update_title": "更新启动器",
"modal_self_update_msg": "是否要更新启动器?",
"btn_yes": "是",
"btn_no": "否",
"btn_download": "下载",
"error_game_dir": "无效的游戏目录",
"error_file_type": "文件类型错误",
"game_path_success": "游戏路径设置成功",
"no_updates": "暂无更新",
"toast_pick_folder_error": "选择文件夹错误: ",
"toast_start_proxy_failed": "启动代理失败: ",
"toast_start_server_failed": "启动服务器失败: ",
"toast_start_game_failed": "启动游戏失败: ",
"toast_start_game_error": "启动游戏错误: "
},
"diff": {
"header_title": "🎮 使用 Diff Tool 更新游戏",
"header_desc": "帮助您使用 Diff Tool 轻松更新游戏",
"game_dir_title": "游戏目录",
"btn_selecting": "正在选择...",
"btn_select_game": "选择游戏文件夹",
"game_dir_valid": "找到有效的游戏目录!",
"game_dir_invalid": "未找到游戏目录,请选择正确的文件夹。",
"diff_file_title": "Diff 文件目录",
"btn_select_diff": "选择 Diff 文件",
"diff_file_valid": "找到有效的 Diff 文件!",
"diff_file_invalid": "未找到 Diff 文件,请选择正确的文件。",
"btn_updating": "正在更新...",
"btn_update_game": "更新游戏",
"modal_update_title": "更新游戏",
"status_wait": "请稍候...",
"inst_title": "📋 使用说明:",
"inst_step_1": "1. 点击“选择游戏文件夹”并选择您的游戏根目录",
"inst_step_2": "2. 等待系统验证游戏目录",
"inst_step_3": "3. 点击“选择 Diff 文件”并选择您的差异文件",
"inst_step_4": "4. 等待系统验证 Diff 文件",
"inst_step_5": "5. 点击“更新游戏”以保存更改",
"toast_game_dir_not_found": "未找到游戏目录,请选择正确的文件夹。",
"toast_no_folder_selected": "未选择文件夹路径",
"toast_pick_folder_error": "选择文件夹错误:",
"toast_invalid_file_type": "无效的文件类型",
"toast_no_file_selected": "未选择文件路径",
"toast_pick_file_error": "选择文件错误:",
"toast_select_both": "请同时选择游戏目录和 Diff 文件",
"toast_update_completed": "游戏更新完成",
"stage_check_type": "检查 Diff Tool 类型",
"stage_version_validate": "验证版本",
"stage_data_extract": "提取数据",
"stage_cut_data": "裁剪数据",
"stage_patch_data": "修补数据",
"stage_delete_old_files": "删除旧文件"
},
"language": {
"header_title": "🎮 游戏语言管理器",
"header_desc": "管理游戏的文本和语音语言设置",
"game_dir_title": "游戏目录",
"btn_select_game": "选择游戏文件夹",
"btn_selecting": "正在选择...",
"game_dir_valid": "找到有效的游戏目录!",
"game_dir_invalid": "未找到游戏目录,请选择正确的文件夹。",
"current_languages_title": "当前语言",
"text_language": "文本语言",
"voice_language": "语音语言",
"language_settings_title": "语言设置",
"select_text_placeholder": "选择文本语言...",
"select_voice_placeholder": "选择语音语言...",
"btn_apply": "应用语言设置",
"btn_applying": "正在应用...",
"inst_title": "📋 使用说明:",
"inst_step_1": "1. 点击“选择游戏文件夹”并选择您的游戏根目录",
"inst_step_2": "2. 等待系统验证游戏目录",
"inst_step_3": "3. 选择您偏好的文本和语音语言",
"inst_step_4": "4. 点击“应用语言设置”以保存更改",
"toast_no_folder_selected": "未选择文件夹路径",
"toast_pick_folder_error": "选择文件夹错误:",
"toast_set_language_success": "语言设置成功",
"toast_set_language_error": "设置语言错误:"
},
"howto": {
"title": "使用说明",
"sect1_title": "使用启动器功能",
"sect1_auto_update_pre": "启动时自动更新",
"sect1_auto_update_post": "及代理工具。",
"sect1_launch_game": "使用正确的参数和运行环境,直接通过启动器启动游戏。",
"sect1_lang_pre": "支持切换游戏内语言例如EN, JP, ZH, KR通过",
"sect1_lang_link": "语言工具",
"sect1_patch_title": "修补与更新游戏文件",
"sect1_patch_desc_1_pre": "使用",
"sect1_patch_desc_1_link": "Diff Tool",
"sect1_patch_desc_1_post": "DiffPatch进行快速轻量的增量更新。",
"sect1_patch_desc_2_pre": "支持",
"sect1_patch_desc_2_post": "以及自定义 diff 格式。",
"sect2_title": "FireflyGo 聊天指令",
"sect2_desc_pre": "以下是您可以在游戏中使用的聊天指令。部分指令需要您开启",
"sect2_desc_bold": "Theorycraft 模式",
"sect2_desc_post": "。",
"sect2_tc_req_title": "需要开启 Theorycraft 模式",
"sect2_tc_req_desc_pre": "以下指令仅在",
"sect2_tc_req_desc_bold": "Theorycraft 模式",
"sect2_tc_req_desc_post": "开启时可用:",
"sect2_extra_title": "额外设置",
"sect2_hidden_ui_title": "隐藏 UI",
"sect2_hidden_ui_desc": "立即隐藏整个游戏UI — 常用于 DIM 演示视频中。",
"sect2_censor_title": "禁用和谐",
"sect2_censor_desc": "移除镜头光晕和谐效果 💀,获得更纯净的体验。",
"sect2_tc_title": "Theorycraft 模式",
"sect2_tc_desc": "再也不需要输入聊天指令了 — 通过网页配置一切调整怪物HP、设置轮数、查看日志等。",
"sect2_cmd_title": "可用指令:",
"sect2_cmd_tc": "Theorycraft 模式",
"sect2_cmd_tc_enable": "— 启用 Theorycraft 模式",
"sect2_cmd_tc_disable": "— 禁用 Theorycraft 模式",
"sect2_cmd_cycle": "轮数控制",
"sect2_cmd_tc_only": "(仅 Theorycraft 模式)",
"sect2_cmd_cycle_desc": "— 设置战斗中的轮数",
"sect2_cmd_cycle_ex1_pre": "示例:",
"sect2_cmd_cycle_ex1_post": "将战斗设置为30轮",
"sect2_cmd_cycle_ex2_post": "禁用自定义轮数",
"sect2_cmd_hp": "HP 覆写",
"sect2_cmd_hp_desc1": "— 设置怪物 HP仅在 Theorycraft 模式下可用)",
"sect2_cmd_hp_desc2": "— 禁用 HP 设置功能",
"sect2_cmd_hp_desc3": "— 为特定波次中的每个怪物设置 HP",
"sect2_cmd_hp_ex_pre": "示例:",
"sect2_cmd_hp_ex_post": "设置第 1 波次怪物 1 的 HP=2,000,000怪物 2 的 HP=3,000,000",
"sect2_cmd_log": "战斗日志",
"sect2_cmd_log_desc1": "— 启用战斗日志输出",
"sect2_cmd_log_desc2": "— 禁用战斗日志",
"sect2_cmd_log_out_pre": "输出将被写入为",
"sect2_cmd_skip": "跳过节点",
"sect2_cmd_skip_desc": "— 跳过 混沌回忆 / 虚构叙事 中的节点",
"sect2_cmd_skip_ex1_pre": "示例:",
"sect2_cmd_skip_ex1_post": "跳过节点 2",
"sect2_cmd_skip_ex2_post": "禁用跳过",
"sect2_cmd_id": "角色命途切换",
"sect2_cmd_id_desc": "— 为多形态角色切换命途",
"sect2_cmd_id_ex1_pre": "示例:",
"sect2_cmd_id_ex1_post": "更改开拓者的形态",
"sect2_cmd_id_ex2_pre": "支持的 ID 如",
"sect2_cmd_update": "刷新数据",
"sect2_cmd_update_desc_pre": "— 从当前文件刷新服务器数据:",
"sect3_title": "其他注意事项",
"sect3_admin_title": "管理员权限",
"sect3_admin_desc": "请始终以管理员身份运行启动器,以获取文件权限。",
"sect3_backup_title": "备份数据",
"sect3_backup_desc_pre": "请定期备份您的",
"sect3_backup_desc_mid": "和",
"sect3_backup_desc_post": "。",
"sect3_voice_title": "在 Beta 客户端中启用语音包",
"sect3_voice_step1_pre": "将所需的语音文件夹(例如:",
"sect3_voice_step1_mid": ")从:",
"sect3_voice_step1_post_pre": "复制到 beta 文件夹中,在首页点击",
"sect3_voice_step1_post_bold": "“打开语音文件夹”",
"sect3_voice_step1_post_post": "即可。",
"sect3_voice_step2": "首次启动游戏时可能会删除语音文件夹。如果发生这种情况,请重复步骤 1 进行恢复。",
"btn_back": "返回主页"
},
"about": {
"title": "关于",
"p1_pre": "你好!我们是 ",
"p1_post": ",一个致力于构建实用工具并改善用户体验的开发者团队。",
"p2_pre": "我创建了一个轻量且现代的 ",
"p2_highlight": "游戏启动器",
"p2_post": ",旨在帮助用户以更好的性能和极简的操作轻松启动与管理他们的游戏。",
"p3_pre": "启动器使用 ",
"p3_mid": " 构建,并采用 ",
"p3_and": " 和 ",
"p3_post": " 设计了简洁响应式的界面。",
"p4": "我的目标是制作快速、高效且使用起来令人愉悦的工具 —— 而这个启动器仅仅是个开始。",
"btn_back": "返回主页"
},
"fireflytools": {
"title": "Firefly 工具",
"sect1_title": "关于 Firefly 工具",
"sect1_p1_pre": "本网站是 ",
"sect1_p1_tool": "Firefly Tools ",
"sect1_p1_mid": "的另一个版本,由 ",
"sect1_p1_author": "Firefly Shelter",
"sect1_master_site": "主站",
"sect1_p2_pre": "原始工具由名为 ",
"sect1_p2_author": "Amazing",
"sect1_p2_post": " 的第三方开发者创建。此版本直接基于该作品,未修改核心逻辑。",
"sect1_p3_pre": "原作者还有一个更现代的版本,请访问:",
"sect2_title": "主要功能",
"sect2_feat1": "在浏览器中轻松配置角色、光锥、遗器、行迹和星魂。",
"sect2_feat2_pre": "使用 ",
"sect2_feat2_server": "Connect PS",
"sect2_feat2_mid": " 立即将设置应用到 ",
"sect2_feat2_conn": "Firefly GO Server",
"sect2_feat2_post": " —— 无需手动上传文件。",
"sect2_feat3_title": "额外设置",
"sect2_feat3_desc_pre": "使用额外功能增强您的 ",
"sect2_feat3_desc_server": "Firefly GO Server",
"sect2_feat3_desc_post": " 体验:",
"sect2_feat3_ui": "隐藏游戏 UI",
"sect2_feat3_ui_desc": "—— 移除整个游戏界面。",
"sect2_feat3_censor": "禁用和谐",
"sect2_feat3_censor_desc": "—— 移除镜头光晕和谐 💀。",
"sect2_feat3_tc": "Theorycraft 模式",
"sect2_feat3_tc_desc": "—— 通过网页配置 HP、轮数等。",
"sect2_feat4_pre": "使用 ",
"sect2_feat4_post": " 导入和导出完整配置。",
"sect2_feat5": "快速测试工作流 —— 无同步冷却,游戏内即时更新。",
"sect3_title": "快速开始",
"sect3_step1": "通过浏览器访问自托管实例中的工具。",
"sect3_step2": "使用直观的 Web 界面配置您的角色搭配。",
"sect3_step3_pre": "使用 ",
"sect3_step3_bold": "Connect PS",
"sect3_step3_post": " 功能立即与您的私服同步。",
"sect3_step4": "通过实时更新和修改在游戏中测试您的搭配。",
"btn_back": "返回主页"
},
"analysis": {
"title": "Firefly 分析 & Veritas 插件",
"sect1_title": "关于 Veritas",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": " 是一款强大的",
"sect1_p1_highlight": "伤害记录器",
"sect1_p1_post": ",专为在游戏过程中实时分析伤害而设计。",
"sect1_p2": "它轻量、快速且易于使用,适合全面的伤害分析。",
"sect1_github": "GitHub 仓库",
"sect2_title": "Web 分析工具",
"sect2_desc": "使用这些 Web 应用程序与 Veritas 进行实时伤害分析:",
"sect2_master": "主站",
"sect2_backup": "备用网站",
"sect2_tip_pre": "提示:",
"sect2_tip_post": " 如果您的国家加载主站有困难,请改用备用网站。",
"sect3_title": "安装说明",
"sect3_subtitle": "重要设置步骤",
"sect3_desc": "下载 Veritas 后,您必须重命名该文件才能正常工作:",
"sect3_rename": "重命名:",
"sect3_place": "然后将 ",
"sect3_place_post": " 放入您的游戏目录。",
"sect4_title": "如何使用 Web 应用",
"sect4_sub1": "适用于 Firefly GO 本地端",
"sect4_sub1_step1_pre": "启动 ",
"sect4_sub1_step1_game": "游戏",
"sect4_sub1_step1_mid": " 和您的 ",
"sect4_sub1_step1_server": "Firefly GO 服务器 (PS)",
"sect4_sub1_step1_post": "。",
"sect4_sub1_step2": "打开其中一个 Web 分析工具。",
"sect4_sub1_step3_pre": "转到 ",
"sect4_sub1_step3_conn": "Connection Settings",
"sect4_sub1_step3_mid1": " → 选择 ",
"sect4_sub1_step3_type": "Connection Type: PS",
"sect4_sub1_step3_mid2": " → 点击 ",
"sect4_sub1_step3_btn": "Connect",
"sect4_sub1_step3_post": "。",
"sect4_sub1_step4": "连接后,游玩游戏。工具将在后台自动进行分析。",
"sect4_sub2": "适用于其他私服",
"sect4_sub2_step1_pre": "启动 ",
"sect4_sub2_step1_game": "游戏",
"sect4_sub2_step1_mid": " 和您的 ",
"sect4_sub2_step1_server": "私服",
"sect4_sub2_step1_post": "。",
"sect4_sub2_step2": "打开其中一个 Web 分析工具。",
"sect4_sub2_step3_pre": "转到 ",
"sect4_sub2_step3_conn": "Connection Settings",
"sect4_sub2_step3_mid1": " → 选择 ",
"sect4_sub2_step3_type": "Connection Type: Native",
"sect4_sub2_step3_mid2": " → 点击 ",
"sect4_sub2_step3_btn": "Connect",
"sect4_sub2_step3_post": "。",
"sect4_sub2_step4": "连接后,正常游玩游戏即可。",
"btn_back": "返回主页"
}
}

View File

@@ -0,0 +1,193 @@
'use client'
import { useState, useRef } from 'react'
import { X, Image as ImageIcon, Plus, Upload, Check } from 'lucide-react'
import useSettingStore from '@/stores/settingStore'
import Cropper from 'react-easy-crop'
import getCroppedImg from '@/utils/cropImage'
import { useTranslation } from 'react-i18next'
import { toast } from "react-toastify"
const initialImages = {
"bg-1": "bg-1.jpeg",
"bg-2": "bg-2.png",
"bg-3": "bg-3.png",
"bg-6": "bg-6.png",
"bg-7": "bg-7.jpeg",
"bg-8": "bg-8.png",
"bg-9": "bg-9.jpeg",
"bg-10": "bg-10.jpg",
"bg-11": "bg-11.jpeg",
"bg-12": "bg-12.jpg",
"bg-13": "bg-13.jpg",
"bg-16": "bg-16.jpg",
}
export const BackgroundSelector = () => {
const [isOpen, setIsOpen] = useState(false)
const [newUrl, setNewUrl] = useState('')
const [croppingImage, setCroppingImage] = useState<string | null>(null)
const [crop, setCrop] = useState({ x: 0, y: 0 })
const [zoom, setZoom] = useState(1)
const [croppedAreaPixels, setCroppedAreaPixels] = useState<any>(null)
const { background, setBackground, extraBackgrounds, setExtraBackgrounds } = useSettingStore()
const fileInputRef = useRef<HTMLInputElement>(null)
const { t } = useTranslation()
const handleSelect = (img: string) => {
setIsOpen(false)
setBackground(img)
}
const isImageUrl = (url: string) => {
return /^https?:\/\/.+\.(jpg|jpeg|png|webp|gif)(\?.*)?$/i.test(url)
}
const handleAddUrl = () => {
const url = newUrl.trim()
if (!url) {
return
}
if (!isImageUrl(url)) {
toast.error(t("background.invalid_url"))
return
}
setCroppingImage(url)
setNewUrl('')
}
const handleRemoveExtra = (url: string) => {
setExtraBackgrounds(extraBackgrounds.filter(bg => bg !== url))
}
const handleUploadFile = (file: File) => {
const reader = new FileReader()
reader.onload = () => setCroppingImage(reader.result as string)
reader.readAsDataURL(file)
}
const handleCropComplete = async () => {
if (!croppingImage || !croppedAreaPixels) return
const croppedBase64 = await getCroppedImg(croppingImage, croppedAreaPixels)
setExtraBackgrounds([croppedBase64, ...extraBackgrounds])
setCroppingImage(null)
}
const allBackgrounds = [...extraBackgrounds, ...Object.values(initialImages)]
return (
<div className="flex flex-col items-center justify-center gap-4 w-full">
<div className="tooltip tooltip-right" data-tip={t("background.select_bg")}>
<button
className="group btn btn-primary btn-circle flex items-center justify-center shadow-md transition-all duration-300 hover:scale-110 hover:shadow-lg hover:bg-primary/80"
onClick={() => setIsOpen(true)}
>
<ImageIcon size={22} className="text-white transition-all duration-300 group-hover:rotate-6 group-hover:scale-110 group-hover:text-yellow-300" />
</button>
</div>
{isOpen && (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-base-200/60 pt-10">
<div className="bg-base-200 text-white rounded-xl shadow-xl p-6 w-[90%] max-w-2xl relative">
<button className="btn btn-ghost btn-circle absolute top-3 right-3" onClick={() => setIsOpen(false)}>
<X size={20} />
</button>
<h2 className="text-lg font-semibold mb-4">{t("background.choose_bg")}</h2>
{/* Add via URL */}
<div className="flex gap-2 mb-4">
<input
type="text"
placeholder={t("background.paste_url")}
className="input input-bordered w-full text-info"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
/>
<button className="btn btn-success flex items-center gap-1" onClick={handleAddUrl}>
<Plus size={16} /> {t("background.add")}
</button>
</div>
{/* Upload from computer */}
<div className="flex mb-4">
<button
className="btn btn-warning flex items-center gap-1"
onClick={() => fileInputRef.current?.click()}
>
<Upload size={16} /> {t("background.upload_from_computer")}
</button>
<input
type="file"
accept="image/*"
className="hidden"
ref={fileInputRef}
onChange={(e) => {
const file = e.target.files?.[0]
if (file) handleUploadFile(file)
e.target.value = ''
}}
/>
</div>
{/* Crop Modal */}
{(croppingImage != null && croppingImage != "") && (
<div className="fixed inset-0 z-60 flex flex-col items-center justify-center bg-black/70 p-4">
<div className="relative w-full max-w-5xl h-150 bg-gray-800 rounded-lg">
<Cropper
image={croppingImage}
crop={crop}
zoom={zoom}
aspect={16 / 9}
onCropChange={setCrop}
onZoomChange={setZoom}
onCropComplete={(_, croppedAreaPixels) => setCroppedAreaPixels(croppedAreaPixels)}
/>
<button
className="absolute bottom-4 left-1/2 -translate-x-1/2 btn btn-success"
onClick={handleCropComplete}
>
<Check size={20} /> {t("background.done")}
</button>
<button
className="absolute top-2 right-2 btn btn-ghost btn-circle"
onClick={() => setCroppingImage(null)}
>
<X size={20} />
</button>
</div>
</div>
)}
<div className="grid grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto">
{allBackgrounds.map((value, i) => {
const isExtra = i < extraBackgrounds.length
return (
<div
key={i}
className={`relative rounded-lg overflow-hidden cursor-pointer border-2 transition-all duration-200 ${value === background ? 'border-blue-500' : 'border-transparent hover:border-gray-500'
}`}
onClick={() => handleSelect(value)}
>
<img src={value} alt={`bg-${i}`} loading="lazy" className="w-full h-28 object-cover" />
{isExtra && (
<button
className="absolute top-1 right-1 bg-black/50 hover:bg-black/70 text-white rounded-full p-1"
onClick={(e) => {
e.stopPropagation()
handleRemoveExtra(value)
}}
>
<X size={14} />
</button>
)}
</div>
)
})}
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -2,6 +2,7 @@ import { motion } from "motion/react"
import { AppService } from "@bindings/firefly-launcher/internal/app-service"
import { toast } from "react-toastify"
import useSettingStore from "@/stores/settingStore"
import { useTranslation } from "react-i18next"
export default function CloseModal({
isOpen,
@@ -12,72 +13,73 @@ export default function CloseModal({
}) {
if (!isOpen) return null
const { closingOption, setClosingOption } = useSettingStore()
const { t } = useTranslation()
return (
<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-2xl 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 flex justify-between items-center">
<h3 className="font-bold text-xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-600">
Confirm Action
</h3>
<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={onClose}
>
</motion.button>
</div>
<div className="px-6 pt-2 pb-6">
<p className="mb-4 text-lg">
Do you want to minimize the application to the system tray or close the application?
</p>
<div className="flex items-center mb-4">
<input
id="dontAskAgain"
type="checkbox"
className="checkbox checkbox-sm mr-2"
checked={!closingOption.isAsk}
onChange={(e) => setClosingOption({ isMinimize: closingOption.isMinimize, isAsk: !e.target.checked })}
/>
<label htmlFor="dontAskAgain" className="text-sm font-semibold text-accent">
Do not ask me again
</label>
<div className="relative w-[90%] max-w-2xl 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 flex justify-between items-center">
<h3 className="font-bold text-xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-600">
{t("close.title")}
</h3>
<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={onClose}
>
</motion.button>
</div>
<div className="grid grid-cols-2 justify-end gap-3">
<button
className="btn btn-warning"
onClick={async () => {
onClose()
const [success, message] = await AppService.MinimizeApp()
if (!success) toast.error(message)
if (!closingOption.isAsk) {
setClosingOption({ isMinimize: true, isAsk: false })
}
}}
>
Minimize
</button>
<button
className="btn btn-error btn-outline"
onClick={async () => {
onClose()
const [success, message] = await AppService.CloseApp()
if (!success) toast.error(message)
if (!closingOption.isAsk) {
setClosingOption({ isMinimize: false, isAsk: false })
}
}}
>
Close
</button>
<div className="px-6 pt-2 pb-6">
<p className="mb-4 text-lg">
{t("close.description")}
</p>
<div className="flex items-center mb-4">
<input
id="dontAskAgain"
type="checkbox"
className="checkbox checkbox-sm mr-2"
checked={!closingOption.isAsk}
onChange={(e) => setClosingOption({ isMinimize: closingOption.isMinimize, isAsk: !e.target.checked })}
/>
<label htmlFor="dontAskAgain" className="text-sm font-semibold text-accent">
{t("close.dont_ask")}
</label>
</div>
<div className="grid grid-cols-2 justify-end gap-3">
<button
className="btn btn-warning"
onClick={async () => {
onClose()
const [success, message] = await AppService.HideApp()
if (!success) toast.error(message)
if (!closingOption.isAsk) {
setClosingOption({ isMinimize: true, isAsk: false })
}
}}
>
{t("close.minimize")}
</button>
<button
className="btn btn-error btn-outline"
onClick={async () => {
onClose()
const [success, message] = await AppService.CloseApp()
if (!success) toast.error(message)
if (!closingOption.isAsk) {
setClosingOption({ isMinimize: false, isAsk: false })
}
}}
>
{t("close.close")}
</button>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -4,7 +4,7 @@ export default function Footer() {
<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>
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Wails & DaisyUi)</p>
</aside>
</footer>
)

View File

@@ -1,86 +1,187 @@
import { Link } from "@tanstack/react-router";
import ThemeController from "../themeController";
import useModalStore from "@/stores/modalStore";
import { Settings2 } from "lucide-react";
import { Blend, BookOpen, Diff, Home, Info, Languages, Minus, Puzzle, Settings, TrendingUpDown, Wrench, X } from "lucide-react";
import { AppService } from "@bindings/firefly-launcher/internal/app-service";
import LanguageSwitcher from "../languageSwitcher";
import { useTranslation } from "react-i18next";
import useSettingStore from "@/stores/settingStore";
export default function Header() {
const { setIsOpenSettingModal } = useModalStore()
const { setIsOpenSettingModal, setIsOpenCloseModal } = useModalStore()
const { closingOption } = useSettingStore()
const { t } = useTranslation()
const controlButtons = [
{
icon: <Settings className="w-5 h-5" />,
action: () => setIsOpenSettingModal(true),
tip: t("header.settings"),
},
{
icon: <Minus className="w-5 h-5" />,
action: () => AppService.MinimizeApp(),
tip: t("header.minimize"),
},
{
icon: <X className="w-5 h-5" />,
action: () => {
if (closingOption.isAsk) {
setIsOpenCloseModal(true)
return
}
if (closingOption.isMinimize) {
AppService.HideApp()
return
}
AppService.CloseApp()
},
tip: t("header.close"),
},
]
return (
<div className="navbar bg-base-100 shadow-sm sticky top-0 z-50 px-3">
<div
className="navbar sticky top-0 z-50 px-3"
style={{ '--wails-draggable': 'drag' } as any}
>
{/* LEFT */}
<div className="navbar-start">
<div className="dropdown">
<div className="dropdown" style={{ '--wails-draggable': 'no-drag' } as any}>
<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>
<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>
className="menu menu-sm dropdown-content bg-black/50 backdrop-blur-md rounded-box z-1 mt-3 w-52 p-2 shadow"
>
<li><Link to="/">{t("header.home")}</Link></li>
<li>
<a>Tools</a>
<a>{t("header.tools")}</a>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/diff">Diff</Link></li>
<li><Link to="/language">{t("header.language")}</Link></li>
<li><Link to="/diff">{t("header.client_update")}</Link></li>
</ul>
</li>
<li>
<a>Plugins</a>
<a>{t("header.plugins")}</a>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li>
<li><Link to="/analysis">{t("header.analysis")}</Link></li>
<li><Link to="/srtools">{t("header.firefly_tools")}</Link></li>
</ul>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/howto">{t("header.how_to")}</Link></li>
<li><Link to="/about">{t("header.about")}</Link></li>
</ul>
</div>
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0">
<div className="flex items-center justify-center">
<img src="/appicon.png" alt="Logo" className='w-13 h-13 rounded-lg mx-2' />
<div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold">
<span className="text-emerald-500">Firefly </span>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
Launcher
</span>
</h1>
<p className="text-sm text-gray-500">By Kain</p>
<p className="text-white text-sm">{t("header.by")}</p>
</div>
</div>
</Link>
</div>
<div className="navbar-center hidden md:flex">
<ul className="menu menu-horizontal px-1 gap-4">
<li><Link to="/">Home</Link></li>
{/* CENTER */}
<div
className="navbar-center hidden md:flex bg-black/40 backdrop-blur-sm rounded-lg shadow-lg"
style={{ '--wails-draggable': 'no-drag' } as any}
>
<ul className="menu menu-horizontal px-1 gap-4 text-white">
<li>
<Link to="/" className="flex items-center gap-2 hover:text-cyan-300">
<Home size={18} /> {t("header.home")}
</Link>
</li>
<li>
<details>
<summary>Tools</summary>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/diff">Diff</Link></li>
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300 w-full">
<Wrench size={18} /> {t("header.tools")}
</summary>
<ul className="p-2 bg-black/75 text-white rounded-lg min-w-40 whitespace-nowrap">
<li>
<Link to="/language" className="flex items-center gap-2 hover:text-cyan-300">
<Languages size={18} /> {t("header.language")}
</Link>
</li>
<li>
<Link to="/diff" className="flex items-center gap-2 hover:text-cyan-300">
<Diff size={18} /> {t("header.client_update")}
</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">Firefly Tools</Link></li>
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300 w-full">
<Puzzle size={18} /> {t("header.plugins")}
</summary>
<ul className="p-2 bg-black/75 text-white rounded-lg min-w-40 whitespace-nowrap">
<li>
<Link to="/analysis" className="flex items-center gap-2 hover:text-cyan-300">
<TrendingUpDown size={18} /> {t("header.analysis")}
</Link>
</li>
<li>
<Link to="/srtools" className="flex items-center gap-2 hover:text-cyan-300">
<Blend size={18} /> {t("header.firefly_tools")}
</Link>
</li>
</ul>
</details>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
<li>
<Link to="/howto" className="flex items-center gap-2 hover:text-cyan-300">
<BookOpen size={18} /> {t("header.how_to")}
</Link>
</li>
<li>
<Link to="/about" className="flex items-center gap-2 hover:text-cyan-300">
<Info size={18} /> {t("header.about")}
</Link>
</li>
</ul>
</div>
<div className="navbar-end flex gap-2">
<ThemeController />
<button className="btn btn-ghost btn-circle" onClick={() => setIsOpenSettingModal(true)}>
<Settings2 className="w-5 h-5" />
</button>
{/* RIGHT */}
<div
className="navbar-end flex gap-2 z-52"
>
<div className="flex items-center gap-2 bg-black/40 backdrop-blur-sm rounded-lg" style={{ '--wails-draggable': 'no-drag' } as any}>
<LanguageSwitcher />
{controlButtons.map((btn, i) => (
<div key={i} className="tooltip tooltip-bottom" data-tip={btn.tip}>
<button
onClick={btn.action}
className="btn btn-ghost btn-circle"
>
{btn.icon}
</button>
</div>
))}
</div>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,110 @@
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
const languages = [
{ code: "en", label: "EN", native: "English" },
{ code: "vi", label: "VI", native: "Tiếng Việt" },
{ code: "ja", label: "JA", native: "日本語" },
{ code: "ko", label: "KO", native: "한국어" },
{ code: "zh", label: "ZH", native: "中文" },
];
const LanguageSwitcher = () => {
const { i18n, t } = useTranslation();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const current = languages.find((l) => l.code === i18n.language) ?? languages[0];
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
const select = (code: string) => {
i18n.changeLanguage(code);
setOpen(false);
};
return (
<div ref={ref} className="relative">
{/* Trigger */}
<button
onClick={() => setOpen((v) => !v)}
className="
btn btn-ghost px-2
flex items-center gap-1
text-sm font-semibold tracking-wider
text-white/80+
select-none
tooltip tooltip-bottom
"
data-tip={t("header.language")}
>
<span className="uppercase">{current.label}</span>
{/* Chevron */}
<svg
className={`w-3 h-3 text-white/50 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Dropdown */}
{open && (
<ul
className="
absolute right-0 mt-2 w-32
bg-black/60 backdrop-blur-md
border border-white/10 rounded-xl
shadow-2xl shadow-black/50
overflow-hidden z-999
py-1
animate-in fade-in slide-in-from-top-2 duration-150
"
>
{languages.map((lang) => {
const isActive = lang.code === i18n.language;
return (
<li key={lang.code}>
<button
onClick={() => select(lang.code)}
className={`
w-full flex items-center gap-3 px-4 py-2.5 text-sm
transition-colors duration-150
${isActive
? "bg-emerald-500/20 text-emerald-400"
: "text-white/70 hover:bg-white/8 hover:text-white"
}
`}
>
<span className="flex-1 text-left">{lang.native}</span>
{isActive && (
<svg className="w-3.5 h-3.5 text-emerald-400 shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export default LanguageSwitcher;

View File

@@ -3,6 +3,7 @@ import useModalStore from "@/stores/modalStore"
import useSettingStore from "@/stores/settingStore"
import useLauncherStore from "@/stores/launcherStore"
import { toast } from "react-toastify"
import { useTranslation } from "react-i18next"
export default function SettingModal({
isOpen,
@@ -12,14 +13,15 @@ export default function SettingModal({
onClose: () => void
}) {
if (!isOpen) return null
const { t } = useTranslation()
const { setIsOpenSelfUpdateModal } = useModalStore()
const { closingOption, setClosingOption } = useSettingStore()
const { setUpdateData, updateData } = useLauncherStore()
const { closingOption, setClosingOption, serverVersion,
proxyVersion, } = useSettingStore()
const { setUpdateData, updateData, launcherVersion } = useLauncherStore()
const CheckUpdate = async () => {
const launcherData = await CheckUpdateLauncher()
if (!launcherData.isUpdate) {
toast.success("Launcher is already up to date")
toast.success(t("setting.launcher_update_success"))
return
}
setUpdateData({
@@ -32,12 +34,12 @@ export default function SettingModal({
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-md bg-base-100 text-base-content rounded-2xl border border-purple-500/30 shadow-2xl shadow-purple-500/30 p-6">
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h3 className="font-extrabold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-500">
Settings
<h3 className="font-extrabold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-500">
{t("setting.title")}
</h3>
<button
className="btn btn-circle btn-sm bg-red-600 hover:bg-red-700 text-white border-none shadow-lg"
@@ -51,28 +53,27 @@ export default function SettingModal({
<div className="flex flex-col gap-6">
{/* Section 1: Launcher Update */}
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
<h4 className="font-bold text-lg mb-2">Launcher Update</h4>
<h4 className="font-bold text-lg mb-2">{t("setting.launcher_update_title")}</h4>
<p className="text-sm text-info mb-3">
Check if your launcher is up to date.
{t("setting.launcher_update_desc")}
</p>
<button
className="btn btn-primary bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-400 hover:to-red-500 text-white shadow-md hover:shadow-lg transition-all duration-200"
className="btn btn-primary bg-linear-to-r from-orange-500 to-red-500 hover:from-orange-400 hover:to-red-500 text-white shadow-md hover:shadow-lg transition-all duration-200"
onClick={CheckUpdate}
>
Check for Launcher Updates
{t("setting.launcher_update_btn")}
</button>
</div>
{/* Section 2: Closing Option */}
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
<h4 className="font-bold text-lg mb-2">Closing Options</h4>
<h4 className="font-bold text-lg mb-2">{t("setting.closing_options_title")}</h4>
<label className="flex items-start gap-3 cursor-pointer select-none">
<input
type="checkbox"
className="checkbox checkbox-primary w-5 h-5 mt-1"
checked={!closingOption.isAsk}
onChange={(e) => {
console.log(!e.target.checked)
setClosingOption({
isMinimize: closingOption.isMinimize,
isAsk: !e.target.checked
@@ -81,16 +82,32 @@ export default function SettingModal({
/>
<div className="flex flex-col">
<span className="text-base font-medium text-info">
Set do not ask again
{t("setting.set_dont_ask_again")}
</span>
<span className="text-sm text-warning">
Next time you close the app, it will automatically{" "}
{closingOption.isMinimize ? "minimize to system tray" : "quit the app"}{" "}
without asking.
<span className="text-sm text-accent">
{t('setting.closing_auto_desc', { action: closingOption.isMinimize ? t('setting.action_minimize') : t('setting.action_quit') })}
</span>
</div>
</label>
</div>
{/* Section 3: Launcher Version */}
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
<h4 className="font-bold text-lg mb-2">{t("setting.version_label")}</h4>
<div className="flex flex-wrap gap-2">
<p className="text-base text-info">
{t("setting.server_label")}: {serverVersion}
</p>
<p className="text-base text-info">
{t("setting.proxy_label")}: {proxyVersion}
</p>
<p className="text-base text-info">
{t("setting.launcher_label")}: {launcherVersion}
</p>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -28,14 +28,14 @@ export default function UpdateModal({ isOpen, title, message, buttons, onClose }
</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">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">{message}</p>
<p className="text-accent text-lg">{message}</p>
</div>
<div className="flex justify-end gap-3">
@@ -46,7 +46,7 @@ export default function UpdateModal({ isOpen, title, message, buttons, onClose }
whileTap={{ scale: 0.95 }}
className={`btn ${
btn.variant === "primary"
? "btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
? "btn-primary bg-linear-to-r from-orange-200 to-red-400 border-none"
: btn.variant === "error"
? "btn-error"
: "btn-outline btn-error"

View File

@@ -23,11 +23,9 @@ export async function UpdateProxy(proxyVersion: string) : Promise<void> {
setDownloadType("Downloading proxy...")
const [ok, error] = await GitService.DownloadProxyProgress(proxyVersion)
if (ok) {
setDownloadType("Unzipping proxy...")
GitService.UnzipProxy()
setDownloadType("Download proxy successfully")
setProxyVersion(proxyVersion)
setProxyPath("./proxy/FireflyProxy.exe")
setProxyPath("./proxy/firefly-go-proxy.exe")
} else {
toast.error(error)
setDownloadType("Download proxy failed")

View File

@@ -19,24 +19,25 @@ export function useGlobalEvents() {
const onProxyExit = () => setProxyRunning(false);
const onDownload = (event: any) => {
const { percent, speed } = event.data[0];
const { percent, speed } = event.data;
setProgressDownload(Number(percent));
setDownloadSpeed(speed);
};
const onUpdateProgress = (event: any) => {
const { progress, maxProgress } = event.data[0];
console.log(event)
const { progress, maxProgress } = event.data;
setProgressUpdate(Number(progress));
setMaxProgressUpdate(Number(maxProgress));
};
const onMessageUpdate = (event: any) => {
const { message } = event.data[0];
const { message } = event.data;
setMessageUpdate(message);
};
const onStageUpdate = (event: any) => {
const { stage } = event.data[0];
const { stage } = event.data;
setStageType(stage);
};
@@ -49,7 +50,7 @@ export function useGlobalEvents() {
Events.On("diff:message", onMessageUpdate);
Events.On("diff:stage", onStageUpdate);
Events.On("diff:error", (event: any) => {
const { message } = event.data[0];
const { message } = event.data;
toast.error(message);
});
Events.On("window:close", async () => {

18
frontend/src/i18n.ts Normal file
View File

@@ -0,0 +1,18 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import httpBackend from "i18next-http-backend";
i18n
.use(httpBackend)
.use(LanguageDetector) // detects browser language
.use(initReactI18next)
.init({
fallbackLng: "en", //fallback language
debug: true,
interpolation: {
escapeValue: false,
},
backend: {
loadPath: `/locales/{{lng}}.json`, //path of the languages
},
});

View File

@@ -5,6 +5,7 @@ import './styles/index.css'
import "../runtime.js"
import "@wailsio/runtime";
import { routeTree } from './routeTree.gen.js'
import "./i18n";
const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {

View File

@@ -1,31 +1,33 @@
import { Link } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export default function AboutPage() {
const { t } = useTranslation();
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="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>
<h1 className="text-4xl font-bold text-primary text-center">{t("about.title")}</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.
{t("about.p1_pre")}<span className="font-semibold text-error">Firefly Shelter</span>{t("about.p1_post")}
</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.
{t("about.p2_pre")}<span className="font-semibold text-success">{t("about.p2_highlight")}</span>{t("about.p2_post")}
</p>
<p className="text-lg leading-relaxed">
The launcher is built using <span className="font-mono text-info">Go + Wails3</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>.
{t("about.p3_pre")}<span className="font-mono text-info">Go + Wails3</span>{t("about.p3_mid")}<span className="text-accent">Tailwind CSS</span>{t("about.p3_and")}<span className="text-accent">DaisyUI</span>{t("about.p3_post")}
</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.
{t("about.p4")}
</p>
</div>
<div className="text-center pt-4">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
<Link to="/" className="btn btn-primary btn-wide">{t("about.btn_back")}</Link>
</div>
</div>
</div>
);
}
}

View File

@@ -1,18 +1,20 @@
import { Link } from '@tanstack/react-router';
import { useTranslation } from 'react-i18next';
export default function AnalysisPage() {
const { t } = useTranslation();
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="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
{t("analysis.title")}
</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>
<span>{t("analysis.sect1_title")}</span>
</h2>
<div className="space-y-4 text-green-700">
@@ -20,17 +22,19 @@ export default function AnalysisPage() {
<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.
<span className="font-semibold text-success">{t("analysis.sect1_p1_pre")}</span>
{t("analysis.sect1_p1_mid")}
<span className="text-info font-mono bg-blue-100 px-2 py-1 rounded">{t("analysis.sect1_p1_highlight")}</span>
{t("analysis.sect1_p1_post")}
</p>
<p className="text-sm">It's lightweight, fast, and easy to use for comprehensive damage analysis.</p>
<p className="text-sm">{t("analysis.sect1_p2")}</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>
<span className="font-semibold text-green-800">{t("analysis.sect1_github")}</span>
</div>
<a
href="https://github.com/hessiser/veritas"
@@ -44,46 +48,45 @@ export default function AnalysisPage() {
</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>
<span>{t("analysis.sect2_title")}</span>
</h2>
<div className="space-y-4">
<p className="text-blue-700">
Use these web applications for real-time damage analysis with Veritas:
{t("analysis.sect2_desc")}
</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>
<span className="font-semibold text-blue-800">{t("analysis.sect2_master")}</span>
</div>
<a
href="https://sranalysis.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
href="https://sranalysis.punklorde.org"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://sranalysis.kain.id.vn/
https://sranalysis.punklorde.org
</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>
<span className="font-semibold text-blue-800">{t("analysis.sect2_backup")}</span>
</div>
<a
href="https://firefly-sranalysis.vercel.app/"
className="link link-warning font-mono text-sm break-all"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://firefly-sranalysis.vercel.app/
https://firefly-sranalysis.vercel.app
</a>
</div>
</div>
@@ -92,34 +95,33 @@ export default function AnalysisPage() {
<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.
<strong>{t("analysis.sect2_tip_pre")}</strong>{t("analysis.sect2_tip_post")}
</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>
<span>{t("analysis.sect3_title")}</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>
<h3 className="font-semibold text-red-800 mb-2">{t("analysis.sect3_subtitle")}</h3>
<p className="text-red-700 mb-2">
After downloading Veritas, you must rename the file for it to work properly:
{t("analysis.sect3_desc")}
</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>
{t("analysis.sect3_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.
{t("analysis.sect3_place")}<code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code>{t("analysis.sect3_place_post")}
</p>
</div>
</div>
@@ -127,62 +129,59 @@ export default function AnalysisPage() {
</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>
<span>{t("analysis.sect4_title")}</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>
<span>{t("analysis.sect4_sub1")}</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 GO Server (PS)</span>.</p>
<span className="font-medium min-w-5 text-purple-600">1.</span>
<p>{t("analysis.sect4_sub1_step1_pre")}<span className="font-semibold">{t("analysis.sect4_sub1_step1_game")}</span>{t("analysis.sect4_sub1_step1_mid")}<span className="font-semibold">{t("analysis.sect4_sub1_step1_server")}</span>{t("analysis.sect4_sub1_step1_post")}</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>
<span className="font-medium min-w-5 text-purple-600">2.</span>
<p>{t("analysis.sect4_sub1_step2")}</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>
<span className="font-medium min-w-5 text-purple-600">3.</span>
<p>{t("analysis.sect4_sub1_step3_pre")}<strong>{t("analysis.sect4_sub1_step3_conn")}</strong>{t("analysis.sect4_sub1_step3_mid1")}<strong>{t("analysis.sect4_sub1_step3_type")}</strong>{t("analysis.sect4_sub1_step3_mid2")}<strong>{t("analysis.sect4_sub1_step3_btn")}</strong>{t("analysis.sect4_sub1_step3_post")}</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>
<span className="font-medium min-w-5 text-purple-600">4.</span>
<p>{t("analysis.sect4_sub1_step4")}</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>
<span>{t("analysis.sect4_sub2")}</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>
<span className="font-medium min-w-5 text-purple-600">1.</span>
<p>{t("analysis.sect4_sub2_step1_pre")}<span className="font-semibold">{t("analysis.sect4_sub2_step1_game")}</span>{t("analysis.sect4_sub2_step1_mid")}<span className="font-semibold">{t("analysis.sect4_sub2_step1_server")}</span>{t("analysis.sect4_sub2_step1_post")}</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>
<span className="font-medium min-w-5 text-purple-600">2.</span>
<p>{t("analysis.sect4_sub2_step2")}</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>
<span className="font-medium min-w-5 text-purple-600">3.</span>
<p>{t("analysis.sect4_sub2_step3_pre")}<strong>{t("analysis.sect4_sub2_step3_conn")}</strong>{t("analysis.sect4_sub2_step3_mid1")}<strong>{t("analysis.sect4_sub2_step3_type")}</strong>{t("analysis.sect4_sub2_step3_mid2")}<strong>{t("analysis.sect4_sub2_step3_btn")}</strong>{t("analysis.sect4_sub2_step3_post")}</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>
<span className="font-medium min-w-5 text-purple-600">4.</span>
<p>{t("analysis.sect4_sub2_step4")}</p>
</div>
</div>
</div>
@@ -190,7 +189,7 @@ export default function AnalysisPage() {
</div>
<div className="text-center pt-6">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
<Link to="/" className="btn btn-primary btn-wide">{t("analysis.btn_back")}</Link>
</div>
</div>
</div>

View File

@@ -2,17 +2,18 @@ import useSettingStore from "@/stores/settingStore"
import { Check, Folder, File, X, Settings } from "lucide-react"
import { useEffect } from "react"
import { toast } from "react-toastify"
import { DiffService} from "@bindings/firefly-launcher/internal/diff-service"
import { DiffService } from "@bindings/firefly-launcher/internal/diff-service"
import { FSService } from "@bindings/firefly-launcher/internal/fs-service"
import { motion } from "motion/react"
import useDiffStore from "@/stores/diffStore"
import { useTranslation } from "react-i18next"
export default function DiffPage() {
const { gameDir, setGameDir } = useSettingStore()
const {
isLoading,
setIsLoading,
folderCheckResult,
const {
isLoading,
setIsLoading,
folderCheckResult,
setFolderCheckResult,
diffDir,
setDiffDir,
@@ -29,6 +30,7 @@ export default function DiffPage() {
messageUpdate,
setMessageUpdate
} = useDiffStore()
const { t } = useTranslation()
useEffect(() => {
const getLanguage = async () => {
@@ -50,7 +52,7 @@ export default function DiffPage() {
const handlePickGameFolder = async () => {
try {
setIsLoading({game: true, diff: false})
setIsLoading({ game: true, diff: false })
const basePath = await FSService.PickFolder()
if (basePath) {
setGameDir(basePath)
@@ -61,28 +63,28 @@ export default function DiffPage() {
setFolderCheckResult(exists ? 'success' : 'error')
setGameDir(exists ? basePath : '')
if (!exists) {
toast.error('Game directory not found. Please select the correct folder.')
toast.error(t("diff.toast_game_dir_not_found"))
}
} else {
toast.error('No folder path selected')
toast.error(t("diff.toast_no_folder_selected"))
setFolderCheckResult('error')
setGameDir('')
}
} catch (err: any) {
toast.error('PickFolder error:', err)
toast.error(t("diff.toast_pick_folder_error"), err)
setFolderCheckResult('error')
} finally {
setIsLoading({game: false, diff: false})
setIsLoading({ game: false, diff: false })
}
}
const handlePickDiffFile = async () => {
try {
setIsLoading({game: false, diff: true})
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')
toast.error(t("diff.toast_invalid_file_type"))
setDiffCheckResult('error')
setDiffDir('')
return
@@ -90,15 +92,15 @@ export default function DiffPage() {
setDiffDir(basePath)
setDiffCheckResult('success')
} else {
toast.error('No file path selected')
toast.error(t("diff.toast_no_file_selected"))
setDiffCheckResult('error')
setDiffDir('')
}
} catch (err: any) {
toast.error('PickFile error:', err)
toast.error(t("diff.toast_pick_file_error"), err)
setDiffCheckResult('error')
} finally {
setIsLoading({game: false, diff: false})
setIsLoading({ game: false, diff: false })
}
}
@@ -110,25 +112,25 @@ export default function DiffPage() {
}
return true
}
try {
setIsDiffLoading(true)
if (!gameDir || !diffDir) {
toast.error('Please select game directory and diff file')
toast.error(t("diff.toast_select_both"))
return
}
setStageType('Check Type HDiff')
setStageType(t("diff.stage_check_type"))
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir)
if (!handleResult(isOk, errorType)) return
setProgressUpdate(1)
if (['hdiffmap.json', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)) {
setStageType('Version Validate')
setStageType(t("diff.stage_version_validate"))
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir)
@@ -136,37 +138,37 @@ export default function DiffPage() {
setProgressUpdate(1)
}
setStageType('Data Extract')
setStageType(t("diff.stage_data_extract"))
const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir)
if (!handleResult(validData, errorData)) return
setStageType('Cut Data')
setStageType(t("diff.stage_cut_data"))
setMessageUpdate('')
const [validCut, errorCut] = await DiffService.CutData(gameDir)
if (!handleResult(validCut, errorCut)) return
switch (validType) {
case 'hdifffiles.txt':
case 'hdiffmap.json':
case 'hdifffiles.json': {
setStageType('Patch Data')
setStageType(t("diff.stage_patch_data"))
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
if (!handleResult(validPatch, errorPatch)) return
setStageType('Delete old files')
setStageType(t("diff.stage_delete_old_files"))
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
if (!handleResult(validDelete, errorDelete)) return
break
}
case 'manifest': {
setStageType('Patch Data')
setStageType(t("diff.stage_patch_data"))
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
if (!handleResult(validPatch, errorPatch)) return
break
}
}
toast.success('Update game completed')
toast.success(t("diff.toast_update_completed"))
} catch (err: any) {
console.error(err)
toast.error(`PickFile error: ${err}`)
@@ -174,7 +176,7 @@ export default function DiffPage() {
setIsDiffLoading(false)
}
}
return (
<div className="p-2 mx-4">
@@ -182,9 +184,9 @@ export default function DiffPage() {
{/* Header */}
<div className="text-center mb-2">
<h1 className="text-4xl font-bold mb-2">
🎮 Game Update by Hdiffz
{t("diff.header_title")}
</h1>
<p className="">Help you update game with hdiffz</p>
<p className="">{t("diff.header_desc")}</p>
</div>
{/* Main Content */}
@@ -194,7 +196,7 @@ export default function DiffPage() {
<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
{t("diff.game_dir_title")}
</h2>
<div className="space-y-1">
@@ -205,7 +207,7 @@ export default function DiffPage() {
className="btn btn-primary"
>
<Folder size={20} />
{isLoading.game ? 'Selecting...' : 'Select Game Folder'}
{isLoading.game ? t("diff.btn_selecting") : t("diff.btn_select_game")}
</button>
{gameDir && (
@@ -224,12 +226,12 @@ export default function DiffPage() {
{folderCheckResult === 'success' ? (
<>
<Check size={20} />
<span>Valid game directory found!</span>
<span>{t("diff.game_dir_valid")}</span>
</>
) : (
<>
<X size={20} />
<span>Game directory not found. Please select the correct folder.</span>
<span>{t("diff.game_dir_invalid")}</span>
</>
)}
</div>
@@ -241,7 +243,7 @@ export default function DiffPage() {
<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
{t("diff.diff_file_title")}
</h2>
<div className="space-y-1">
@@ -252,7 +254,7 @@ export default function DiffPage() {
className="btn btn-primary"
>
<File size={20} />
{isLoading.diff ? 'Selecting...' : 'Select Diff file Folder'}
{isLoading.diff ? t("diff.btn_selecting") : t("diff.btn_select_diff")}
</button>
{diffDir && (
@@ -271,12 +273,12 @@ export default function DiffPage() {
{diffCheckResult === 'success' ? (
<>
<Check size={20} />
<span>Valid diff file found!</span>
<span>{t("diff.diff_file_valid")}</span>
</>
) : (
<>
<X size={20} />
<span>Diff file not found. Please select the correct file.</span>
<span>{t("diff.diff_file_invalid")}</span>
</>
)}
</div>
@@ -288,10 +290,10 @@ export default function DiffPage() {
<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"
className="bg-linear-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'}
{isDiffLoading ? t("diff.btn_updating") : t("diff.btn_update_game") }
</button>
</div>
</div>
@@ -300,8 +302,8 @@ export default function DiffPage() {
<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 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{t("diff.modal_update_title")}
</h3>
</div>
@@ -309,22 +311,22 @@ export default function DiffPage() {
<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>
<span className="font-bold text-lg text-accent">{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>}
{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"
className="h-full bg-linear-to-r from-cyan-400 to-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${(progressUpdate/maxProgressUpdate)*100}%` }}
animate={{ width: `${(progressUpdate / maxProgressUpdate) * 100}%` }}
transition={{ duration: 0.3 }}
/>
</div>
<div className="text-center text-lg text-white/60">
Please wait...
{t("diff.status_wait")}
</div>
</div>
</div>
@@ -336,13 +338,13 @@ export default function DiffPage() {
{/* 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>
<h3 className="font-medium text-error mb-2">{t("diff.inst_title")}</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>
<li>{t("diff.inst_step_1")}</li>
<li>{t("diff.inst_step_2")}</li>
<li>{t("diff.inst_step_3")}</li>
<li>{t("diff.inst_step_4")}</li>
<li>{t("diff.inst_step_5")}</li>
</ol>
</div>
</div>

View File

@@ -1,156 +1,130 @@
import { Link } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export default function FireflyToolsPage() {
const { t } = useTranslation();
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="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 Tools</h1>
<h1 className="text-4xl font-bold text-primary text-center">{t("fireflytools.title")}</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 Firefly Tools</span>
<span>{t("fireflytools.sect1_title")}</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 is a another version of {" "}
<span className="font-semibold text-success">Firefly Tools {" "}</span>
developed by {" "}
<span className="font-semibold text-warning">Me {"(Kain)"}</span>
{t("fireflytools.sect1_p1_pre")}
<span className="font-semibold text-success">{t("fireflytools.sect1_p1_tool")}</span>
{t("fireflytools.sect1_p1_mid")}
<span className="font-semibold text-accent">{t("fireflytools.sect1_p1_author")}</span>
</p>
</div>
<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>
<span className="font-semibold text-blue-800">{t("fireflytools.sect1_master_site")}</span>
</div>
<a
href="https://srtools.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
href="https://srtools.punklorde.org"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://srtools.kain.id.vn/
https://srtools.punklorde.org
</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://firefly-srtools.vercel.app/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://firefly-srtools.vercel.app/
</a>
</div>
</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>
<p>{t("fireflytools.sect1_p2_pre")}<span className="font-semibold text-accent">{t("fireflytools.sect1_p2_author")}</span>{t("fireflytools.sect1_p2_post")}</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{" "}
<p>{t("fireflytools.sect1_p3_pre")}
<a
href="https://srtools.neonteam.dev/"
className="link link-warning"
href="https://srtools.neonteam.dev"
className="link link-accent"
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>
<span>{t("fireflytools.sect2_title")}</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>
<p>{t("fireflytools.sect2_feat1")}</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 GO Server</span> using <span className="font-semibold">Connect PS</span> no manual file uploads required.</p>
<p>{t("fireflytools.sect2_feat2_pre")}<span className="font-semibold text-accent">{t("fireflytools.sect2_feat2_server")}</span>{t("fireflytools.sect2_feat2_mid")}<span className="font-semibold">{t("fireflytools.sect2_feat2_conn")}</span>{t("fireflytools.sect2_feat2_post")}</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-2xl"></div>
<div>
<h4 className="font-semibold text-green-800 text-lg">Extra Settings</h4>
<h4 className="font-semibold text-green-800 text-lg">{t("fireflytools.sect2_feat3_title")}</h4>
<p className="text-green-700 mt-1">
Enhance your <span className="font-semibold text-warning">Firefly GO Server</span> experience with extra features:
{t("fireflytools.sect2_feat3_desc_pre")}<span className="font-semibold text-accent">{t("fireflytools.sect2_feat3_desc_server")}</span>{t("fireflytools.sect2_feat3_desc_post")}
</p>
<ul className="list-disc list-inside mt-2 space-y-1 text-green-700">
<li>🎭 <span className="font-medium">Hidden Game UI</span> remove the entire game interface.</li>
<li>🚫 <span className="font-medium">Disable Censorship</span> get rid of Lens Flare censor 💀.</li>
<li>🧪 <span className="font-medium">Theorycraft Mode</span> configure HP, cycles, and more via the web.</li>
<li>🎭 <span className="font-medium">{t("fireflytools.sect2_feat3_ui")}</span> {t("fireflytools.sect2_feat3_ui_desc")}</li>
<li>🚫 <span className="font-medium">{t("fireflytools.sect2_feat3_censor")}</span> {t("fireflytools.sect2_feat3_censor_desc")}</li>
<li>🧪 <span className="font-medium">{t("fireflytools.sect2_feat3_tc")}</span> {t("fireflytools.sect2_feat3_tc_desc")}</li>
</ul>
</div>
</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>
<p>{t("fireflytools.sect2_feat4_pre")}<code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>{t("fireflytools.sect2_feat4_post")}</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>
<p>{t("fireflytools.sect2_feat5")}</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>
<span>{t("fireflytools.sect3_title")}</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>
<p>{t("fireflytools.sect3_step1")}</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>
<p>{t("fireflytools.sect3_step2")}</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>
<p>{t("fireflytools.sect3_step3_pre")}<span className="font-semibold">{t("fireflytools.sect3_step3_bold")}</span>{t("fireflytools.sect3_step3_post")}</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>
<p>{t("fireflytools.sect3_step4")}</p>
</div>
</div>
</div>
<div className="text-center pt-4">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
<Link to="/" className="btn btn-primary btn-wide">{t("fireflytools.btn_back")}</Link>
</div>
</div>
</div>

View File

@@ -1,49 +1,48 @@
import { Link } from '@tanstack/react-router';
import { useTranslation } from 'react-i18next';
export default function HowToPage() {
const { t } = useTranslation();
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className=" 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>
<h1 className="text-4xl font-bold text-primary text-center">{t("howto.title")}</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>
<span>{t("howto.sect1_title")}</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>
<p>{t("howto.sect1_auto_update_pre")} <span className="font-semibold text-amber-600">Firefly Go</span> {t("howto.sect1_auto_update_post")}</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>
<p>{t("howto.sect1_launch_game")}</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>{t("howto.sect1_lang_pre")} <Link to="/language" className="link link-info font-mono">{t("howto.sect1_lang_link")}</Link>
</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-2xl">📦</div>
<div>
<p className="text-green-800 font-semibold">
Patch & Update Game Files
{t("howto.sect1_patch_title")}
</p>
<p className="text-green-700">
Use the{" "}
<a href="/diff" className="link link-info font-mono">Diff Tool</a>{" "}
(<span className="font-medium">DiffPatch</span>) for fast & lightweight incremental updates.
{t("howto.sect1_patch_desc_1_pre")} <Link to="/diff" className="link link-info font-mono">{t("howto.sect1_patch_desc_1_link")}</Link> {t("howto.sect1_patch_desc_1_post")}
</p>
<p className="text-green-700 mt-1">
Supports <span className="font-semibold">Hdiff</span>, <span className="font-semibold">Ldiff</span>, and custom diff formats.
{t("howto.sect1_patch_desc_2_pre")} <span className="font-semibold">Hdiff</span>, <span className="font-semibold">Ldiff</span>{t("howto.sect1_patch_desc_2_post")}
</p>
</div>
</div>
</div>
</div>
@@ -51,12 +50,11 @@ export default function HowToPage() {
<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>
<span>{t("howto.sect2_title")}</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>.
{t("howto.sect2_desc_pre")} <span className="font-semibold text-accent">{t("howto.sect2_desc_bold")}</span>{t("howto.sect2_desc_post")}
</p>
{/* Theorycraft Mode Warning */}
@@ -64,8 +62,8 @@ export default function HowToPage() {
<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>
<h3 className="font-semibold text-red-800 mb-2">{t("howto.sect2_tc_req_title")}</h3>
<p className="text-red-700 mb-2">{t("howto.sect2_tc_req_desc_pre")} <strong>{t("howto.sect2_tc_req_desc_bold")}</strong> {t("howto.sect2_tc_req_desc_post")}</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>
@@ -79,36 +77,34 @@ export default function HowToPage() {
<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">Extra Settings</h4>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_extra_title")}</h4>
<div className="space-y-4 text-blue-700 text-sm">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🎭 Hidden UI
🎭 {t("howto.sect2_hidden_ui_title")}
</h5>
<p className="mt-1">
Instantly hides the entire game UI often used in DIM showcase videos.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🚫 Disable Censorship
</h5>
<p className="mt-1">
Remove the Lens Flare censor effect 💀 for a cleaner experience.
{t("howto.sect2_hidden_ui_desc")}
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🧪 Theorycraft Mode
🚫 {t("howto.sect2_censor_title")}
</h5>
<p className="mt-1">
No need to type chat commands anymore configure everything through the
web: adjust monster HP, set cycles, view logs, and more.
{t("howto.sect2_censor_desc")}
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🧪 {t("howto.sect2_tc_title")}
</h5>
<p className="mt-1">
{t("howto.sect2_tc_desc")}
</p>
</div>
</div>
@@ -120,7 +116,7 @@ export default function HowToPage() {
title="Extra Settings Tutorial"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="rounded-lg w-full h-[300px]"
className="rounded-lg w-full h-75"
></iframe>
</div>
</div>
@@ -129,17 +125,17 @@ export default function HowToPage() {
{/* Commands List */}
<div className="space-y-4 mt-4">
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
<h3 className="text-lg font-semibold text-blue-800">{t("howto.sect2_cmd_title")}</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>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_tc")}</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>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 1</code> {t("howto.sect2_cmd_tc_enable")}</p>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 0</code> {t("howto.sect2_cmd_tc_disable")}</p>
</div>
</div>
</div>
@@ -150,11 +146,11 @@ export default function HowToPage() {
<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>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_cycle")} <span className="text-red-600 text-sm">{t("howto.sect2_cmd_tc_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>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle N</code> {t("howto.sect2_cmd_cycle_desc")}</p>
<p className="text-sm">{t("howto.sect2_cmd_cycle_ex1_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 30</code> {t("howto.sect2_cmd_cycle_ex1_post")}</p>
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 0</code> {t("howto.sect2_cmd_cycle_ex2_post")}</p>
</div>
</div>
</div>
@@ -166,37 +162,36 @@ export default function HowToPage() {
<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>
{t("howto.sect2_cmd_hp")} <span className="text-red-600 text-sm">{t("howto.sect2_cmd_tc_only")}</span>
</h4>
<div className="space-y-2 text-blue-700 text-sm">
<p>
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp N</code> Set monster HP (only available in Theorycraft mode)
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp N</code> {t("howto.sect2_cmd_hp_desc1")}
</p>
<p>
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> Disable the set HP feature
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> {t("howto.sect2_cmd_hp_desc2")}
</p>
<p>
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp Wave V1 V2 ...</code> Set HP for each monster in a specific wave
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp Wave V1 V2 ...</code> {t("howto.sect2_cmd_hp_desc3")}
</p>
<p className="ml-4">
Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000 3000000</code> sets wave 1 monster1 HP=2,000,000 and monster2 HP=3,000,000
{t("howto.sect2_cmd_hp_ex_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000 3000000</code> {t("howto.sect2_cmd_hp_ex_post")}
</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>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_log")} <span className="text-red-600 text-sm">{t("howto.sect2_cmd_tc_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>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 1</code> {t("howto.sect2_cmd_log_desc1")}</p>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 0</code> {t("howto.sect2_cmd_log_desc2")}</p>
<p className="text-sm">{t("howto.sect2_cmd_log_out_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">.json</code></p>
</div>
</div>
</div>
@@ -207,11 +202,11 @@ export default function HowToPage() {
<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>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_skip")}</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>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip N</code> {t("howto.sect2_cmd_skip_desc")}</p>
<p className="text-sm">{t("howto.sect2_cmd_skip_ex1_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 2</code> {t("howto.sect2_cmd_skip_ex1_post")}</p>
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 0</code> {t("howto.sect2_cmd_skip_ex2_post")}</p>
</div>
</div>
</div>
@@ -222,11 +217,11 @@ export default function HowToPage() {
<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>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_id")}</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>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id CHAR_ID</code> {t("howto.sect2_cmd_id_desc")}</p>
<p className="text-sm">{t("howto.sect2_cmd_id_ex1_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id 8008</code> {t("howto.sect2_cmd_id_ex1_post")}</p>
<p className="text-sm">{t("howto.sect2_cmd_id_ex2_pre")} <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>
@@ -237,9 +232,9 @@ export default function HowToPage() {
<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>
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_update")}</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>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/update</code> {t("howto.sect2_cmd_update_desc_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code></p>
</div>
</div>
</div>
@@ -251,7 +246,7 @@ export default function HowToPage() {
<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>
<span>{t("howto.sect3_title")}</span>
</h2>
<div className="space-y-4">
@@ -260,9 +255,9 @@ export default function HowToPage() {
<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>
<h3 className="font-semibold text-yellow-800 mb-1">{t("howto.sect3_admin_title")}</h3>
<p className="text-yellow-700">
Always run the launcher as Administrator for file permission access.
{t("howto.sect3_admin_desc")}
</p>
</div>
</div>
@@ -273,10 +268,9 @@ export default function HowToPage() {
<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>
<h3 className="font-semibold text-blue-800 mb-1">{t("howto.sect3_backup_title")}</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.
{t("howto.sect3_backup_desc_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">config.json</code> {t("howto.sect3_backup_desc_mid")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code> {t("howto.sect3_backup_desc_post")}
</p>
</div>
</div>
@@ -287,21 +281,21 @@ export default function HowToPage() {
<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>
<h3 className="font-semibold text-green-800 mb-2">{t("howto.sect3_voice_title")}</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>
<span className="font-medium min-w-5 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>
<p className="mb-1">{t("howto.sect3_voice_step1_pre")} <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>{t("howto.sect3_voice_step1_mid")}</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>
<p className="mt-1">{t("howto.sect3_voice_step1_post_pre")} <strong>{t("howto.sect3_voice_step1_post_bold")}</strong> {t("howto.sect3_voice_step1_post_post")}</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>
<span className="font-medium min-w-5 text-green-600">2.</span>
<p>{t("howto.sect3_voice_step2")}</p>
</div>
</div>
</div>
@@ -311,7 +305,7 @@ export default function HowToPage() {
</div>
<div className="text-center pt-4">
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
<Link to="/" className="btn btn-primary btn-wide">{t("howto.btn_back")}</Link>
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { FSService } from '@bindings/firefly-launcher/internal/fs-service'
import { LanguageService } from '@bindings/firefly-launcher/internal/language-service'
import { toast } from 'react-toastify'
import useSettingStore from '@/stores/settingStore'
import { useTranslation } from "react-i18next"
export default function LanguagePage() {
const { gameDir, setGameDir } = useSettingStore()
@@ -24,6 +25,7 @@ export default function LanguagePage() {
{ value: 'jp', label: 'Japanese', flag: '🇯🇵' },
{ value: 'kr', label: 'Korean', flag: '🇰🇷' }
]
const { t } = useTranslation()
useEffect(() => {
const getLanguage = async () => {
@@ -78,15 +80,15 @@ export default function LanguagePage() {
setFolderCheckResult(exists ? 'success' : 'error')
setGameDir(exists ? basePath : "")
if (!exists) {
toast.error('Game directory not found. Please select the correct folder.')
toast.error(t("diff.toast_game_dir_not_found"))
}
} else {
toast.error('No folder path selected')
toast.error(t("language.toast_no_folder_selected"))
setFolderCheckResult('error')
setGameDir('')
}
} catch (err: any) {
toast.error('PickFolder error:', err)
toast.error(t("language.toast_pick_folder_error"), err)
setFolderCheckResult('error')
} finally {
setIsLoading(false)
@@ -95,7 +97,7 @@ export default function LanguagePage() {
const handleSetLanguage = async () => {
if (!gameDir) {
toast.error('No folder path selected')
toast.error(t("language.toast_no_folder_selected"))
return
}
try {
@@ -106,7 +108,7 @@ export default function LanguagePage() {
selectedVoiceLang
)
if (ok) {
toast.success('Language set successfully')
toast.success(t("language.toast_set_language_success"))
setTextLang(selectedTextLang)
setVoiceLang(selectedVoiceLang)
}
@@ -115,7 +117,7 @@ export default function LanguagePage() {
}
} catch (err: any) {
toast.error('SetLanguage error:', err)
toast.error(t("language.toast_set_language_error"), err)
} finally {
setIsSettingLanguage(false)
}
@@ -132,9 +134,9 @@ export default function LanguagePage() {
{/* Header */}
<div className="text-center mb-2">
<h1 className="text-4xl font-bold mb-2">
🎮 Game Language Manager
{t("language.header_title")}
</h1>
<p className="">Manage text and voice language settings for your game</p>
<p className="">{t("language.header_desc")}</p>
</div>
{/* Main Content */}
@@ -144,7 +146,7 @@ export default function LanguagePage() {
<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
{t("language.game_dir_title")}
</h2>
<div className="space-y-1">
@@ -155,7 +157,7 @@ export default function LanguagePage() {
className="btn btn-primary"
>
<Folder size={20} />
{isLoading ? 'Selecting...' : 'Select Game Folder'}
{isLoading ? t("language.btn_selecting") : t("language.btn_select_game") }
</button>
{gameDir && (
@@ -174,12 +176,12 @@ export default function LanguagePage() {
{folderCheckResult === 'success' ? (
<>
<Check size={20} />
<span>Valid game directory found!</span>
<span>{t("language.game_dir_valid")}</span>
</>
) : (
<>
<X size={20} />
<span>Game directory not found. Please select the correct folder.</span>
<span>{t("language.game_dir_invalid")}</span>
</>
)}
</div>
@@ -192,14 +194,14 @@ export default function LanguagePage() {
<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
{t("language.current_languages_title")}
</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>
<span className="font-bold text-success">{t("language.text_language")}</span>
</div>
<p className="text-2xl font-bold text-success">
{getLanguageLabel(textLang)}
@@ -208,10 +210,10 @@ export default function LanguagePage() {
<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>
<Mic size={20} className="text-accent" />
<span className="font-bold text-accent">{t("language.voice_language")}</span>
</div>
<p className="text-2xl font-bold text-warning">
<p className="text-2xl font-bold text-accent">
{getLanguageLabel(voiceLang)}
</p>
</div>
@@ -224,22 +226,21 @@ export default function LanguagePage() {
}`}>
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<Settings className="text-primary" size={24} />
Language Settings
{t("language.language_settings_title")}
</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
{t("language.text_language")}
</label>
<select
value={selectedTextLang}
onChange={(e) => setSelectedTextLang(e.target.value)}
className="w-full select select-success"
>
<option value="">Select text language...</option>
<option value="">{t("language.select_text_placeholder")}</option>
{languageOptions.map(lang => (
<option key={lang.value} value={lang.value}>
{lang.flag} {lang.label}
@@ -248,18 +249,17 @@ export default function LanguagePage() {
</select>
</div>
{/* Voice Language */}
<div className="space-y-3">
<label className="flex text-sm font-medium text-warning items-center gap-2">
<label className="flex text-sm font-medium text-accent items-center gap-2">
<Mic size={16} />
Voice Language
{t("language.voice_language")}
</label>
<select
value={selectedVoiceLang}
onChange={(e) => setSelectedVoiceLang(e.target.value)}
className="w-full select select-warning"
>
<option value="">Select voice language...</option>
<option value="">{t("language.select_voice_placeholder")}.</option>
{languageOptions.map(lang => (
<option key={lang.value} value={lang.value}>
{lang.flag} {lang.label}
@@ -274,22 +274,22 @@ export default function LanguagePage() {
<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"
className="bg-linear-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'}
{isSettingLanguage ? t("language.btn_applying") : t("language.btn_apply")}
</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>
<h3 className="font-medium text-error mb-2">{t("language.inst_title")}</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>
<li>{t("language.inst_step_1")}</li>
<li>{t("language.inst_step_2")}</li>
<li>{t("language.inst_step_3")}</li>
<li>{t("language.inst_step_4")}</li>
</ol>
</div>
</div>

View File

@@ -11,9 +11,13 @@ import { motion } from 'motion/react';
import { Link } from '@tanstack/react-router';
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
import UpdateModal from '@/components/updateModal';
import { BackgroundSelector } from '@/components/backgroudModal';
import { useTranslation } from 'react-i18next';
export default function LauncherPage() {
const { gamePath,
const {
gamePath,
setGamePath,
setGameDir,
serverPath,
@@ -21,8 +25,9 @@ export default function LauncherPage() {
gameDir,
serverVersion,
proxyVersion,
background
} = useSettingStore()
const { t } = useTranslation()
const {
isOpenDownloadDataModal,
isOpenUpdateDataModal,
@@ -44,7 +49,7 @@ export default function LauncherPage() {
progressDownload,
downloadSpeed,
updateData,
launcherVersion,
// launcherVersion,
setLauncherVersion,
setIsLoading,
setDownloadType,
@@ -60,25 +65,19 @@ export default function LauncherPage() {
const widgetLinks = [
{
tooltip: "Firefly SRAnalysis",
href: "https://sranalysis.kain.id.vn/",
img: "https://sranalysis.kain.id.vn/ff-sranalysis.png",
href: "https://sranalysis.punklorde.org",
img: "https://sranalysis.punklorde.org/ff-sranalysis.png",
btnClass: "btn-primary"
},
{
tooltip: "Firefly SRTools",
href: "https://srtools.kain.id.vn/",
img: "https://srtools.kain.id.vn/ff-srtool.png",
href: "https://srtools.punklorde.org",
img: "https://srtools.punklorde.org/ff-srtool.png",
btnClass: "btn-secondary"
},
{
tooltip: "Amazing's SRTools (Original UI)",
href: "https://srtools.pages.dev/",
img: "https://icons.duckduckgo.com/ip3/srtools.pages.dev.ico",
btnClass: "btn-primary"
},
{
tooltip: "Amazing's SRTools (Modern UI)",
href: "https://srtools.neonteam.dev/",
tooltip: "Amazing's SRTools",
href: "https://srtools.neonteam.dev",
img: "https://icons.duckduckgo.com/ip3/srtools.neonteam.dev.ico",
btnClass: "btn-secondary"
}
@@ -158,24 +157,22 @@ export default function LauncherPage() {
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.')
toast.error(t("home.error_game_dir"))
} else {
setGamePath(basePath)
setGameDir(folderPath)
toast.success('Game path set successfully')
toast.success(t("home.game_path_success"))
}
} else {
toast.error('Not valid file type')
toast.error(t("home.error_file_type"))
}
} catch (err: any) {
toast.error('PickFolder error:', err)
toast.error(t("home.toast_pick_folder_error"), err)
} finally {
setIsLoading(false)
}
}
const handleStartGame = async () => {
if (!gamePath) {
return
@@ -186,40 +183,43 @@ export default function LauncherPage() {
try {
setIsLoading(true)
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
const resultProxy = await FSService.StartWithConsole(proxyPath)
const [resultProxy, error] = await FSService.StartWithConsole(proxyPath)
if (!resultProxy) {
toast.error('Failed to start proxy')
toast.error(t("home.toast_start_proxy_failed") + error)
return
}
setProxyRunning(true)
}
await sleep(500)
if (!serverRunning) {
const resultServer = await FSService.StartWithConsole(serverPath)
const [resultServer, error] = await FSService.StartWithConsole(serverPath)
if (!resultServer) {
toast.error('Failed to start server')
toast.error(t("home.toast_start_server_failed") + error)
return
}
setServerRunning(true)
}
await sleep(2000)
await sleep(1000)
if (gamePath.endsWith("launcher.exe")) {
const resultGame = await FSService.StartWithConsole(gamePath)
const [resultGame, error] = await FSService.StartWithConsole(gamePath)
if (!resultGame) {
toast.error('Failed to start game')
toast.error(t("home.toast_start_game_failed") + error)
return
}
} else {
const resultGame = await FSService.StartApp(gamePath)
const [resultGame, error] = await FSService.StartApp(gamePath)
if (!resultGame) {
toast.error('Failed to start game')
toast.error(t("home.toast_start_game_failed") + error)
return
}
}
setGameRunning(true)
} catch (err: any) {
toast.error('StartGame error:', err)
console.log(err)
toast.error(t("home.toast_start_game_error"), err)
} finally {
setIsLoading(false)
}
@@ -265,24 +265,24 @@ export default function LauncherPage() {
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"
<img
src={background}
alt="background"
className="fixed inset-0 w-full h-full object-cover z-0"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement
target.src = "bg-12.jpg"
}}
></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 className="hidden sm:flex fixed z-10 items-center justify-between py-6 px-4 ">
<div className="text-2xl font-bold text-white bg-gray-500/5 rounded-full p-1">{t("home.header_title")}</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="hidden sm:flex fixed top-1/4 right-4 z-10 flex-col space-y-3 bg-white/5 rounded-xl p-3 shadow-lg">
{widgetLinks.map((link, idx) => (
<div key={idx} className="tooltip tooltip-right" data-tip={link.tooltip}>
<div key={idx} className="tooltip tooltip-left" data-tip={link.tooltip}>
<a
className={`btn btn-circle ${link.btnClass}`}
target="_blank"
@@ -297,7 +297,7 @@ export default function LauncherPage() {
</div>
))}
<div className="tooltip tooltip-right" data-tip="How to use all tools & commands">
<div className="tooltip tooltip-left" data-tip={t("home.tooltip_how_to")}>
<Link
to="/howto"
className="btn btn-warning btn-circle"
@@ -307,10 +307,13 @@ export default function LauncherPage() {
</div>
</div>
<div className="hidden sm:flex fixed top-1/2 left-5 z-10 ">
<BackgroundSelector />
</div>
{/* Bottom Panel */}
{serverReady && proxyReady && !isDownloading && (
<div className="fixed bottom-0 right-0 p-8 z-10">
<div className="fixed bottom-0 right-0 p-8 z-2">
<div className="flex flex-wrap items-center justify-center gap-2">
{gamePath === "" ? (
<button
@@ -318,7 +321,7 @@ export default function LauncherPage() {
onClick={handlePickFile}
>
<FolderOpen className="w-6 h-6" />
{isLoading ? 'Selecting...' : 'Select Game file'}
{isLoading ? t("home.btn_selecting") : t("home.btn_select_game")}
</button>
) : (
<button
@@ -326,7 +329,7 @@ export default function LauncherPage() {
onClick={handleStartGame}
>
<Play className="w-6 h-6" />
{isLoading ? 'Selecting...' : gameRunning ? 'Game is running' : 'Start Game'}
{isLoading ? t("home.btn_selecting") : gameRunning ? t("home.btn_game_running") : t("home.btn_start_game")}
</button>
)}
@@ -335,7 +338,7 @@ export default function LauncherPage() {
<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={handlePickFile}>{t("home.menu_change_path")}</button></li>
<li>
<button
onClick={async () => {
@@ -355,29 +358,44 @@ export default function LauncherPage() {
setIsOpenUpdateDataModal(true)
return
}
toast.success("No updates available")
toast.success(t("home.no_updates"))
}}>
Check for Updates Server & Proxy
{t("home.menu_check_update")}
</button>
</li>
<li>
</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>
<li>
<button disabled={!serverPath} onClick={() => {
if (serverPath) {
FSService.OpenFolder("./server")
}
}}
>
{t("home.menu_open_server")}
</button>
</li>
<li>
<button disabled={!proxyPath} onClick={() => {
if (proxyPath) {
FSService.OpenFolder("./proxy")
}
}}
>
{t("home.menu_open_proxy")}
</button>
</li>
<li>
<button disabled={!gameDir} onClick={() => {
if (gameDir) {
FSService.OpenFolder(gameDir + "/StarRail_Data/Persistent/Audio/AudioPackage/Windows")
}
}}
>
{t("home.menu_open_voice")}
</button>
</li>
</ul>
</div>
@@ -392,7 +410,7 @@ export default function LauncherPage() {
|| !updateData.proxy.isExists
|| !updateData.server.isExists
) && (
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
<div className="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>
@@ -403,14 +421,14 @@ export default function LauncherPage() {
</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"
className="h-full bg-linear-to-r from-cyan-400 to-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progressDownload}%` }}
transition={{ type: "tween", ease: "linear", duration: 0.03 }}
/>
</div>
<div className="text-center text-xs text-white/60">
{progressDownload < 100 ? 'Please wait...' : 'Complete!'}
{progressDownload < 100 ? t("home.status_wait") : t("home.status_complete")}
</div>
</div>
</div>
@@ -429,16 +447,16 @@ export default function LauncherPage() {
: "text-red-200 text-xl"
}`}
>
{downloadType === "update:launcher:downloading" && "Updating launcher"}
{downloadType === "update:launcher:success" && "Launcher updated successfully, auto closing after 5s"}
{downloadType === "update:launcher:failed" && "Launcher update failed, auto closing after 5s"}
{downloadType === "update:launcher:downloading" && t("home.status_updating_launcher")}
{downloadType === "update:launcher:success" && t("home.status_update_success")}
{downloadType === "update:launcher:failed" && t("home.status_update_failed")}
<span className="dot-animation ml-1"></span>
</span>
</div>
)}
<div className="text-xs text-white/60">
{progressDownload < 100 ? "Please wait..." : "Complete!"}
{progressDownload < 100 ? t("home.status_wait") : t("home.status_complete")}
</div>
</div>
@@ -459,45 +477,41 @@ export default function LauncherPage() {
</div>
)}
{/* Version Info */}
{serverReady && proxyReady && !isDownloading && (
<div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-black/20 backdrop-blur-sm rounded-lg p-2 shadow">
<p className="text-primary">Version server: {serverVersion}</p>
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
<p className="mt-2 text-success">Version launcher: {launcherVersion}</p>
</div>
)}
<div className="hidden md:block fixed bottom-4 left-5 z-10">
<img src="/heart-hsr.gif" alt="firefly animation" className="rounded-lg w-24 h-24" />
</div>
{/* Modal */}
<UpdateModal
isOpen={isOpenUpdateDataModal}
onClose={() => setIsOpenUpdateDataModal(false)}
title="Update Data"
message="Do you want to update data server and proxy?"
title={t("home.modal_update_title")}
message={t("home.modal_update_msg")}
buttons={[
{ text: "No", onClick: () => setIsOpenUpdateDataModal(false), variant: "outline" },
{ text: "Yes", onClick: async () => { setIsOpenUpdateDataModal(false); await handlerUpdateData() }, variant: "primary" }
{ text: t("home.btn_no"), onClick: () => setIsOpenUpdateDataModal(false), variant: "outline" },
{ text: t("home.btn_yes"), onClick: async () => { setIsOpenUpdateDataModal(false); await handlerUpdateData() }, variant: "primary" }
]}
/>
<UpdateModal
isOpen={isOpenDownloadDataModal}
onClose={() => setIsOpenDownloadDataModal(false)}
title="Download Data"
message="Data server and proxy download required"
title={t("home.modal_download_title")}
message={t("home.modal_download_msg")}
buttons={[
{ text: "Download", onClick: async () => { setIsOpenDownloadDataModal(false); await handlerUpdateData() }, variant: "primary" }
{ text: t("home.btn_download"), onClick: async () => { setIsOpenDownloadDataModal(false); await handlerUpdateData() }, variant: "primary" }
]}
/>
<UpdateModal
isOpen={isOpenSelfUpdateModal}
onClose={() => setIsOpenSelfUpdateModal(false)}
title="Update Launcher"
message="Do you want to update launcher?"
title={t("home.modal_self_update_title")}
message={t("home.modal_self_update_msg")}
buttons={[
{ text: "No", onClick: () => setIsOpenSelfUpdateModal(false), variant: "outline" },
{ text: "Yes", onClick: async () => { setIsOpenSelfUpdateModal(false); await handlerUpdateData() }, variant: "primary" }
{ text: t("home.btn_no"), onClick: () => setIsOpenSelfUpdateModal(false), variant: "outline" },
{ text: t("home.btn_yes"), onClick: async () => { setIsOpenSelfUpdateModal(false); await handlerUpdateData() }, variant: "primary" }
]}
/>

View File

@@ -14,6 +14,10 @@ interface SettingState {
isMinimize: boolean;
isAsk: boolean;
}
background: string;
extraBackgrounds: string[];
setExtraBackgrounds: (newExtraBackgrounds: string[]) => void;
setBackground: (newBackground: string) => void;
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => void;
setLocale: (newLocale: string) => void;
setGamePath: (newGamePath: string) => void;
@@ -38,6 +42,10 @@ const useSettingStore = create<SettingState>()(
isMinimize: false,
isAsk: true,
},
background: "bg-12.jpg",
extraBackgrounds: [],
setExtraBackgrounds: (newExtraBackgrounds: string[]) => set({ extraBackgrounds: newExtraBackgrounds }),
setBackground: (newBackground: string) => set({ background: newBackground }),
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => set({ closingOption: newClosingOption }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
setGamePath: (newGamePath: string) => set({ gamePath: newGamePath }),

View File

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

View File

@@ -0,0 +1,26 @@
export default function getCroppedImg(imageSrc: string, crop: any): Promise<string> {
const image = new Image()
image.crossOrigin = "anonymous"
image.src = imageSrc
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
canvas.width = crop.width
canvas.height = crop.height
return new Promise((resolve) => {
image.onload = () => {
ctx.drawImage(
image,
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
crop.width,
crop.height
)
resolve(canvas.toDataURL('image/jpeg'))
}
})
}

View File

@@ -13,10 +13,9 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@bindings/*": ["bindings/*"],
"@/*": ["src/*"]
"@bindings/*": ["./bindings/*"],
"@/*": ["./src/*"]
},
/* Linting */
"strict": true,

View File

@@ -2,7 +2,6 @@ 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: '/',
@@ -20,9 +19,6 @@ export default defineConfig({
react(),
],
resolve: {
alias: {
'@bindings': path.resolve(__dirname, 'bindings'),
'@': path.resolve(__dirname, 'src'),
},
},
tsconfigPaths: true
}
})

57
go.mod
View File

@@ -1,55 +1,54 @@
module firefly-launcher
go 1.25
go 1.26.1
require (
github.com/klauspost/compress v1.18.0
github.com/klauspost/compress v1.18.5
github.com/minio/selfupdate v0.6.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.34
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/sys v0.31.0
google.golang.org/protobuf v1.33.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.74
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/sys v0.42.0
google.golang.org/protobuf v1.36.11
)
require (
aead.dev/minisign v0.2.0 // indirect
dario.cat/mergo v1.0.1 // indirect
aead.dev/minisign v0.3.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/go-git/go-git/v5 v5.17.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.7 // indirect
github.com/lmittmann/tint v1.1.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/text v0.35.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

126
go.sum
View File

@@ -1,12 +1,13 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA=
aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -15,33 +16,37 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -50,12 +55,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -67,8 +74,8 @@ github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
@@ -80,8 +87,8 @@ github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDw
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -91,41 +98,38 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.34 h1:t6NwHOLJzXuESb3YSXarSd1C/U1h2CpXF+BLr0ekj2g=
github.com/wailsapp/wails/v3 v3.0.0-alpha.34/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw=
github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -137,25 +141,23 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -26,7 +26,7 @@ func (a *AppService) CloseApp() (bool, string) {
return true, ""
}
func (a *AppService) MinimizeApp() (bool, string) {
func (a *AppService) HideApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
@@ -35,3 +35,29 @@ func (a *AppService) MinimizeApp() (bool, string) {
return true, ""
}
func (a *AppService) MinimizeApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Minimise()
return true, ""
}
func (a *AppService) MaximizeApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Maximise()
return true, ""
}
func (a *AppService) RestoreApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Restore()
return true, ""
}

View File

@@ -22,7 +22,7 @@ func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
return true, "hdifffiles.txt", ""
}
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.json"); err == nil && ok {
return true, "hdifffiles.json", ""
}
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json"); err == nil && ok {
@@ -68,9 +68,11 @@ func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string)
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff", constant.TempUrl); err != nil {
return false, err.Error()
}
patchBinFile := filepath.Join(constant.TempUrl, "BinaryVersion.bytes.hdiff")
sourceBinFile := oldBinPath
tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
if err := hpatchz.ApplyPatch(sourceBinFile, patchBinFile, tempBinFile); err != nil {
os.Remove(patchBinFile)
return false, err.Error()
@@ -172,9 +174,11 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
if err != nil {
return false, err.Error()
}
if err := json.Unmarshal(data, &jsonData); err != nil {
var hdiffJson []*models.HDiffData
if err := json.Unmarshal(data, &hdiffJson); err != nil {
return false, err.Error()
}
jsonData.DiffMap = append(jsonData.DiffMap, hdiffJson...)
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
files, err := models.LoadHDiffFiles(hdiffFilesPath)
if err != nil {
@@ -188,6 +192,7 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
}
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
for i, entry := range jsonData.DiffMap {
application.Get().Event.Emit(
"diff:progress", map[string]int{

View File

@@ -2,12 +2,12 @@ package fsService
import (
"firefly-launcher/pkg/sevenzip"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
"golang.org/x/sys/windows"
)
@@ -15,7 +15,7 @@ import (
type FSService struct{}
func (f *FSService) PickFolder() (string, error) {
dialog := application.OpenFileDialog().
dialog := application.Get().Dialog.OpenFile().
CanChooseDirectories(true).
CanCreateDirectories(true).
ResolvesAliases(true)
@@ -31,7 +31,7 @@ func (f *FSService) PickFolder() (string, error) {
}
func (f *FSService) PickFile(filter string) (string, error) {
dialog := application.OpenFileDialog().
dialog := application.Get().Dialog.OpenFile().
CanChooseFiles(true).
ResolvesAliases(true)
if runtime.GOOS == "darwin" {
@@ -63,12 +63,28 @@ func (f *FSService) FileExists(path string) bool {
return false
}
func (f *FSService) StartApp(path string) (bool, error) {
func (f *FSService) GetDir(path string) string {
return filepath.Dir(path)
}
func (f *FSService) Join(paths ...string) string {
return filepath.Join(paths...)
}
func (f *FSService) RemoveFile(path string) error {
return os.Remove(path)
}
func (f *FSService) StartApp(path string) (bool, string) {
dir := filepath.Dir(path)
cmd := exec.Command(path)
cmd.Dir = dir
err := cmd.Start()
if err != nil {
return false, err
return false, err.Error()
}
if strings.HasSuffix(path, "StarRail.exe") {
go func() {
_ = cmd.Wait()
@@ -76,17 +92,17 @@ func (f *FSService) StartApp(path string) (bool, error) {
}()
}
return true, nil
return true, ""
}
func (f *FSService) StartWithConsole(path string) (bool, error) {
func (f *FSService) StartWithConsole(path string) (bool, string) {
absPath, err := filepath.Abs(path)
if err != nil {
return false, err
return false, err.Error()
}
if _, err := os.Stat(absPath); os.IsNotExist(err) {
return false, fmt.Errorf("file not found: %s", absPath)
return false, "file not found: " + absPath
}
cmd := exec.Command(absPath)
cmd.Dir = filepath.Dir(absPath)
@@ -103,7 +119,7 @@ func (f *FSService) StartWithConsole(path string) (bool, error) {
err = cmd.Start()
if err != nil {
return false, err
return false, err.Error()
}
go func() {
@@ -112,11 +128,11 @@ func (f *FSService) StartWithConsole(path string) (bool, error) {
application.Get().Event.Emit("game:exit")
} else if strings.HasSuffix(path, "firefly-go_win.exe") {
application.Get().Event.Emit("server:exit")
} else if strings.HasSuffix(path, "FireflyProxy.exe") {
} else if strings.HasSuffix(path, "firefly-go-proxy.exe") {
application.Get().Event.Emit("proxy:exit")
}
}()
return true, nil
return true, ""
}
func (f *FSService) OpenFolder(path string) (bool, string) {
@@ -142,4 +158,3 @@ func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, strin
}
return exists, ""
}

View File

@@ -37,7 +37,7 @@ func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
}
func (g *GitService) DownloadProxyProgress(version string) (bool, string) {
asset, ok := g.getReleaseAsset(version, constant.ProxyGitUrl, constant.ProxyZipFile)
asset, ok := g.getReleaseAsset(version, constant.ProxyGitUrl, constant.ProxyFile)
if !ok {
return false, "no release found"
}
@@ -65,7 +65,3 @@ func (g *GitService) DownloadProxyProgress(version string) (bool, string) {
return false, "failed to rename tmp file after retries"
}
func (g *GitService) UnzipProxy() {
g.unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile))
}

12
main.go
View File

@@ -68,7 +68,7 @@ func main() {
// Create application
app := application.New(application.Options{
Name: "firefly-launcher",
Description: "Firefly Launcher - Kain",
Description: "Firefly Launcher - Firefly Shelter",
Services: []application.Service{
application.NewService(&fsService.FSService{}),
application.NewService(&languageService.LanguageService{}),
@@ -86,17 +86,19 @@ func main() {
// Create window
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Firefly Launcher - Kain",
Title: "Firefly Launcher - Firefly Shelter",
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset,
},
BackgroundColour: application.NewRGB(27, 38, 54),
Width: 1200,
Height: 600,
Width: 1280,
Height: 720,
URL: "/",
DevToolsEnabled: true,
Frameless: true,
DisableResize: true,
})
iconBytes, _ := tools.ReadFile("assets/appicon.png")
@@ -115,8 +117,6 @@ func main() {
systemTray.SetMenu(menu)
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Event.Emit("window:close")
e.Cancel()

View File

@@ -1,16 +1,16 @@
package constant
const ProxyGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Proxy/releases"
const ProxyGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases"
const ServerGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Local_Archive/releases"
const LauncherGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Launcher/releases"
const ServerStorageUrl = "./server"
const ProxyStorageUrl = "./proxy"
const ServerZipFile = "prebuild_win_x86.zip"
const ProxyZipFile = "64bit.zip"
const ProxyFile = "firefly-go-proxy.exe"
const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp"
const CurrentLauncherVersion = "2.1.0"
const CurrentLauncherVersion = "2.5.1"
type ToolFile string