9 Commits
1.0 ... 1.4.1

Author SHA1 Message Date
99b9df1ce5 UPDATE: update to go 1.25 2025-08-24 17:22:30 +07:00
b2adcd7981 UPDATE: update new language patch 2025-08-21 21:44:16 +07:00
ba58d24e06 FIX: fix bug constant 2025-08-19 22:31:20 +07:00
ec72b812de UPDATE: native with new hdiff type 2025-08-19 20:01:31 +07:00
448ced1260 FIX: fix admin require 2025-08-08 12:26:45 +07:00
d4c75b341f UPDATE: update icon 2025-07-26 22:19:47 +07:00
fb1cd82434 FIX: fix bug about checking version 2025-07-26 22:07:28 +07:00
461dfd93ba UPDATE: Add self update 2025-07-26 20:59:35 +07:00
95f8ed357d add skip check 2025-07-22 14:07:33 +07:00
57 changed files with 2873 additions and 2140 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -6,7 +6,7 @@
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Firefly Launcher</string> <string>Firefly Launcher</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Firefly Launcher</string> <string>firefly-launcher</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.fireflyshelter.fireflylauncher</string> <string>com.fireflyshelter.fireflylauncher</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>

View File

@@ -6,7 +6,7 @@
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Firefly Launcher</string> <string>Firefly Launcher</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Firefly Launcher</string> <string>firefly-launcher</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.fireflyshelter.fireflylauncher</string> <string>com.fireflyshelter.fireflylauncher</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>

Binary file not shown.

View File

@@ -3,7 +3,7 @@
# #
# The lines below are called `modelines`. See `:help modeline` # The lines below are called `modelines`. See `:help modeline`
name: "Firefly Launcher" name: "firefly-launcher"
arch: ${GOARCH} arch: ${GOARCH}
platform: "linux" platform: "linux"
version: "0.1.0" version: "0.1.0"
@@ -17,12 +17,12 @@ license: "MIT"
release: "1" release: "1"
contents: contents:
- src: "./bin/Firefly Launcher" - src: "./bin/firefly-launcher"
dst: "/usr/local/bin/Firefly Launcher" dst: "/usr/local/bin/firefly-launcher"
- src: "./build/appicon.png" - src: "./build/appicon.png"
dst: "/usr/share/icons/hicolor/128x128/apps/Firefly Launcher.png" dst: "/usr/share/icons/hicolor/128x128/apps/firefly-launcher.png"
- src: "./build/linux/Firefly Launcher.desktop" - src: "./build/linux/firefly-launcher.desktop"
dst: "/usr/share/applications/Firefly Launcher.desktop" dst: "/usr/share/applications/firefly-launcher.desktop"
depends: depends:
- gtk3 - gtk3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="0.1.0" processorArchitecture="*"/> <assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="0.1.0" processorArchitecture="*"/>
<dependency> <dependency>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly> </dependentAssembly>
</dependency> </dependency>
<!-- Yêu cầu quyền admin -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security> <security>
<requestedPrivileges> <requestedPrivileges>

View File

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

View File

@@ -25,20 +25,26 @@ export function DownloadServerProgress(version) {
} }
/** /**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/ */
export function GetLatestProxyVersion(oldVersion) { export function GetLatestLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(1462362449, oldVersion)); let $resultPromise = /** @type {any} */($Call.ByID(3191972855));
return $resultPromise; return $resultPromise;
} }
/** /**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/ */
export function GetLatestServerVersion(oldVersion) { export function GetLatestProxyVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(1447982978, oldVersion)); let $resultPromise = /** @type {any} */($Call.ByID(1462362449));
return $resultPromise;
}
/**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function GetLatestServerVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(1447982978));
return $resultPromise; return $resultPromise;
} }
@@ -57,3 +63,12 @@ export function UnzipServer() {
let $resultPromise = /** @type {any} */($Call.ByID(4110296071)); let $resultPromise = /** @type {any} */($Call.ByID(4110296071));
return $resultPromise; return $resultPromise;
} }
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function UpdateLauncherProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(2648938152, version));
return $resultPromise;
}

View File

@@ -6,6 +6,15 @@
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {string} patchPath
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function CheckTypeHDiff(patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(1068035136, patchPath));
return $resultPromise;
}
/** /**
* @param {string} gamePath * @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {Promise<[boolean, string]> & { cancel(): void }}
@@ -18,10 +27,11 @@ export function CutData(gamePath) {
/** /**
* @param {string} gamePath * @param {string} gamePath
* @param {string} patchPath * @param {string} patchPath
* @param {boolean} isSkipVerify
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {Promise<[boolean, string]> & { cancel(): void }}
*/ */
export function DataExtract(gamePath, patchPath) { export function DataExtract(gamePath, patchPath, isSkipVerify) {
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath)); let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath, isSkipVerify));
return $resultPromise; return $resultPromise;
} }

View File

@@ -2,11 +2,13 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import * as AppService from "./appservice.js";
import * as FSService from "./fsservice.js"; import * as FSService from "./fsservice.js";
import * as GitService from "./gitservice.js"; import * as GitService from "./gitservice.js";
import * as HdiffzService from "./hdiffzservice.js"; import * as HdiffzService from "./hdiffzservice.js";
import * as LanguageService from "./languageservice.js"; import * as LanguageService from "./languageservice.js";
export { export {
AppService,
FSService, FSService,
GitService, GitService,
HdiffzService, HdiffzService,

View File

@@ -8,7 +8,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<[string, string]> & { cancel(): void }} * @returns {Promise<[boolean, string, string, string]> & { cancel(): void }}
*/ */
export function GetLanguage(path) { export function GetLanguage(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path)); let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path));
@@ -19,7 +19,7 @@ export function GetLanguage(path) {
* @param {string} path * @param {string} path
* @param {string} text * @param {string} text
* @param {string} voice * @param {string} voice
* @returns {Promise<boolean> & { cancel(): void }} * @returns {Promise<[boolean, string]> & { cancel(): void }}
*/ */
export function SetLanguage(path, text, voice) { export function SetLanguage(path, text, voice) {
let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice)); let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice));

File diff suppressed because it is too large Load Diff

View File

@@ -11,33 +11,33 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.12",
"@tanstack/react-router": "^1.124.0", "@tanstack/react-router": "^1.131.27",
"@tanstack/react-router-devtools": "^1.124.0", "@tanstack/react-router-devtools": "^1.131.27",
"lucide-react": "^0.525.0", "lucide-react": "^0.541.0",
"motion": "^12.23.0", "motion": "^12.23.12",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"react": "^18.2.0", "react": "^19.1.1",
"react-dom": "^18.2.0", "react-dom": "^19.1.1",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.12",
"zustand": "^5.0.6" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/router-plugin": "^1.124.0", "@tanstack/router-plugin": "^1.131.27",
"@types/node": "^24.0.10", "@types/node": "^24.3.0",
"@types/path-browserify": "^1.0.3", "@types/path-browserify": "^1.0.3",
"@types/react": "^18.2.43", "@types/react": "^19.1.11",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^8.40.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^5.0.1",
"@wailsio/runtime": "^3.0.0-alpha.66", "@wailsio/runtime": "^3.0.0-alpha.66",
"daisyui": "^5.0.43", "daisyui": "^5.0.50",
"eslint": "^8.55.0", "eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.20",
"typescript": "^5.2.2", "typescript": "^5.9.2",
"vite": "^5.0.8" "vite": "^7.1.3"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,4 @@
export * from "./server"
export * from "./proxy"
export * from "./launcher"
export * from "./sleep"

View File

@@ -0,0 +1,37 @@
import useLauncherStore from "@/stores/launcherStore";
import { AppService, GitService } from "@bindings/firefly-launcher/internal";
import { toast } from "react-toastify";
import { sleep } from "./sleep";
export async function CheckUpdateLauncher(): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
const [currentOk, currentVersion] = await AppService.GetCurrentLauncherVersion()
if (!currentOk) {
toast.error("Launcher error: cannot get current version")
return { isUpdate: false, isExists: true, version: "" }
}
const [latestOk, latestVersion, latestError] = await GitService.GetLatestLauncherVersion()
if (!latestOk) {
toast.error("Launcher error: " + latestError)
return { isUpdate: false, isExists: true, version: currentVersion }
}
const isUpdate = latestVersion !== currentVersion
return { isUpdate, isExists: true, version: latestVersion }
}
export async function UpdateLauncher(launcherVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
setDownloadType("update:launcher:downloading")
const [ok, error] = await GitService.UpdateLauncherProgress(launcherVersion)
if (ok) {
setDownloadType("update:launcher:success")
} else {
toast.error(error)
setDownloadType("update:launcher:failed")
}
AppService.CloseAppAfterTimeout(5)
await sleep(5000)
}

View File

@@ -0,0 +1,34 @@
import useLauncherStore from "@/stores/launcherStore";
import useSettingStore from "@/stores/settingStore";
import { FSService, GitService } from "@bindings/firefly-launcher/internal";
import { toast } from "react-toastify";
export async function CheckUpdateProxy(proxyPath: string, proxyVersion: string) : Promise<{isUpdate: boolean, isExists: boolean, version: string}> {
const [ok, latestVersion, error] = await GitService.GetLatestProxyVersion()
const isExists = await FSService.FileExists(proxyPath)
if (!ok) {
toast.error("Proxy error: " + error)
return { isUpdate: false, isExists, version: "" }
}
const isUpdate = latestVersion !== proxyVersion
return { isUpdate, isExists, version: latestVersion }
}
export async function UpdateProxy(proxyVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
const {setProxyPath, setProxyVersion} = useSettingStore.getState()
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")
} else {
toast.error(error)
setDownloadType("Download proxy failed")
}
}

View File

