feat(ui): rebrand to G Launcher, add side server selector, update wallpaper and app icon, integrate GaCy-GO PS

This commit is contained in:
CAtlas0784 committed 2026-10-30 22:33:21 +07:00
1 parent 023ae32b58
commit 25843372ad
21 files changed
+2418 -2144

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
import { Link } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import useModalStore from "@/stores/modalStore";
import { Blend, BookOpen, Diff, Home, Info, Languages, Minus, Puzzle, Settings, TrendingUpDown, Wrench, X } from "lucide-react";
import { AppService } from "@bindings/g-launcher/internal/app-service";
+18 -7
View File
@@ -9,7 +9,15 @@ export async function CheckUpdateServer(
serverVersion: string
): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
const [ok, latestVersion, error] = await GitService.GetLatestServerVersion()
const isExists = await FSService.FileExists(serverPath)
let isExists = await FSService.FileExists(serverPath)
if (!isExists) {
const [gameServer] = await GitService.FindServerExecutables()
if (gameServer) {
useSettingStore.getState().setServerPath(gameServer)
isExists = true
}
}
if (!ok) {
toast.error("Server error: " + error)
@@ -23,17 +31,20 @@ export async function CheckUpdateServer(
export async function UpdateServer(serverVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
const {setServerPath, setServerVersion} = useSettingStore.getState()
setDownloadType("Downloading server...")
const {setServerPath, setSdkServerPath, setServerVersion} = useSettingStore.getState()
setDownloadType("Downloading GaCy PS server...")
const [ok, error] = await GitService.DownloadServerProgress(serverVersion)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Configuring server files...")
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
setDownloadType("Download server successfully")
setServerVersion(serverVersion)
setServerPath("./server/firefly-go_win.exe")
setServerPath(gameServer || "./server/gameserver-go.exe")
if (sdkServer) {
setSdkServerPath(sdkServer)
}
} else {
toast.error(error)
setDownloadType("Download server failed")
}
}
}
+184 -91
View File
@@ -1,7 +1,8 @@
import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark, Server, Globe, CheckCircle2 } from 'lucide-react';
import { AppService } from '@bindings/g-launcher/internal/app-service';
import { FSService } from '@bindings/g-launcher/internal/fs-service';
import { GitService } from '@bindings/g-launcher/internal/git-service';
import { toast } from 'react-toastify';
import path from 'path-browserify'
import useSettingStore from '@/stores/settingStore';
@@ -21,11 +22,16 @@ export default function LauncherPage() {
setGamePath,
setGameDir,
serverPath,
setServerPath,
sdkServerPath,
setSdkServerPath,
proxyPath,
gameDir,
serverVersion,
proxyVersion,
background
background,
selectedServer,
setSelectedServer
} = useSettingStore()
const { t } = useTranslation()
const {
@@ -40,8 +46,6 @@ export default function LauncherPage() {
const {
isLoading,
downloadType,
serverReady,
proxyReady,
isDownloading,
serverRunning,
proxyRunning,
@@ -49,7 +53,6 @@ export default function LauncherPage() {
progressDownload,
downloadSpeed,
updateData,
// launcherVersion,
setLauncherVersion,
setIsLoading,
setDownloadType,
@@ -64,13 +67,13 @@ export default function LauncherPage() {
const widgetLinks = [
{
tooltip: "Firefly SRAnalysis",
tooltip: "SRAnalysis",
href: "https://sranalysis.punklorde.org",
img: "https://sranalysis.punklorde.org/ff-sranalysis.png",
btnClass: "btn-primary"
},
{
tooltip: "Firefly SRTools",
tooltip: "SRTools",
href: "https://srtools.punklorde.org",
img: "https://srtools.punklorde.org/ff-srtool.png",
btnClass: "btn-secondary"
@@ -91,7 +94,18 @@ export default function LauncherPage() {
return
}
const serverExists = await FSService.FileExists(serverPath)
let serverExists = await FSService.FileExists(serverPath)
if (!serverExists) {
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
if (gameServer) {
setServerPath(gameServer)
serverExists = true
}
if (sdkServer) {
setSdkServerPath(sdkServer)
}
}
const proxyExists = await FSService.FileExists(proxyPath)
setServerReady(serverExists)
setProxyReady(proxyExists)
@@ -121,17 +135,10 @@ export default function LauncherPage() {
proxy: proxyData,
launcher: launcherData
})
const exitGame = await FSService.FileExists(gamePath)
if (!exitGame) {
setGameRunning(false)
setGamePath("")
setGameDir("")
}
if (!serverData.isExists || !proxyData.isExists) {
setServerReady(false)
setProxyReady(false)
setIsOpenDownloadDataModal(true)
return
}
@@ -175,6 +182,7 @@ export default function LauncherPage() {
const handleStartGame = async () => {
if (!gamePath) {
toast.info("Please select the game executable first")
return
}
if (gameRunning) {
@@ -182,36 +190,59 @@ export default function LauncherPage() {
}
try {
setIsLoading(true)
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
const [resultProxy, error] = await FSService.StartWithConsole(proxyPath)
if (!resultProxy) {
toast.error(t("home.toast_start_proxy_failed") + error)
if (selectedServer === "gacy-ps") {
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
const activeServer = gameServer || serverPath
if (!activeServer) {
toast.error("Please download GaCy PS server files first")
setIsOpenDownloadDataModal(true)
return
}
setProxyRunning(true)
}
await sleep(500)
if (!serverRunning) {
const [resultServer, error] = await FSService.StartWithConsole(serverPath)
if (!resultServer) {
toast.error(t("home.toast_start_server_failed") + error)
return
// 1. Start Proxy
const activeProxy = proxyPath || "./proxy/firefly-go-proxy.exe"
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
const [resultProxy, error] = await FSService.StartWithConsole(activeProxy)
if (!resultProxy) {
toast.error(t("home.toast_start_proxy_failed") + ": " + error)
return
}
setProxyRunning(true)
}
setServerRunning(true)
}
await sleep(1000)
await sleep(500)
// 2. Start SDK Server if exists
const activeSdk = sdkServer || sdkServerPath
if (activeSdk) {
await FSService.StartWithConsole(activeSdk)
await sleep(400)
}
// 3. Start Game Server
if (!serverRunning) {
const [resultServer, error] = await FSService.StartWithConsole(activeServer)
if (!resultServer) {
toast.error(t("home.toast_start_server_failed") + ": " + error)
return
}
setServerRunning(true)
}
await sleep(1000)
}
// 4. Start Game
if (gamePath.endsWith("launcher.exe")) {
const [resultGame, error] = await FSService.StartWithConsole(gamePath)
if (!resultGame) {
toast.error(t("home.toast_start_game_failed") + error)
toast.error(t("home.toast_start_game_failed") + ": " + error)
return
}
} else {
const [resultGame, error] = await FSService.StartApp(gamePath)
if (!resultGame) {
toast.error(t("home.toast_start_game_failed") + error)
toast.error(t("home.toast_start_game_failed") + ": " + error)
return
}
}
@@ -266,58 +297,116 @@ export default function LauncherPage() {
return (
<div className="relative min-h-fit overflow-hidden">
<img
src={background}
src={background || "bg-1.jpeg"}
alt="background"
className="fixed inset-0 w-full h-full object-cover z-0"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement
target.src = "bg-12.jpg"
target.src = "bg-1.jpeg"
}}
/>
{/* Header */}
<header className="hidden sm:flex fixed z-10 items-center justify-between py-6 px-4 ">
<div className="text-2xl font-bold text-white bg-gray-500/5 rounded-full p-1">{t("home.header_title")}</div>
{/* Header Title */}
<header className="hidden sm:flex fixed z-10 items-center justify-between py-6 px-4">
<div className="text-2xl font-black tracking-wide text-white bg-black/40 backdrop-blur-md rounded-2xl px-4 py-1.5 border border-white/10 shadow-lg">
<span className="text-cyan-400">G</span> Launcher
</div>
</header>
<div className="hidden sm:flex fixed top-1/4 right-4 z-10 flex-col space-y-3 bg-white/5 rounded-xl p-3 shadow-lg">
{widgetLinks.map((link, idx) => (
<div key={idx} className="tooltip tooltip-left" data-tip={link.tooltip}>
<a
className={`btn btn-circle ${link.btnClass}`}
target="_blank"
href={link.href}
>
<img
src={link.img}
alt={link.tooltip}
className="w-8 h-8 rounded-full"
/>
</a>
</div>
))}
{/* Side Server Selector */}
<div className="hidden sm:flex fixed top-1/4 right-5 z-20 flex-col items-end gap-3">
<div className="text-[11px] uppercase tracking-wider font-bold text-white/80 bg-black/50 backdrop-blur-md px-3.5 py-1 rounded-full border border-white/15 shadow-sm">
เลือกเซิร์ฟเวอร์ (Select Server)
</div>
<div className="tooltip tooltip-left" data-tip={t("home.tooltip_how_to")}>
<Link
to="/howto"
className="btn btn-warning btn-circle"
>
<MessageCircleQuestionMark className="w-6 h-6 font-bold rounded-full" />
</Link>
{/* Server Option 1: GaCy PS */}
<button
onClick={() => setSelectedServer("gacy-ps")}
className={`flex items-center gap-3 w-64 p-3 rounded-2xl backdrop-blur-md transition-all duration-200 border cursor-pointer ${
selectedServer === "gacy-ps"
? "bg-cyan-500/30 border-cyan-400 text-white shadow-xl shadow-cyan-500/20 ring-2 ring-cyan-400"
: "bg-black/55 border-white/15 text-white/80 hover:bg-black/75 hover:border-cyan-400/50"
}`}
>
<div className={`p-2.5 rounded-xl ${selectedServer === "gacy-ps" ? "bg-cyan-500 text-black font-bold" : "bg-cyan-500/20 text-cyan-400"}`}>
<Server className="w-5 h-5" />
</div>
<div className="flex flex-col text-left flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="font-bold text-sm tracking-tight truncate">GaCy PS</span>
<span className="badge badge-xs badge-info font-mono text-[10px]">4.6.54</span>
</div>
<span className="text-xs text-white/60 truncate">Private Server (Local)</span>
</div>
{selectedServer === "gacy-ps" && (
<CheckCircle2 className="w-5 h-5 text-cyan-400 shrink-0" />
)}
</button>
{/* Server Option 2: Official Server */}
<button
onClick={() => setSelectedServer("official")}
className={`flex items-center gap-3 w-64 p-3 rounded-2xl backdrop-blur-md transition-all duration-200 border cursor-pointer ${
selectedServer === "official"
? "bg-emerald-500/30 border-emerald-400 text-white shadow-xl shadow-emerald-500/20 ring-2 ring-emerald-400"
: "bg-black/55 border-white/15 text-white/80 hover:bg-black/75 hover:border-emerald-400/50"
}`}
>
<div className={`p-2.5 rounded-xl ${selectedServer === "official" ? "bg-emerald-500 text-black font-bold" : "bg-emerald-500/20 text-emerald-400"}`}>
<Globe className="w-5 h-5" />
</div>
<div className="flex flex-col text-left flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="font-bold text-sm tracking-tight truncate">Official Server</span>
<span className="badge badge-xs badge-success font-mono text-[10px]">Live</span>
</div>
<span className="text-xs text-white/60 truncate">Hoyoverse Official</span>
</div>
{selectedServer === "official" && (
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0" />
)}
</button>
{/* External Tools Mini Bar */}
<div className="flex items-center gap-2 pt-2">
{widgetLinks.map((link, idx) => (
<div key={idx} className="tooltip tooltip-bottom" data-tip={link.tooltip}>
<a
className="btn btn-circle btn-sm bg-black/50 border-white/20 hover:border-cyan-400"
target="_blank"
href={link.href}
>
<img
src={link.img}
alt={link.tooltip}
className="w-5 h-5 rounded-full"
/>
</a>
</div>
))}
<div className="tooltip tooltip-bottom" data-tip={t("home.tooltip_how_to")}>
<Link
to="/howto"
className="btn btn-circle btn-sm btn-warning"
>
<MessageCircleQuestionMark className="w-4 h-4 font-bold rounded-full" />
</Link>
</div>
</div>
</div>
<div className="hidden sm:flex fixed top-1/2 left-5 z-10 ">
{/* Left Background Selector */}
<div className="hidden sm:flex fixed top-1/2 left-5 z-10">
<BackgroundSelector />
</div>
{/* Bottom Panel */}
{serverReady && proxyReady && !isDownloading && (
<div className="fixed bottom-0 right-0 p-8 z-2">
{/* Bottom Play Panel: Appears once a server is selected! */}
{selectedServer !== "" && !isDownloading && (
<div className="fixed bottom-0 right-0 p-8 z-2 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div className="flex flex-wrap items-center justify-center gap-2">
{gamePath === "" ? (
<button
className="btn btn-accent btn-xl font-bold"
className="btn btn-accent btn-xl font-bold shadow-xl"
onClick={handlePickFile}
>
<FolderOpen className="w-6 h-6" />
@@ -325,19 +414,27 @@ export default function LauncherPage() {
</button>
) : (
<button
className="btn btn-warning btn-xl font-bold"
className={`btn btn-xl font-bold shadow-2xl transition-all ${
selectedServer === "gacy-ps"
? "btn-warning text-black hover:brightness-110"
: "btn-success text-white hover:brightness-110"
}`}
onClick={handleStartGame}
>
<Play className="w-6 h-6" />
{isLoading ? t("home.btn_selecting") : gameRunning ? t("home.btn_game_running") : t("home.btn_start_game")}
<Play className="w-6 h-6 fill-current" />
{isLoading
? t("home.btn_selecting")
: gameRunning
? t("home.btn_game_running")
: `${t("home.btn_start_game")} [${selectedServer === "gacy-ps" ? "GaCy PS" : "Official"}]`}
</button>
)}
<div className="dropdown dropdown-top dropdown-end">
<div tabIndex={0} role="button" className="btn btn-black btn-circle btn-xl m-1">
<div tabIndex={0} role="button" className="btn btn-black btn-circle btn-xl m-1 border border-white/20 shadow-xl">
<Menu className="w-6 h-6" />
</div>
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<ul tabIndex={0} className="dropdown-content menu bg-black/80 backdrop-blur-md rounded-box z-1 w-56 p-2 shadow-2xl border border-white/10 text-white">
<li><button onClick={handlePickFile}>{t("home.menu_change_path")}</button></li>
<li>
<button
@@ -364,23 +461,16 @@ export default function LauncherPage() {
</button>
</li>
<li>
</li>
<li>
<button disabled={!serverPath} onClick={() => {
if (serverPath) {
FSService.OpenFolder("./server")
}
<button onClick={() => {
FSService.OpenFolder("./server")
}}
>
{t("home.menu_open_server")}
</button>
</li>
<li>
<button disabled={!proxyPath} onClick={() => {
if (proxyPath) {
FSService.OpenFolder("./proxy")
}
<button onClick={() => {
FSService.OpenFolder("./proxy")
}}
>
{t("home.menu_open_proxy")}
@@ -397,20 +487,27 @@ export default function LauncherPage() {
</button>
</li>
</ul>
</div>
</div>
</div>
)}
{/* Downloading */}
{/* Notification when no server is selected yet */}
{selectedServer === "" && !isDownloading && (
<div className="fixed bottom-8 right-8 z-2 flex items-center gap-2 bg-black/60 backdrop-blur-md px-5 py-3 rounded-2xl border border-white/20 text-white shadow-xl animate-pulse">
<span className="text-cyan-400 text-lg">👉</span>
<span className="text-sm font-medium">กรุณาเลือกเซิร์ฟเวอร์ด้านบนเพื่อเริ่มเล่น (Select a server to play)</span>
</div>
)}
{/* Downloading Progress Bar */}
{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/60 backdrop-blur-md rounded-2xl p-4 shadow-2xl border border-white/10">
<div className="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80">
<span>{downloadType}</span>
@@ -434,8 +531,9 @@ export default function LauncherPage() {
</div>
)}
{/* Launcher Self Update Bar */}
{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="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/60 backdrop-blur-md rounded-2xl p-4 shadow-2xl border border-white/10">
<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">
@@ -477,12 +575,7 @@ export default function LauncherPage() {
</div>
)}
<div className="hidden md:block fixed bottom-4 left-5 z-10">
<img src="/heart-hsr.gif" alt="firefly animation" className="rounded-lg w-24 h-24" />
</div>
{/* Modal */}
{/* Modals */}
<UpdateModal
isOpen={isOpenUpdateDataModal}
onClose={() => setIsOpenUpdateDataModal(false)}
+10 -2
View File
@@ -7,9 +7,11 @@ interface SettingState {
gamePath: string;
gameDir: string;
serverPath: string;
sdkServerPath: string;
proxyPath: string;
serverVersion: string;
proxyVersion: string;
selectedServer: string;
closingOption: {
isMinimize: boolean;
isAsk: boolean;
@@ -18,11 +20,13 @@ interface SettingState {
extraBackgrounds: string[];
setExtraBackgrounds: (newExtraBackgrounds: string[]) => void;
setBackground: (newBackground: string) => void;
setSelectedServer: (newServer: string) => void;
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => void;
setLocale: (newLocale: string) => void;
setGamePath: (newGamePath: string) => void;
setGameDir: (newGameDir: string) => void;
setServerPath: (newServerPath: string) => void;
setSdkServerPath: (newSdkServerPath: string) => void;
setProxyPath: (newProxyPath: string) => void;
setServerVersion: (newServerVersion: string) => void;
setProxyVersion: (newProxyVersion: string) => void;
@@ -35,22 +39,26 @@ const useSettingStore = create<SettingState>()(
gamePath: "",
gameDir: "",
serverPath: "",
sdkServerPath: "",
proxyPath: "",
serverVersion: "",
proxyVersion: "",
selectedServer: "",
closingOption: {
isMinimize: false,
isAsk: true,
},
background: "bg-12.jpg",
background: "bg-1.jpeg",
extraBackgrounds: [],
setExtraBackgrounds: (newExtraBackgrounds: string[]) => set({ extraBackgrounds: newExtraBackgrounds }),
setBackground: (newBackground: string) => set({ background: newBackground }),
setSelectedServer: (newServer: string) => set({ selectedServer: newServer }),
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => set({ closingOption: newClosingOption }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
setGamePath: (newGamePath: string) => set({ gamePath: newGamePath }),
setGameDir: (newGameDir: string) => set({ gameDir: newGameDir }),
setServerPath: (newServerPath: string) => set({ serverPath: newServerPath }),
setSdkServerPath: (newSdkServerPath: string) => set({ sdkServerPath: newSdkServerPath }),
setProxyPath: (newProxyPath: string) => set({ proxyPath: newProxyPath }),
setServerVersion: (newServerVersion: string) => set({ serverVersion: newServerVersion }),
setProxyVersion: (newProxyVersion: string) => set({ proxyVersion: newProxyVersion }),
@@ -62,4 +70,4 @@ const useSettingStore = create<SettingState>()(
)
);
export default useSettingStore;
export default useSettingStore;