Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edbe04b9fc | |||
| a6b49bef24 | |||
| dcdb3863ca | |||
| d5710c9102 | |||
| 100c93e8da | |||
| 1e17f76340 | |||
| 1f9c95d6ac | |||
| d58e76d821 | |||
| a4de02bc18 | |||
| a021658fa9 | |||
| 6b222bfa70 |
3
.gitignore
vendored
@@ -1,9 +1,10 @@
|
||||
.task
|
||||
bin
|
||||
bin/
|
||||
tests/
|
||||
server/
|
||||
proxy/
|
||||
temp/
|
||||
.history/
|
||||
frontend/dist
|
||||
frontend/node_modules
|
||||
build/linux/appimage/build
|
||||
|
||||
34
Makefile
Normal file
@@ -0,0 +1,34 @@
|
||||
PROTO_SRC=./proto
|
||||
GEN_DEST=./pkg
|
||||
|
||||
proto:
|
||||
@echo Compiling Protobuf files...
|
||||
protoc --go_out=${GEN_DEST} --go-grpc_out=${GEN_DEST} ${PROTO_SRC}/*.proto
|
||||
@echo Done!
|
||||
|
||||
icon:
|
||||
@echo Updating icons...
|
||||
magick build/appicon.png -define icon:auto-resize=16,24,32,48,64,128,256 build/windows/icon.ico
|
||||
magick build/appicon.png -define icon:auto-resize=16,24,32,48,64,128,256,512 build/windows/icons.ico
|
||||
@echo Done!
|
||||
|
||||
dev:
|
||||
@echo Starting development server...
|
||||
wails3 dev
|
||||
@echo Done!
|
||||
|
||||
build:
|
||||
@echo Building application...
|
||||
wails3 build
|
||||
@echo Done!
|
||||
|
||||
release:
|
||||
@echo Building release application...
|
||||
wails3 package
|
||||
@echo Done!
|
||||
|
||||
update:
|
||||
@echo Updating build assets...
|
||||
wails3 update build-assets -name "firefly-launcher" -binaryname "firefly-launcher" -config build/config.yml
|
||||
@echo Done!
|
||||
|
||||
BIN
assets/appicon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/hpatchz.exe
Normal file
@@ -22,15 +22,19 @@ tasks:
|
||||
- npm install
|
||||
|
||||
build:frontend:
|
||||
label: build:frontend (PRODUCTION={{.PRODUCTION}})
|
||||
summary: Build the frontend project
|
||||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
generates:
|
||||
- dist/*
|
||||
- dist/**/*
|
||||
deps:
|
||||
- task: install:frontend:deps
|
||||
- task: generate:bindings
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
cmds:
|
||||
- npm run {{.BUILD_COMMAND}} -q
|
||||
env:
|
||||
@@ -40,15 +44,19 @@ tasks:
|
||||
|
||||
|
||||
generate:bindings:
|
||||
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
|
||||
summary: Generates bindings for the frontend
|
||||
deps:
|
||||
- task: go:mod:tidy
|
||||
sources:
|
||||
- "**/*.[jt]s"
|
||||
- exclude: frontend/**/*
|
||||
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- "frontend/bindings/**/*"
|
||||
- frontend/bindings/**/*
|
||||
cmds:
|
||||
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
|
||||
|
||||
@@ -58,10 +66,10 @@ tasks:
|
||||
sources:
|
||||
- "appicon.png"
|
||||
generates:
|
||||
- "icons.icns"
|
||||
- "icons.ico"
|
||||
- "darwin/icons.icns"
|
||||
- "windows/icon.ico"
|
||||
cmds:
|
||||
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icons.ico
|
||||
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
|
||||
|
||||
dev:frontend:
|
||||
summary: Runs the frontend in development mode
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 304 KiB |
@@ -8,10 +8,10 @@ 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
|
||||
version: "0.1.0" # The application version
|
||||
comments: "Firefly Launcher" # Comments
|
||||
version: "1.0.0" # The application version
|
||||
|
||||
# Dev mode configuration
|
||||
dev_mode:
|
||||
@@ -44,9 +44,19 @@ dev_mode:
|
||||
type: primary
|
||||
|
||||
# File Associations
|
||||
# More information at: https://v3alpha.wails.io/noit/done/yet
|
||||
# More information at: https://v3.wails.io/noit/done/yet
|
||||
fileAssociations:
|
||||
|
||||
# - ext: wails
|
||||
# name: Wails
|
||||
# description: Wails Application File
|
||||
# iconName: wailsFileIcon
|
||||
# role: Editor
|
||||
# - ext: jpg
|
||||
# name: JPEG
|
||||
# description: Image File
|
||||
# iconName: jpegFileIcon
|
||||
# role: Editor
|
||||
# mimeType: image/jpeg # (optional)
|
||||
|
||||
# Other data
|
||||
other:
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.fireflyshelter.fireflylauncher</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<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>0.1.0</string>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.fireflyshelter.fireflylauncher</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<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>0.1.0</string>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
@@ -9,6 +9,11 @@ tasks:
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||
@@ -64,7 +69,13 @@ tasks:
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
||||
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS
|
||||
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
|
||||
|
||||
@@ -9,6 +9,11 @@ tasks:
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||
@@ -105,7 +110,7 @@ tasks:
|
||||
vars:
|
||||
APP_NAME: '{{.APP_NAME}}'
|
||||
EXEC: '{{.APP_NAME}}'
|
||||
ICON: 'appicon'
|
||||
ICON: '{{.APP_NAME}}'
|
||||
CATEGORIES: 'Development;'
|
||||
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
|
||||
|
||||
|
||||
@@ -14,12 +14,21 @@ cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
|
||||
cp "${ICON_PATH}" "${APP_DIR}/"
|
||||
cp "${DESKTOP_FILE}" "${APP_DIR}/"
|
||||
|
||||
# Download linuxdeploy and make it executable
|
||||
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage
|
||||
if [[ $(uname -m) == *x86_64* ]]; then
|
||||
# Download linuxdeploy and make it executable
|
||||
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage
|
||||
|
||||
# Run linuxdeploy to bundle the application
|
||||
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||
# Run linuxdeploy to bundle the application
|
||||
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||
else
|
||||
# Download linuxdeploy and make it executable (arm64)
|
||||
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
|
||||
chmod +x linuxdeploy-aarch64.AppImage
|
||||
|
||||
# Run linuxdeploy to bundle the application (arm64)
|
||||
./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||
fi
|
||||
|
||||
# Rename the generated AppImage
|
||||
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
|
||||
|
||||
13
build/linux/desktop
Normal file
@@ -0,0 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Firefly Launcher
|
||||
Comment=Firefly Launcher
|
||||
# The Exec line includes %u to pass the URL to the application
|
||||
Exec=/usr/local/bin/firefly-launcher %u
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=firefly-launcher
|
||||
Categories=Utility;
|
||||
StartupWMClass=firefly-launcher
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
name: "firefly-launcher"
|
||||
arch: ${GOARCH}
|
||||
platform: "linux"
|
||||
version: "0.1.0"
|
||||
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"
|
||||
@@ -24,9 +24,31 @@ contents:
|
||||
- src: "./build/linux/firefly-launcher.desktop"
|
||||
dst: "/usr/share/applications/firefly-launcher.desktop"
|
||||
|
||||
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1
|
||||
depends:
|
||||
- libgtk-3-0
|
||||
- libwebkit2gtk-4.1-0
|
||||
|
||||
# Distribution-specific overrides for different package formats and WebKit versions
|
||||
overrides:
|
||||
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
|
||||
rpm:
|
||||
depends:
|
||||
- gtk3
|
||||
- libwebkit2gtk
|
||||
- webkit2gtk4.1
|
||||
|
||||
# Arch Linux packages (WebKit 4.1)
|
||||
archlinux:
|
||||
depends:
|
||||
- gtk3
|
||||
- webkit2gtk-4.1
|
||||
|
||||
# scripts section to ensure desktop database is updated after install
|
||||
scripts:
|
||||
postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
|
||||
# You can also add preremove, postremove if needed
|
||||
# preremove: "./build/linux/nfpm/scripts/preremove.sh"
|
||||
# postremove: "./build/linux/nfpm/scripts/postremove.sh"
|
||||
|
||||
# replaces:
|
||||
# - foobar
|
||||
@@ -43,8 +65,3 @@ depends:
|
||||
# - not-foo
|
||||
# - not-bar
|
||||
# changelog: "changelog.yaml"
|
||||
# scripts:
|
||||
# preinstall: ./build/linux/nfpm/scripts/preinstall.sh
|
||||
# postinstall: ./build/linux/nfpm/scripts/postinstall.sh
|
||||
# preremove: ./build/linux/nfpm/scripts/preremove.sh
|
||||
# postremove: ./build/linux/nfpm/scripts/postremove.sh
|
||||
|
||||
@@ -1 +1,21 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
# Update desktop database for .desktop file changes
|
||||
# This makes the application appear in application menus and registers its capabilities.
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
echo "Updating desktop database..."
|
||||
update-desktop-database -q /usr/share/applications
|
||||
else
|
||||
echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
|
||||
fi
|
||||
|
||||
# Update MIME database for custom URL schemes (x-scheme-handler)
|
||||
# This ensures the system knows how to handle your custom protocols.
|
||||
if command -v update-mime-database >/dev/null 2>&1; then
|
||||
echo "Updating MIME database..."
|
||||
update-mime-database -n /usr/share/mime
|
||||
else
|
||||
echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -10,7 +10,10 @@ tasks:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
PRODUCTION: '{{.PRODUCTION}}'
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- task: generate:syso
|
||||
@@ -28,9 +31,11 @@ tasks:
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application into a `.exe` bundle
|
||||
summary: Packages a production build of the application
|
||||
vars:
|
||||
FORMAT: '{{.FORMAT | default "nsis"}}'
|
||||
cmds:
|
||||
- task: create:nsis:installer
|
||||
- task: '{{if eq .FORMAT "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
|
||||
|
||||
generate:syso:
|
||||
summary: Generates Windows `.syso` file
|
||||
@@ -49,12 +54,42 @@ tasks:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
|
||||
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}\build\windows\nsis"
|
||||
- makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
|
||||
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
|
||||
- cmd: >
|
||||
powershell -NoProfile -Command "& makensis ('-DARG_WAILS_{{.ARG_FLAG}}_BINARY=' + (\"{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe\" -replace '/', '\\')) 'project.nsi'"
|
||||
platforms: [windows]
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||
|
||||
create:msix:package:
|
||||
summary: Creates an MSIX package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- |-
|
||||
wails3 tool msix \
|
||||
--config "{{.ROOT_DIR}}/wails.json" \
|
||||
--name "{{.APP_NAME}}" \
|
||||
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
|
||||
--arch "{{.ARCH}}" \
|
||||
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
|
||||
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
|
||||
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
|
||||
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
CERT_PATH: '{{.CERT_PATH | default ""}}'
|
||||
PUBLISHER: '{{.PUBLISHER | default ""}}'
|
||||
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
||||
|
||||
install:msix:tools:
|
||||
summary: Installs tools required for MSIX packaging
|
||||
cmds:
|
||||
- wails3 tool msix-install-tools
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 592 KiB |
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "0.1.0"
|
||||
"file_version": "1.0.0"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "0.1.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
build/windows/msix/app_manifest.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
|
||||
|
||||
<Identity
|
||||
Name="com.wails.firefly-launcher"
|
||||
Publisher="CN=My Company"
|
||||
Version="0.1.0.0"
|
||||
ProcessorArchitecture="x64" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>My Product</DisplayName>
|
||||
<PublisherDisplayName>My Company</PublisherDisplayName>
|
||||
<Description>My Product Description</Description>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="en-us" />
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="com.wails.firefly-launcher" Executable="firefly-launcher.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="My Product"
|
||||
Description="My Product Description"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
|
||||
<Extensions>
|
||||
<desktop:Extension Category="windows.fullTrustProcess" Executable="firefly-launcher.exe" />
|
||||
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
|
||||
</Capabilities>
|
||||
</Package>
|
||||
54
build/windows/msix/template.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MsixPackagingToolTemplate
|
||||
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
|
||||
<Settings
|
||||
AllowTelemetry="false"
|
||||
ApplyACLsToPackageFiles="true"
|
||||
GenerateCommandLineFile="true"
|
||||
AllowPromptForPassword="false">
|
||||
</Settings>
|
||||
<Installer
|
||||
Path="firefly-launcher.exe"
|
||||
Arguments=""
|
||||
InstallLocation="C:\Program Files\My Company\My Product">
|
||||
</Installer>
|
||||
<PackageInformation
|
||||
PackageName="My Product"
|
||||
PackageDisplayName="My Product"
|
||||
PublisherName="CN=My Company"
|
||||
PublisherDisplayName="My Company"
|
||||
Version="0.1.0.0"
|
||||
PackageDescription="My Product Description">
|
||||
<Capabilities>
|
||||
<Capability Name="runFullTrust" />
|
||||
|
||||
</Capabilities>
|
||||
<Applications>
|
||||
<Application
|
||||
Id="com.wails.firefly-launcher"
|
||||
Description="My Product Description"
|
||||
DisplayName="My Product"
|
||||
ExecutableName="firefly-launcher.exe"
|
||||
EntryPoint="Windows.FullTrustApplication">
|
||||
|
||||
</Application>
|
||||
</Applications>
|
||||
<Resources>
|
||||
<Resource Language="en-us" />
|
||||
</Resources>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
<Properties>
|
||||
<Framework>false</Framework>
|
||||
<DisplayName>My Product</DisplayName>
|
||||
<PublisherDisplayName>My Company</PublisherDisplayName>
|
||||
<Description>My Product Description</Description>
|
||||
<Logo>Assets\AppIcon.png</Logo>
|
||||
</Properties>
|
||||
</PackageInformation>
|
||||
<SaveLocation PackagePath="firefly-launcher.msix" />
|
||||
<PackageIntegrity>
|
||||
<CertificatePath></CertificatePath>
|
||||
</PackageIntegrity>
|
||||
</MsixPackagingToolTemplate>
|
||||
@@ -20,10 +20,10 @@ Unicode true
|
||||
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "my-project" # Default "firefly-launcher"
|
||||
## !define INFO_COMPANYNAME "My Company" # Default "FireflyShelter"
|
||||
## !define INFO_PRODUCTNAME "My Product Name" # Default "Firefly Launcher"
|
||||
## !define INFO_COMPANYNAME "My Company" # Default "My Company"
|
||||
## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
|
||||
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "@ 2025, FireflyShelter"
|
||||
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
@@ -92,6 +92,7 @@ Section
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
@@ -107,6 +108,7 @@ Section "uninstall"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
!define INFO_PRODUCTNAME "Firefly Launcher"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "0.1.0"
|
||||
!define INFO_PRODUCTVERSION "1.0.0"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "@ 2025, Firefly Shelter"
|
||||
@@ -158,7 +158,7 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
@@ -210,3 +210,27 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
; Delete app associations
|
||||
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
|
||||
!macroend
|
||||
@@ -1,25 +1,22 @@
|
||||
<?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">
|
||||
<assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="0.1.0" processorArchitecture="*"/>
|
||||
<assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="1.0.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<!-- Yêu cầu quyền admin -->
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,36 @@
|
||||
// @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, 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]>}
|
||||
*/
|
||||
export function CloseAppAfterTimeout(timeout) {
|
||||
return $Call.ByID(1705931481, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function GetCurrentLauncherVersion() {
|
||||
return $Call.ByID(3575133982);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function MinimizeApp() {
|
||||
return $Call.ByID(3434614194);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as AppService from "./appservice.js";
|
||||
export {
|
||||
AppService
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
// @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;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* @param {string} patchPath
|
||||
* @returns {$CancellablePromise<[boolean, string, string]>}
|
||||
*/
|
||||
export function CheckTypeHDiff(patchPath) {
|
||||
return $Call.ByID(3717449114, patchPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function CutData(gamePath) {
|
||||
return $Call.ByID(2019290107, gamePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @param {string} patchPath
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function DataExtract(gamePath, patchPath) {
|
||||
return $Call.ByID(2161622254, gamePath, patchPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function DeleteFiles(gamePath) {
|
||||
return $Call.ByID(1103091613, gamePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function HDiffPatchData(gamePath) {
|
||||
return $Call.ByID(3944051994, gamePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function LDiffPatchData(gamePath) {
|
||||
return $Call.ByID(360123238, gamePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @param {string} patchPath
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function VersionValidate(gamePath, patchPath) {
|
||||
return $Call.ByID(2105077257, gamePath, patchPath);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as DiffService from "./diffservice.js";
|
||||
export {
|
||||
DiffService
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function DirExists(path) {
|
||||
return $Call.ByID(1772289644, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function FileExists(path) {
|
||||
return $Call.ByID(1782610747, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} archivePath
|
||||
* @param {string} fileInside
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function FileExistsInZip(archivePath, fileInside) {
|
||||
return $Call.ByID(2509699047, archivePath, fileInside);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function OpenFolder(path) {
|
||||
return $Call.ByID(1635714001, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filter
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function PickFile(filter) {
|
||||
return $Call.ByID(3756474934, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function PickFolder() {
|
||||
return $Call.ByID(3654471460);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function StartApp(path) {
|
||||
return $Call.ByID(1267568402, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function StartWithConsole(path) {
|
||||
return $Call.ByID(3249271428, path);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as FSService from "./fsservice.js";
|
||||
export {
|
||||
FSService
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
// @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 {string} path
|
||||
* @returns {Promise<boolean> & { cancel(): void }}
|
||||
*/
|
||||
export function DirExists(path) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(1291974398, path));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Promise<boolean> & { cancel(): void }}
|
||||
*/
|
||||
export function FileExists(path) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3373618865, path));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} archivePath
|
||||
* @param {string} fileInside
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function FileExistsInZip(archivePath, fileInside) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(1737012553, archivePath, fileInside));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function OpenFolder(path) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(2733319263, path));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string> & { cancel(): void }}
|
||||
*/
|
||||
export function PickFile() {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(2181012076));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string> & { cancel(): void }}
|
||||
*/
|
||||
export function PickFolder() {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3906046322));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Promise<boolean> & { cancel(): void }}
|
||||
*/
|
||||
export function StartApp(path) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3825990132, path));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Promise<boolean> & { cancel(): void }}
|
||||
*/
|
||||
export function StartWithConsole(path) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(2364569062, path));
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* @param {string} version
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function DownloadProxyProgress(version) {
|
||||
return $Call.ByID(3559275334, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} version
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function DownloadServerProgress(version) {
|
||||
return $Call.ByID(1954767259, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<[boolean, string, string]>}
|
||||
*/
|
||||
export function GetLatestLauncherVersion() {
|
||||
return $Call.ByID(262637602);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<[boolean, string, string]>}
|
||||
*/
|
||||
export function GetLatestProxyVersion() {
|
||||
return $Call.ByID(289488362);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<[boolean, string, string]>}
|
||||
*/
|
||||
export function GetLatestServerVersion() {
|
||||
return $Call.ByID(2918980975);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function UnzipProxy() {
|
||||
return $Call.ByID(2563246729);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function UnzipServer() {
|
||||
return $Call.ByID(1126363284);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} version
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function UpdateLauncherProgress(version) {
|
||||
return $Call.ByID(97272861, version);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as GitService from "./gitservice.js";
|
||||
export {
|
||||
GitService
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
// @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 {string} version
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function DownloadProxyProgress(version) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(1951249093, version));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} version
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function DownloadServerProgress(version) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(314135954, version));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function GetLatestLauncherVersion() {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3191972855));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function GetLatestProxyVersion() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void> & { cancel(): void }}
|
||||
*/
|
||||
export function UnzipProxy() {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(4071181044));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void> & { cancel(): void }}
|
||||
*/
|
||||
export function UnzipServer() {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(4110296071));
|
||||
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;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// @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 {string} patchPath
|
||||
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function CheckTypeHDiff(patchPath) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(1068035136, patchPath));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function CutData(gamePath) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3671642725, gamePath));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @param {string} patchPath
|
||||
* @param {boolean} isSkipVerify
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function DataExtract(gamePath, patchPath, isSkipVerify) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath, isSkipVerify));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function DeleteFiles(gamePath) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(989019003, gamePath));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function PatchData(gamePath) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3608591627, gamePath));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} gamePath
|
||||
* @param {string} patchPath
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function VersionValidate(gamePath, patchPath) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3916254383, gamePath, patchPath));
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as AppService from "./appservice.js";
|
||||
import * as FSService from "./fsservice.js";
|
||||
import * as GitService from "./gitservice.js";
|
||||
import * as HdiffzService from "./hdiffzservice.js";
|
||||
import * as LanguageService from "./languageservice.js";
|
||||
export {
|
||||
AppService,
|
||||
FSService,
|
||||
GitService,
|
||||
HdiffzService,
|
||||
LanguageService
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as LanguageService from "./languageservice.js";
|
||||
export {
|
||||
LanguageService
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<[boolean, string, string, string]>}
|
||||
*/
|
||||
export function GetLanguage(path) {
|
||||
return $Call.ByID(3574191687, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {string} text
|
||||
* @param {string} voice
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function SetLanguage(path, text, voice) {
|
||||
return $Call.ByID(2768939795, path, text, voice);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// @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 {string} path
|
||||
* @returns {Promise<[boolean, string, string, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function GetLanguage(path) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path));
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {string} text
|
||||
* @param {string} voice
|
||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||
*/
|
||||
export function SetLanguage(path, text, voice) {
|
||||
let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice));
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="cupcake" lang="en">
|
||||
<head>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/appicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wails + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<title>Firefly Launcher</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1326
frontend/package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tanstack/react-router": "^1.131.27",
|
||||
"@tanstack/react-router-devtools": "^1.131.27",
|
||||
"lucide-react": "^0.541.0",
|
||||
@@ -20,7 +20,7 @@
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -33,7 +33,7 @@
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"@vitejs/plugin-react": "^5.0.1",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.66",
|
||||
"daisyui": "^5.0.50",
|
||||
"daisyui": "^5.1.27",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
|
||||
BIN
frontend/public/appicon.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
83
frontend/src/components/closeModal/index.tsx
Normal 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.MinimizeApp()
|
||||
if (!success) toast.error(message)
|
||||
if (!closingOption.isAsk) {
|
||||
setClosingOption({ isMinimize: true, isAsk: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Minimize
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-error btn-outline"
|
||||
onClick={async () => {
|
||||
onClose()
|
||||
const [success, message] = await AppService.CloseApp()
|
||||
if (!success) toast.error(message)
|
||||
if (!closingOption.isAsk) {
|
||||
setClosingOption({ isMinimize: false, isAsk: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
86
frontend/src/components/header/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import ThemeController from "../themeController";
|
||||
import useModalStore from "@/stores/modalStore";
|
||||
import { Settings2 } from "lucide-react";
|
||||
|
||||
export default function Header() {
|
||||
const { setIsOpenSettingModal } = useModalStore()
|
||||
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>
|
||||
|
||||
<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 flex gap-2">
|
||||
<ThemeController />
|
||||
<button className="btn btn-ghost btn-circle" onClick={() => setIsOpenSettingModal(true)}>
|
||||
<Settings2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
99
frontend/src/components/settingModal/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
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) => {
|
||||
console.log(!e.target.checked)
|
||||
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-warning">
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ThemeController() {
|
||||
const [theme, setTheme] = useState(localStorage.getItem("theme") ?? "light");
|
||||
const [theme, setTheme] = useState(localStorage.getItem("theme") ?? "night");
|
||||
|
||||
const handleToggle = (e: any) => {
|
||||
if (e.target.checked) {
|
||||
setTheme("cupcake");
|
||||
|
||||
64
frontend/src/components/updateModal/index.tsx
Normal 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-warning 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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import useLauncherStore from "@/stores/launcherStore";
|
||||
import { AppService, GitService } from "@bindings/firefly-launcher/internal";
|
||||
import { AppService, } from "@bindings/firefly-launcher/internal/app-service";
|
||||
import { toast } from "react-toastify";
|
||||
import { sleep } from "./sleep";
|
||||
import { GitService } from "@bindings/firefly-launcher/internal/git-service";
|
||||
|
||||
export async function CheckUpdateLauncher(): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
|
||||
const [currentOk, currentVersion] = await AppService.GetCurrentLauncherVersion()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import useLauncherStore from "@/stores/launcherStore";
|
||||
import useSettingStore from "@/stores/settingStore";
|
||||
import { FSService, GitService } from "@bindings/firefly-launcher/internal";
|
||||
import { FSService } from "@bindings/firefly-launcher/internal/fs-service";
|
||||
import { GitService } from "@bindings/firefly-launcher/internal/git-service";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export async function CheckUpdateProxy(proxyPath: string, proxyVersion: string) : Promise<{isUpdate: boolean, isExists: boolean, version: string}> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import useLauncherStore from '@/stores/launcherStore';
|
||||
import useSettingStore from '@/stores/settingStore';
|
||||
import { FSService, GitService } from '@bindings/firefly-launcher/internal';
|
||||
import { FSService } from '@bindings/firefly-launcher/internal/fs-service';
|
||||
import { GitService } from '@bindings/firefly-launcher/internal/git-service';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export async function CheckUpdateServer(
|
||||
|
||||
@@ -2,27 +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,
|
||||
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: number) => void;
|
||||
setMessageUpdate: (v: string) => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const onGameExit = () => setGameRunning(false);
|
||||
const onServerExit = () => setServerRunning(false);
|
||||
@@ -31,7 +21,7 @@ export function useGlobalEvents({
|
||||
const onDownload = (event: any) => {
|
||||
const { percent, speed } = event.data[0];
|
||||
setProgressDownload(Number(percent));
|
||||
setDownloadSpeed(Number(speed));
|
||||
setDownloadSpeed(speed);
|
||||
};
|
||||
|
||||
const onUpdateProgress = (event: any) => {
|
||||
@@ -45,17 +35,37 @@ export function useGlobalEvents({
|
||||
setMessageUpdate(message);
|
||||
};
|
||||
|
||||
const onStageUpdate = (event: any) => {
|
||||
const { stage } = event.data[0];
|
||||
setStageType(stage);
|
||||
};
|
||||
|
||||
Events.On("download:server", onDownload);
|
||||
Events.On("download:proxy", onDownload);
|
||||
Events.On("game:exit", onGameExit);
|
||||
Events.On("server:exit", onServerExit);
|
||||
Events.On("proxy:exit", onProxyExit);
|
||||
Events.On("hdiffz:progress", onUpdateProgress);
|
||||
Events.On("hdiffz:message", onMessageUpdate);
|
||||
Events.On("hdiffz:error", (event: any) => {
|
||||
Events.On("diff:progress", onUpdateProgress);
|
||||
Events.On("diff:message", onMessageUpdate);
|
||||
Events.On("diff:stage", onStageUpdate);
|
||||
Events.On("diff:error", (event: any) => {
|
||||
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");
|
||||
@@ -63,9 +73,11 @@ export function useGlobalEvents({
|
||||
Events.Off("game:exit");
|
||||
Events.Off("server:exit");
|
||||
Events.Off("proxy:exit");
|
||||
Events.Off("hdiffz:progress");
|
||||
Events.Off("hdiffz:message");
|
||||
Events.Off("diff:progress")
|
||||
Events.Off("diff:message");
|
||||
Events.Off("diff:stage");
|
||||
Events.Off("version:check");
|
||||
Events.Off("window:close");
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -4,20 +4,15 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import './styles/index.css'
|
||||
import "../runtime.js"
|
||||
import "@wailsio/runtime";
|
||||
// Import the generated route tree
|
||||
import { routeTree } from './routeTree.gen.js'
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById('root')!
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from "@tanstack/react-router";
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="max-w-3xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-6">
|
||||
<div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-6">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">About</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router';
|
||||
export default function AnalysisPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">
|
||||
Firefly Analysis & Veritas Plugin
|
||||
</h1>
|
||||
@@ -144,7 +144,7 @@ export default function AnalysisPage() {
|
||||
<div className="space-y-2 text-purple-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">1.</span>
|
||||
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Firefly Private Server (PS)</span>.</p>
|
||||
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Firefly GO Server (PS)</span>.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
|
||||
|
||||
@@ -2,11 +2,12 @@ import useSettingStore from "@/stores/settingStore"
|
||||
import { Check, Folder, File, X, Settings } from "lucide-react"
|
||||
import { useEffect } from "react"
|
||||
import { toast } from "react-toastify"
|
||||
import { FSService, HdiffzService} from "@bindings/firefly-launcher/internal"
|
||||
import { DiffService} from "@bindings/firefly-launcher/internal/diff-service"
|
||||
import { FSService } from "@bindings/firefly-launcher/internal/fs-service"
|
||||
import { motion } from "motion/react"
|
||||
import useHdiffzStore from "@/stores/hdiffzStore"
|
||||
import useDiffStore from "@/stores/diffStore"
|
||||
|
||||
export default function HdiffzPage() {
|
||||
export default function DiffPage() {
|
||||
const { gameDir, setGameDir } = useSettingStore()
|
||||
const {
|
||||
isLoading,
|
||||
@@ -27,7 +28,7 @@ export default function HdiffzPage() {
|
||||
setStageType,
|
||||
messageUpdate,
|
||||
setMessageUpdate
|
||||
} = useHdiffzStore()
|
||||
} = useDiffStore()
|
||||
|
||||
useEffect(() => {
|
||||
const getLanguage = async () => {
|
||||
@@ -78,7 +79,7 @@ export default function HdiffzPage() {
|
||||
const handlePickDiffFile = async () => {
|
||||
try {
|
||||
setIsLoading({game: false, diff: true})
|
||||
const basePath = await FSService.PickFile()
|
||||
const basePath = await FSService.PickFile("")
|
||||
if (basePath) {
|
||||
if (!basePath.endsWith(".7z") && !basePath.endsWith(".zip") && !basePath.endsWith(".rar")) {
|
||||
toast.error('Not valid file type')
|
||||
@@ -86,13 +87,6 @@ export default function HdiffzPage() {
|
||||
setDiffDir('')
|
||||
return
|
||||
}
|
||||
const [exists, error] = await FSService.FileExistsInZip(basePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
|
||||
if (!exists) {
|
||||
toast.error(error)
|
||||
setDiffCheckResult('error')
|
||||
setDiffDir('')
|
||||
return
|
||||
}
|
||||
setDiffDir(basePath)
|
||||
setDiffCheckResult('success')
|
||||
} else {
|
||||
@@ -109,79 +103,79 @@ export default function HdiffzPage() {
|
||||
}
|
||||
|
||||
const handleUpdateGame = async () => {
|
||||
const handleResult = (ok: boolean, error: string) => {
|
||||
if (!ok) {
|
||||
toast.error(error)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
setIsDiffLoading(true)
|
||||
|
||||
if (!gameDir || !diffDir) {
|
||||
toast.error('Please select game directory and diff file')
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setStageType('Check Type HDiff')
|
||||
setProgressUpdate(0)
|
||||
setMaxProgressUpdate(1)
|
||||
const [isOk, validType, errorType] = await HdiffzService.CheckTypeHDiff(diffDir)
|
||||
if (!isOk) {
|
||||
toast.error(errorType)
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir)
|
||||
if (!handleResult(isOk, errorType)) return
|
||||
setProgressUpdate(1)
|
||||
|
||||
if (validType === 'hdiffmap.json') {
|
||||
if (['hdiffmap.json', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)) {
|
||||
setStageType('Version Validate')
|
||||
setProgressUpdate(0)
|
||||
setMaxProgressUpdate(1)
|
||||
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
|
||||
if (!validVersion) {
|
||||
toast.error(errorVersion)
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
}
|
||||
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir)
|
||||
if (!handleResult(validVersion, errorVersion)) return
|
||||
setProgressUpdate(1)
|
||||
}
|
||||
|
||||
setStageType('Data Extract')
|
||||
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir, validType === 'hdifffiles.txt')
|
||||
if (!validData) {
|
||||
toast.error(errorData)
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
}
|
||||
const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir)
|
||||
if (!handleResult(validData, errorData)) return
|
||||
|
||||
setStageType('Cut Data')
|
||||
setMessageUpdate('')
|
||||
const [validCut, errorCut] = await HdiffzService.CutData(gameDir)
|
||||
if (!validCut) {
|
||||
toast.error(errorCut)
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
}
|
||||
const [validCut, errorCut] = await DiffService.CutData(gameDir)
|
||||
if (!handleResult(validCut, errorCut)) return
|
||||
|
||||
switch (validType) {
|
||||
case 'hdifffiles.txt':
|
||||
case 'hdiffmap.json':
|
||||
case 'hdifffiles.json': {
|
||||
setStageType('Patch Data')
|
||||
const [validPatch, errorPatch] = await HdiffzService.PatchData(gameDir)
|
||||
if (!validPatch) {
|
||||
toast.error(errorPatch)
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
}
|
||||
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
|
||||
if (!handleResult(validPatch, errorPatch)) return
|
||||
|
||||
setStageType('Delete old files')
|
||||
const [validDelete, errorDelete] = await HdiffzService.DeleteFiles(gameDir)
|
||||
if (!validDelete) {
|
||||
toast.error(errorDelete)
|
||||
setIsDiffLoading(false)
|
||||
return
|
||||
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
|
||||
if (!handleResult(validDelete, errorDelete)) return
|
||||
break
|
||||
}
|
||||
case 'manifest': {
|
||||
setStageType('Patch Data')
|
||||
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
|
||||
if (!handleResult(validPatch, errorPatch)) return
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Update game completed')
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
toast.error('PickFile error:', err)
|
||||
setIsDiffLoading(false)
|
||||
toast.error(`PickFile error: ${err}`)
|
||||
} finally {
|
||||
setIsDiffLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-2 mx-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
158
frontend/src/pages/fireflytools/index.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export default function FireflyToolsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">Firefly Tools</h1>
|
||||
|
||||
{/* Section 1: About SR Tools */}
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
|
||||
<span>ℹ️</span>
|
||||
<span>About Firefly Tools</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-blue-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🏠</div>
|
||||
<p>
|
||||
This site is a another version of {" "}
|
||||
<span className="font-semibold text-success">Firefly Tools {" "}</span>
|
||||
developed by {" "}
|
||||
<span className="font-semibold text-warning">Me {"(Kain)"}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-blue-600 text-lg">🏆</span>
|
||||
<span className="font-semibold text-blue-800">Master Website</span>
|
||||
</div>
|
||||
<a
|
||||
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="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>
|
||||
</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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
srtools.neonteam.dev
|
||||
</a>
|
||||
{" "}and the original version at{" "}
|
||||
<a
|
||||
href="https://srtools.pages.dev/"
|
||||
className="link link-warning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
srtools.pages.dev
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Main Features */}
|
||||
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
|
||||
<span>🔧</span>
|
||||
<span>Main Features</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-green-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">⚙️</div>
|
||||
<p>Configure characters, light cones, relics, traces, and eidolons easily in your browser.</p>
|
||||
</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>
|
||||
</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:
|
||||
</p>
|
||||
<ul className="list-disc list-inside mt-2 space-y-1 text-green-700">
|
||||
<li>🎭 <span className="font-medium">Hidden Game UI</span> — remove the entire game interface.</li>
|
||||
<li>🚫 <span className="font-medium">Disable Censorship</span> — get rid of Lens Flare censor 💀.</li>
|
||||
<li>🧪 <span className="font-medium">Theorycraft Mode</span> — configure HP, cycles, and more via the web.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">📂</div>
|
||||
<p>Export and import full builds using <code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">⚡</div>
|
||||
<p>Fast testing workflow — no sync cooldowns, instant in-game updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Getting Started */}
|
||||
<div className="bg-purple-50 border-l-4 border-purple-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-purple-800 flex items-center gap-2 mb-4">
|
||||
<span>🚀</span>
|
||||
<span>Getting Started</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-purple-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">1️⃣</div>
|
||||
<p>Access the tool through your browser at the self-hosted instance.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">2️⃣</div>
|
||||
<p>Configure your character builds with the intuitive web interface.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">3️⃣</div>
|
||||
<p>Use <span className="font-semibold">Connect PS</span> feature to instantly sync with your private server.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">4️⃣</div>
|
||||
<p>Test your builds in-game with real-time updates and modifications.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router';
|
||||
export default function HowToPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<div className=" w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">How to Use</h1>
|
||||
|
||||
{/* Section 1: Launcher Features */}
|
||||
@@ -28,13 +28,23 @@ export default function HowToPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">📦</div>
|
||||
<p>Patch and update game files using{" "}
|
||||
<a href="/hdiff" className="link link-info font-mono">Hdiffz Tool</a>
|
||||
{" "}(HDiffPatch) — fast & lightweight incremental updates.
|
||||
<div className="text-green-600 text-2xl">📦</div>
|
||||
<div>
|
||||
<p className="text-green-800 font-semibold">
|
||||
Patch & Update Game Files
|
||||
</p>
|
||||
<p className="text-green-700">
|
||||
Use the{" "}
|
||||
<a href="/diff" className="link link-info font-mono">Diff Tool</a>{" "}
|
||||
(<span className="font-medium">DiffPatch</span>) for fast & lightweight incremental updates.
|
||||
</p>
|
||||
<p className="text-green-700 mt-1">
|
||||
Supports <span className="font-semibold">Hdiff</span>, <span className="font-semibold">Ldiff</span>, and custom diff formats.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: FireflyGo Commands */}
|
||||
@@ -65,8 +75,60 @@ export default function HowToPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">✨</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Extra Settings</h4>
|
||||
|
||||
<div className="space-y-4 text-blue-700 text-sm">
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🎭 Hidden UI
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
Instantly hides the entire game UI — often used in DIM showcase videos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🚫 Disable Censorship
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
Remove the Lens Flare censor effect 💀 for a cleaner experience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🧪 Theorycraft Mode
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
No need to type chat commands anymore — configure everything through the
|
||||
web: adjust monster HP, set cycles, view logs, and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-4 aspect-w-16 aspect-h-9">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/uiKdFrvn9NQ"
|
||||
title="Extra Settings Tutorial"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="rounded-lg w-full h-[300px]"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Commands List */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 mt-4">
|
||||
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
|
||||
|
||||
{/* Theorycraft Toggle */}
|
||||
@@ -103,16 +165,28 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">❤️</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp WAVE HP</code> — Set monster HP for a specific wave</p>
|
||||
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000</code> sets Wave 1 monster HP to 2,000,000</p>
|
||||
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> disables HP override</p>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">
|
||||
HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span>
|
||||
</h4>
|
||||
<div className="space-y-2 text-blue-700 text-sm">
|
||||
<p>
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp N</code> — Set monster HP (only available in Theorycraft mode)
|
||||
</p>
|
||||
<p>
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> — Disable the set HP feature
|
||||
</p>
|
||||
<p>
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp Wave V1 V2 ...</code> — Set HP for each monster in a specific wave
|
||||
</p>
|
||||
<p className="ml-4">
|
||||
Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000 3000000</code> sets wave 1 monster1 HP=2,000,000 and monster2 HP=3,000,000
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Log Command */}
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Folder, Settings, Check, X, Globe, Mic } from 'lucide-react'
|
||||
import { FSService } from '@bindings/firefly-launcher/internal'
|
||||
import { LanguageService } from '@bindings/firefly-launcher/internal'
|
||||
import { FSService } from '@bindings/firefly-launcher/internal/fs-service'
|
||||
import { LanguageService } from '@bindings/firefly-launcher/internal/language-service'
|
||||
import { toast } from 'react-toastify'
|
||||
import useSettingStore from '@/stores/settingStore'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
|
||||
import { FSService, AppService } from '@bindings/firefly-launcher/internal';
|
||||
import { AppService } from '@bindings/firefly-launcher/internal/app-service';
|
||||
import { FSService } from '@bindings/firefly-launcher/internal/fs-service';
|
||||
import { toast } from 'react-toastify';
|
||||
import path from 'path-browserify'
|
||||
import useSettingStore from '@/stores/settingStore';
|
||||
@@ -9,6 +10,7 @@ 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';
|
||||
|
||||
export default function LauncherPage() {
|
||||
const { gamePath,
|
||||
@@ -20,6 +22,7 @@ export default function LauncherPage() {
|
||||
serverVersion,
|
||||
proxyVersion,
|
||||
} = useSettingStore()
|
||||
|
||||
const {
|
||||
isOpenDownloadDataModal,
|
||||
isOpenUpdateDataModal,
|
||||
@@ -28,6 +31,7 @@ export default function LauncherPage() {
|
||||
setIsOpenUpdateDataModal,
|
||||
setIsOpenSelfUpdateModal
|
||||
} = useModalStore()
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
downloadType,
|
||||
@@ -53,6 +57,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) {
|
||||
@@ -120,7 +151,7 @@ export default function LauncherPage() {
|
||||
const handlePickFile = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const basePath = await FSService.PickFile()
|
||||
const basePath = await FSService.PickFile("exe")
|
||||
if (basePath.endsWith("StarRail.exe") || basePath.endsWith("launcher.exe")) {
|
||||
const normalized = basePath.replace(/\\/g, '/')
|
||||
const folderPath = path.dirname(normalized)
|
||||
@@ -199,18 +230,18 @@ export default function LauncherPage() {
|
||||
setIsDownloading(true)
|
||||
if (updateData.launcher.isUpdate) {
|
||||
await UpdateLauncher(updateData.launcher.version)
|
||||
setUpdateData({...updateData, launcher: { isUpdate: false, isExists: true, version: updateData.launcher.version }})
|
||||
setUpdateData({ ...updateData, launcher: { isUpdate: false, isExists: true, version: updateData.launcher.version } })
|
||||
setIsOpenSelfUpdateModal(true)
|
||||
}
|
||||
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 }})
|
||||
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 }})
|
||||
setUpdateData({ ...updateData, proxy: { isUpdate: false, isExists: true, version: updateData.proxy.version } })
|
||||
}
|
||||
|
||||
setDownloadType("")
|
||||
@@ -250,60 +281,21 @@ export default function LauncherPage() {
|
||||
</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="tooltip tooltip-right" data-tip="Firefly SRAnalysis">
|
||||
{widgetLinks.map((link, idx) => (
|
||||
<div key={idx} className="tooltip tooltip-right" data-tip={link.tooltip}>
|
||||
<a
|
||||
className="btn btn-circle btn-primary"
|
||||
className={`btn btn-circle ${link.btnClass}`}
|
||||
target="_blank"
|
||||
href="https://sranalysis.kain.id.vn/"
|
||||
href={link.href}
|
||||
>
|
||||
<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"
|
||||
src={link.img}
|
||||
alt={link.tooltip}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="tooltip tooltip-right" data-tip="How to use all tools & commands">
|
||||
<Link
|
||||
@@ -344,20 +336,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
|
||||
@@ -368,8 +357,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")
|
||||
@@ -404,7 +397,7 @@ export default function LauncherPage() {
|
||||
<div className="flex justify-center items-center text-sm text-white/80">
|
||||
<span>{downloadType}</span>
|
||||
<div className="flex items-center gap-4 ml-4">
|
||||
<span className="text-cyan-400 font-semibold">{downloadSpeed.toFixed(1)} MB/s</span>
|
||||
<span className="text-cyan-400 font-semibold">{downloadSpeed}</span>
|
||||
<span className="text-white font-bold">{progressDownload.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -413,7 +406,7 @@ export default function LauncherPage() {
|
||||
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progressDownload}%` }}
|
||||
transition={{ duration: 0.3 }}
|
||||
transition={{ type: "tween", ease: "linear", duration: 0.03 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center text-xs text-white/60">
|
||||
@@ -422,6 +415,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">
|
||||
@@ -475,141 +469,38 @@ export default function LauncherPage() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export default function SrToolsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">SR Tools</h1>
|
||||
|
||||
{/* Section 1: About SR Tools */}
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
|
||||
<span>ℹ️</span>
|
||||
<span>About SR Tools</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-blue-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🏠</div>
|
||||
<p>
|
||||
This site is a another version of {" "}
|
||||
<span className="font-semibold text-success">SR Tools {" "}</span>
|
||||
developed by {" "}
|
||||
<span className="font-semibold text-warning">Me {"(Kain)"}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-blue-600 text-lg">🏆</span>
|
||||
<span className="font-semibold text-blue-800">Master Website</span>
|
||||
</div>
|
||||
<a
|
||||
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="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>
|
||||
</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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
srtools.neonteam.dev
|
||||
</a>
|
||||
{" "}and the original version at{" "}
|
||||
<a
|
||||
href="https://srtools.pages.dev/"
|
||||
className="link link-warning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
srtools.pages.dev
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Main Features */}
|
||||
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
|
||||
<span>🔧</span>
|
||||
<span>Main Features</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-green-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">⚙️</div>
|
||||
<p>Configure characters, light cones, relics, traces, and eidolons easily in your browser.</p>
|
||||
</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 Private 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-lg">📂</div>
|
||||
<p>Export and import full builds using <code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">⚡</div>
|
||||
<p>Fast testing workflow — no sync cooldowns, instant in-game updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Getting Started */}
|
||||
<div className="bg-purple-50 border-l-4 border-purple-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-purple-800 flex items-center gap-2 mb-4">
|
||||
<span>🚀</span>
|
||||
<span>Getting Started</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-purple-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">1️⃣</div>
|
||||
<p>Access the tool through your browser at the self-hosted instance.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">2️⃣</div>
|
||||
<p>Configure your character builds with the intuitive web interface.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">3️⃣</div>
|
||||
<p>Use <span className="font-semibold">Connect PS</span> feature to instantly sync with your private server.</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">4️⃣</div>
|
||||
<p>Test your builds in-game with real-time updates and modifications.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SrtoolsRouteImport } from './routes/srtools'
|
||||
import { Route as LanguageRouteImport } from './routes/language'
|
||||
import { Route as HowtoRouteImport } from './routes/howto'
|
||||
import { Route as HdiffzRouteImport } from './routes/hdiffz'
|
||||
import { Route as DiffRouteImport } from './routes/diff'
|
||||
import { Route as AnalysisRouteImport } from './routes/analysis'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
@@ -32,9 +32,9 @@ const HowtoRoute = HowtoRouteImport.update({
|
||||
path: '/howto',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const HdiffzRoute = HdiffzRouteImport.update({
|
||||
id: '/hdiffz',
|
||||
path: '/hdiffz',
|
||||
const DiffRoute = DiffRouteImport.update({
|
||||
id: '/diff',
|
||||
path: '/diff',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AnalysisRoute = AnalysisRouteImport.update({
|
||||
@@ -57,7 +57,7 @@ export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/analysis': typeof AnalysisRoute
|
||||
'/hdiffz': typeof HdiffzRoute
|
||||
'/diff': typeof DiffRoute
|
||||
'/howto': typeof HowtoRoute
|
||||
'/language': typeof LanguageRoute
|
||||
'/srtools': typeof SrtoolsRoute
|
||||
@@ -66,7 +66,7 @@ export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/analysis': typeof AnalysisRoute
|
||||
'/hdiffz': typeof HdiffzRoute
|
||||
'/diff': typeof DiffRoute
|
||||
'/howto': typeof HowtoRoute
|
||||
'/language': typeof LanguageRoute
|
||||
'/srtools': typeof SrtoolsRoute
|
||||
@@ -76,7 +76,7 @@ export interface FileRoutesById {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/analysis': typeof AnalysisRoute
|
||||
'/hdiffz': typeof HdiffzRoute
|
||||
'/diff': typeof DiffRoute
|
||||
'/howto': typeof HowtoRoute
|
||||
'/language': typeof LanguageRoute
|
||||
'/srtools': typeof SrtoolsRoute
|
||||
@@ -87,7 +87,7 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/analysis'
|
||||
| '/hdiffz'
|
||||
| '/diff'
|
||||
| '/howto'
|
||||
| '/language'
|
||||
| '/srtools'
|
||||
@@ -96,7 +96,7 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/analysis'
|
||||
| '/hdiffz'
|
||||
| '/diff'
|
||||
| '/howto'
|
||||
| '/language'
|
||||
| '/srtools'
|
||||
@@ -105,7 +105,7 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/analysis'
|
||||
| '/hdiffz'
|
||||
| '/diff'
|
||||
| '/howto'
|
||||
| '/language'
|
||||
| '/srtools'
|
||||
@@ -115,7 +115,7 @@ export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
AnalysisRoute: typeof AnalysisRoute
|
||||
HdiffzRoute: typeof HdiffzRoute
|
||||
DiffRoute: typeof DiffRoute
|
||||
HowtoRoute: typeof HowtoRoute
|
||||
LanguageRoute: typeof LanguageRoute
|
||||
SrtoolsRoute: typeof SrtoolsRoute
|
||||
@@ -144,11 +144,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof HowtoRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/hdiffz': {
|
||||
id: '/hdiffz'
|
||||
path: '/hdiffz'
|
||||
fullPath: '/hdiffz'
|
||||
preLoaderRoute: typeof HdiffzRouteImport
|
||||
'/diff': {
|
||||
id: '/diff'
|
||||
path: '/diff'
|
||||
fullPath: '/diff'
|
||||
preLoaderRoute: typeof DiffRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/analysis': {
|
||||
@@ -179,7 +179,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
AnalysisRoute: AnalysisRoute,
|
||||
HdiffzRoute: HdiffzRoute,
|
||||
DiffRoute: DiffRoute,
|
||||
HowtoRoute: HowtoRoute,
|
||||
LanguageRoute: LanguageRoute,
|
||||
SrtoolsRoute: SrtoolsRoute,
|
||||
|
||||
@@ -1,106 +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 useHdiffzStore from '@/stores/hdiffzStore';
|
||||
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 } = useHdiffzStore()
|
||||
useGlobalEvents({
|
||||
setGameRunning,
|
||||
setServerRunning,
|
||||
setProxyRunning,
|
||||
setProgressUpdate,
|
||||
setMaxProgressUpdate,
|
||||
setProgressDownload,
|
||||
setDownloadSpeed,
|
||||
setMessageUpdate,
|
||||
});
|
||||
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="/hdiffz">Hdiffz</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="/ff-launcher.png" alt="Logo" className='w-13 h-13' />
|
||||
<div className="flex flex-col justify-center items-start">
|
||||
<h1 className="text-xl font-bold">
|
||||
<span className="text-emerald-500">Firefly </span>
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||
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">
|
||||
<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="/hdiffz">Hdiffz</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">SrTools</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 />
|
||||
</>
|
||||
)
|
||||
|
||||
7
frontend/src/routes/diff.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import DiffPage from '@/pages/diff'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/diff')({
|
||||
component: DiffPage,
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import HdiffzPage from '@/pages/hdiffz'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/hdiffz')({
|
||||
component: HdiffzPage,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import SrToolsPage from '@/pages/srtools'
|
||||
import FireflyToolsPage from '@/pages/fireflytools'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/srtools')({
|
||||
component: SrToolsPage,
|
||||
component: FireflyToolsPage,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface LauncherState {
|
||||
interface DiffState {
|
||||
folderCheckResult: 'success' | 'error' | null,
|
||||
isLoading: {game: boolean, diff: boolean},
|
||||
diffDir: string,
|
||||
@@ -22,7 +22,7 @@ interface LauncherState {
|
||||
setStageType: (value: string) => void,
|
||||
}
|
||||
|
||||
const useLauncherStore = create<LauncherState>((set, get) => ({
|
||||
const useDiffStore = create<DiffState>((set, get) => ({
|
||||
isLoading: {game: false, diff: false},
|
||||
folderCheckResult: null,
|
||||
diffDir: "",
|
||||
@@ -43,4 +43,4 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
|
||||
setStageType: (value: string) => set({ stageType: value }),
|
||||
}));
|
||||
|
||||
export default useLauncherStore;
|
||||
export default useDiffStore;
|
||||
@@ -11,7 +11,7 @@ interface LauncherState {
|
||||
isLoading: boolean;
|
||||
gameRunning: boolean;
|
||||
progressDownload: number;
|
||||
downloadSpeed: number;
|
||||
downloadSpeed: string;
|
||||
launcherVersion: string;
|
||||
updateData: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>;
|
||||
setDownloadType: (value: string) => void;
|
||||
@@ -24,7 +24,7 @@ interface LauncherState {
|
||||
setGameRunning: (value: boolean) => void;
|
||||
setProgressDownload: (value: number) => void;
|
||||
setLauncherVersion: (value: string) => void;
|
||||
setDownloadSpeed: (value: number) => void;
|
||||
setDownloadSpeed: (value: string) => void;
|
||||
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => void;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
|
||||
proxyRunning: false,
|
||||
gameRunning: false,
|
||||
progressDownload: 0,
|
||||
downloadSpeed: 0,
|
||||
downloadSpeed: "",
|
||||
launcherVersion: "",
|
||||
updateData: {
|
||||
server: { isUpdate: false, isExists: false, version: "" },
|
||||
@@ -55,7 +55,7 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
|
||||
setGameRunning: (value: boolean) => set({ gameRunning: value }),
|
||||
setProgressDownload: (value: number) => set({ progressDownload: value }),
|
||||
setLauncherVersion: (value: string) => set({ launcherVersion: value }),
|
||||
setDownloadSpeed: (value: number) => set({ downloadSpeed: value }),
|
||||
setDownloadSpeed: (value: string) => set({ downloadSpeed: value }),
|
||||
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => set({ updateData: value }),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -10,6 +10,11 @@ interface SettingState {
|
||||
proxyPath: string;
|
||||
serverVersion: string;
|
||||
proxyVersion: string;
|
||||
closingOption: {
|
||||
isMinimize: boolean;
|
||||
isAsk: boolean;
|
||||
}
|
||||
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => void;
|
||||
setLocale: (newLocale: string) => void;
|
||||
setGamePath: (newGamePath: string) => void;
|
||||
setGameDir: (newGameDir: string) => void;
|
||||
@@ -29,6 +34,11 @@ const useSettingStore = create<SettingState>()(
|
||||
proxyPath: "",
|
||||
serverVersion: "",
|
||||
proxyVersion: "",
|
||||
closingOption: {
|
||||
isMinimize: false,
|
||||
isAsk: true,
|
||||
},
|
||||
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 }),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@plugin "daisyui" {
|
||||
@plugin "daisyui"{
|
||||
themes: night --default, night --prefersdark, cupcake;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
base: '/',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
chunkSizeWarningLimit: 2000,
|
||||
},
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
|
||||
45
go.mod
@@ -3,54 +3,53 @@ module firefly-launcher
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.26
|
||||
golang.org/x/sys v0.35.0
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.34
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
golang.org/x/sys v0.31.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
aead.dev/minisign v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
aead.dev/minisign v0.2.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.13.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/lmittmann/tint v1.1.2 // indirect
|
||||
github.com/lmittmann/tint v1.0.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.51.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.21 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
87
go.sum
@@ -1,13 +1,12 @@
|
||||
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||
aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA=
|
||||
aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
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.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
@@ -16,17 +15,17 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
|
||||
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -37,8 +36,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
|
||||
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
@@ -51,10 +50,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -66,8 +67,8 @@ github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
||||
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
|
||||
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
@@ -79,8 +80,8 @@ github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDw
|
||||
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -90,12 +91,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
@@ -108,23 +109,23 @@ github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2
|
||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.26 h1:co3eifrIcG/J7sQ1oMPmLfvtAE4bezdhOtabI4Ua5fg=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.26/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.34 h1:t6NwHOLJzXuESb3YSXarSd1C/U1h2CpXF+BLr0ekj2g=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.34/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -136,17 +137,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package appService
|
||||
|
||||
import (
|
||||
"firefly-launcher/pkg/constant"
|
||||
@@ -20,3 +20,18 @@ func (a *AppService) CloseAppAfterTimeout(timeout int) (bool, string) {
|
||||
}()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (a *AppService) CloseApp() (bool, string) {
|
||||
application.Get().Quit()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (a *AppService) MinimizeApp() (bool, string) {
|
||||
window := application.Get().Window.Current()
|
||||
if window == nil {
|
||||
return false, "not found window"
|
||||
}
|
||||
window.Hide()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
259
internal/diff-service/hdiffz.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/hpatchz"
|
||||
"firefly-launcher/pkg/models"
|
||||
"firefly-launcher/pkg/sevenzip"
|
||||
"firefly-launcher/pkg/verifier"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type DiffService struct{}
|
||||
|
||||
func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
|
||||
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
|
||||
return true, "hdifffiles.txt", ""
|
||||
}
|
||||
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
|
||||
return true, "hdifffiles.json", ""
|
||||
}
|
||||
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json"); err == nil && ok {
|
||||
return true, "hdiffmap.json", ""
|
||||
}
|
||||
if ok, err := sevenzip.IsFileIn7z(patchPath, "manifest"); err == nil && ok {
|
||||
return true, "manifest", ""
|
||||
}
|
||||
|
||||
return false, "", "not found hdifffiles.txt or hdiffmap.json"
|
||||
}
|
||||
|
||||
func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string) {
|
||||
oldBinPath := filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
|
||||
if _, err := os.Stat(oldBinPath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if _, err := os.Stat(patchPath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
okFull, errFull := sevenzip.IsFileIn7z(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
|
||||
okDiff, errDiff := sevenzip.IsFileIn7z(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff")
|
||||
|
||||
if (errFull != nil && errDiff != nil) || (!okFull && !okDiff) {
|
||||
return false, "BinaryVersion file not found in patch"
|
||||
}
|
||||
|
||||
var tempBinFile string
|
||||
|
||||
if okFull {
|
||||
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
|
||||
} else {
|
||||
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff", constant.TempUrl); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
patchBinFile := filepath.Join(constant.TempUrl, "BinaryVersion.bytes.hdiff")
|
||||
sourceBinFile := oldBinPath
|
||||
tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
|
||||
if err := hpatchz.ApplyPatch(sourceBinFile, patchBinFile, tempBinFile); err != nil {
|
||||
os.Remove(patchBinFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
os.Remove(patchBinFile)
|
||||
}
|
||||
|
||||
okFullPkg, err1 := sevenzip.IsFileIn7z(patchPath, "pkg_version")
|
||||
okDiffPkg, err2 := sevenzip.IsFileIn7z(patchPath, "pkg_version.hdiff")
|
||||
if err1 != nil && err2 != nil {
|
||||
return false, err1.Error()
|
||||
}
|
||||
if okFullPkg {
|
||||
if err := sevenzip.ExtractAFileFromZip(patchPath, "pkg_version", constant.TempUrl); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
if okDiffPkg {
|
||||
if err := sevenzip.ExtractAFileFromZip(patchPath, "pkg_version.hdiff", constant.TempUrl); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
patchPkgFile := filepath.Join(constant.TempUrl, "pkg_version.hdiff")
|
||||
sourcePkgFile := filepath.Join(gamePath, "pkg_version")
|
||||
tempPkgFile := filepath.Join(constant.TempUrl, "pkg_version")
|
||||
if err := hpatchz.ApplyPatch(sourcePkgFile, patchPkgFile, tempPkgFile); err != nil {
|
||||
os.Remove(patchPkgFile)
|
||||
os.Remove(tempPkgFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
os.Remove(patchPkgFile)
|
||||
}
|
||||
|
||||
tempPkgFile := filepath.Join(constant.TempUrl, "pkg_version")
|
||||
pkgDataList, err := models.LoadPkgVersion(tempPkgFile)
|
||||
if err != nil {
|
||||
os.Remove(tempPkgFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
os.Remove(tempPkgFile)
|
||||
|
||||
// MD5 check BinaryVersion
|
||||
flags := false
|
||||
for _, pkgData := range pkgDataList {
|
||||
if filepath.ToSlash(pkgData.RemoteFile) == "StarRail_Data/StreamingAssets/BinaryVersion.bytes" {
|
||||
flags = true
|
||||
md5, err := verifier.FileMD5(tempBinFile)
|
||||
if err != nil {
|
||||
os.Remove(tempBinFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
if md5 != pkgData.MD5 {
|
||||
os.Remove(tempBinFile)
|
||||
return false, fmt.Sprintf("md5 mismatch for %s: expected %s, got %s",
|
||||
tempBinFile, pkgData.MD5, md5)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !flags {
|
||||
os.Remove(tempBinFile)
|
||||
return false, "BinaryVersion file not found in patch"
|
||||
}
|
||||
_, err = models.ParseBinaryVersion(tempBinFile)
|
||||
if err != nil {
|
||||
os.Remove(tempBinFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
os.Remove(tempBinFile)
|
||||
return true, "validated"
|
||||
}
|
||||
|
||||
func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
|
||||
hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
|
||||
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
|
||||
hdifffilesJsonPath := filepath.Join(gamePath, "hdifffiles.json")
|
||||
|
||||
var jsonData struct {
|
||||
DiffMap []*models.HDiffData `json:"diff_map"`
|
||||
}
|
||||
|
||||
if _, err := os.Stat(hdiffMapPath); err == nil {
|
||||
data, err := os.ReadFile(hdiffMapPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
var jsonDataDiffMap struct {
|
||||
DiffMap []*models.DiffMapType `json:"diff_map"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &jsonDataDiffMap); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
for _, entry := range jsonDataDiffMap.DiffMap {
|
||||
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
|
||||
}
|
||||
} else if _, err := os.Stat(hdifffilesJsonPath); err == nil {
|
||||
data, err := os.ReadFile(hdifffilesJsonPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
|
||||
files, err := models.LoadHDiffFiles(hdiffFilesPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
for _, entry := range files {
|
||||
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
|
||||
}
|
||||
} else {
|
||||
return false, "no hdiff entries map exist"
|
||||
}
|
||||
|
||||
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
|
||||
for i, entry := range jsonData.DiffMap {
|
||||
application.Get().Event.Emit(
|
||||
"diff:progress", map[string]int{
|
||||
"progress": i,
|
||||
"maxProgress": len(jsonData.DiffMap),
|
||||
})
|
||||
|
||||
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
|
||||
patchFile := filepath.Join(gamePath, entry.PatchFileName)
|
||||
targetFile := filepath.Join(gamePath, entry.TargetFileName)
|
||||
|
||||
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.SourceFileName == "" {
|
||||
hpatchz.ApplyPatchEmpty(patchFile, targetFile)
|
||||
os.Remove(patchFile)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
|
||||
if entry.SourceFileName != entry.TargetFileName {
|
||||
os.Remove(sourceFile)
|
||||
}
|
||||
os.Remove(patchFile)
|
||||
}
|
||||
|
||||
os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
|
||||
os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
|
||||
os.Remove(filepath.Join(gamePath, "hdifffiles.json"))
|
||||
return true, "patching completed"
|
||||
}
|
||||
|
||||
func (h *DiffService) DeleteFiles(gamePath string) (bool, string) {
|
||||
var deleteFiles []string
|
||||
|
||||
file, err := os.Open(filepath.Join(gamePath, "deletefiles.txt"))
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "" {
|
||||
deleteFiles = append(deleteFiles, line)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
file.Close()
|
||||
return false, "no delete files exist"
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
for i, file := range deleteFiles {
|
||||
os.Remove(filepath.Join(gamePath, file))
|
||||
application.Get().Event.Emit("diff:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
|
||||
}
|
||||
os.Remove(filepath.Join(gamePath, "deletefiles.txt"))
|
||||
return true, ""
|
||||
}
|
||||
172
internal/diff-service/ldifff.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"firefly-launcher/pkg/firefly"
|
||||
"firefly-launcher/pkg/firefly/pb"
|
||||
"firefly-launcher/pkg/hpatchz"
|
||||
"firefly-launcher/pkg/models"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func (h *DiffService) LDiffPatchData(gamePath string) (bool, string) {
|
||||
entries, err := os.ReadDir(gamePath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
ldiffPath := filepath.Join(gamePath, "ldiff")
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !entry.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, "manifest") {
|
||||
manifestName := entry.Name()
|
||||
manifestPath := filepath.Join(gamePath, manifestName)
|
||||
|
||||
manifest, err := firefly.LoadManifestProto(manifestPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ldiffEntries, err := os.ReadDir(ldiffPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Processing LDiff"})
|
||||
for i, ldiffEntry := range ldiffEntries {
|
||||
assetName := ldiffEntry.Name()
|
||||
var matchingAssets []struct {
|
||||
AssetName string
|
||||
AssetSize int64
|
||||
Asset *pb.AssetManifest
|
||||
}
|
||||
|
||||
application.Get().Event.Emit(
|
||||
"diff:progress", map[string]int{
|
||||
"progress": i,
|
||||
"maxProgress": len(ldiffEntries),
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for _, assetGroup := range manifest.Assets {
|
||||
assetGroup := assetGroup
|
||||
wg.Go(func() {
|
||||
if data := assetGroup.AssetData; data != nil {
|
||||
for _, asset := range data.Assets {
|
||||
if asset.ChunkFileName == assetName {
|
||||
mu.Lock()
|
||||
matchingAssets = append(matchingAssets, struct {
|
||||
AssetName string
|
||||
AssetSize int64
|
||||
Asset *pb.AssetManifest
|
||||
}{assetGroup.AssetName, assetGroup.AssetSize, asset})
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, ma := range matchingAssets {
|
||||
err := firefly.LDiffFile(ma.Asset, ma.AssetName, ma.AssetSize, ldiffPath, gamePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diffMapNames := make([]string, len(ldiffEntries))
|
||||
for i, e := range ldiffEntries {
|
||||
diffMapNames[i] = e.Name()
|
||||
}
|
||||
|
||||
diffMapList, err := MakeDiffMap(manifest, diffMapNames)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
|
||||
for i, entry := range diffMapList {
|
||||
application.Get().Event.Emit(
|
||||
"diff:progress", map[string]int{
|
||||
"progress": i,
|
||||
"maxProgress": len(diffMapList),
|
||||
})
|
||||
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
|
||||
patchFile := filepath.Join(gamePath, entry.PatchFileName)
|
||||
targetFile := filepath.Join(gamePath, entry.TargetFileName)
|
||||
|
||||
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.SourceFileName == "" {
|
||||
hpatchz.ApplyPatchEmpty(patchFile, targetFile)
|
||||
os.Remove(patchFile)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
|
||||
if entry.SourceFileName != entry.TargetFileName {
|
||||
os.Remove(sourceFile)
|
||||
}
|
||||
os.Remove(patchFile)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
os.RemoveAll(ldiffPath)
|
||||
return true, "patching completed"
|
||||
}
|
||||
|
||||
func MakeDiffMap(manifest *pb.ManifestProto, chunkNames []string) ([]*models.HDiffData, error) {
|
||||
var hdiffFiles []*models.HDiffData
|
||||
|
||||
for _, asset := range manifest.Assets {
|
||||
assetName := asset.AssetName
|
||||
assetSize := asset.AssetSize
|
||||
|
||||
if asset.AssetData != nil {
|
||||
for _, chunk := range asset.AssetData.Assets {
|
||||
matched := false
|
||||
for _, name := range chunkNames {
|
||||
if name == chunk.ChunkFileName {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
|
||||
if chunk.OriginalFileSize != 0 || chunk.HdiffFileSize != assetSize {
|
||||
hdiffFiles = append(hdiffFiles, &models.HDiffData{
|
||||
SourceFileName: chunk.OriginalFilePath,
|
||||
TargetFileName: assetName,
|
||||
PatchFileName: fmt.Sprintf("%s.hdiff", assetName),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hdiffFiles, nil
|
||||
}
|
||||
87
internal/diff-service/utils.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/sevenzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func (h *DiffService) DataExtract(gamePath, patchPath string) (bool, string) {
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
if _, err := os.Stat(gamePath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(patchPath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
if err := sevenzip.ExtractAllFilesFromZip(patchPath, constant.TempUrl); err != nil {
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, "extract completed"
|
||||
}
|
||||
|
||||
func (h *DiffService) CutData(gamePath string) (bool, string) {
|
||||
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
err := filepath.Walk(constant.TempUrl, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(constant.TempUrl, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(gamePath, relPath)
|
||||
application.Get().Event.Emit("diff:message", map[string]string{"message": destPath})
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(destPath, os.ModePerm)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(path, destPath); err != nil {
|
||||
srcFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
return false, err.Error()
|
||||
}
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
return true, "cut completed"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package fsService
|
||||
|
||||
import (
|
||||
"firefly-launcher/pkg/sevenzip"
|
||||
@@ -30,7 +30,7 @@ func (f *FSService) PickFolder() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (f *FSService) PickFile() (string, error) {
|
||||
func (f *FSService) PickFile(filter string) (string, error) {
|
||||
dialog := application.OpenFileDialog().
|
||||
CanChooseFiles(true).
|
||||
ResolvesAliases(true)
|
||||
@@ -39,7 +39,9 @@ func (f *FSService) PickFile() (string, error) {
|
||||
} else {
|
||||
dialog.SetTitle("Select a file/directory")
|
||||
}
|
||||
if filter == "exe" {
|
||||
dialog.AddFilter("Executable Files (*.exe)", "*.exe")
|
||||
}
|
||||
if path, err := dialog.PromptForSingleSelection(); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/models"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/selfupdate"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type GitService struct{}
|
||||
|
||||
func (g *GitService) GetLatestServerVersion() (bool, string, string) {
|
||||
resp, err := http.Get(constant.ServerGitUrl)
|
||||
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) DownloadServerProgress(version string) (bool, string) {
|
||||
resp, err := http.Get(constant.ServerGitUrl)
|
||||
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.ServerZipFile {
|
||||
assetWin = asset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if assetWin.Name == "" {
|
||||
return false, "no assets found"
|
||||
}
|
||||
|
||||
if err := os.Mkdir(constant.ServerStorageUrl, 0755); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
saveFile := filepath.Join(constant.ServerStorageUrl, assetWin.Name)
|
||||
|
||||
resp, err = http.Get(assetWin.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
|
||||
application.Get().Event.Emit("download:server", map[string]interface{}{
|
||||
"percent": fmt.Sprintf("%.2f", percent),
|
||||
"speed": fmt.Sprintf("%.2f", speed),
|
||||
})
|
||||
})
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (g *GitService) UnzipServer() {
|
||||
unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl)
|
||||
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
|
||||
}
|
||||
|
||||
func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
|
||||
resp, err := http.Get(constant.ProxyGitUrl)
|
||||
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) DownloadProxyProgress(version string) (bool, string) {
|
||||
resp, err := http.Get(constant.ProxyGitUrl)
|
||||
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.ProxyZipFile {
|
||||
assetWin = asset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if assetWin.Name == "" {
|
||||
return false, "no assets found"
|
||||
}
|
||||
|
||||
if err := os.Mkdir(constant.ProxyStorageUrl, 0755); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
saveFile := filepath.Join(constant.ProxyStorageUrl, assetWin.Name)
|
||||
|
||||
resp, err = http.Get(assetWin.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
|
||||
application.Get().Event.Emit("download:proxy", map[string]interface{}{
|
||||
"percent": fmt.Sprintf("%.2f", percent),
|
||||
"speed": fmt.Sprintf("%.2f", speed),
|
||||
})
|
||||
|
||||
})
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (g *GitService) UnzipProxy() {
|
||||
unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
|
||||
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, ""
|
||||
}
|
||||
57
internal/git-service/launcher.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/models"
|
||||
"io"
|
||||
"net/http"
|
||||
"github.com/minio/selfupdate"
|
||||
|
||||
)
|
||||
|
||||
type GitService struct{}
|
||||
|
||||
|
||||
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) {
|
||||
asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
|
||||
if !ok {
|
||||
return false, "no release found"
|
||||
}
|
||||
|
||||
resp, err := http.Get(asset.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, ""
|
||||
}
|
||||
71
internal/git-service/proxy.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/models"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
|
||||
resp, err := http.Get(constant.ProxyGitUrl)
|
||||
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) DownloadProxyProgress(version string) (bool, string) {
|
||||
asset, ok := g.getReleaseAsset(version, constant.ProxyGitUrl, constant.ProxyZipFile)
|
||||
if !ok {
|
||||
return false, "no release found"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(constant.ProxyStorageUrl, 0755); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
saveFile := filepath.Join(constant.ProxyStorageUrl, asset.Name)
|
||||
tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) {
|
||||
application.Get().Event.Emit("download:proxy", map[string]interface{}{
|
||||
"percent": fmt.Sprintf("%.2f", percent),
|
||||
"speed": speed,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := os.Rename(tmpPath, saveFile); err == nil {
|
||||
return true, ""
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
return false, "failed to rename tmp file after retries"
|
||||
}
|
||||
|
||||
func (g *GitService) UnzipProxy() {
|
||||
g.unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
|
||||
os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile))
|
||||
}
|
||||
71
internal/git-service/server.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/models"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func (g *GitService) GetLatestServerVersion() (bool, string, string) {
|
||||
resp, err := http.Get(constant.ServerGitUrl)
|
||||
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) DownloadServerProgress(version string) (bool, string) {
|
||||
asset, ok := g.getReleaseAsset(version, constant.ServerGitUrl, constant.ServerZipFile)
|
||||
if !ok {
|
||||
return false, "no release found"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(constant.ServerStorageUrl, 0755); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
saveFile := filepath.Join(constant.ServerStorageUrl, asset.Name)
|
||||
tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) {
|
||||
application.Get().Event.Emit("download:server", map[string]interface{}{
|
||||
"percent": fmt.Sprintf("%.2f", percent),
|
||||
"speed": speed,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := os.Rename(tmpPath, saveFile); err == nil {
|
||||
return true, ""
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
return false, "failed to rename tmp file after retries"
|
||||
}
|
||||
|
||||
func (g *GitService) UnzipServer() {
|
||||
g.unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl)
|
||||
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
|
||||
}
|
||||
251
internal/git-service/util.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/models"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
)
|
||||
|
||||
func HumanFormat(bytes float64) string {
|
||||
if math.IsNaN(bytes) || math.IsInf(bytes, 0) {
|
||||
return fmt.Sprintf("%v", bytes)
|
||||
}
|
||||
if bytes == 0 {
|
||||
return "0B"
|
||||
}
|
||||
|
||||
neg := bytes < 0
|
||||
if neg {
|
||||
bytes = -bytes
|
||||
}
|
||||
|
||||
units := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
i := 0
|
||||
for bytes >= 1024 && i < len(units)-1 {
|
||||
bytes /= 1024
|
||||
i++
|
||||
}
|
||||
|
||||
res := fmt.Sprintf("%.1f%s", bytes, units[i])
|
||||
if neg {
|
||||
res = "-" + res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type WriteCounter struct {
|
||||
Total uint64
|
||||
StartTime time.Time
|
||||
OnEmit func(percent float64, speed string)
|
||||
TotalSize int64
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewWriteCounter(total int64, onEmit func(percent float64, speed string)) *WriteCounter {
|
||||
return &WriteCounter{
|
||||
StartTime: time.Now(),
|
||||
TotalSize: total,
|
||||
OnEmit: onEmit,
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WriteCounter) Add(n int) {
|
||||
wc.mu.Lock()
|
||||
defer wc.mu.Unlock()
|
||||
wc.Total += uint64(n)
|
||||
wc.PrintProgress()
|
||||
}
|
||||
|
||||
func (wc *WriteCounter) PrintProgress() {
|
||||
elapsed := time.Since(wc.StartTime).Seconds()
|
||||
if elapsed < 0.001 {
|
||||
elapsed = 0.001
|
||||
}
|
||||
speed := float64(wc.Total) / elapsed
|
||||
percent := float64(wc.Total) / float64(wc.TotalSize) * 100
|
||||
if wc.OnEmit != nil {
|
||||
wc.OnEmit(percent, fmt.Sprintf("%s/s", HumanFormat(speed)))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GitService) downloadFileParallel(filePath, url string, numParts int, onEmit func(percent float64, speed string)) (tmpPath string, err error) {
|
||||
resp, err := http.Head(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get head: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
size := resp.ContentLength
|
||||
tmpPath = filePath + ".tmp"
|
||||
|
||||
out, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create tmp file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
counter := NewWriteCounter(size, onEmit)
|
||||
partSize := size / int64(numParts)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for i := 0; i < numParts; i++ {
|
||||
start := int64(i) * partSize
|
||||
end := start + partSize - 1
|
||||
if i == numParts-1 {
|
||||
end = size - 1
|
||||
}
|
||||
|
||||
_ = start
|
||||
_ = end
|
||||
|
||||
wg.Go(func() {
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
var written int64
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
mu.Lock()
|
||||
out.Seek(start+written, 0)
|
||||
out.Write(buf[:n])
|
||||
mu.Unlock()
|
||||
written += int64(n)
|
||||
counter.Add(n)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return tmpPath, nil
|
||||
}
|
||||
|
||||
// --- Helper getReleaseAsset ---
|
||||
func (g *GitService) getReleaseAsset(version, url, fileName string) (models.AssetType, bool) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return models.AssetType{}, false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var releases []*models.ReleaseType
|
||||
if err := json.Unmarshal(body, &releases); err != nil || len(releases) == 0 {
|
||||
return models.AssetType{}, false
|
||||
}
|
||||
|
||||
for _, release := range releases {
|
||||
if release.TagName == version {
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == fileName {
|
||||
return asset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return models.AssetType{}, false
|
||||
}
|
||||
|
||||
func (g *GitService) unzipParallel(src string, dest string) error {
|
||||
numCPU := runtime.NumCPU()
|
||||
|
||||
reserved := 1
|
||||
if numCPU > 4 {
|
||||
reserved = 2
|
||||
}
|
||||
maxWorkers := numCPU - reserved
|
||||
if maxWorkers < 1 {
|
||||
maxWorkers = 1
|
||||
}
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
err = os.MkdirAll(dest, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type job struct {
|
||||
f *zip.File
|
||||
}
|
||||
jobs := make(chan job)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < maxWorkers; i++ {
|
||||
|
||||
wg.Go(func() {
|
||||
for j := range jobs {
|
||||
err := g.extractFile(j.f, dest)
|
||||
if err != nil {
|
||||
fmt.Printf("Error extracting %s: %v\n", j.f.Name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, f := range r.File {
|
||||
jobs <- job{f}
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GitService) extractFile(f *zip.File, dest string) error {
|
||||
fp := filepath.Join(dest, f.Name)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
return os.MkdirAll(fp, f.Mode())
|
||||
}
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(fp), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, rc)
|
||||
return err
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"firefly-launcher/pkg/constant"
|
||||
"firefly-launcher/pkg/models"
|
||||
"firefly-launcher/pkg/sevenzip"
|
||||
"firefly-launcher/pkg/verifier"
|
||||
"firefly-launcher/pkg/hpatchz"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type HdiffzService struct{}
|
||||
|
||||
func (h *HdiffzService) CheckTypeHDiff(patchPath string) (bool, string, string) {
|
||||
isFileInTxt, _ := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt")
|
||||
if isFileInTxt {
|
||||
return true, "hdifffiles.txt", ""
|
||||
}
|
||||
isFileInJson, _ := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json")
|
||||
if isFileInJson {
|
||||
return true, "hdiffmap.json", ""
|
||||
}
|
||||
return false, "", "not found hdifffiles.txt or hdiffmap.json"
|
||||
}
|
||||
|
||||
func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) {
|
||||
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(patchPath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
binPath := filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
|
||||
newVersionData, err := models.ParseBinaryVersion(binPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer os.Remove(binPath)
|
||||
|
||||
v := newVersionData.Subtract(*oldVersionData)
|
||||
if v != 0 && v != 1 {
|
||||
return false, fmt.Sprintf("the diff version %s not valid with game version %s", newVersionData, oldVersionData)
|
||||
}
|
||||
|
||||
return true, "validated"
|
||||
}
|
||||
|
||||
func (h *HdiffzService) DataExtract(gamePath, patchPath string, isSkipVerify bool) (bool, string) {
|
||||
if _, err := os.Stat(gamePath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(patchPath); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
if err := sevenzip.ExtractAllFilesFromZip(patchPath, constant.TempUrl); err != nil {
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if !isSkipVerify {
|
||||
validator, err := verifier.NewVerifier(gamePath, constant.TempUrl)
|
||||
if err != nil {
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if err := validator.VerifyAll(); err != nil {
|
||||
os.RemoveAll(constant.TempUrl)
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return true, "validated"
|
||||
}
|
||||
|
||||
func (h *HdiffzService) CutData(gamePath string) (bool, string) {
|
||||
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
err := filepath.Walk(constant.TempUrl, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(constant.TempUrl, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(gamePath, relPath)
|
||||
application.Get().Event.Emit("hdiffz:message", map[string]string{"message": destPath})
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(destPath, os.ModePerm)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(path, destPath); err != nil {
|
||||
srcFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
return true, "cut completed"
|
||||
}
|
||||
|
||||
func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
|
||||
hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
|
||||
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
|
||||
|
||||
var jsonData struct {
|
||||
DiffMap []*models.HDiffData `json:"diff_map"`
|
||||
}
|
||||
|
||||
if _, err := os.Stat(hdiffMapPath); err == nil {
|
||||
data, err := os.ReadFile(hdiffMapPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
var jsonDataDiffMap struct {
|
||||
DiffMap []*models.DiffMapType `json:"diff_map"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &jsonDataDiffMap); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
for _, entry := range jsonDataDiffMap.DiffMap {
|
||||
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
|
||||
}
|
||||
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
|
||||
files, err := models.LoadHDiffFiles(hdiffFilesPath)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
for _, entry := range files {
|
||||
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
|
||||
}
|
||||
} else {
|
||||
return false, "no hdiff entries map exist"
|
||||
}
|
||||
|
||||
for i, entry := range jsonData.DiffMap {
|
||||
application.Get().Event.Emit(
|
||||
"hdiffz:progress", map[string]int{
|
||||
"progress": i,
|
||||
"maxProgress": len(jsonData.DiffMap),
|
||||
})
|
||||
|
||||
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
|
||||
patchFile := filepath.Join(gamePath, entry.PatchFileName)
|
||||
targetFile := filepath.Join(gamePath, entry.TargetFileName)
|
||||
|
||||
// Check patch file tồn tại chưa
|
||||
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Nếu không có source file hoặc SourceFileName rỗng → apply_patch_empty
|
||||
if entry.SourceFileName == "" {
|
||||
err := hpatchz.ApplyPatchEmpty(patchFile, targetFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
|
||||
_ = os.Remove(patchFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
_ = os.Remove(patchFile)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Có source file → apply_patch
|
||||
err := hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
|
||||
_ = os.Remove(patchFile)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if entry.SourceFileName != entry.TargetFileName {
|
||||
_ = os.Remove(sourceFile)
|
||||
}
|
||||
_ = os.Remove(patchFile)
|
||||
}
|
||||
|
||||
os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
|
||||
os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
|
||||
return true, "patching completed"
|
||||
}
|
||||
|
||||
func (h *HdiffzService) DeleteFiles(gamePath string) (bool, string) {
|
||||
var deleteFiles []string
|
||||
|
||||
file, err := os.Open(filepath.Join(gamePath, "deletefiles.txt"))
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "" {
|
||||
deleteFiles = append(deleteFiles, line)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return false, "no delete files exist"
|
||||
}
|
||||
|
||||
for i, file := range deleteFiles {
|
||||
os.Remove(filepath.Join(gamePath, file))
|
||||
application.Get().Event.Emit("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
|
||||
}
|
||||
_ = os.Remove(filepath.Join(gamePath, "deletefiles.txt"))
|
||||
return true, ""
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package languageService
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1 +0,0 @@
|
||||
package internal
|
||||
188
internal/util.go
@@ -1,188 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func humanFormat(bytes int64) string {
|
||||
n := float64(bytes)
|
||||
for _, unit := range []string{"", "Ki", "Mi", "Gi"} {
|
||||
if math.Abs(n) < 1024.0 {
|
||||
return fmt.Sprintf("%3.1f%sB", n, unit)
|
||||
}
|
||||
n /= 1024.0
|
||||
}
|
||||
return fmt.Sprintf("%.1fTiB", n)
|
||||
}
|
||||
|
||||
type WriteCounter struct {
|
||||
Total uint64
|
||||
StartTime time.Time
|
||||
OnEmit func(percent float64, speedMBps float64)
|
||||
TotalSize int64
|
||||
lastLoggedPercent int
|
||||
}
|
||||
|
||||
func NewWriteCounter(total int64, onEmit func(percent float64, speedMBps float64)) *WriteCounter {
|
||||
return &WriteCounter{
|
||||
StartTime: time.Now(),
|
||||
TotalSize: total,
|
||||
lastLoggedPercent: -1,
|
||||
OnEmit: onEmit,
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WriteCounter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
wc.Total += uint64(n)
|
||||
wc.PrintProgress()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (wc *WriteCounter) PrintProgress() {
|
||||
elapsed := time.Since(wc.StartTime).Seconds()
|
||||
if elapsed < 0.001 {
|
||||
elapsed = 0.001
|
||||
}
|
||||
|
||||
speed := float64(wc.Total) / 1024 / 1024 / elapsed // MB/s
|
||||
percent := float64(wc.Total) / float64(wc.TotalSize) * 100
|
||||
if wc.OnEmit != nil {
|
||||
wc.OnEmit(percent, speed)
|
||||
}
|
||||
}
|
||||
|
||||
func DownloadFile(filepath string, url string, onEmit func(percent float64, speed float64)) error {
|
||||
tmpPath := filepath + ".tmp"
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
out, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tmp file: %w", err)
|
||||
}
|
||||
|
||||
counter := NewWriteCounter(resp.ContentLength, onEmit)
|
||||
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
|
||||
if closeErr := out.Close(); closeErr != nil {
|
||||
return fmt.Errorf("failed to close tmp file: %w", closeErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download file: %w", err)
|
||||
}
|
||||
|
||||
// Delete destination file if it exists
|
||||
if _, err := os.Stat(filepath); err == nil {
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
return fmt.Errorf("failed to remove existing file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err = os.Rename(tmpPath, filepath)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename after retries: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unzipParallel(src string, dest string) error {
|
||||
numCPU := runtime.NumCPU()
|
||||
|
||||
reserved := 1
|
||||
if numCPU > 4 {
|
||||
reserved = 2
|
||||
}
|
||||
maxWorkers := numCPU - reserved
|
||||
if maxWorkers < 1 {
|
||||
maxWorkers = 1
|
||||
}
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
err = os.MkdirAll(dest, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type job struct {
|
||||
f *zip.File
|
||||
}
|
||||
jobs := make(chan job)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Worker pool
|
||||
for i := 0; i < maxWorkers; i++ {
|
||||
wg.Go(func() {
|
||||
for j := range jobs {
|
||||
err := extractFile(j.f, dest)
|
||||
if err != nil {
|
||||
fmt.Printf("Error extracting %s: %v\n", j.f.Name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Feed jobs
|
||||
for _, f := range r.File {
|
||||
jobs <- job{f}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFile(f *zip.File, dest string) error {
|
||||
fp := filepath.Join(dest, f.Name)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
return os.MkdirAll(fp, f.Mode())
|
||||
}
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(fp), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, rc)
|
||||
return err
|
||||
}
|
||||
43
main.go
@@ -3,7 +3,13 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"firefly-launcher/internal"
|
||||
|
||||
appService "firefly-launcher/internal/app-service"
|
||||
diffService "firefly-launcher/internal/diff-service"
|
||||
fsService "firefly-launcher/internal/fs-service"
|
||||
gitService "firefly-launcher/internal/git-service"
|
||||
languageService "firefly-launcher/internal/language-service"
|
||||
|
||||
"firefly-launcher/pkg/constant"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -11,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
|
||||
@@ -63,11 +70,11 @@ func main() {
|
||||
Name: "firefly-launcher",
|
||||
Description: "Firefly Launcher - Kain",
|
||||
Services: []application.Service{
|
||||
application.NewService(&internal.FSService{}),
|
||||
application.NewService(&internal.LanguageService{}),
|
||||
application.NewService(&internal.GitService{}),
|
||||
application.NewService(&internal.HdiffzService{}),
|
||||
application.NewService(&internal.AppService{}),
|
||||
application.NewService(&fsService.FSService{}),
|
||||
application.NewService(&languageService.LanguageService{}),
|
||||
application.NewService(&gitService.GitService{}),
|
||||
application.NewService(&diffService.DiffService{}),
|
||||
application.NewService(&appService.AppService{}),
|
||||
},
|
||||
Assets: application.AssetOptions{
|
||||
Handler: application.AssetFileServerFS(assets),
|
||||
@@ -78,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,6 +99,28 @@ func main() {
|
||||
DevToolsEnabled: true,
|
||||
})
|
||||
|
||||
iconBytes, _ := tools.ReadFile("assets/appicon.png")
|
||||
systemTray := app.SystemTray.New()
|
||||
systemTray.SetIcon(iconBytes)
|
||||
systemTray.SetTooltip("Firefly Launcher")
|
||||
|
||||
// Attach the window to the system tray
|
||||
menu := application.NewMenu()
|
||||
menu.Add("Open").OnClick(func(ctx *application.Context) {
|
||||
window.Show()
|
||||
})
|
||||
menu.Add("Quit").OnClick(func(ctx *application.Context) {
|
||||
app.Quit()
|
||||
})
|
||||
|
||||
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 {
|
||||
|
||||
@@ -10,8 +10,7 @@ const ProxyZipFile = "64bit.zip"
|
||||
const LauncherFile = "firefly-launcher.exe"
|
||||
const TempUrl = "./temp"
|
||||
|
||||
|
||||
const CurrentLauncherVersion = "1.4.1"
|
||||
const CurrentLauncherVersion = "2.1.0"
|
||||
|
||||
type ToolFile string
|
||||
|
||||
@@ -20,15 +19,13 @@ const (
|
||||
Tool7zaDLL ToolFile = "bin/7za.dll"
|
||||
Tool7zxaDLL ToolFile = "bin/7zxa.dll"
|
||||
ToolHPatchzExe ToolFile = "bin/hpatchz.exe"
|
||||
ToolHDiffzExe ToolFile = "bin/hdiffz.exe"
|
||||
)
|
||||
|
||||
var RequiredFiles = map[ToolFile]string{
|
||||
Tool7zaExe: "assets/7zip/7za.exe",
|
||||
Tool7zaDLL: "assets/7zip/7za.dll",
|
||||
Tool7zxaDLL: "assets/7zip/7zxa.dll",
|
||||
ToolHPatchzExe: "assets/HDiffPatch/hpatchz.exe",
|
||||
ToolHDiffzExe: "assets/HDiffPatch/hdiffz.exe",
|
||||
Tool7zaExe: "assets/7za.exe",
|
||||
Tool7zaDLL: "assets/7za.dll",
|
||||
Tool7zxaDLL: "assets/7zxa.dll",
|
||||
ToolHPatchzExe: "assets/hpatchz.exe",
|
||||
}
|
||||
|
||||
func (t ToolFile) GetEmbedPath() string {
|
||||
|
||||