@@ -0,0 +1,38 @@
import useLauncherStore from '@/stores/launcherStore';
import useSettingStore from '@/stores/settingStore';
import { FSService, GitService } from '@bindings/firefly-launcher/internal';
import { toast } from 'react-toastify';
export async function CheckUpdateServer(
serverPath: string,
serverVersion: string
): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
const [ok, latestVersion, error] = await GitService.GetLatestServerVersion()
const isExists = await FSService.FileExists(serverPath)
if (!ok) {
toast.error("Server error: " + error)
return { isUpdate: false, isExists, version: "" }
}
const isUpdate = latestVersion !== serverVersion
return { isUpdate, isExists, version: latestVersion }
}
export async function UpdateServer(serverVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
const {setServerPath, setServerVersion} = useSettingStore.getState()
setDownloadType("Downloading server...")
const [ok, error] = await GitService.DownloadServerProgress(serverVersion)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Download server successfully")
setServerVersion(serverVersion)
setServerPath("./server/firefly-go_win.exe")
} else {
toast.error(error)
setDownloadType("Download server failed")
}
}

View File

@@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,6 +1,7 @@
// useGlobalEvents.ts // useGlobalEvents.ts
import { useEffect } from "react"; import { useEffect } from "react";
import { Events } from "@wailsio/runtime"; import { Events } from "@wailsio/runtime";
import { toast } from "react-toastify";
export function useGlobalEvents({ export function useGlobalEvents({
setGameRunning, setGameRunning,
@@ -51,6 +52,10 @@ export function useGlobalEvents({
Events.On("proxy:exit", onProxyExit); Events.On("proxy:exit", onProxyExit);
Events.On("hdiffz:progress", onUpdateProgress); Events.On("hdiffz:progress", onUpdateProgress);
Events.On("hdiffz:message", onMessageUpdate); Events.On("hdiffz:message", onMessageUpdate);
Events.On("hdiffz:error", (event: any) => {
const { message } = event.data[0];
toast.error(message);
});
return () => { return () => {
Events.Off("download:server"); Events.Off("download:server");
@@ -60,6 +65,7 @@ export function useGlobalEvents({
Events.Off("proxy:exit"); Events.Off("proxy:exit");
Events.Off("hdiffz:progress"); Events.Off("hdiffz:progress");
Events.Off("hdiffz:message"); Events.Off("hdiffz:message");
Events.Off("version:check");
}; };
}, []); }, []);
} }

View File

@@ -14,7 +14,7 @@ export default function AboutPage() {
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. I created a lightweight and modern <span className="font-semibold text-success">Game Launcher</span> to help users easily launch and manage their games with better performance and simplicity.
</p> </p>
<p className="text-lg leading-relaxed"> <p className="text-lg leading-relaxed">
The launcher is built using <span className="font-mono text-info">Go + Wails</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>. 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>.
</p> </p>
<p className="text-lg leading-relaxed"> <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. My goal is to make tools that are fast, efficient, and enjoyable to use and this launcher is just the beginning.

View File

@@ -78,12 +78,12 @@ export default function AnalysisPage() {
<span className="font-semibold text-blue-800">Backup Website</span> <span className="font-semibold text-blue-800">Backup Website</span>
</div> </div>
<a <a
href="https://sr-analysis.vercel.app/" href="https://firefly-sranalysis.vercel.app/"
className="link link-warning font-mono text-sm break-all" className="link link-warning font-mono text-sm break-all"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
https://sr-analysis.vercel.app/ https://firefly-sranalysis.vercel.app/
</a> </a>
</div> </div>
</div> </div>

View File

@@ -116,6 +116,18 @@ export default function HdiffzPage() {
setIsDiffLoading(false) setIsDiffLoading(false)
return return
} }
setStageType('Check Type HDiff')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [isOk, validType, errorType] = await HdiffzService.CheckTypeHDiff(diffDir)
if (!isOk) {
toast.error(errorType)
setIsDiffLoading(false)
return
}
setProgressUpdate(1)
if (validType === 'hdiffmap.json') {
setStageType('Version Validate') setStageType('Version Validate')
setProgressUpdate(0) setProgressUpdate(0)
setMaxProgressUpdate(1) setMaxProgressUpdate(1)
@@ -126,9 +138,10 @@ export default function HdiffzPage() {
return return
} }
setProgressUpdate(1) setProgressUpdate(1)
}
setStageType('Data Extract') setStageType('Data Extract')
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir) const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir, validType === 'hdifffiles.txt')
if (!validData) { if (!validData) {
toast.error(errorData) toast.error(errorData)
setIsDiffLoading(false) setIsDiffLoading(false)

View File

@@ -27,28 +27,42 @@ export default function LanguagePage() {
useEffect(() => { useEffect(() => {
const getLanguage = async () => { const getLanguage = async () => {
if (gameDir) { if (!gameDir) return
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
const subPath = "StarRail_Data/StreamingAssets"
const fullPath = `${gameDir}/${subPath}` const fullPath = `${gameDir}/${subPath}`
const exists = await FSService.DirExists(fullPath) const exists = await FSService.DirExists(fullPath)
if (exists) { if (!exists) {
const [textLang, voiceLang] = await LanguageService.GetLanguage(fullPath) setTextLang("")
setVoiceLang("")
setSelectedTextLang("")
setSelectedVoiceLang("")
setFolderCheckResult("error")
setGameDir("")
return
}
const [ok, textLang, voiceLang, err] = await LanguageService.GetLanguage(fullPath)
if (!ok) {
setTextLang("")
setVoiceLang("")
setSelectedTextLang("")
setSelectedVoiceLang("")
setFolderCheckResult("error")
setGameDir("")
toast.error(err)
return
}
// success
setTextLang(textLang) setTextLang(textLang)
setVoiceLang(voiceLang) setVoiceLang(voiceLang)
setFolderCheckResult('success') setFolderCheckResult("success")
setSelectedTextLang(textLang) setSelectedTextLang(textLang)
setSelectedVoiceLang(voiceLang) setSelectedVoiceLang(voiceLang)
} else {
setTextLang('')
setVoiceLang('')
setSelectedTextLang('')
setSelectedVoiceLang('')
setFolderCheckResult('error')
setGameDir('')
}
}
} }
getLanguage() getLanguage()
}, [gameDir]) }, [gameDir])
@@ -86,18 +100,18 @@ export default function LanguagePage() {
} }
try { try {
setIsSettingLanguage(true) setIsSettingLanguage(true)
const result = await LanguageService.SetLanguage( const [ok, err] = await LanguageService.SetLanguage(
`${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`, `${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`,
selectedTextLang, selectedTextLang,
selectedVoiceLang selectedVoiceLang
) )
if (result) { if (ok) {
toast.success('Language set successfully') toast.success('Language set successfully')
setTextLang(selectedTextLang) setTextLang(selectedTextLang)
setVoiceLang(selectedVoiceLang) setVoiceLang(selectedVoiceLang)
} }
else { else {
toast.error('Language set failed') toast.error(err)
} }
} catch (err: any) { } catch (err: any) {

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react'; import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { FSService, GitService } from '@bindings/firefly-launcher/internal'; import { FSService, AppService } from '@bindings/firefly-launcher/internal';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import path from 'path-browserify' import path from 'path-browserify'
import useSettingStore from '@/stores/settingStore'; import useSettingStore from '@/stores/settingStore';
@@ -8,6 +8,7 @@ import useModalStore from '@/stores/modalStore';
import useLauncherStore from '@/stores/launcherStore'; import useLauncherStore from '@/stores/launcherStore';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
export default function LauncherPage() { export default function LauncherPage() {
const { gamePath, const { gamePath,
@@ -18,15 +19,17 @@ export default function LauncherPage() {
gameDir, gameDir,
serverVersion, serverVersion,
proxyVersion, proxyVersion,
setServerVersion,
setProxyVersion,
setServerPath,
setProxyPath
} = useSettingStore() } = useSettingStore()
const { isOpenNotification, setIsOpenNotification } = useModalStore() const {
isOpenDownloadDataModal,
isOpenUpdateDataModal,
isOpenSelfUpdateModal,
setIsOpenDownloadDataModal,
setIsOpenUpdateDataModal,
setIsOpenSelfUpdateModal
} = useModalStore()
const { const {
isLoading, isLoading,
modelName,
downloadType, downloadType,
serverReady, serverReady,
proxyReady, proxyReady,
@@ -36,8 +39,10 @@ export default function LauncherPage() {
gameRunning, gameRunning,
progressDownload, progressDownload,
downloadSpeed, downloadSpeed,
updateData,
launcherVersion,
setLauncherVersion,
setIsLoading, setIsLoading,
setModelName,
setDownloadType, setDownloadType,
setServerReady, setServerReady,
setProxyReady, setProxyReady,
@@ -45,12 +50,12 @@ export default function LauncherPage() {
setServerRunning, setServerRunning,
setProxyRunning, setProxyRunning,
setGameRunning, setGameRunning,
setUpdateData,
} = useLauncherStore() } = useLauncherStore()
useEffect(() => { useEffect(() => {
const check = async () => { const check = async () => {
if (!serverPath || !proxyPath || !serverVersion || !proxyVersion) { if (!serverVersion || !proxyVersion) {
setServerReady(false) setServerReady(false)
setProxyReady(false) setProxyReady(false)
return return
@@ -67,30 +72,47 @@ export default function LauncherPage() {
useEffect(() => { useEffect(() => {
const checkStartUp = async (): Promise<void> => { const checkStartUp = async (): Promise<void> => {
let isExists = false const [_, version] = await AppService.GetCurrentLauncherVersion()
if (serverPath && proxyPath && serverVersion && proxyVersion) { setLauncherVersion(version)
isExists = true const launcherData = await CheckUpdateLauncher()
setServerReady(true) if (launcherData.isUpdate) {
setProxyReady(true) setUpdateData({
server: { isUpdate: false, isExists: false, version: "" },
proxy: { isUpdate: false, isExists: false, version: "" },
launcher: launcherData
})
setIsOpenSelfUpdateModal(true)
return
} }
const dataUpdate = await handlerCheckUpdate() const serverData = await CheckUpdateServer(serverPath, serverVersion)
const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
setUpdateData({
server: serverData,
proxy: proxyData,
launcher: launcherData
})
const exitGame = await FSService.FileExists(gamePath) const exitGame = await FSService.FileExists(gamePath)
if (!exitGame) { if (!exitGame) {
setGameRunning(false) setGameRunning(false)
setGamePath("") setGamePath("")
setGameDir("") setGameDir("")
} }
if (!dataUpdate.server.isUpdate && !dataUpdate.proxy.isUpdate) {
setServerReady(true) if (!serverData.isExists || !proxyData.isExists) {
setProxyReady(true)
return
}
if (!isExists) {
setServerReady(false) setServerReady(false)
setProxyReady(false) setProxyReady(false)
setIsOpenDownloadDataModal(true)
return
} }
setModelName(!isExists ? "Download Data" : "Update Data")
setIsOpenNotification(true) if (serverData.isUpdate || proxyData.isUpdate) {
setServerReady(true)
setProxyReady(true)
setIsOpenUpdateDataModal(true)
return
}
setServerReady(true)
setProxyReady(true)
} }
checkStartUp() checkStartUp()
}, []); }, []);
@@ -121,9 +143,7 @@ export default function LauncherPage() {
} }
} }
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const handleStartGame = async () => { const handleStartGame = async () => {
if (!gamePath) { if (!gamePath) {
@@ -174,84 +194,42 @@ export default function LauncherPage() {
} }
} }
const handlerCheckUpdate = async (): Promise<Record<string, { isUpdate: boolean, version: string }>> => {
let isUpdateServer = false
let isUpdateProxy = false
const [serverOk, serverNewVersion, serverError] = await GitService.GetLatestServerVersion(serverVersion)
const serverExists = await FSService.FileExists(serverPath)
if (serverOk) {
if (serverNewVersion !== serverVersion || !serverExists) {
isUpdateServer = true
}
} else {
toast.error("Server error: " + serverError)
}
const [proxyOk, proxyNewVersion, proxyError] = await GitService.GetLatestProxyVersion(proxyVersion)
const proxyExists = await FSService.FileExists(proxyPath)
if (proxyOk) {
if (proxyNewVersion !== proxyVersion || !proxyExists) {
isUpdateProxy = true
}
} else {
toast.error("Proxy error: " + proxyError)
}
return { server: { isUpdate: isUpdateServer, version: serverNewVersion }, proxy: { isUpdate: isUpdateProxy, version: proxyNewVersion } }
}
const handlerUpdateServer = async (updateInfo: Record<string, { isUpdate: boolean, version: string }>) => { const handlerUpdateData = async () => {
setIsDownloading(true) setIsDownloading(true)
for (const [key, value] of Object.entries(updateInfo)) { if (updateData.launcher.isUpdate) {
if (value.isUpdate) { await UpdateLauncher(updateData.launcher.version)
if (key === "server") { setUpdateData({...updateData, launcher: { isUpdate: false, isExists: true, version: updateData.launcher.version }})
setDownloadType("Downloading server...") setIsOpenSelfUpdateModal(true)
const [ok, error] = await GitService.DownloadServerProgress(value.version)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Download server successfully")
setServerVersion(value.version)
setServerPath("./server/firefly-go_win.exe")
} else {
toast.error(error)
setDownloadType("Download server failed")
} }
} else if (key === "proxy") { if (updateData.server.isUpdate || !updateData.server.isExists) {
setDownloadType("Downloading proxy...") await UpdateServer(updateData.server.version)
const [ok, error] = await GitService.DownloadProxyProgress(value.version) setServerReady(true)
if (ok) { setUpdateData({...updateData, server: { isUpdate: false, isExists: true, version: updateData.server.version }})
setDownloadType("Unzipping proxy...")
GitService.UnzipProxy()
setDownloadType("Download proxy successfully")
setProxyVersion(value.version)
setProxyPath("./proxy/FireflyProxy.exe")
} else {
toast.error(error)
setDownloadType("Download proxy failed")
} }
if (updateData.proxy.isUpdate || !updateData.proxy.isExists) {
await UpdateProxy(updateData.proxy.version)
setProxyReady(true)
setUpdateData({...updateData, proxy: { isUpdate: false, isExists: true, version: updateData.proxy.version }})
} }
}
}
setDownloadType("") setDownloadType("")
setIsDownloading(false) setIsDownloading(false)
setServerReady(true)
setProxyReady(true)
} }
// Handle ESC key to close modal // Handle ESC key to close modal
useEffect(() => { useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => { const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
setIsOpenNotification(false); setIsOpenDownloadDataModal(false);
setIsOpenUpdateDataModal(false);
setIsOpenSelfUpdateModal(false);
} }
}; };
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenNotification]); }, [isOpenDownloadDataModal, isOpenUpdateDataModal, isOpenSelfUpdateModal]);
return ( return (
@@ -280,11 +258,10 @@ export default function LauncherPage() {
href="https://sranalysis.kain.id.vn/" href="https://sranalysis.kain.id.vn/"
> >
<img <img
src="https://icons.duckduckgo.com/ip3/sranalysis.kain.id.vn.ico" src="https://sranalysis.kain.id.vn/ff-sranalysis.png"
alt="SRAnalysis Logo" alt="SRAnalysis Logo"
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
</a> </a>
</div> </div>
@@ -295,7 +272,7 @@ export default function LauncherPage() {
href="https://srtools.kain.id.vn/" href="https://srtools.kain.id.vn/"
> >
<img <img
src="https://icons.duckduckgo.com/ip3/srtools.kain.id.vn.ico" src="https://srtools.kain.id.vn/ff-srtool.png"
alt="SRTools Logo" alt="SRTools Logo"
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
@@ -369,13 +346,27 @@ export default function LauncherPage() {
<li><button onClick={handlePickFile}>Change Game Path</button></li> <li><button onClick={handlePickFile}>Change Game Path</button></li>
<li><button <li><button
onClick={async () => { onClick={async () => {
const updateInfo = await handlerCheckUpdate() const serverData = await CheckUpdateServer(serverPath, serverVersion)
if (updateInfo.server.isUpdate || updateInfo.proxy.isUpdate) { const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
setModelName("Update Data") const launcherData = await CheckUpdateLauncher()
setIsOpenNotification(true) setUpdateData({
} else { server: serverData,
toast.success("No updates available") proxy: proxyData,
launcher: launcherData
})
if (launcherData.isUpdate) {
setIsOpenSelfUpdateModal(true)
return
} }
if (!serverData.isExists || !proxyData.isExists) {
setIsOpenDownloadDataModal(true)
return
}
if (serverData.isUpdate || proxyData.isUpdate) {
setIsOpenUpdateDataModal(true)
return
}
toast.success("No updates available")
}}> }}>
Check for Updates Check for Updates
</button></li> </button></li>
@@ -403,6 +394,11 @@ export default function LauncherPage() {
{/* Downloading */} {/* Downloading */}
{isDownloading && ( {isDownloading && (
updateData.proxy.isUpdate
|| updateData.server.isUpdate
|| !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="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80"> <div className="flex justify-center items-center text-sm text-white/80">
@@ -426,64 +422,185 @@ export default function LauncherPage() {
</div> </div>
</div> </div>
)} )}
{isDownloading && updateData.launcher.isUpdate && (
<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 text-sm text-white/80 text-center">
{["update:launcher:downloading", "update:launcher:success", "update:launcher:failed"].includes(downloadType) && (
<div className="flex justify-center items-center gap-4 ml-4">
<span
className={`font-bold ${downloadType === "update:launcher:downloading"
? "text-yellow-200 text-2xl"
: downloadType === "update:launcher:success"
? "text-emerald-200 text-xl"
: "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"}
<span className="dot-animation ml-1"></span>
</span>
</div>
)}
<div className="text-xs text-white/60">
{progressDownload < 100 ? "Please wait..." : "Complete!"}
</div>
</div>
<style>{`
.dot-animation::after {
content: '';
animation: dots 1.2s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
`}
</style>
</div>
)}
{/* Version Info */} {/* Version Info */}
{serverReady && proxyReady && !isDownloading && ( {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"> <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="text-primary">Version server: {serverVersion}</p>
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p> <p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
<p className="mt-2 text-success">Version launcher: {launcherVersion}</p>
</div> </div>
)} )}
{/* Modal */} {/* Modal */}
{isOpenNotification && ( {isOpenUpdateDataModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
{modelName !== "Download Data" && (
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="btn btn-circle btn-md btn-error absolute right-3 top-3" className="btn btn-circle btn-md btn-error absolute right-3 top-3"
onClick={() => setIsOpenNotification(false)} onClick={() => setIsOpenUpdateDataModal(false)}
> >
</motion.button> </motion.button>
)}
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <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-gradient-to-r from-pink-400 to-cyan-400">
{modelName} Update Data
</h3> </h3>
</div> </div>
<div className="px-6 pb-6"> <div className="px-6 pb-6">
<div className="mb-6"> <div className="mb-6">
<p className="text-warning text-lg"> <p className="text-warning text-lg">
{modelName === "Download Data" Do you want to update data server and proxy?
? "Download required data!"
: "Do you want to update data?"}
</p> </p>
</div> </div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
{modelName !== "Download Data" && (
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error" className="btn btn-outline btn-error"
onClick={() => setIsOpenNotification(false)} onClick={() => setIsOpenUpdateDataModal(false)}
> >
Cancel No
</motion.button> </motion.button>
)}
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none" className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => { onClick={async () => {
setIsOpenNotification(false) setIsOpenUpdateDataModal(false)
const dataCheck = await handlerCheckUpdate() await handlerUpdateData()
await handlerUpdateServer(dataCheck) }}
>
Yes
</motion.button>
</div>
</div>
</div>
</div>
)}
{isOpenDownloadDataModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<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">
Download Data
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Data server and proxy download required
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenDownloadDataModal(false)
await handlerUpdateData()
}}
>
Download
</motion.button>
</div>
</div>
</div>
</div>
)}
{isOpenSelfUpdateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<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={() => setIsOpenSelfUpdateModal(false)}
>
</motion.button>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Update Launcher
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Do you want to update launcher?
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenSelfUpdateModal(false)}
>
No
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenSelfUpdateModal(false)
await handlerUpdateData()
}} }}
> >
Yes Yes

View File

@@ -15,16 +15,43 @@ export default function SrToolsPage() {
<div className="space-y-3 text-blue-700"> <div className="space-y-3 text-blue-700">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🏠</div> <div className="text-blue-600 text-lg">🏠</div>
<p>This site uses a self-hosted version of <span className="font-semibold text-success">SR Tools</span> available at{" "} <p>
This site is a another version of {" "}
<span className="font-semibold text-success">SR Tools {" "}</span>
developed by {" "}
<span className="font-semibold text-warning">Me {"(Kain)"}</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>
</div>
<a <a
href="https://srtools.kain.id.vn/" href="https://srtools.kain.id.vn/"
className="link link-info" className="link link-warning font-mono text-sm break-all"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
srtools.kain.id.vn https://srtools.kain.id.vn/
</a> </a>
</p> </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>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">👨💻</div> <div className="text-blue-600 text-lg">👨💻</div>

View File

@@ -55,13 +55,18 @@ function RootLayout() {
</ul> </ul>
</div> </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 hover:scale-105 px-2">
<h1 className="text-lg font-bold"> <div className="flex items-center justify-center">
<span className="text-emerald-500">Firefly Lauc</span> <img src="/ff-launcher.png" alt="Logo" className='w-13 h-13' />
<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-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
her Launcher
</span> </span>
</h1> </h1>
<p className="text-sm text-gray-500">By Kain</p> <p className="text-sm text-gray-500">By Kain</p>
</div>
</div>
</Link> </Link>
</div> </div>
<div className="navbar-center hidden md:flex"> <div className="navbar-center hidden md:flex">

View File

@@ -2,7 +2,6 @@
import { create } from 'zustand' import { create } from 'zustand'
interface LauncherState { interface LauncherState {
modelName: string;
downloadType: string; downloadType: string;
serverReady: boolean; serverReady: boolean;
proxyReady: boolean; proxyReady: boolean;
@@ -13,7 +12,8 @@ interface LauncherState {
gameRunning: boolean; gameRunning: boolean;
progressDownload: number; progressDownload: number;
downloadSpeed: number; downloadSpeed: number;
setModelName: (value: string) => void; launcherVersion: string;
updateData: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>;
setDownloadType: (value: string) => void; setDownloadType: (value: string) => void;
setServerReady: (value: boolean) => void; setServerReady: (value: boolean) => void;
setProxyReady: (value: boolean) => void; setProxyReady: (value: boolean) => void;
@@ -23,12 +23,13 @@ interface LauncherState {
setIsLoading: (value: boolean) => void; setIsLoading: (value: boolean) => void;
setGameRunning: (value: boolean) => void; setGameRunning: (value: boolean) => void;
setProgressDownload: (value: number) => void; setProgressDownload: (value: number) => void;
setLauncherVersion: (value: string) => void;
setDownloadSpeed: (value: number) => void; setDownloadSpeed: (value: number) => void;
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => void;
} }
const useLauncherStore = create<LauncherState>((set, get) => ({ const useLauncherStore = create<LauncherState>((set, get) => ({
isLoading: false, isLoading: false,
modelName: "Download Data",
downloadType: "", downloadType: "",
serverReady: false, serverReady: false,
proxyReady: false, proxyReady: false,
@@ -38,8 +39,13 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
gameRunning: false, gameRunning: false,
progressDownload: 0, progressDownload: 0,
downloadSpeed: 0, downloadSpeed: 0,
launcherVersion: "",
updateData: {
server: { isUpdate: false, isExists: false, version: "" },
proxy: { isUpdate: false, isExists: false, version: "" },
launcher: { isUpdate: false, isExists: true, version: "" },
},
setIsLoading: (value: boolean) => set({ isLoading: value }), setIsLoading: (value: boolean) => set({ isLoading: value }),
setModelName: (value: string) => set({ modelName: value }),
setDownloadType: (value: string) => set({ downloadType: value }), setDownloadType: (value: string) => set({ downloadType: value }),
setServerReady: (value: boolean) => set({ serverReady: value }), setServerReady: (value: boolean) => set({ serverReady: value }),
setProxyReady: (value: boolean) => set({ proxyReady: value }), setProxyReady: (value: boolean) => set({ proxyReady: value }),
@@ -48,7 +54,9 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
setProxyRunning: (value: boolean) => set({ proxyRunning: value }), setProxyRunning: (value: boolean) => set({ proxyRunning: value }),
setGameRunning: (value: boolean) => set({ gameRunning: value }), setGameRunning: (value: boolean) => set({ gameRunning: value }),
setProgressDownload: (value: number) => set({ progressDownload: value }), setProgressDownload: (value: number) => set({ progressDownload: value }),
setLauncherVersion: (value: string) => set({ launcherVersion: value }),
setDownloadSpeed: (value: number) => set({ downloadSpeed: value }), setDownloadSpeed: (value: number) => set({ downloadSpeed: value }),
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => set({ updateData: value }),
})); }));
export default useLauncherStore; export default useLauncherStore;

View File

@@ -2,13 +2,21 @@
import { create } from 'zustand' import { create } from 'zustand'
interface ModalState { interface ModalState {
isOpenNotification: boolean; isOpenDownloadDataModal: boolean;
setIsOpenNotification: (modal: boolean) => void; isOpenUpdateDataModal: boolean;
isOpenSelfUpdateModal: boolean;
setIsOpenDownloadDataModal: (modal: boolean) => void;
setIsOpenUpdateDataModal: (modal: boolean) => void;
setIsOpenSelfUpdateModal: (modal: boolean) => void;
} }
const useModalStore = create<ModalState>((set, get) => ({ const useModalStore = create<ModalState>((set, get) => ({
isOpenNotification: false, isOpenDownloadDataModal: false,
setIsOpenNotification: (modal: boolean) => set({ isOpenNotification: modal }), isOpenUpdateDataModal: false,
isOpenSelfUpdateModal: false,
setIsOpenDownloadDataModal: (modal: boolean) => set({ isOpenDownloadDataModal: modal }),
setIsOpenUpdateDataModal: (modal: boolean) => set({ isOpenUpdateDataModal: modal }),
setIsOpenSelfUpdateModal: (modal: boolean) => set({ isOpenSelfUpdateModal: modal }),
})); }));
export default useModalStore; export default useModalStore;

68
go.mod
View File

@@ -1,54 +1,56 @@
module firefly-launcher module firefly-launcher
go 1.22.4 go 1.25
toolchain go1.24.4
require ( require (
github.com/wailsapp/wails/v3 v3.0.0-alpha.9 github.com/wailsapp/wails/v3 v3.0.0-alpha.26
golang.org/x/sys v0.28.0 golang.org/x/sys v0.35.0
) )
require ( require (
dario.cat/mergo v1.0.1 // indirect aead.dev/minisign v0.3.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect golang.org/x/text v0.28.0 // indirect
github.com/adrg/xdg v0.5.0 // indirect )
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/emirpasic/gods v1.18.1 // 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/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-git/go-git/v5 v5.16.2 // indirect
github.com/go-ole/go-ole v1.3.0 // 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.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.4.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.4 // indirect github.com/lmittmann/tint v1.1.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/minio/selfupdate v0.6.0
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.38.1 // indirect github.com/samber/lo v1.51.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/tools v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
) )

193
go.sum
View File

@@ -1,59 +1,60 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
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.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
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 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 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/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 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 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-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -63,23 +64,25 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
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 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -87,99 +90,63 @@ 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU= github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= 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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.9 h1:b8CfRrhPno8Fra0xFp4Ifyj+ogmXBc35rsQWvcrHtsI= github.com/wailsapp/wails/v3 v3.0.0-alpha.26 h1:co3eifrIcG/J7sQ1oMPmLfvtAE4bezdhOtabI4Ua5fg=
github.com/wailsapp/wails/v3 v3.0.0-alpha.9/go.mod h1:dSv6s722nSWaUyUiapAM1DHc5HKggNGY1a79shO85/g= github.com/wailsapp/wails/v3 v3.0.0-alpha.26/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 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= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

22
internal/app-serrvice.go Normal file
View File

@@ -0,0 +1,22 @@
package internal
import (
"firefly-launcher/pkg/constant"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
type AppService struct{}
func (a *AppService) GetCurrentLauncherVersion() (bool, string) {
return true, constant.CurrentLauncherVersion
}
func (a *AppService) CloseAppAfterTimeout(timeout int) (bool, string) {
go func() {
time.Sleep(time.Duration(timeout) * time.Second)
application.Get().Quit()
}()
return true, ""
}

View File

@@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -40,6 +39,7 @@ func (f *FSService) PickFile() (string, error) {
} else { } else {
dialog.SetTitle("Select a file/directory") dialog.SetTitle("Select a file/directory")
} }
dialog.AddFilter("Executable Files (*.exe)", "*.exe")
if path, err := dialog.PromptForSingleSelection(); err == nil { if path, err := dialog.PromptForSingleSelection(); err == nil {
return path, nil return path, nil
} }
@@ -70,7 +70,7 @@ func (f *FSService) StartApp(path string) (bool, error) {
if strings.HasSuffix(path, "StarRail.exe") { if strings.HasSuffix(path, "StarRail.exe") {
go func() { go func() {
_ = cmd.Wait() _ = cmd.Wait()
application.Get().EmitEvent("game:exit") application.Get().Event.Emit("game:exit")
}() }()
} }
@@ -107,11 +107,11 @@ func (f *FSService) StartWithConsole(path string) (bool, error) {
go func() { go func() {
_ = cmd.Wait() _ = cmd.Wait()
if strings.HasSuffix(path, "launcher.exe") { if strings.HasSuffix(path, "launcher.exe") {
application.Get().EmitEvent("game:exit") application.Get().Event.Emit("game:exit")
} else if strings.HasSuffix(path, "firefly-go_win.exe") { } else if strings.HasSuffix(path, "firefly-go_win.exe") {
application.Get().EmitEvent("server:exit") application.Get().Event.Emit("server:exit")
} else if strings.HasSuffix(path, "FireflyProxy.exe") { } else if strings.HasSuffix(path, "FireflyProxy.exe") {
application.Get().EmitEvent("proxy:exit") application.Get().Event.Emit("proxy:exit")
} }
}() }()
return true, nil return true, nil
@@ -128,7 +128,7 @@ func (f *FSService) OpenFolder(path string) (bool, string) {
} }
url := "file:///" + filepath.ToSlash(absPath) url := "file:///" + filepath.ToSlash(absPath)
application.Get().BrowserOpenURL(url) application.Get().Browser.OpenURL(url)
return true, "" return true, ""
} }
@@ -140,3 +140,4 @@ func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, strin
} }
return exists, "" return exists, ""
} }

View File

@@ -10,12 +10,13 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/minio/selfupdate"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
type GitService struct{} type GitService struct{}
func (g *GitService) GetLatestServerVersion(oldVersion string) (bool, string, string) { func (g *GitService) GetLatestServerVersion() (bool, string, string) {
resp, err := http.Get(constant.ServerGitUrl) resp, err := http.Get(constant.ServerGitUrl)
if err != nil { if err != nil {
return false, "", err.Error() return false, "", err.Error()
@@ -38,7 +39,6 @@ func (g *GitService) GetLatestServerVersion(oldVersion string) (bool, string, st
} }
func (g *GitService) DownloadServerProgress(version string) (bool, string) { func (g *GitService) DownloadServerProgress(version string) (bool, string) {
resp, err := http.Get(constant.ServerGitUrl) resp, err := http.Get(constant.ServerGitUrl)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -96,7 +96,7 @@ func (g *GitService) DownloadServerProgress(version string) (bool, string) {
defer resp.Body.Close() defer resp.Body.Close()
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) { DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
application.Get().EmitEvent("download:server", map[string]interface{}{ application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent), "percent": fmt.Sprintf("%.2f", percent),
"speed": fmt.Sprintf("%.2f", speed), "speed": fmt.Sprintf("%.2f", speed),
}) })
@@ -109,7 +109,7 @@ func (g *GitService) UnzipServer() {
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile)) os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
} }
func (g *GitService) GetLatestProxyVersion(oldVersion string) (bool, string, string) { func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
resp, err := http.Get(constant.ProxyGitUrl) resp, err := http.Get(constant.ProxyGitUrl)
if err != nil { if err != nil {
return false, "", err.Error() return false, "", err.Error()
@@ -188,7 +188,7 @@ func (g *GitService) DownloadProxyProgress(version string) (bool, string) {
defer resp.Body.Close() defer resp.Body.Close()
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) { DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
application.Get().EmitEvent("download:proxy", map[string]interface{}{ application.Get().Event.Emit("download:proxy", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent), "percent": fmt.Sprintf("%.2f", percent),
"speed": fmt.Sprintf("%.2f", speed), "speed": fmt.Sprintf("%.2f", speed),
}) })
@@ -201,3 +201,82 @@ func (g *GitService) UnzipProxy() {
unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl) unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile)) os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile))
} }
func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
resp, err := http.Get(constant.LauncherGitUrl)
if err != nil {
return false, "", err.Error()
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var releases []models.ReleaseType
err = json.Unmarshal(body, &releases)
if err != nil {
return false, "", err.Error()
}
if len(releases) == 0 {
return false, "", "no releases found"
}
return true, releases[0].TagName, ""
}
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
resp, err := http.Get(constant.LauncherGitUrl)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var releases []*models.ReleaseType
err = json.Unmarshal(body, &releases)
if err != nil {
return false, err.Error()
}
if len(releases) == 0 {
return false, "no releases found"
}
var releaseData *models.ReleaseType
for _, release := range releases {
if release.TagName == version {
releaseData = release
break
}
}
if releaseData == nil || releaseData.TagName == "" {
return false, "no release found"
}
var assetWin models.AssetType
for _, asset := range releaseData.Assets {
if asset.Name == constant.LauncherFile {
assetWin = asset
break
}
}
if assetWin.Name == "" {
return false, "no assets found"
}
resp, err = http.Get(assetWin.BrowserDownloadURL)
if err != nil {
return false, err.Error()
}
defer resp.Body.Close()
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil {
return false, err.Error()
}
return true, ""
}

View File

@@ -7,19 +7,30 @@ import (
"firefly-launcher/pkg/models" "firefly-launcher/pkg/models"
"firefly-launcher/pkg/sevenzip" "firefly-launcher/pkg/sevenzip"
"firefly-launcher/pkg/verifier" "firefly-launcher/pkg/verifier"
"firefly-launcher/pkg/hpatchz"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
type HdiffzService struct{} type HdiffzService struct{}
func (h *HdiffzService) CheckTypeHDiff(patchPath string) (bool, string, string) {
isFileInTxt, _ := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt")
if isFileInTxt {
return true, "hdifffiles.txt", ""
}
isFileInJson, _ := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json")
if isFileInJson {
return true, "hdiffmap.json", ""
}
return false, "", "not found hdifffiles.txt or hdiffmap.json"
}
func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) { func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) {
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")) oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
if err != nil { if err != nil {
@@ -53,7 +64,8 @@ func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, strin
return true, "validated" return true, "validated"
} }
func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
func (h *HdiffzService) DataExtract(gamePath, patchPath string, isSkipVerify bool) (bool, string) {
if _, err := os.Stat(gamePath); err != nil { if _, err := os.Stat(gamePath); err != nil {
return false, err.Error() return false, err.Error()
} }
@@ -73,6 +85,7 @@ func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
return false, err.Error() return false, err.Error()
} }
if !isSkipVerify {
validator, err := verifier.NewVerifier(gamePath, constant.TempUrl) validator, err := verifier.NewVerifier(gamePath, constant.TempUrl)
if err != nil { if err != nil {
os.RemoveAll(constant.TempUrl) os.RemoveAll(constant.TempUrl)
@@ -83,6 +96,7 @@ func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
os.RemoveAll(constant.TempUrl) os.RemoveAll(constant.TempUrl)
return false, err.Error() return false, err.Error()
} }
}
return true, "validated" return true, "validated"
} }
@@ -102,7 +116,7 @@ func (h *HdiffzService) CutData(gamePath string) (bool, string) {
return err return err
} }
destPath := filepath.Join(gamePath, relPath) destPath := filepath.Join(gamePath, relPath)
application.Get().EmitEvent("hdiffz:message", map[string]string{"message": destPath}) application.Get().Event.Emit("hdiffz:message", map[string]string{"message": destPath})
if info.IsDir() { if info.IsDir() {
return os.MkdirAll(destPath, os.ModePerm) return os.MkdirAll(destPath, os.ModePerm)
} }
@@ -136,49 +150,92 @@ func (h *HdiffzService) CutData(gamePath string) (bool, string) {
return false, err.Error() return false, err.Error()
} }
_ = os.RemoveAll(constant.TempUrl)
return true, "cut completed" return true, "cut completed"
} }
func (h *HdiffzService) PatchData(gamePath string) (bool, string) { func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
data, err := os.ReadFile(filepath.Join(gamePath, "hdiffmap.json")) hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
var jsonData struct {
DiffMap []*models.HDiffData `json:"diff_map"`
}
if _, err := os.Stat(hdiffMapPath); err == nil {
data, err := os.ReadFile(hdiffMapPath)
if err != nil { if err != nil {
return false, err.Error() return false, err.Error()
} }
var jsonData struct { var jsonDataDiffMap struct {
DiffMap []*models.DiffMapType `json:"diff_map"` DiffMap []*models.DiffMapType `json:"diff_map"`
} }
if err := json.Unmarshal(data, &jsonData); err != nil { if err := json.Unmarshal(data, &jsonDataDiffMap); err != nil {
return false, err.Error() return false, err.Error()
} }
for _, entry := range jsonDataDiffMap.DiffMap {
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
}
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
files, err := models.LoadHDiffFiles(hdiffFilesPath)
if err != nil {
return false, err.Error()
}
for _, entry := range files {
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
}
} else {
return false, "no hdiff entries map exist"
}
for i, entry := range jsonData.DiffMap { for i, entry := range jsonData.DiffMap {
application.Get().EmitEvent( application.Get().Event.Emit(
"hdiffz:progress", map[string]int{ "hdiffz:progress", map[string]int{
"progress": i, "progress": i,
"maxProgress": len(jsonData.DiffMap), "maxProgress": len(jsonData.DiffMap),
}) })
sourceFile := filepath.Join(gamePath, entry.SourceFileName) sourceFile := filepath.Join(gamePath, entry.SourceFileName)
patchFile := filepath.Join(gamePath, entry.PatchFileName) patchFile := filepath.Join(gamePath, entry.PatchFileName)
targetFile := filepath.Join(gamePath, entry.TargetFileName) targetFile := filepath.Join(gamePath, entry.TargetFileName)
if _, err := os.Stat(sourceFile); os.IsNotExist(err) { // Check patch file tồn tại chưa
continue
}
if _, err := os.Stat(patchFile); os.IsNotExist(err) { if _, err := os.Stat(patchFile); os.IsNotExist(err) {
continue continue
} }
cmd := exec.Command(constant.ToolHPatchzExe.String(), sourceFile, patchFile, targetFile) // Nếu không có source file hoặc SourceFileName rỗng → apply_patch_empty
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} if entry.SourceFileName == "" {
_, err := cmd.CombinedOutput() err := hpatchz.ApplyPatchEmpty(patchFile, targetFile)
if err != nil { if err != nil {
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
_ = os.Remove(patchFile)
return false, err.Error()
}
_ = os.Remove(patchFile)
continue continue
} }
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
continue
} }
// Có source file → apply_patch
err := hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
if err != nil {
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
_ = os.Remove(patchFile)
return false, err.Error()
}
if entry.SourceFileName != entry.TargetFileName {
_ = os.Remove(sourceFile)
}
_ = os.Remove(patchFile)
}
os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
return true, "patching completed" return true, "patching completed"
} }
@@ -200,13 +257,14 @@ func (h *HdiffzService) DeleteFiles(gamePath string) (bool, string) {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return false, "" return false, "no delete files exist"
} }
for i, file := range deleteFiles { for i, file := range deleteFiles {
os.Remove(filepath.Join(gamePath, file)) os.Remove(filepath.Join(gamePath, file))
application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)}) application.Get().Event.Emit("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
} }
_ = os.Remove(filepath.Join(gamePath, "deletefiles.txt"))
return true, "" return true, ""
} }

View File

@@ -2,145 +2,174 @@ package internal
import ( import (
"bytes" "bytes"
"fmt" assetMeta "firefly-launcher/pkg/language-patch/asset-meta"
excelLanguage "firefly-launcher/pkg/language-patch/excel-language"
"firefly-launcher/pkg/models"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings"
) )
type LanguageService struct{} type LanguageService struct{}
func isValidLang(lang string) bool { func isValidLang(lang string) bool {
valid := []string{"en", "jp", "cn", "kr"} valid := []string{"en", "jp", "cn", "kr"}
for _, v := range valid { return slices.Contains(valid, lang)
if lang == v {
return true
}
}
return false
} }
func (l *LanguageService) GetLanguage(path string) (string, string, error) { func (l *LanguageService) GetLanguage(path string) (bool, string, string, string) {
files, err := os.ReadDir(path) currentVersionGame, err := models.ParseBinaryVersion(filepath.Join(path, "BinaryVersion.bytes"))
if err != nil { if err != nil {
return "", "", err return false, "", "", err.Error()
} }
for _, file := range files { typeVersionGame := "os"
filePath := filepath.Join(path, file.Name()) if strings.Contains(currentVersionGame.Name, "CN") {
typeVersionGame = "cn"
}
content, err := os.ReadFile(filePath) assetPath := filepath.Join(path, "DesignData\\Windows")
indexHash, err := assetMeta.GetIndexHash(assetPath)
if err != nil { if err != nil {
return false, "", "", err.Error()
}
DesignIndex, err := assetMeta.DesignIndexFromBytes(assetPath, indexHash)
if err != nil {
return false, "", "", err.Error()
}
dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346)
if err != nil {
return false, "", "", err.Error()
}
allowedLanguage := excelLanguage.NewExcelLanguage(assetPath, &dataEntry, &fileEntry)
languageRows, err := allowedLanguage.Parse()
if err != nil {
return false, "", "", err.Error()
}
currentTextLang := ""
currentVoiceLang := ""
pairs := []struct {
area string
typ *uint8
}{
{"os", nil},
{"cn", func() *uint8 { v := uint8(1); return &v }()},
{"os", func() *uint8 { v := uint8(1); return &v }()},
{"cn", nil},
}
for _, p := range pairs {
var found *excelLanguage.LanguageRow
for i := range languageRows {
if languageRows[i].Area != nil && *languageRows[i].Area == p.area {
if (languageRows[i].Type == nil && p.typ == nil) ||
(languageRows[i].Type != nil && p.typ != nil && *languageRows[i].Type == *p.typ) {
found = &languageRows[i]
break
}
}
}
if found == nil {
continue continue
} }
if found.DefaultLanguage != nil && found.Area != nil && *found.Area == typeVersionGame && found.Type == nil {
patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf") currentTextLang = *found.DefaultLanguage
idx := bytes.Index(content, patternToFind) }
if idx == -1 { if found.DefaultLanguage != nil && found.Area != nil && *found.Area == typeVersionGame && found.Type != nil {
continue currentVoiceLang = *found.DefaultLanguage
}
} }
pattern := []byte("Korean") if currentTextLang == "" || currentVoiceLang == "" || !isValidLang(currentTextLang) || !isValidLang(currentVoiceLang) {
idx = bytes.Index(content, pattern) return false, "", "", "not found language"
if idx == -1 {
continue
} }
// Move to os text language return true, currentTextLang, currentVoiceLang, ""
idx += 10
idx += 4
osText := string(content[idx : idx+2])
idx += 3 * 4
// Move to cn voice language
idx += 1
idx += 5
cnVoice := string(content[idx : idx+2])
idx += 3 * 2 // skip 2 entries
// Move to os voice language
idx += 1
idx += 5
osVoice := string(content[idx : idx+2])
idx += 3 * 5 // skip 5 entries
// Move to cn text language
idx += 1
idx += 4
cnText := string(content[idx : idx+2])
textLang := osText
voiceLang := osVoice
if !isValidLang(textLang) {
textLang = cnText
}
if !isValidLang(voiceLang) {
voiceLang = cnVoice
}
return textLang, voiceLang, nil
}
return "", "", fmt.Errorf("couldn't find file to read language from")
} }
func replaceBytes(content []byte, idx int, choice string, param int) int {
for i := 0; i < param; i++ {
copy(content[idx:idx+2], []byte(choice)) func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, string) {
idx += 3 indexHash, err := assetMeta.GetIndexHash(path)
if err != nil {
return false, err.Error()
} }
return idx
} DesignIndex, err := assetMeta.DesignIndexFromBytes(path, indexHash)
if err != nil {
func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, error) { return false, err.Error()
files, err := os.ReadDir(path) }
if err != nil { dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346)
return false, err if err != nil {
} return false, err.Error()
}
for _, file := range files { allowedLanguage := excelLanguage.NewExcelLanguage(path, &dataEntry, &fileEntry)
filePath := filepath.Join(path, file.Name()) languageRows, err := allowedLanguage.Parse()
if err != nil {
content, err := os.ReadFile(filePath) return false, err.Error()
if err != nil { }
continue
} pairs := []struct {
area string
patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf") typ *uint8
idx := bytes.Index(content, patternToFind) lang string
if idx == -1 { }{
continue {"os", nil, text},
} {"cn", func() *uint8 { v := uint8(1); return &v }(), voice},
{"os", func() *uint8 { v := uint8(1); return &v }(), voice},
pattern := []byte("Korean") {"cn", nil, text},
idx = bytes.Index(content, pattern) }
if idx == -1 {
continue for _, p := range pairs {
} var found *excelLanguage.LanguageRow
for i := range languageRows {
idx += 10 if languageRows[i].Area != nil && *languageRows[i].Area == p.area {
idx += 4 if (languageRows[i].Type == nil && p.typ == nil) ||
idx = replaceBytes(content, idx, text, 4) (languageRows[i].Type != nil && p.typ != nil && *languageRows[i].Type == *p.typ) {
idx += 1 found = &languageRows[i]
break
idx += 5 }
idx = replaceBytes(content, idx, voice, 2) }
idx += 1 }
if found == nil {
idx += 5 continue
idx = replaceBytes(content, idx, voice, 5) }
idx += 1 found.DefaultLanguage = &p.lang
found.LanguageList = []string{p.lang}
idx += 4 }
_ = replaceBytes(content, idx, text, 2)
data, err := allowedLanguage.Unmarshal(languageRows)
err = os.WriteFile(filePath, content, 0644) if err != nil {
if err != nil { return false, err.Error()
return false, err }
}
filePath := filepath.Join(path, fileEntry.FileByteName+".bytes")
return true, nil
} f, err := os.OpenFile(filePath, os.O_RDWR, 0644)
if err != nil {
return false, fmt.Errorf("couldn't find file to patch. Make sure this file is placed in the correct folder") return false, err.Error()
}
defer f.Close()
if _, err := f.Seek(int64(dataEntry.Offset), 0); err != nil {
return false, err.Error()
}
if _, err := f.Write(data); err != nil {
return false, err.Error()
}
if len(data) < int(dataEntry.Size) {
remaining := int(dataEntry.Size) - len(data)
zeros := bytes.Repeat([]byte{0}, remaining)
if _, err := f.Write(zeros); err != nil {
return false, err.Error()
}
}
return true, "success"
} }

View File

@@ -0,0 +1 @@
package internal

View File

@@ -109,8 +109,6 @@ func DownloadFile(filepath string, url string, onEmit func(percent float64, spee
return nil return nil
} }
func unzipParallel(src string, dest string) error { func unzipParallel(src string, dest string) error {
numCPU := runtime.NumCPU() numCPU := runtime.NumCPU()
@@ -141,16 +139,14 @@ func unzipParallel(src string, dest string) error {
// Worker pool // Worker pool
for i := 0; i < maxWorkers; i++ { for i := 0; i < maxWorkers; i++ {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
for j := range jobs { for j := range jobs {
err := extractFile(j.f, dest) err := extractFile(j.f, dest)
if err != nil { if err != nil {
fmt.Printf("Error extracting %s: %v\n", j.f.Name, err) fmt.Printf("Error extracting %s: %v\n", j.f.Name, err)
} }
} }
}() })
} }
// Feed jobs // Feed jobs

42
main.go
View File

@@ -13,11 +13,6 @@ import (
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
// Wails uses Go's `embed` package to embed the frontend files into the binary.
// Any files in the frontend/dist folder will be embedded into the binary and
// made available to the frontend.
// See https://pkg.go.dev/embed for more information.
//go:embed all:frontend/dist //go:embed all:frontend/dist
var assets embed.FS var assets embed.FS
@@ -42,10 +37,8 @@ func extractFile(embedPath, outPath string) error {
return os.WriteFile(outPath, data, 0755) return os.WriteFile(outPath, data, 0755)
} }
// main function serves as the application's entry point. It initializes the application, creates a window,
// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and
// logs any error that might occur.
func main() { func main() {
// Extract required files
for outPath, embedPath := range constant.RequiredFiles { for outPath, embedPath := range constant.RequiredFiles {
if !fileExists(outPath.String()) { if !fileExists(outPath.String()) {
err := extractFile(embedPath, outPath.String()) err := extractFile(embedPath, outPath.String())
@@ -54,11 +47,18 @@ func main() {
} }
} }
} }
// Create a new Wails application by providing the necessary options.
// Variables 'Name' and 'Description' are for application metadata. // Remove old executable
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. exePath, err := os.Executable()
// 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. if err == nil {
// 'Mac' options tailor the application when running an macOS. dir := filepath.Dir(exePath)
base := filepath.Base(exePath)
oldPath := filepath.Join(dir, "."+base+".old")
fmt.Println("Old executable path:", oldPath)
os.Remove(oldPath)
}
// Create application
app := application.New(application.Options{ app := application.New(application.Options{
Name: "firefly-launcher", Name: "firefly-launcher",
Description: "Firefly Launcher - Kain", Description: "Firefly Launcher - Kain",
@@ -67,31 +67,24 @@ func main() {
application.NewService(&internal.LanguageService{}), application.NewService(&internal.LanguageService{}),
application.NewService(&internal.GitService{}), application.NewService(&internal.GitService{}),
application.NewService(&internal.HdiffzService{}), application.NewService(&internal.HdiffzService{}),
application.NewService(&internal.AppService{}),
}, },
Assets: application.AssetOptions{ Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets), Handler: application.AssetFileServerFS(assets),
}, },
Mac: application.MacOptions{ Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true, ApplicationShouldTerminateAfterLastWindowClosed: true,
}, },
}) })
// Create a new window with the necessary options. // Create window
// 'Title' is the title of the window. app.Window.NewWithOptions(application.WebviewWindowOptions{
// 'Mac' options tailor the window when running on macOS.
// 'BackgroundColour' is the background colour of the window.
// 'URL' is the URL that will be loaded into the webview.
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Firefly Launcher - Kain", Title: "Firefly Launcher - Kain",
Mac: application.MacWindow{ Mac: application.MacWindow{
InvisibleTitleBarHeight: 50, InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent, Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset, TitleBar: application.MacTitleBarHiddenInset,
}, },
BackgroundColour: application.NewRGB(27, 38, 54), BackgroundColour: application.NewRGB(27, 38, 54),
Width: 1200, Width: 1200,
Height: 600, Height: 600,
@@ -99,7 +92,8 @@ func main() {
DevToolsEnabled: true, DevToolsEnabled: true,
}) })
err := app.Run()
err = app.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -2,12 +2,17 @@ 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/Firefly_Proxy/releases"
const ServerGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Local_Archive/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 ServerStorageUrl = "./server"
const ProxyStorageUrl = "./proxy" const ProxyStorageUrl = "./proxy"
const ServerZipFile = "prebuild_win_x86.zip" const ServerZipFile = "prebuild_win_x86.zip"
const ProxyZipFile = "64bit.zip" const ProxyZipFile = "64bit.zip"
const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp" const TempUrl = "./temp"
const CurrentLauncherVersion = "1.4.1"
type ToolFile string type ToolFile string
const ( const (

34
pkg/hpatchz/hpatchz.go Normal file
View File

@@ -0,0 +1,34 @@
package hpatchz
import (
"firefly-launcher/pkg/constant"
"fmt"
"os/exec"
"syscall"
)
func ApplyPatch(oldFile, diffFile, newFile string) error {
cmd := exec.Command(constant.ToolHPatchzExe.String(), "-f", oldFile, diffFile, newFile)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to execute hpatchz: %w", err)
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("hpatchz failed: %s", string(output))
}
return nil
}
func ApplyPatchEmpty(diffFile, newFile string) error {
cmd := exec.Command(constant.ToolHPatchzExe.String(), "-f", "", diffFile, newFile)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to execute hpatchz: %w", err)
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("hpatchz failed: %s", string(output))
}
return nil
}

View File

@@ -0,0 +1,30 @@
package assetMeta
import (
"fmt"
"io"
)
type ByteHash16 []byte
func ByteHash16FromBytes(r io.ReadSeeker) (ByteHash16, error) {
fullHash := make([]byte, 16)
buf := make([]byte, 4)
for i := 0; i < 4; i++ {
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
for j := 0; j < 4; j++ {
fullHash[i*4+j] = buf[3-j]
}
}
return ByteHash16(fullHash), nil
}
func (b ByteHash16) String() string {
s := ""
for _, v := range b {
s += fmt.Sprintf("%02x", v)
}
return s
}

View File

@@ -0,0 +1,28 @@
package assetMeta
import (
"encoding/binary"
"io"
)
type DataEntry struct {
NameHash int32
Size uint32
Offset uint32
}
func DataEntryFromBytes(r io.Reader) (*DataEntry, error) {
var d DataEntry
if err := binary.Read(r, binary.BigEndian, &d.NameHash); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.Size); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.Offset); err != nil {
return nil, err
}
return &d, nil
}

View File

@@ -0,0 +1,98 @@
package assetMeta
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
)
type DesignIndex struct {
UnkI64 int64
FileCount int32
DesignDataCount int32
FileList []FileEntry
}
func (d *DesignIndex) FindDataAndFileByTarget(target int32) (DataEntry, FileEntry, error) {
for _, file := range d.FileList {
for _, entry := range file.DataEntries {
if entry.NameHash == target {
return entry, file, nil
}
}
}
return DataEntry{}, FileEntry{}, errors.New("not found")
}
func DesignIndexFromBytes(assetFolder string, indexHash string) (*DesignIndex, error) {
path := filepath.Join(assetFolder, fmt.Sprintf("DesignV_%s.bytes", indexHash))
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
r := bytes.NewReader(data)
var d DesignIndex
if err := binary.Read(r, binary.BigEndian, &d.UnkI64); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.FileCount); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.DesignDataCount); err != nil {
return nil, err
}
d.FileList = make([]FileEntry, 0, d.FileCount)
for i := int32(0); i < d.FileCount; i++ {
entry, err := FileEntryFromBytes(r)
if err != nil {
return nil, err
}
d.FileList = append(d.FileList, *entry)
}
return &d, nil
}
func GetIndexHash(assetFolder string) (string, error) {
path := filepath.Join(assetFolder, "M_DesignV.bytes")
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.Seek(0x1C, 0)
if err != nil {
return "", err
}
hash := make([]byte, 0x10)
index := 0
for i := 0; i < 4; i++ {
chunk := make([]byte, 4)
_, err := f.Read(chunk)
if err != nil {
return "", err
}
for bytePos := 3; bytePos >= 0; bytePos-- {
hash[index] = chunk[bytePos]
index++
}
}
return hex.EncodeToString(hash), nil
}

View File

@@ -0,0 +1,63 @@
package assetMeta
import (
"encoding/binary"
"fmt"
"io"
)
type FileEntry struct {
NameHash int32
FileByteName string
Size int64
DataCount int32
DataEntries []DataEntry
Unk uint8
}
func FileEntryFromBytes(r io.Reader) (*FileEntry, error) {
var f FileEntry
if err := binary.Read(r, binary.BigEndian, &f.NameHash); err != nil {
return nil, err
}
buf := make([]byte, 16)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
f.FileByteName = toHex(buf)
if err := binary.Read(r, binary.BigEndian, &f.Size); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &f.DataCount); err != nil {
return nil, err
}
f.DataEntries = make([]DataEntry, 0, f.DataCount)
for i := int32(0); i < f.DataCount; i++ {
entry, err := DataEntryFromBytes(r)
if err != nil {
return nil, err
}
f.DataEntries = append(f.DataEntries, *entry)
}
// read 1 byte
b := make([]byte, 1)
if _, err := r.Read(b); err != nil {
return nil, err
}
f.Unk = b[0]
return &f, nil
}
func toHex(buf []byte) string {
s := ""
for _, b := range buf {
s += fmt.Sprintf("%02x", b)
}
return s
}

View File

@@ -0,0 +1,32 @@
package assetMeta
import (
"encoding/binary"
"io"
)
type MiniAsset struct {
RevisionID uint32
DesignIndexHash ByteHash16
}
func MiniAssetFromBytes(r io.ReadSeeker) (*MiniAsset, error) {
if _, err := r.Seek(6*4, io.SeekCurrent); err != nil {
return nil, err
}
var revID uint32
if err := binary.Read(r, binary.LittleEndian, &revID); err != nil {
return nil, err
}
hash, err := ByteHash16FromBytes(r)
if err != nil {
return nil, err
}
return &MiniAsset{
RevisionID: revID,
DesignIndexHash: hash,
}, nil
}

View File

@@ -0,0 +1,114 @@
package excelLanguage
import (
"bytes"
assetMeta "firefly-launcher/pkg/language-patch/asset-meta"
"io"
"os"
"path/filepath"
)
type ExcelLanguage struct {
AssetFolder string
ExcelDataEntry *assetMeta.DataEntry
ExcelFileEntry *assetMeta.FileEntry
}
func NewExcelLanguage(assetFolder string, dataEntry *assetMeta.DataEntry, fileEntry *assetMeta.FileEntry) *ExcelLanguage {
return &ExcelLanguage{
AssetFolder: assetFolder,
ExcelDataEntry: dataEntry,
ExcelFileEntry: fileEntry,
}
}
func (a *ExcelLanguage) Unmarshal(rows []LanguageRow) ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(0)
if err := writeI8Varint(buf, int8(len(rows))); err != nil {
return nil, err
}
for _, row := range rows {
rowData, err := row.Unmarshal()
if err != nil {
return nil, err
}
if _, err := buf.Write(rowData); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func (a *ExcelLanguage) Parse() ([]LanguageRow, error) {
excelPath := filepath.Join(a.AssetFolder, a.ExcelFileEntry.FileByteName+".bytes")
f, err := os.Open(excelPath)
if err != nil {
return nil, err
}
defer f.Close()
if _, err := f.Seek(int64(a.ExcelDataEntry.Offset), io.SeekStart); err != nil {
return nil, err
}
buffer := make([]byte, a.ExcelDataEntry.Size)
if _, err := io.ReadFull(f, buffer); err != nil {
return nil, err
}
reader := bytes.NewReader(buffer)
_, _ = reader.ReadByte() // skip first byte
count, err := readI8Varint(reader)
if err != nil {
return nil, err
}
rows := make([]LanguageRow, 0, count)
for i := 0; i < count; i++ {
bitmask, err := reader.ReadByte()
if err != nil {
return nil, err
}
row := LanguageRow{}
if bitmask&(1<<0) != 0 {
s, err := readString(reader)
if err != nil {
return nil, err
}
row.Area = &s
}
if bitmask&(1<<1) != 0 {
t, err := reader.ReadByte()
if err != nil {
return nil, err
}
row.Type = &t
}
if bitmask&(1<<2) != 0 {
arr, err := readStringArray(reader)
if err != nil {
return nil, err
}
row.LanguageList = arr
}
if bitmask&(1<<3) != 0 {
s, err := readString(reader)
if err != nil {
return nil, err
}
row.DefaultLanguage = &s
}
rows = append(rows, row)
}
return rows, nil
}

View File

@@ -0,0 +1,81 @@
package excelLanguage
import (
"bytes"
"errors"
)
type LanguageRow struct {
Area *string
Type *uint8
LanguageList []string
DefaultLanguage *string
}
func (r *LanguageRow) Unmarshal() ([]byte, error) {
buf := new(bytes.Buffer)
var bitmask uint8
if r.Area != nil {
bitmask |= 1 << 0
}
if r.Type != nil {
bitmask |= 1 << 1
}
if len(r.LanguageList) > 0 {
bitmask |= 1 << 2
}
if r.DefaultLanguage != nil {
bitmask |= 1 << 3
}
if err := buf.WriteByte(bitmask); err != nil {
return nil, err
}
if r.Area != nil {
if err := writeString(buf, *r.Area); err != nil {
return nil, err
}
}
if r.Type != nil {
if err := buf.WriteByte(*r.Type); err != nil {
return nil, err
}
}
if len(r.LanguageList) > 0 {
if err := writeStringArray(buf, r.LanguageList); err != nil {
return nil, err
}
}
if r.DefaultLanguage != nil {
if err := writeString(buf, *r.DefaultLanguage); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func writeString(buf *bytes.Buffer, s string) error {
if len(s) > 255 {
return errors.New("string too long")
}
if err := buf.WriteByte(uint8(len(s))); err != nil {
return err
}
_, err := buf.Write([]byte(s))
return err
}
func writeStringArray(buf *bytes.Buffer, arr []string) error {
if err := writeI8Varint(buf, int8(len(arr))); err != nil {
return err
}
for _, s := range arr {
if err := writeString(buf, s); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,53 @@
package excelLanguage
import (
"bytes"
"encoding/binary"
"io"
)
func writeI8Varint(buf *bytes.Buffer, v int8) error {
uv := uint64((uint32(v) << 1) ^ uint32(v>>7)) // zigzag encode
b := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(b, uv)
_, err := buf.Write(b[:n])
return err
}
func readI8Varint(r *bytes.Reader) (int, error) {
uv, err := binary.ReadUvarint(r)
if err != nil {
return 0, err
}
// zigzag decode
v := int((uv >> 1) ^ uint64((int64(uv&1)<<63)>>63))
return v, nil
}
func readString(r *bytes.Reader) (string, error) {
l, err := r.ReadByte()
if err != nil {
return "", err
}
buf := make([]byte, l)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
}
func readStringArray(r *bytes.Reader) ([]string, error) {
length, err := readI8Varint(r)
if err != nil {
return nil, err
}
arr := make([]string, 0, length)
for i := 0; i < length; i++ {
s, err := readString(r)
if err != nil {
return nil, err
}
arr = append(arr, s)
}
return arr, nil
}

View File

@@ -4,11 +4,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
) )
type BinaryVersion struct { type BinaryVersion struct {
Name string
Major int Major int
Minor int Minor int
Patch int Patch int
@@ -22,42 +24,47 @@ func ParseBinaryVersion(path string) (*BinaryVersion, error) {
content := string(data) content := string(data)
dashPos := strings.LastIndex(content, "-") lastDash := strings.LastIndex(content, "-")
if dashPos == -1 { if lastDash == -1 {
return nil, errors.New("no dash found in version string") return nil, errors.New("no dash found in version string")
} }
start := dashPos - 6 secondLastDash := strings.LastIndex(content[:lastDash], "-")
if start < 0 { if secondLastDash == -1 {
start = 0 return nil, errors.New("only one dash found in version string")
} }
versionSlice := content[start:] versionSlice := content[secondLastDash+1 : lastDash]
end := strings.Index(versionSlice, "-") re := regexp.MustCompile(`^([A-Za-z]+)([\d\.]+)$`)
if end == -1 { matches := re.FindStringSubmatch(versionSlice)
end = len(versionSlice) if len(matches) < 3 {
}
versionStr := versionSlice[:end]
parts := strings.SplitN(versionStr, ".", 3)
if len(parts) != 3 {
return nil, errors.New("invalid version format") return nil, errors.New("invalid version format")
} }
binaryVersion := BinaryVersion{
Name: matches[1],
}
numbers := strings.Split(matches[2], ".")
major, err := strconv.Atoi(parts[0]) if len(numbers) > 0 {
binaryVersion.Major, err = strconv.Atoi(numbers[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
minor, err := strconv.Atoi(parts[1]) }
if len(numbers) > 1 {
binaryVersion.Minor, err = strconv.Atoi(numbers[1])
if err != nil { if err != nil {
return nil, err return nil, err
} }
patch, err := strconv.Atoi(parts[2]) }
if len(numbers) > 2 {
binaryVersion.Patch, err = strconv.Atoi(numbers[2])
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
return &BinaryVersion{major, minor, patch}, nil return &binaryVersion, nil
} }
func (v *BinaryVersion) String() string { func (v *BinaryVersion) String() string {

43
pkg/models/hdiffFiles.go Normal file
View File

@@ -0,0 +1,43 @@
package models
import (
"bufio"
"encoding/json"
"fmt"
"os"
)
type HDiffFiles struct {
RemoteFile string `json:"remoteName"`
}
func (h *HDiffFiles) ToHDiffData() *HDiffData {
return &HDiffData{
SourceFileName: h.RemoteFile,
TargetFileName: h.RemoteFile,
PatchFileName: fmt.Sprintf("%s.hdiff", h.RemoteFile),
}
}
func LoadHDiffFiles(path string) ([]*HDiffFiles, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var results []*HDiffFiles
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
var item HDiffFiles
if err := json.Unmarshal([]byte(line), &item); err == nil {
results = append(results, &item)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return results, nil
}

View File

@@ -13,3 +13,17 @@ type DiffMapType struct {
PatchFileMD5 string `json:"patch_file_md5"` PatchFileMD5 string `json:"patch_file_md5"`
PatchFileSize int64 `json:"patch_file_size"` PatchFileSize int64 `json:"patch_file_size"`
} }
type HDiffData struct {
SourceFileName string `json:"source_file_name"`
TargetFileName string `json:"target_file_name"`
PatchFileName string `json:"patch_file_name"`
}
func (d *DiffMapType) ToHDiffData() *HDiffData {
return &HDiffData{
SourceFileName: d.SourceFileName,
TargetFileName: d.TargetFileName,
PatchFileName: d.PatchFileName,
}
}

36
pkg/models/pkgVersion.go Normal file
View File

@@ -0,0 +1,36 @@
package models
import (
"bufio"
"encoding/json"
"os"
)
type PkgVersion struct {
RemoteFile string `json:"remoteName"`
MD5 string `json:"md5"`
}
func LoadPkgVersion(path string) ([]*PkgVersion, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var results []*PkgVersion
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
var item PkgVersion
if err := json.Unmarshal([]byte(line), &item); err == nil {
results = append(results, &item)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return results, nil
}

View File

@@ -29,6 +29,41 @@ func IsFileIn7z(archivePath, fileInside string) (bool, error) {
return false, fmt.Errorf("%s not found in %s", fileInside, archivePath) return false, fmt.Errorf("%s not found in %s", fileInside, archivePath)
} }
func ListFilesInZip(archivePath string) ([]string, error) {
cmd := exec.Command(constant.Tool7zaExe.String(), "l", archivePath)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("7za list failed: %v\nOutput: %s", err, out.String())
}
lines := strings.Split(out.String(), "\n")
var files []string
foundTable := false
for _, line := range lines {
if strings.HasPrefix(line, "----------") {
if foundTable {
break
}
foundTable = true
continue
}
if foundTable {
fields := strings.Fields(line)
if len(fields) >= 6 {
fileName := strings.Join(fields[5:], " ")
files = append(files, fileName)
}
}
}
return files, nil
}
func ExtractAFileFromZip(archivePath, fileInside, outDir string) error { func ExtractAFileFromZip(archivePath, fileInside, outDir string) error {
cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y") cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y")
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout

View File

@@ -41,17 +41,15 @@ func NewVerifier(gamePath, hdiffPath string) (*Verifier, error) {
func (v *Verifier) VerifyAll() error { func (v *Verifier) VerifyAll() error {
for i, entry := range v.DiffMapEntries { for i, entry := range v.DiffMapEntries {
application.Get().EmitEvent( application.Get().Event.Emit(
"hdiffz:progress", map[string]int{ "hdiffz:progress", map[string]int{
"progress": i, "progress": i,
"maxProgress": len(v.DiffMapEntries), "maxProgress": len(v.DiffMapEntries),
}) })
if err := check(entry.SourceFileName, entry.SourceFileSize, entry.SourceFileMD5, v.GamePath); err != nil { if err := check(entry.SourceFileName, entry.SourceFileSize, entry.SourceFileMD5, v.GamePath); err != nil {
return fmt.Errorf("source_file failed: %w", err) application.Get().Event.Emit("hdiffz:error", err.Error())
continue
} }
// if err := check(entry.PatchFileName, entry.PatchFileSize, entry.PatchFileMD5, v.HdiffPath); err != nil {
// return fmt.Errorf("patch_file failed: %w", err)
// }
} }
return nil return nil
} }