5 Commits
2.0.0 ... 2.2.1

Author SHA1 Message Date
ca612797ee FIX: Drag drop, ui/ux 2025-10-20 17:20:16 +07:00
787962c6d0 UPDATE: Backgroud setting, Remake ui/ux 2025-10-19 23:37:04 +07:00
09434fcc5b UPDATE: System tray 2025-10-13 09:09:55 +07:00
edbe04b9fc UPDATE: System tray setting 2025-10-07 13:14:18 +07:00
a6b49bef24 UPDATE: Change app description 2025-10-07 11:23:39 +07:00
47 changed files with 1132 additions and 545 deletions

View File

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

View File

@@ -8,9 +8,9 @@ info:
companyName: "Firefly Shelter" # The name of the company
productName: "Firefly Launcher" # The name of the application
productIdentifier: "com.fireflyshelter.fireflylauncher" # The unique product identifier
description: "Custom game launcher built for convenience and quick access" # The application description
description: "Firefly Launcher" # The application description
copyright: "@ 2025, Firefly Shelter" # Copyright text
comments: "Custom game launcher built for convenience and quick access" # Comments
comments: "Firefly Launcher" # Comments
version: "1.0.0" # The application version
# Dev mode configuration

View File

@@ -12,7 +12,7 @@
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>Custom game launcher built for convenience and quick access</string>
<string>Firefly Launcher</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleIconFile</key>

View File

@@ -12,7 +12,7 @@
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>Custom game launcher built for convenience and quick access</string>
<string>Firefly Launcher</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleIconFile</key>

View File

@@ -1,7 +1,7 @@
[Desktop Entry]
Version=1.0
Name=Firefly Launcher
Comment=Custom game launcher built for convenience and quick access
Comment=Firefly Launcher
# The Exec line includes %u to pass the URL to the application
Exec=/usr/local/bin/firefly-launcher %u
Terminal=false

View File

@@ -10,7 +10,7 @@ version: "1.0.0"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
description: "Custom game launcher built for convenience and quick access"
description: "Firefly Launcher"
vendor: "Firefly Shelter"
homepage: "https://wails.io"
license: "MIT"

View File

@@ -6,10 +6,10 @@
"0000": {
"ProductVersion": "1.0.0",
"CompanyName": "Firefly Shelter",
"FileDescription": "Custom game launcher built for convenience and quick access",
"FileDescription": "Firefly Launcher",
"LegalCopyright": "@ 2025, Firefly Shelter",
"ProductName": "Firefly Launcher",
"Comments": "Custom game launcher built for convenience and quick access"
"Comments": "Firefly Launcher"
}
}
}

View File

