5 Commits
1.0 ... 1.2.1

Author SHA1 Message Date
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
33 changed files with 639 additions and 201 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

@@ -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,

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

@@ -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") {
setDownloadType("Downloading proxy...")
const [ok, error] = await GitService.DownloadProxyProgress(value.version)
if (ok) {
setDownloadType("Unzipping proxy...")
GitService.UnzipProxy()
setDownloadType("Download proxy successfully")
setProxyVersion(value.version)
setProxyPath("./proxy/FireflyProxy.exe")
} else {
toast.error(error)
setDownloadType("Download proxy failed")
}
}
}
} }
if (updateData.server.isUpdate || !updateData.server.isExists) {
await UpdateServer(updateData.server.version)
setServerReady(true)
setUpdateData({...updateData, server: { isUpdate: false, isExists: true, version: updateData.server.version }})
}
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={() => setIsOpenUpdateDataModal(false)}
onClick={() => setIsOpenNotification(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={() => setIsOpenUpdateDataModal(false)}
onClick={() => setIsOpenNotification(false)} >
> No
Cancel </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,17 +15,44 @@ 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>
<a This site is a another version of {" "}
href="https://srtools.kain.id.vn/" <span className="font-semibold text-success">SR Tools {" "}</span>
className="link link-info" developed by {" "}
target="_blank" <span className="font-semibold text-warning">Me {"(Kain)"}</span>
rel="noopener noreferrer"
>
srtools.kain.id.vn
</a>
</p> </p>
</div> </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
href="https://srtools.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://srtools.kain.id.vn/
</a>
</div>
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 text-lg">🔄</span>
<span className="font-semibold text-blue-800">Backup Website</span>
</div>
<a
href="https://firefly-srtools.vercel.app/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://firefly-srtools.vercel.app/
</a>
</div>
</div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">👨💻</div> <div className="text-blue-600 text-lg">👨💻</div>
<p>The original tool was created by a third-party developer named <span className="font-semibold text-warning">Amazing</span>. This version is directly based on that work, without modification to core logic.</p> <p>The original tool was created by a third-party developer named <span className="font-semibold text-warning">Amazing</span>. This version is directly based on that work, without modification to core logic.</p>

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' />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500"> <div className="flex flex-col justify-center items-start">
her <h1 className="text-xl font-bold">
</span> <span className="text-emerald-500">Firefly </span>
</h1> <span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
<p className="text-sm text-gray-500">By Kain</p> Launcher
</span>
</h1>
<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;

3
go.mod
View File

@@ -9,6 +9,8 @@ require (
golang.org/x/sys v0.28.0 golang.org/x/sys v0.28.0
) )
require aead.dev/minisign v0.2.0 // indirect
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -34,6 +36,7 @@ require (
github.com/lmittmann/tint v1.0.4 // indirect github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/selfupdate v0.6.0
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect

9
go.sum
View File

@@ -1,3 +1,5 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -74,6 +76,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
@@ -111,7 +115,9 @@ 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= 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-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
@@ -123,6 +129,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-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=
@@ -143,6 +150,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
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-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -157,6 +165,7 @@ 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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

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"
) )
@@ -140,3 +139,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)
@@ -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()
@@ -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, ""
}

40
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.
// '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{ 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.2.1"
type ToolFile string type ToolFile string
const ( const (

View File

@@ -47,11 +47,9 @@ func (v *Verifier) VerifyAll() error {
"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().EmitEvent("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
} }