@@ -6,6 +6,13 @@
// @ts-ignore: Unused imports
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function CloseApp() {
return $Call.ByID(3422460836);
}
/**
* @param {number} timeout
* @returns {$CancellablePromise<[boolean, string]>}
@@ -20,3 +27,31 @@ export function CloseAppAfterTimeout(timeout) {
export function GetCurrentLauncherVersion() {
return $Call.ByID(3575133982);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function HideApp() {
return $Call.ByID(88003266);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function MaximizeApp() {
return $Call.ByID(1257306588);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function MinimizeApp() {
return $Call.ByID(3434614194);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function RestoreApp() {
return $Call.ByID(3115625834);
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dracula">
<head>
<meta charset="UTF-8" />

View File

@@ -16,6 +16,7 @@
"path-browserify": "^1.0.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-easy-crop": "^5.5.3",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.14",
"zustand": "^5.0.8"
@@ -3783,6 +3784,12 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-wheel": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
"license": "BSD-3-Clause"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4000,6 +4007,20 @@
"react": "^19.2.0"
}
},
"node_modules/react-easy-crop": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.3.tgz",
"integrity": "sha512-iKwFTnAsq+IVuyF6N0Q3zjRx9DG1NMySkwWxVfM/xAOeHYH1vhvM+V2kFiq5HOIQGWouITjfltCx54mbDpMpmA==",
"license": "MIT",
"dependencies": {
"normalize-wheel": "^1.0.1",
"tslib": "^2.0.1"
},
"peerDependencies": {
"react": ">=16.4.0",
"react-dom": ">=16.4.0"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",

View File

@@ -19,6 +19,7 @@
"path-browserify": "^1.0.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-easy-crop": "^5.5.3",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.14",
"zustand": "^5.0.8"

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

View File

@@ -0,0 +1,83 @@
import { motion } from "motion/react"
import { AppService } from "@bindings/firefly-launcher/internal/app-service"
import { toast } from "react-toastify"
import useSettingStore from "@/stores/settingStore"
export default function CloseModal({
isOpen,
onClose
}: {
isOpen: boolean
onClose: () => void
}) {
if (!isOpen) return null
const { closingOption, setClosingOption } = useSettingStore()
return (
<div className="fixed inset-0 z-50 h-full flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-2xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="border-b border-purple-500/30 px-6 py-4 mb-4 flex justify-between items-center">
<h3 className="font-bold text-xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-600">
Confirm Action
</h3>
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md btn-error absolute right-3 top-3"
onClick={onClose}
>
</motion.button>
</div>
<div className="px-6 pt-2 pb-6">
<p className="mb-4 text-lg">
Do you want to minimize the application to the system tray or close the application?
</p>
<div className="flex items-center mb-4">
<input
id="dontAskAgain"
type="checkbox"
className="checkbox checkbox-sm mr-2"
checked={!closingOption.isAsk}
onChange={(e) => setClosingOption({ isMinimize: closingOption.isMinimize, isAsk: !e.target.checked })}
/>
<label htmlFor="dontAskAgain" className="text-sm font-semibold text-accent">
Do not ask me again
</label>
</div>
<div className="grid grid-cols-2 justify-end gap-3">
<button
className="btn btn-warning"
onClick={async () => {
onClose()
const [success, message] = await AppService.HideApp()
if (!success) toast.error(message)
if (!closingOption.isAsk) {
setClosingOption({ isMinimize: true, isAsk: false })
}
}}
>
Minimize
</button>
<button
className="btn btn-error btn-outline"
onClick={async () => {
onClose()
const [success, message] = await AppService.CloseApp()
if (!success) toast.error(message)
if (!closingOption.isAsk) {
setClosingOption({ isMinimize: false, isAsk: false })
}
}}
>
Close
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,163 @@
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/firefly-launcher/internal/app-service";
export default function Header() {
const { setIsOpenSettingModal } = useModalStore()
const controlButtons = [
{
icon: <Settings className="w-5 h-5" />,
action: () => setIsOpenSettingModal(true),
tip: "Settings",
},
{
icon: <Minus className="w-5 h-5" />,
action: () => AppService.MinimizeApp(),
tip: "Minimize",
},
{
icon: <X className="w-5 h-5" />,
action: () => AppService.CloseApp(),
tip: "Close",
},
]
return (
<div className="navbar sticky top-0 z-50 px-3" style={{ '--wails-draggable': 'drag' } as any}>
<div className="navbar-start">
<div className="dropdown">
<div tabIndex={0} role="button" className="btn btn-ghost md:hidden">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" /> </svg>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-black/50 backdrop-blur-md rounded-box z-1 mt-3 w-52 p-2 shadow">
<li><Link to="/">Home</Link></li>
<li>
<a>Tools</a>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/diff">Diff</Link></li>
</ul>
</li>
<li>
<a>Plugins</a>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li>
</ul>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
</div>
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0">
<div className="flex items-center justify-center">
<img src="/appicon.png" alt="Logo" className='w-13 h-13 rounded-lg mx-2' />
<div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold">
<span className="text-emerald-500"
style={{
textShadow: '0 1px 2px rgba(255, 255, 255, 0.2)',
}}
>Firefly </span>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500"
style={{
textShadow: '0 1px 2px rgba(255, 255, 255, 0.2)',
}}
>
Launcher
</span>
</h1>
<p
className="text-white text-sm"
style={{
textShadow: '0 1px 2px rgba(0, 0, 0, 0.8)',
}}
>
By Kain
</p>
</div>
</div>
</Link>
</div>
<div className="navbar-center hidden md:flex bg-black/40 backdrop-blur-sm rounded-lg shadow-lg">
<ul className="menu menu-horizontal px-1 gap-4 text-white">
<li>
<Link to="/" className="flex items-center gap-2 hover:text-cyan-300 transition-colors">
<Home size={18} /> Home
</Link>
</li>
<li>
<details>
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300 transition-colors">
<Wrench size={18} /> Tools
</summary>
<ul className="p-2 bg-black/75 text-white rounded-lg">
<li>
<Link to="/language" className="flex items-center gap-2 hover:text-cyan-300">
<Languages size={18} /> Language
</Link>
</li>
<li>
<Link to="/diff" className="flex items-center gap-2 hover:text-cyan-300">
<Diff size={18} /> Client update
</Link>
</li>
</ul>
</details>
</li>
<li>
<details>
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300 transition-colors">
<Puzzle size={18} /> Plugins
</summary>
<ul className="p-2 bg-black/75 text-white rounded-lg">
<li>
<Link to="/analysis" className="flex items-center gap-2 hover:text-cyan-300">
<TrendingUpDown size={18} /> Analysis (Veritas)
</Link>
</li>
<li>
<Link to="/srtools" className="flex items-center gap-2 hover:text-cyan-300">
<Blend size={18} /> Firefly Tools
</Link>
</li>
</ul>
</details>
</li>
<li>
<Link to="/howto" className="flex items-center gap-2 hover:text-cyan-300 transition-colors">
<BookOpen size={18} /> How to?
</Link>
</li>
<li>
<Link to="/about" className="flex items-center gap-2 hover:text-cyan-300 transition-colors">
<Info size={18} /> About
</Link>
</li>
</ul>
</div>
<div className="navbar-end flex gap-2 z-52">
<div className="flex items-center gap-2 bg-black/40 backdrop-blur-sm rounded-lg">
{controlButtons.map((btn, i) => (
<div key={i} className="tooltip tooltip-bottom" data-tip={btn.tip}>
<button
onClick={btn.action}
className="btn btn-ghost btn-circle bg-transparent border-none flex items-center justify-center"
>
<div className="hover:text-cyan-300 transition-colors">
{btn.icon}
</div>
</button>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,98 @@
import { CheckUpdateLauncher } from "@/helper"
import useModalStore from "@/stores/modalStore"
import useSettingStore from "@/stores/settingStore"
import useLauncherStore from "@/stores/launcherStore"
import { toast } from "react-toastify"
export default function SettingModal({
isOpen,
onClose
}: {
isOpen: boolean
onClose: () => void
}) {
if (!isOpen) return null
const { setIsOpenSelfUpdateModal } = useModalStore()
const { closingOption, setClosingOption } = useSettingStore()
const { setUpdateData, updateData } = useLauncherStore()
const CheckUpdate = async () => {
const launcherData = await CheckUpdateLauncher()
if (!launcherData.isUpdate) {
toast.success("Launcher is already up to date")
return
}
setUpdateData({
server: updateData.server,
proxy: updateData.proxy,
launcher: launcherData
})
setIsOpenSelfUpdateModal(true)
}
return (
<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-md bg-base-100 text-base-content rounded-2xl border border-purple-500/30 shadow-2xl shadow-purple-500/30 p-6">
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h3 className="font-extrabold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-500">
Settings
</h3>
<button
className="btn btn-circle btn-sm bg-red-600 hover:bg-red-700 text-white border-none shadow-lg"
onClick={onClose}
>
</button>
</div>
{/* Content */}
<div className="flex flex-col gap-6">
{/* Section 1: Launcher Update */}
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
<h4 className="font-bold text-lg mb-2">Launcher Update</h4>
<p className="text-sm text-info mb-3">
Check if your launcher is up to date.
</p>
<button
className="btn btn-primary bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-400 hover:to-red-500 text-white shadow-md hover:shadow-lg transition-all duration-200"
onClick={CheckUpdate}
>
Check for Launcher Updates
</button>
</div>
{/* Section 2: Closing Option */}
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
<h4 className="font-bold text-lg mb-2">Closing Options</h4>
<label className="flex items-start gap-3 cursor-pointer select-none">
<input
type="checkbox"
className="checkbox checkbox-primary w-5 h-5 mt-1"
checked={!closingOption.isAsk}
onChange={(e) => {
setClosingOption({
isMinimize: closingOption.isMinimize,
isAsk: !e.target.checked
})
}}
/>
<div className="flex flex-col">
<span className="text-base font-medium text-info">
Set do not ask again
</span>
<span className="text-sm text-accent">
Next time you close the app, it will automatically{" "}
{closingOption.isMinimize ? "minimize to system tray" : "quit the app"}{" "}
without asking.
</span>
</div>
</label>
</div>
</div>
</div>
</div>
)
}

View File

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

View File

@@ -0,0 +1,64 @@
import { motion } from "framer-motion"
interface UpdateModalProps {
isOpen: boolean
title: string
message: string
buttons: {
text: string
onClick: () => Promise<void> | void
variant?: "primary" | "error" | "outline"
}[]
onClose: () => void
}
export default function UpdateModal({ isOpen, title, message, buttons, onClose }: UpdateModalProps) {
if (!isOpen) return null
return (
<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={onClose}
>
</motion.button>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-accent text-lg">{message}</p>
</div>
<div className="flex justify-end gap-3">
{buttons.map((btn, idx) => (
<motion.button
key={idx}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`btn ${
btn.variant === "primary"
? "btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
: btn.variant === "error"
? "btn-error"
: "btn-outline btn-error"
}`}
onClick={btn.onClick}
>
{btn.text}
</motion.button>
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -2,29 +2,17 @@
import { useEffect } from "react";
import { Events } from "@wailsio/runtime";
import { toast } from "react-toastify";
import useSettingStore from "@/stores/settingStore";
import { AppService } from "@bindings/firefly-launcher/internal/app-service";
import useModalStore from "@/stores/modalStore";
import useDiffStore from "@/stores/diffStore";
import useLauncherStore from "@/stores/launcherStore";
export function useGlobalEvents({
setGameRunning,
setServerRunning,
setProxyRunning,
setProgressUpdate,
setMaxProgressUpdate,
setProgressDownload,
setDownloadSpeed,
setMessageUpdate,
setStageType,
export function useGlobalEvents() {
const { setIsOpenCloseModal } = useModalStore()
const { setGameRunning, setServerRunning, setProxyRunning, setProgressDownload, setDownloadSpeed } = useLauncherStore()
const { setProgressUpdate, setMaxProgressUpdate, setMessageUpdate, setStageType } = useDiffStore()
}: {
setGameRunning: (v: boolean) => void;
setServerRunning: (v: boolean) => void;
setProxyRunning: (v: boolean) => void;
setProgressUpdate: (v: number) => void;
setMaxProgressUpdate: (v: number) => void;
setProgressDownload: (v: number) => void;
setDownloadSpeed: (v: string) => void;
setMessageUpdate: (v: string) => void;
setStageType: (v: string) => void,
}) {
useEffect(() => {
const onGameExit = () => setGameRunning(false);
const onServerExit = () => setServerRunning(false);
@@ -64,6 +52,20 @@ export function useGlobalEvents({
const { message } = event.data[0];
toast.error(message);
});
Events.On("window:close", async () => {
const option = useSettingStore.getState().closingOption
if (option.isAsk) {
setIsOpenCloseModal(true);
return
}
if (option.isMinimize) {
const [success, message] = await AppService.MinimizeApp()
if (!success) toast.error(message)
} else {
const [success, message] = await AppService.CloseApp()
if (!success) toast.error(message)
}
});
return () => {
Events.Off("download:server");
@@ -75,6 +77,7 @@ export function useGlobalEvents({
Events.Off("diff:message");
Events.Off("diff:stage");
Events.Off("version:check");
Events.Off("window:close");
};
}, []);
}

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.
</p>
<p className="text-lg leading-relaxed">
The launcher is built using <span className="font-mono text-info">Go + Wails3</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>.
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-accent">Tailwind CSS</span> and <span className="text-accent">DaisyUI</span>.
</p>
<p className="text-lg leading-relaxed">
My goal is to make tools that are fast, efficient, and enjoyable to use and this launcher is just the beginning.

View File

@@ -64,7 +64,7 @@ export default function AnalysisPage() {
</div>
<a
href="https://sranalysis.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
@@ -79,7 +79,7 @@ export default function AnalysisPage() {
</div>
<a
href="https://firefly-sranalysis.vercel.app/"
className="link link-warning font-mono text-sm break-all"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -309,7 +309,7 @@ export default function DiffPage() {
<div className="w-full p-4">
<div className="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80">
<span className="font-bold text-lg text-warning">{stageType}:</span>
<span className="font-bold text-lg text-accent">{stageType}:</span>
<div className="flex items-center gap-4 ml-2">
{stageType !== 'Cut Data' && <span className="text-white font-bold">{progressUpdate.toFixed(0)} / {maxProgressUpdate.toFixed(0)}</span>}
{stageType === 'Cut Data' && <span className="text-white font-bold truncate max-w-full overflow-hidden whitespace-nowrap">{messageUpdate}</span>}

View File

@@ -19,7 +19,7 @@ export default function FireflyToolsPage() {
This site is a another version of {" "}
<span className="font-semibold text-success">Firefly Tools {" "}</span>
developed by {" "}
<span className="font-semibold text-warning">Me {"(Kain)"}</span>
<span className="font-semibold text-accent">Me {"(Kain)"}</span>
</p>
</div>
<div className="grid md:grid-cols-2 gap-4">
@@ -30,7 +30,7 @@ export default function FireflyToolsPage() {
</div>
<a
href="https://srtools.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
@@ -45,7 +45,7 @@ export default function FireflyToolsPage() {
</div>
<a
href="https://firefly-srtools.vercel.app/"
className="link link-warning font-mono text-sm break-all"
className="link link-accent font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
@@ -55,14 +55,14 @@ export default function FireflyToolsPage() {
</div>
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">👨💻</div>
<p>The original tool was created by a third-party developer named <span className="font-semibold text-warning">Amazing</span>. This version is directly based on that work, without modification to core logic.</p>
<p>The original tool was created by a third-party developer named <span className="font-semibold text-accent">Amazing</span>. This version is directly based on that work, without modification to core logic.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🔗</div>
<p>There is also a more modern version by the same author available at{" "}
<a
href="https://srtools.neonteam.dev/"
className="link link-warning"
className="link link-accent"
target="_blank"
rel="noopener noreferrer"
>
@@ -71,7 +71,7 @@ export default function FireflyToolsPage() {
{" "}and the original version at{" "}
<a
href="https://srtools.pages.dev/"
className="link link-warning"
className="link link-accent"
target="_blank"
rel="noopener noreferrer"
>
@@ -95,14 +95,14 @@ export default function FireflyToolsPage() {
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">🔌</div>
<p>Instantly apply setups to <span className="font-semibold text-warning">Firefly GO Server</span> using <span className="font-semibold">Connect PS</span> no manual file uploads required.</p>
<p>Instantly apply setups to <span className="font-semibold text-accent">Firefly GO Server</span> using <span className="font-semibold">Connect PS</span> no manual file uploads required.</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-2xl"></div>
<div>
<h4 className="font-semibold text-green-800 text-lg">Extra Settings</h4>
<p className="text-green-700 mt-1">
Enhance your <span className="font-semibold text-warning">Firefly GO Server</span> experience with extra features:
Enhance your <span className="font-semibold text-accent">Firefly GO Server</span> experience with extra features:
</p>
<ul className="list-disc list-inside mt-2 space-y-1 text-green-700">
<li>🎭 <span className="font-medium">Hidden Game UI</span> remove the entire game interface.</li>

View File

@@ -56,7 +56,7 @@ export default function HowToPage() {
<p className="text-blue-700 mb-4">
Below are in-game chat commands you can use. Some commands require you to enable{" "}
<span className="font-semibold text-warning">Theorycraft Mode</span>.
<span className="font-semibold text-accent">Theorycraft Mode</span>.
</p>
{/* Theorycraft Mode Warning */}

View File

@@ -208,10 +208,10 @@ export default function LanguagePage() {
<div className="bg-warning/5 rounded-lg p-2 border border-warning/30">
<div className="flex items-center gap-2 mb-1">
<Mic size={20} className="text-warning" />
<span className="font-bold text-warning">Voice Language</span>
<Mic size={20} className="text-accent" />
<span className="font-bold text-accent">Voice Language</span>
</div>
<p className="text-2xl font-bold text-warning">
<p className="text-2xl font-bold text-accent">
{getLanguageLabel(voiceLang)}
</p>
</div>
@@ -250,7 +250,7 @@ export default function LanguagePage() {
{/* Voice Language */}
<div className="space-y-3">
<label className="flex text-sm font-medium text-warning items-center gap-2">
<label className="flex text-sm font-medium text-accent items-center gap-2">
<Mic size={16} />
Voice Language
</label>

View File

@@ -10,6 +10,10 @@ import useLauncherStore from '@/stores/launcherStore';
import { motion } from 'motion/react';
import { Link } from '@tanstack/react-router';
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
import UpdateModal from '@/components/updateModal';
import { BackgroundSelector } from '@/components/backgroudModal';
export default function LauncherPage() {
const { gamePath,
@@ -20,7 +24,9 @@ export default function LauncherPage() {
gameDir,
serverVersion,
proxyVersion,
background
} = useSettingStore()
const {
isOpenDownloadDataModal,
isOpenUpdateDataModal,
@@ -29,6 +35,7 @@ export default function LauncherPage() {
setIsOpenUpdateDataModal,
setIsOpenSelfUpdateModal
} = useModalStore()
const {
isLoading,
downloadType,
@@ -54,6 +61,33 @@ export default function LauncherPage() {
setUpdateData,
} = useLauncherStore()
const widgetLinks = [
{
tooltip: "Firefly SRAnalysis",
href: "https://sranalysis.kain.id.vn/",
img: "https://sranalysis.kain.id.vn/ff-sranalysis.png",
btnClass: "btn-primary"
},
{
tooltip: "Firefly SRTools",
href: "https://srtools.kain.id.vn/",
img: "https://srtools.kain.id.vn/ff-srtool.png",
btnClass: "btn-secondary"
},
{
tooltip: "Amazing's SRTools (Original UI)",
href: "https://srtools.pages.dev/",
img: "https://icons.duckduckgo.com/ip3/srtools.pages.dev.ico",
btnClass: "btn-primary"
},
{
tooltip: "Amazing's SRTools (Modern UI)",
href: "https://srtools.neonteam.dev/",
img: "https://icons.duckduckgo.com/ip3/srtools.neonteam.dev.ico",
btnClass: "btn-secondary"
}
]
useEffect(() => {
const check = async () => {
if (!serverVersion || !proxyVersion) {
@@ -235,78 +269,39 @@ export default function LauncherPage() {
return (
<div className="relative min-h-fit overflow-hidden">
<div
className="fixed inset-0 z-0 w-full h-full"
style={{
backgroundImage: "url('/bg.jpg')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat"
<img
src={background}
alt="background"
className="fixed inset-0 w-full h-full object-cover z-0"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement
target.src = "bg-12.jpg"
}}
></div>
/>
{/* Header */}
<header className="hidden sm:flex fixed z-10 items-center justify-between p-6">
<div className="text-2xl font-bold text-white">Firefly GO</div>
<header className="hidden sm:flex fixed z-10 items-center justify-between py-6 px-4 ">
<div className="text-2xl font-bold text-white bg-gray-500/5 rounded-full p-1">Firefly GO</div>
</header>
<div className="hidden sm:flex fixed top-1/4 left-4 z-10 flex-col space-y-3 bg-black/30 backdrop-blur-md rounded-xl p-3 shadow-lg">
<div className="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>
))}
<div className="tooltip tooltip-right" data-tip="Firefly SRAnalysis">
<a
className="btn btn-circle btn-primary"
target="_blank"
href="https://sranalysis.kain.id.vn/"
>
<img
src="https://sranalysis.kain.id.vn/ff-sranalysis.png"
alt="SRAnalysis Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="Firefly SRTools">
<a
className="btn btn-circle btn-secondary"
target="_blank"
href="https://srtools.kain.id.vn/"
>
<img
src="https://srtools.kain.id.vn/ff-srtool.png"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="Amazing's SRTools (Original UI)">
<a
className="btn btn-circle btn-primary"
target="_blank"
href="https://srtools.pages.dev/"
>
<img
src="https://icons.duckduckgo.com/ip3/srtools.pages.dev.ico"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="Amazing's SRTools (Modern UI)">
<a
className="btn btn-circle btn-secondary"
target="_blank"
href="https://srtools.neonteam.dev/"
>
<img
src="https://icons.duckduckgo.com/ip3/srtools.neonteam.dev.ico"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
<div className="tooltip tooltip-right" data-tip="How to use all tools & commands">
<div className="tooltip tooltip-left" data-tip="How to use all tools & commands">
<Link
to="/howto"
className="btn btn-warning btn-circle"
@@ -316,6 +311,9 @@ export default function LauncherPage() {
</div>
</div>
<div className="hidden sm:flex fixed top-1/2 left-4 z-10 ">
<BackgroundSelector />
</div>
{/* Bottom Panel */}
{serverReady && proxyReady && !isDownloading && (
@@ -345,20 +343,17 @@ export default function LauncherPage() {
</div>
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<li><button onClick={handlePickFile}>Change Game Path</button></li>
<li><button
<li>
<button
onClick={async () => {
const serverData = await CheckUpdateServer(serverPath, serverVersion)
const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
const launcherData = await CheckUpdateLauncher()
setUpdateData({
server: serverData,
proxy: proxyData,
launcher: launcherData
launcher: updateData.launcher
})
if (launcherData.isUpdate) {
setIsOpenSelfUpdateModal(true)
return
}
if (!serverData.isExists || !proxyData.isExists) {
setIsOpenDownloadDataModal(true)
return
@@ -369,8 +364,12 @@ export default function LauncherPage() {
}
toast.success("No updates available")
}}>
Check for Updates
</button></li>
Check for Updates Server & Proxy
</button>
</li>
<li>
</li>
<li><button disabled={!serverPath} onClick={() => {
if (serverPath) {
FSService.OpenFolder("./server")
@@ -423,6 +422,7 @@ export default function LauncherPage() {
</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">
@@ -468,149 +468,86 @@ export default function LauncherPage() {
{/* Version Info */}
{serverReady && proxyReady && !isDownloading && (
<div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-black/20 backdrop-blur-sm rounded-lg p-2 shadow">
<p className="text-primary">Version server: {serverVersion}</p>
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
<p className="mt-2 text-success">Version launcher: {launcherVersion}</p>
<div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-white/5 rounded-lg p-2 shadow">
<p className="text-pink-600 "
style={{
textShadow: `
0.5px 0 rgba(255, 255, 255, 0.5),
-0.5px 0 rgba(255, 255, 255, 0.5),
0 0.5px rgba(255, 255, 255, 0.5),
0 -0.5px rgba(255, 255, 255, 0.5),
0.5px 0.5px rgba(255, 255, 255, 0.5),
-0.5px -0.5px rgba(255, 255, 255, 0.5),
0.5px -0.5px rgba(255, 255, 255, 0.5),
-0.5px 0.5px rgba(255, 255, 255, 0.5)
`,
}}
>Version server: {serverVersion}</p>
<p
className="mt-2 text-purple-600"
style={{
textShadow: `
0.5px 0 rgba(255, 255, 255, 0.5),
-0.5px 0 rgba(255, 255, 255, 0.5),
0 0.5px rgba(255, 255, 255, 0.5),
0 -0.5px rgba(255, 255, 255, 0.5),
0.5px 0.5px rgba(255, 255, 255, 0.5),
-0.5px -0.5px rgba(255, 255, 255, 0.5),
0.5px -0.5px rgba(255, 255, 255, 0.5),
-0.5px 0.5px rgba(255, 255, 255, 0.5)
`,
}}
>Version proxy: {proxyVersion}</p>
<p className="mt-2 text-cyan-600 "
style={{
textShadow: `
0.5px 0 rgba(255, 255, 255, 0.5),
-0.5px 0 rgba(255, 255, 255, 0.5),
0 0.5px rgba(255, 255, 255, 0.5),
0 -0.5px rgba(255, 255, 255, 0.5),
0.5px 0.5px rgba(255, 255, 255, 0.5),
-0.5px -0.5px rgba(255, 255, 255, 0.5),
0.5px -0.5px rgba(255, 255, 255, 0.5),
-0.5px 0.5px rgba(255, 255, 255, 0.5)
`,
}}
>Version launcher: {launcherVersion}</p>
</div>
)}
{/* Modal */}
{isOpenUpdateDataModal && (
<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={() => setIsOpenUpdateDataModal(false)}
>
</motion.button>
<UpdateModal
isOpen={isOpenUpdateDataModal}
onClose={() => setIsOpenUpdateDataModal(false)}
title="Update Data"
message="Do you want to update data server and proxy?"
buttons={[
{ text: "No", onClick: () => setIsOpenUpdateDataModal(false), variant: "outline" },
{ text: "Yes", onClick: async () => { setIsOpenUpdateDataModal(false); await handlerUpdateData() }, variant: "primary" }
]}
/>
<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 Data
</h3>
</div>
<UpdateModal
isOpen={isOpenDownloadDataModal}
onClose={() => setIsOpenDownloadDataModal(false)}
title="Download Data"
message="Data server and proxy download required"
buttons={[
{ text: "Download", onClick: async () => { setIsOpenDownloadDataModal(false); await handlerUpdateData() }, variant: "primary" }
]}
/>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Do you want to update data server and proxy?
</p>
</div>
<UpdateModal
isOpen={isOpenSelfUpdateModal}
onClose={() => setIsOpenSelfUpdateModal(false)}
title="Update Launcher"
message="Do you want to update launcher?"
buttons={[
{ text: "No", onClick: () => setIsOpenSelfUpdateModal(false), variant: "outline" },
{ text: "Yes", onClick: async () => { setIsOpenSelfUpdateModal(false); await handlerUpdateData() }, variant: "primary" }
]}
/>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenUpdateDataModal(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 () => {
setIsOpenUpdateDataModal(false)
await handlerUpdateData()
}}
>
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
</motion.button>
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -1,107 +1,31 @@
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import ThemeController from '../components/themeController'
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { ToastContainer } from 'react-toastify'
import { useGlobalEvents } from '@/hooks';
import useLauncherStore from '@/stores/launcherStore';
import useDiffStore from '@/stores/diffStore';
import useModalStore from '@/stores/modalStore';;
import SettingModal from '@/components/settingModal';
import CloseModal from '@/components/closeModal';
import Header from '@/components/header';
export const Route = createRootRoute({
component: RootLayout
})
function RootLayout() {
const { setGameRunning, setServerRunning, setProxyRunning, setProgressDownload, setDownloadSpeed } = useLauncherStore()
const { setProgressUpdate, setMaxProgressUpdate, setMessageUpdate, setStageType } = useDiffStore()
useGlobalEvents({
setGameRunning,
setServerRunning,
setProxyRunning,
setProgressUpdate,
setMaxProgressUpdate,
setProgressDownload,
setDownloadSpeed,
setMessageUpdate,
setStageType
});
const { setIsOpenCloseModal, isOpenCloseModal, isOpenSettingModal, setIsOpenSettingModal } = useModalStore()
useGlobalEvents();
return (
<>
<div className="navbar bg-base-100 shadow-sm sticky top-0 z-50 px-3">
<div className="navbar-start">
<div className="dropdown">
<div tabIndex={0} role="button" className="btn btn-ghost md:hidden">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" /> </svg>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li><Link to="/">Home</Link></li>
<Header />
<li>
<a>Tools</a>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/diff">Diff</Link></li>
</ul>
</li>
<li>
<a>Plugins</a>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li>
</ul>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
</div>
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<div className="flex items-center justify-center">
<img src="/appicon.png" alt="Logo" className='w-13 h-13 rounded-lg mx-2' />
<div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold">
<span className="text-emerald-500">Firefly </span>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
Launcher
</span>
</h1>
<p className="text-sm text-gray-500">By Kain</p>
</div>
</div>
</Link>
</div>
<div className="navbar-center hidden md:flex">
<ul className="menu menu-horizontal px-1 gap-4">
<li><Link to="/">Home</Link></li>
<li>
<details>
<summary>Tools</summary>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/diff">Diff</Link></li>
</ul>
</details>
</li>
<li>
<details>
<summary>Plugins</summary>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">Firefly Tools</Link></li>
</ul>
</details>
</li>
<li><Link to="/howto">How to?</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
</div>
<div className="navbar-end">
<ThemeController />
</div>
</div>
<div className="min-h-[78vh]">
<Outlet />
</div>
<CloseModal isOpen={isOpenCloseModal} onClose={() => setIsOpenCloseModal(false)} />
<SettingModal isOpen={isOpenSettingModal} onClose={() => setIsOpenSettingModal(false)} />
<ToastContainer />
</>
)

View File

@@ -5,18 +5,26 @@ interface ModalState {
isOpenDownloadDataModal: boolean;
isOpenUpdateDataModal: boolean;
isOpenSelfUpdateModal: boolean;
isOpenCloseModal: boolean;
isOpenSettingModal: boolean;
setIsOpenDownloadDataModal: (modal: boolean) => void;
setIsOpenUpdateDataModal: (modal: boolean) => void;
setIsOpenSelfUpdateModal: (modal: boolean) => void;
setIsOpenCloseModal: (modal: boolean) => void;
setIsOpenSettingModal: (modal: boolean) => void;
}
const useModalStore = create<ModalState>((set, get) => ({
isOpenDownloadDataModal: false,
isOpenUpdateDataModal: false,
isOpenSelfUpdateModal: false,
isOpenCloseModal: false,
isOpenSettingModal: false,
setIsOpenDownloadDataModal: (modal: boolean) => set({ isOpenDownloadDataModal: modal }),
setIsOpenUpdateDataModal: (modal: boolean) => set({ isOpenUpdateDataModal: modal }),
setIsOpenSelfUpdateModal: (modal: boolean) => set({ isOpenSelfUpdateModal: modal }),
setIsOpenCloseModal: (modal: boolean) => set({ isOpenCloseModal: modal }),
setIsOpenSettingModal: (modal: boolean) => set({ isOpenSettingModal: modal }),
}));
export default useModalStore;

View File

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

View File

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

View File

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

View File

@@ -20,3 +20,44 @@ func (a *AppService) CloseAppAfterTimeout(timeout int) (bool, string) {
}()
return true, ""
}
func (a *AppService) CloseApp() (bool, string) {
application.Get().Quit()
return true, ""
}
func (a *AppService) HideApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Hide()
return true, ""
}
func (a *AppService) MinimizeApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Minimise()
return true, ""
}
func (a *AppService) MaximizeApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Maximise()
return true, ""
}
func (a *AppService) RestoreApp() (bool, string) {
window := application.Get().Window.Current()
if window == nil {
return false, "not found window"
}
window.Restore()
return true, ""
}

18
main.go
View File

@@ -17,6 +17,7 @@ import (
"path/filepath"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
//go:embed all:frontend/dist
@@ -84,7 +85,7 @@ func main() {
})
// Create window
app.Window.NewWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Firefly Launcher - Kain",
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
@@ -92,10 +93,12 @@ func main() {
TitleBar: application.MacTitleBarHiddenInset,
},
BackgroundColour: application.NewRGB(27, 38, 54),
Width: 1200,
Height: 600,
Width: 1280,
Height: 720,
URL: "/",
DevToolsEnabled: true,
Frameless: true,
DisableResize: true,
})
iconBytes, _ := tools.ReadFile("assets/appicon.png")
@@ -106,7 +109,7 @@ func main() {
// Attach the window to the system tray
menu := application.NewMenu()
menu.Add("Open").OnClick(func(ctx *application.Context) {
// Handle click
window.Show()
})
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
@@ -114,6 +117,13 @@ func main() {
systemTray.SetMenu(menu)
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Event.Emit("window:close")
e.Cancel()
})
err = app.Run()
if err != nil {
log.Fatal(err)

View File

@@ -10,7 +10,7 @@ const ProxyZipFile = "64bit.zip"
const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp"
const CurrentLauncherVersion = "2.0.0"
const CurrentLauncherVersion = "2.2.1"
type ToolFile string