init
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.task
|
||||||
|
bin
|
||||||
|
tests/
|
||||||
|
server/
|
||||||
|
proxy/
|
||||||
|
temp/
|
||||||
|
frontend/dist
|
||||||
|
frontend/node_modules
|
||||||
|
build/linux/appimage/build
|
||||||
|
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||||
56
README.md
Normal file
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 🚀 Firefly Launcher
|
||||||
|
|
||||||
|
A lightweight and modern launcher for Anime game — designed to make launching, updating, and customizing your game easy and efficient.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🔄 Automatically update Firefly Go and proxy tools on launch
|
||||||
|
- 🎮 Launch the game with correct parameters and environment
|
||||||
|
- 🌐 Switch in-game language (EN, JP, CN, KR) via Language Tools
|
||||||
|
- 📦 Apply game patches using Hdiffz Tool (HDiffPatch-based)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧑💻 About the Developer
|
||||||
|
|
||||||
|
Hi! I'm **Kain**, a developer passionate about building simple and powerful tools.
|
||||||
|
Firefly Launcher is built with:
|
||||||
|
|
||||||
|
- ⚙️ **Go + Wails** for backend/frontend integration
|
||||||
|
- 🎨 **Tailwind CSS** + **DaisyUI** for a clean, responsive UI
|
||||||
|
|
||||||
|
My goal is to create tools that are fast, efficient, and enjoyable to use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
You can choose between:
|
||||||
|
|
||||||
|
- ✅ **Portable** version (no install needed)
|
||||||
|
- 🛠️ **MSI Installer** (for full integration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Common Development Commands (Wails v3)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the app in development mode (hot reload frontend)
|
||||||
|
wails3 dev
|
||||||
|
|
||||||
|
# Build the application (production binary)
|
||||||
|
wails3 build
|
||||||
|
|
||||||
|
# Package the app (NSIS)
|
||||||
|
wails3 package
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License — feel free to use and contribute.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
34
Taskfile.yml
Normal file
34
Taskfile.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ./build/Taskfile.yml
|
||||||
|
windows: ./build/windows/Taskfile.yml
|
||||||
|
darwin: ./build/darwin/Taskfile.yml
|
||||||
|
linux: ./build/linux/Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
APP_NAME: "firefly-launcher"
|
||||||
|
BIN_DIR: "bin"
|
||||||
|
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:build"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:package"
|
||||||
|
|
||||||
|
run:
|
||||||
|
summary: Runs the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:run"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
summary: Runs the application in development mode
|
||||||
|
cmds:
|
||||||
|
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||||
|
|
||||||
BIN
assets/7zip/7za.dll
Normal file
BIN
assets/7zip/7za.dll
Normal file
Binary file not shown.
BIN
assets/7zip/7za.exe
Normal file
BIN
assets/7zip/7za.exe
Normal file
Binary file not shown.
BIN
assets/7zip/7zxa.dll
Normal file
BIN
assets/7zip/7zxa.dll
Normal file
Binary file not shown.
BIN
assets/HDiffPatch/hdiffz.exe
Normal file
BIN
assets/HDiffPatch/hdiffz.exe
Normal file
Binary file not shown.
BIN
assets/HDiffPatch/hpatchz.exe
Normal file
BIN
assets/HDiffPatch/hpatchz.exe
Normal file
Binary file not shown.
78
build/Taskfile.yml
Normal file
78
build/Taskfile.yml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
go:mod:tidy:
|
||||||
|
summary: Runs `go mod tidy`
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
install:frontend:deps:
|
||||||
|
summary: Install frontend dependencies
|
||||||
|
dir: frontend
|
||||||
|
sources:
|
||||||
|
- package.json
|
||||||
|
- package-lock.json
|
||||||
|
generates:
|
||||||
|
- node_modules/*
|
||||||
|
preconditions:
|
||||||
|
- sh: npm version
|
||||||
|
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
|
||||||
|
cmds:
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
build:frontend:
|
||||||
|
summary: Build the frontend project
|
||||||
|
dir: frontend
|
||||||
|
sources:
|
||||||
|
- "**/*"
|
||||||
|
generates:
|
||||||
|
- dist/*
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
- task: generate:bindings
|
||||||
|
cmds:
|
||||||
|
- npm run {{.BUILD_COMMAND}} -q
|
||||||
|
env:
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
vars:
|
||||||
|
BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}'
|
||||||
|
|
||||||
|
|
||||||
|
generate:bindings:
|
||||||
|
summary: Generates bindings for the frontend
|
||||||
|
deps:
|
||||||
|
- task: go:mod:tidy
|
||||||
|
sources:
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
generates:
|
||||||
|
- "frontend/bindings/**/*"
|
||||||
|
cmds:
|
||||||
|
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
|
||||||
|
|
||||||
|
generate:icons:
|
||||||
|
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
||||||
|
dir: build
|
||||||
|
sources:
|
||||||
|
- "appicon.png"
|
||||||
|
generates:
|
||||||
|
- "icons.icns"
|
||||||
|
- "icons.ico"
|
||||||
|
cmds:
|
||||||
|
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icons.ico
|
||||||
|
|
||||||
|
dev:frontend:
|
||||||
|
summary: Runs the frontend in development mode
|
||||||
|
dir: frontend
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
cmds:
|
||||||
|
- npm run dev -- --port {{.VITE_PORT}} --strictPort
|
||||||
|
|
||||||
|
update:build-assets:
|
||||||
|
summary: Updates the build assets
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
|
||||||
BIN
build/appicon.png
Normal file
BIN
build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 944 KiB |
53
build/config.yml
Normal file
53
build/config.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# This file contains the configuration for this project.
|
||||||
|
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
|
||||||
|
# Note that this will overwrite any changes you have made to the assets.
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
# This information is used to generate the build assets.
|
||||||
|
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
|
||||||
|
copyright: "@ 2025, Firefly Shelter" # Copyright text
|
||||||
|
comments: "Custom game launcher built for convenience and quick access" # Comments
|
||||||
|
version: "0.1.0" # The application version
|
||||||
|
|
||||||
|
# Dev mode configuration
|
||||||
|
dev_mode:
|
||||||
|
root_path: .
|
||||||
|
log_level: warn
|
||||||
|
debounce: 1000
|
||||||
|
ignore:
|
||||||
|
dir:
|
||||||
|
- .git
|
||||||
|
- node_modules
|
||||||
|
- frontend
|
||||||
|
- bin
|
||||||
|
file:
|
||||||
|
- .DS_Store
|
||||||
|
- .gitignore
|
||||||
|
- .gitkeep
|
||||||
|
watched_extension:
|
||||||
|
- "*.go"
|
||||||
|
git_ignore: true
|
||||||
|
executes:
|
||||||
|
- cmd: wails3 task common:install:frontend:deps
|
||||||
|
type: once
|
||||||
|
- cmd: wails3 task common:dev:frontend
|
||||||
|
type: background
|
||||||
|
- cmd: go mod tidy
|
||||||
|
type: blocking
|
||||||
|
- cmd: wails3 task build
|
||||||
|
type: blocking
|
||||||
|
- cmd: wails3 task run
|
||||||
|
type: primary
|
||||||
|
|
||||||
|
# File Associations
|
||||||
|
# More information at: https://v3alpha.wails.io/noit/done/yet
|
||||||
|
fileAssociations:
|
||||||
|
|
||||||
|
|
||||||
|
# Other data
|
||||||
|
other:
|
||||||
|
- name: My Other Data
|
||||||
32
build/darwin/Info.dev.plist
Normal file
32
build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Firefly Launcher</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Firefly Launcher</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.fireflyshelter.fireflylauncher</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>Custom game launcher built for convenience and quick access</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icons</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>@ 2025, Firefly Shelter</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
27
build/darwin/Info.plist
Normal file
27
build/darwin/Info.plist
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Firefly Launcher</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Firefly Launcher</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.fireflyshelter.fireflylauncher</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>Custom game launcher built for convenience and quick access</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icons</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>@ 2025, Firefly Shelter</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
70
build/darwin/Taskfile.yml
Normal file
70
build/darwin/Taskfile.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Creates a production build of the application
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||||
|
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
build:universal:
|
||||||
|
summary: Builds darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: amd64
|
||||||
|
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: arm64
|
||||||
|
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
cmds:
|
||||||
|
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application into a `.app` bundle
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
package:universal:
|
||||||
|
summary: Packages darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- task: build:universal
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
|
||||||
|
create:app:bundle:
|
||||||
|
summary: Creates an `.app` bundle
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
||||||
|
- 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
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
BIN
build/darwin/icons.icns
Normal file
BIN
build/darwin/icons.icns
Normal file
Binary file not shown.
114
build/linux/Taskfile.yml
Normal file
114
build/linux/Taskfile.yml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Linux
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application for Linux
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: create:appimage
|
||||||
|
- task: create:deb
|
||||||
|
- task: create:rpm
|
||||||
|
- task: create:aur
|
||||||
|
|
||||||
|
create:appimage:
|
||||||
|
summary: Creates an AppImage
|
||||||
|
dir: build/linux/appimage
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
cmds:
|
||||||
|
- cp {{.APP_BINARY}} {{.APP_NAME}}
|
||||||
|
- cp ../../appicon.png appicon.png
|
||||||
|
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
APP_BINARY: '../../../bin/{{.APP_NAME}}'
|
||||||
|
ICON: '../../appicon.png'
|
||||||
|
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
|
||||||
|
OUTPUT_DIR: '../../../bin'
|
||||||
|
|
||||||
|
create:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:deb
|
||||||
|
|
||||||
|
create:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:rpm
|
||||||
|
|
||||||
|
create:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:aur
|
||||||
|
|
||||||
|
generate:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:dotdesktop:
|
||||||
|
summary: Generates a `.desktop` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
|
||||||
|
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
EXEC: '{{.APP_NAME}}'
|
||||||
|
ICON: 'appicon'
|
||||||
|
CATEGORIES: 'Development;'
|
||||||
|
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
26
build/linux/appimage/build.sh
Normal file
26
build/linux/appimage/build.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2018-Present Lea Anthony
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
# Fail script on any error
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# Define variables
|
||||||
|
APP_DIR="${APP_NAME}.AppDir"
|
||||||
|
|
||||||
|
# Create AppDir structure
|
||||||
|
mkdir -p "${APP_DIR}/usr/bin"
|
||||||
|
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
|
||||||
|
|
||||||
|
# Run linuxdeploy to bundle the application
|
||||||
|
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||||
|
|
||||||
|
# Rename the generated AppImage
|
||||||
|
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
|
||||||
|
|
||||||
50
build/linux/nfpm/nfpm.yaml
Normal file
50
build/linux/nfpm/nfpm.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Feel free to remove those if you don't want/need to use them.
|
||||||
|
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||||
|
#
|
||||||
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
|
name: "Firefly Launcher"
|
||||||
|
arch: ${GOARCH}
|
||||||
|
platform: "linux"
|
||||||
|
version: "0.1.0"
|
||||||
|
section: "default"
|
||||||
|
priority: "extra"
|
||||||
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
|
description: "Custom game launcher built for convenience and quick access"
|
||||||
|
vendor: "Firefly Shelter"
|
||||||
|
homepage: "https://wails.io"
|
||||||
|
license: "MIT"
|
||||||
|
release: "1"
|
||||||
|
|
||||||
|
contents:
|
||||||
|
- src: "./bin/Firefly Launcher"
|
||||||
|
dst: "/usr/local/bin/Firefly Launcher"
|
||||||
|
- src: "./build/appicon.png"
|
||||||
|
dst: "/usr/share/icons/hicolor/128x128/apps/Firefly Launcher.png"
|
||||||
|
- src: "./build/linux/Firefly Launcher.desktop"
|
||||||
|
dst: "/usr/share/applications/Firefly Launcher.desktop"
|
||||||
|
|
||||||
|
depends:
|
||||||
|
- gtk3
|
||||||
|
- libwebkit2gtk
|
||||||
|
|
||||||
|
# replaces:
|
||||||
|
# - foobar
|
||||||
|
# provides:
|
||||||
|
# - bar
|
||||||
|
# depends:
|
||||||
|
# - gtk3
|
||||||
|
# - libwebkit2gtk
|
||||||
|
# recommends:
|
||||||
|
# - whatever
|
||||||
|
# suggests:
|
||||||
|
# - something-else
|
||||||
|
# conflicts:
|
||||||
|
# - 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
build/linux/nfpm/scripts/postinstall.sh
Normal file
1
build/linux/nfpm/scripts/postinstall.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
build/linux/nfpm/scripts/postremove.sh
Normal file
1
build/linux/nfpm/scripts/postremove.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
build/linux/nfpm/scripts/preinstall.sh
Normal file
1
build/linux/nfpm/scripts/preinstall.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
build/linux/nfpm/scripts/preremove.sh
Normal file
1
build/linux/nfpm/scripts/preremove.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
60
build/windows/Taskfile.yml
Normal file
60
build/windows/Taskfile.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Windows
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
PRODUCTION: '{{.PRODUCTION}}'
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- task: generate:syso
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
||||||
|
- cmd: powershell Remove-item *.syso
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: rm -f *.syso
|
||||||
|
platforms: [linux, darwin]
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application into a `.exe` bundle
|
||||||
|
cmds:
|
||||||
|
- task: create:nsis:installer
|
||||||
|
|
||||||
|
generate:syso:
|
||||||
|
summary: Generates Windows `.syso` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
create:nsis:installer:
|
||||||
|
summary: Creates an NSIS installer
|
||||||
|
dir: build/windows/nsis
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
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
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
|
||||||
BIN
build/windows/icon.ico
Normal file
BIN
build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
build/windows/icons.ico
Normal file
BIN
build/windows/icons.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
15
build/windows/info.json
Normal file
15
build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "0.1.0"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "0.1.0",
|
||||||
|
"CompanyName": "Firefly Shelter",
|
||||||
|
"FileDescription": "Custom game launcher built for convenience and quick access",
|
||||||
|
"LegalCopyright": "@ 2025, Firefly Shelter",
|
||||||
|
"ProductName": "Firefly Launcher",
|
||||||
|
"Comments": "Custom game launcher built for convenience and quick access"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
build/windows/nsis/project.nsi
Normal file
112
build/windows/nsis/project.nsi
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## 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_PRODUCTVERSION "1.0.0" # Default "0.1.0"
|
||||||
|
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "@ 2025, FireflyShelter"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||||
|
ManifestDPIAware true
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
||||||
212
build/windows/nsis/wails_tools.nsh
Normal file
212
build/windows/nsis/wails_tools.nsh
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "firefly-launcher"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "Firefly Shelter"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "Firefly Launcher"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "0.1.0"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "@ 2025, Firefly Shelter"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${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"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
25
build/windows/wails.exe.manifest
Normal file
25
build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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="*"/>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" 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>
|
||||||
93
frontend/Inter Font License.txt
Normal file
93
frontend/Inter Font License.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
78
frontend/bindings/firefly-launcher/internal/fsservice.js
Normal file
78
frontend/bindings/firefly-launcher/internal/fsservice.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// @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;
|
||||||
|
}
|
||||||
59
frontend/bindings/firefly-launcher/internal/gitservice.js
Normal file
59
frontend/bindings/firefly-launcher/internal/gitservice.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} oldVersion
|
||||||
|
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
|
||||||
|
*/
|
||||||
|
export function GetLatestProxyVersion(oldVersion) {
|
||||||
|
let $resultPromise = /** @type {any} */($Call.ByID(1462362449, oldVersion));
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} oldVersion
|
||||||
|
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
|
||||||
|
*/
|
||||||
|
export function GetLatestServerVersion(oldVersion) {
|
||||||
|
let $resultPromise = /** @type {any} */($Call.ByID(1447982978, oldVersion));
|
||||||
|
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;
|
||||||
|
}
|
||||||
54
frontend/bindings/firefly-launcher/internal/hdiffzservice.js
Normal file
54
frontend/bindings/firefly-launcher/internal/hdiffzservice.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// @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} 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
|
||||||
|
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||||
|
*/
|
||||||
|
export function DataExtract(gamePath, patchPath) {
|
||||||
|
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath));
|
||||||
|
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;
|
||||||
|
}
|
||||||
14
frontend/bindings/firefly-launcher/internal/index.js
Normal file
14
frontend/bindings/firefly-launcher/internal/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
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 {
|
||||||
|
FSService,
|
||||||
|
GitService,
|
||||||
|
HdiffzService,
|
||||||
|
LanguageService
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// @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<[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> & { cancel(): void }}
|
||||||
|
*/
|
||||||
|
export function SetLanguage(path, text, voice) {
|
||||||
|
let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice));
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-theme="cupcake" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Wails + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5459
frontend/package-lock.json
generated
Normal file
5459
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
frontend/package.json
Normal file
43
frontend/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "react-ts-latest",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build:dev": "tsc && vite build --minify false --mode development",
|
||||||
|
"build": "tsc && vite build --mode production",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-router": "^1.124.0",
|
||||||
|
"@tanstack/react-router-devtools": "^1.124.0",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"motion": "^12.23.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"zustand": "^5.0.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/router-plugin": "^1.124.0",
|
||||||
|
"@types/node": "^24.0.10",
|
||||||
|
"@types/path-browserify": "^1.0.3",
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"@wailsio/runtime": "^3.0.0-alpha.66",
|
||||||
|
"daisyui": "^5.0.43",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/Inter-Medium.ttf
Normal file
BIN
frontend/public/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
frontend/public/bg.jpg
Normal file
BIN
frontend/public/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
1605
frontend/runtime-debug.js
Normal file
1605
frontend/runtime-debug.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/runtime.js
Normal file
1
frontend/runtime.js
Normal file
File diff suppressed because one or more lines are too long
11
frontend/src/components/footer/index.tsx
Normal file
11
frontend/src/components/footer/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return(
|
||||||
|
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Wails & DaisyUi)</p>
|
||||||
|
</aside>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
frontend/src/components/themeController/index.tsx
Normal file
29
frontend/src/components/themeController/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function ThemeController() {
|
||||||
|
const [theme, setTheme] = useState(localStorage.getItem("theme") ?? "light");
|
||||||
|
const handleToggle = (e: any) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setTheme("cupcake");
|
||||||
|
} else {
|
||||||
|
setTheme("night");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('theme', theme!)
|
||||||
|
const localTheme = localStorage.getItem('theme')
|
||||||
|
document.querySelector('html')?.setAttribute('data-theme', localTheme!)
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="toggle text-base-content">
|
||||||
|
<input type="checkbox" onChange={handleToggle} className="theme-controller" />
|
||||||
|
|
||||||
|
<svg aria-label="moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g strokeLinejoin="round" strokeLinecap="round" strokeWidth="2" fill="none" stroke="currentColor"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></g></svg>
|
||||||
|
<svg aria-label="sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g strokeLinejoin="round" strokeLinecap="round" strokeWidth="2" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></g></svg>
|
||||||
|
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
frontend/src/hooks/index.ts
Normal file
1
frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./useGlobalEvents";
|
||||||
65
frontend/src/hooks/useGlobalEvents.tsx
Normal file
65
frontend/src/hooks/useGlobalEvents.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// useGlobalEvents.ts
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Events } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
export function useGlobalEvents({
|
||||||
|
setGameRunning,
|
||||||
|
setServerRunning,
|
||||||
|
setProxyRunning,
|
||||||
|
setProgressUpdate,
|
||||||
|
setMaxProgressUpdate,
|
||||||
|
setProgressDownload,
|
||||||
|
setDownloadSpeed,
|
||||||
|
setMessageUpdate,
|
||||||
|
|
||||||
|
}: {
|
||||||
|
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);
|
||||||
|
const onProxyExit = () => setProxyRunning(false);
|
||||||
|
|
||||||
|
const onDownload = (event: any) => {
|
||||||
|
const { percent, speed } = event.data[0];
|
||||||
|
setProgressDownload(Number(percent));
|
||||||
|
setDownloadSpeed(Number(speed));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateProgress = (event: any) => {
|
||||||
|
const { progress, maxProgress } = event.data[0];
|
||||||
|
setProgressUpdate(Number(progress));
|
||||||
|
setMaxProgressUpdate(Number(maxProgress));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessageUpdate = (event: any) => {
|
||||||
|
const { message } = event.data[0];
|
||||||
|
setMessageUpdate(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Events.Off("download:server");
|
||||||
|
Events.Off("download:proxy");
|
||||||
|
Events.Off("game:exit");
|
||||||
|
Events.Off("server:exit");
|
||||||
|
Events.Off("proxy:exit");
|
||||||
|
Events.Off("hdiffz:progress");
|
||||||
|
Events.Off("hdiffz:message");
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
29
frontend/src/main.tsx
Normal file
29
frontend/src/main.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
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)
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
}
|
||||||
31
frontend/src/pages/about/index.tsx
Normal file
31
frontend/src/pages/about/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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">
|
||||||
|
<h1 className="text-4xl font-bold text-primary text-center">About</h1>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-lg leading-relaxed">
|
||||||
|
Hello! I'm <span className="font-semibold text-error">Kain</span>, a developer passionate about building useful tools and improving user experiences.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg leading-relaxed">
|
||||||
|
I created a lightweight and modern <span className="font-semibold text-success">Game Launcher</span> to help users easily launch and manage their games with better performance and simplicity.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg leading-relaxed">
|
||||||
|
The launcher is built using <span className="font-mono text-info">Go + Wails</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg leading-relaxed">
|
||||||
|
My goal is to make tools that are fast, efficient, and enjoyable to use — and this launcher is just the beginning.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center pt-4">
|
||||||
|
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
198
frontend/src/pages/analysis/index.tsx
Normal file
198
frontend/src/pages/analysis/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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">
|
||||||
|
<h1 className="text-4xl font-bold text-primary text-center">
|
||||||
|
Firefly Analysis & Veritas Plugin
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* About Veritas Section */}
|
||||||
|
<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>About Veritas</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-green-700">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-green-600 text-lg">⚡</div>
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">
|
||||||
|
<span className="font-semibold text-success">Veritas</span> is a powerful{" "}
|
||||||
|
<span className="text-info font-mono bg-blue-100 px-2 py-1 rounded">Damage Logger</span> designed for analyzing damage in real-time during gameplay.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">It's lightweight, fast, and easy to use for comprehensive damage analysis.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-green-600 text-lg">📁</span>
|
||||||
|
<span className="font-semibold text-green-800">GitHub Repository</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://github.com/hessiser/veritas"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="link link-info font-mono break-all"
|
||||||
|
>
|
||||||
|
https://github.com/hessiser/veritas
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Web Analysis Tools Section */}
|
||||||
|
<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>Web Analysis Tools</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-blue-700">
|
||||||
|
Use these web applications for real-time damage analysis with Veritas:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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://sranalysis.kain.id.vn/"
|
||||||
|
className="link link-warning font-mono text-sm break-all"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://sranalysis.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://sr-analysis.vercel.app/"
|
||||||
|
className="link link-warning font-mono text-sm break-all"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://sr-analysis.vercel.app/
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="text-yellow-600 text-lg">💡</div>
|
||||||
|
<p className="text-yellow-800 text-sm">
|
||||||
|
<strong>Tip:</strong> If your country has issues loading from the master site, please use the backup site instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Installation Instructions */}
|
||||||
|
<div className="bg-red-50 border-l-4 border-red-400 p-6 rounded-r-lg">
|
||||||
|
<h2 className="text-2xl font-bold text-red-800 flex items-center gap-2 mb-4">
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span>Installation Instructions</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-white border border-red-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-red-600 text-xl">📋</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-red-800 mb-2">Important Setup Step</h3>
|
||||||
|
<p className="text-red-700 mb-2">
|
||||||
|
After downloading Veritas, you must rename the file for it to work properly:
|
||||||
|
</p>
|
||||||
|
<div className="bg-red-100 p-3 rounded-lg">
|
||||||
|
<p className="text-red-800 font-mono text-sm">
|
||||||
|
Rename: <code className="bg-red-200 px-1 py-0.5 rounded">veritas.dll</code> → <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code>
|
||||||
|
</p>
|
||||||
|
<p className="text-red-800 text-sm mt-1">
|
||||||
|
Then place <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code> into your game directory.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Instructions */}
|
||||||
|
<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>How to Use Web App</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Firefly GO Local */}
|
||||||
|
<div className="bg-white border border-purple-200 rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||||
|
<span className="text-purple-600">🚀</span>
|
||||||
|
<span>For Firefly GO Local</span>
|
||||||
|
</h3>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
|
||||||
|
<p>Open one of the web analysis tools.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-purple-600">3.</span>
|
||||||
|
<p>Go to <strong>Connection Settings</strong> → select <strong>Connection Type: PS</strong> → click <strong>Connect</strong>.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-purple-600">4.</span>
|
||||||
|
<p>Once connected, play the game. The tool will automatically analyze in the background.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other Private Servers */}
|
||||||
|
<div className="bg-white border border-purple-200 rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||||
|
<span className="text-purple-600">🌐</span>
|
||||||
|
<span>For Other Private Servers</span>
|
||||||
|
</h3>
|
||||||
|
<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">Private Server</span>.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
|
||||||
|
<p>Open one of the web analysis tools.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-purple-600">3.</span>
|
||||||
|
<p>Go to <strong>Connection Settings</strong> → select <strong>Connection Type: Native</strong> → click <strong>Connect</strong>.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-purple-600">4.</span>
|
||||||
|
<p>Once connected, play the game normally.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center pt-6">
|
||||||
|
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
345
frontend/src/pages/hdiffz/index.tsx
Normal file
345
frontend/src/pages/hdiffz/index.tsx
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
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 { motion } from "motion/react"
|
||||||
|
import useHdiffzStore from "@/stores/hdiffzStore"
|
||||||
|
|
||||||
|
export default function HdiffzPage() {
|
||||||
|
const { gameDir, setGameDir } = useSettingStore()
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
setIsLoading,
|
||||||
|
folderCheckResult,
|
||||||
|
setFolderCheckResult,
|
||||||
|
diffDir,
|
||||||
|
setDiffDir,
|
||||||
|
diffCheckResult,
|
||||||
|
setDiffCheckResult,
|
||||||
|
isDiffLoading,
|
||||||
|
setIsDiffLoading,
|
||||||
|
progressUpdate,
|
||||||
|
setProgressUpdate,
|
||||||
|
maxProgressUpdate,
|
||||||
|
setMaxProgressUpdate,
|
||||||
|
stageType,
|
||||||
|
setStageType,
|
||||||
|
messageUpdate,
|
||||||
|
setMessageUpdate
|
||||||
|
} = useHdiffzStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getLanguage = async () => {
|
||||||
|
if (gameDir) {
|
||||||
|
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
|
||||||
|
const fullPath = `${gameDir}/${subPath}`
|
||||||
|
|
||||||
|
const exists = await FSService.DirExists(fullPath)
|
||||||
|
if (exists) {
|
||||||
|
setFolderCheckResult('success')
|
||||||
|
} else {
|
||||||
|
setFolderCheckResult('error')
|
||||||
|
setGameDir('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLanguage()
|
||||||
|
}, [gameDir])
|
||||||
|
|
||||||
|
const handlePickGameFolder = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading({game: true, diff: false})
|
||||||
|
const basePath = await FSService.PickFolder()
|
||||||
|
if (basePath) {
|
||||||
|
setGameDir(basePath)
|
||||||
|
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
|
||||||
|
const fullPath = `${basePath}/${subPath}`
|
||||||
|
|
||||||
|
const exists = await FSService.DirExists(fullPath)
|
||||||
|
setFolderCheckResult(exists ? 'success' : 'error')
|
||||||
|
setGameDir(exists ? basePath : '')
|
||||||
|
if (!exists) {
|
||||||
|
toast.error('Game directory not found. Please select the correct folder.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('No folder path selected')
|
||||||
|
setFolderCheckResult('error')
|
||||||
|
setGameDir('')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('PickFolder error:', err)
|
||||||
|
setFolderCheckResult('error')
|
||||||
|
} finally {
|
||||||
|
setIsLoading({game: false, diff: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePickDiffFile = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading({game: false, diff: true})
|
||||||
|
const basePath = await FSService.PickFile()
|
||||||
|
if (basePath) {
|
||||||
|
if (!basePath.endsWith(".7z") && !basePath.endsWith(".zip") && !basePath.endsWith(".rar")) {
|
||||||
|
toast.error('Not valid file type')
|
||||||
|
setDiffCheckResult('error')
|
||||||
|
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 {
|
||||||
|
toast.error('No file path selected')
|
||||||
|
setDiffCheckResult('error')
|
||||||
|
setDiffDir('')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('PickFile error:', err)
|
||||||
|
setDiffCheckResult('error')
|
||||||
|
} finally {
|
||||||
|
setIsLoading({game: false, diff: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateGame = async () => {
|
||||||
|
try {
|
||||||
|
setIsDiffLoading(true)
|
||||||
|
if (!gameDir || !diffDir) {
|
||||||
|
toast.error('Please select game directory and diff file')
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStageType('Version Validate')
|
||||||
|
setProgressUpdate(0)
|
||||||
|
setMaxProgressUpdate(1)
|
||||||
|
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
|
||||||
|
if (!validVersion) {
|
||||||
|
toast.error(errorVersion)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setProgressUpdate(1)
|
||||||
|
|
||||||
|
setStageType('Data Extract')
|
||||||
|
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir)
|
||||||
|
if (!validData) {
|
||||||
|
toast.error(errorData)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStageType('Cut Data')
|
||||||
|
setMessageUpdate('')
|
||||||
|
const [validCut, errorCut] = await HdiffzService.CutData(gameDir)
|
||||||
|
if (!validCut) {
|
||||||
|
toast.error(errorCut)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStageType('Patch Data')
|
||||||
|
const [validPatch, errorPatch] = await HdiffzService.PatchData(gameDir)
|
||||||
|
if (!validPatch) {
|
||||||
|
toast.error(errorPatch)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStageType('Delete old files')
|
||||||
|
const [validDelete, errorDelete] = await HdiffzService.DeleteFiles(gameDir)
|
||||||
|
if (!validDelete) {
|
||||||
|
toast.error(errorDelete)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.success('Update game completed')
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error('PickFile error:', err)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
} finally {
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-2 mx-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-2">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">
|
||||||
|
🎮 Game Update by Hdiffz
|
||||||
|
</h1>
|
||||||
|
<p className="">Help you update game with hdiffz</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="rounded-2xl p-2 space-y-4">
|
||||||
|
|
||||||
|
{/* Folder Selection Section */}
|
||||||
|
<div className="pb-2">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Folder className="text-primary" size={24} />
|
||||||
|
Game Directory
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-2 items-center'>
|
||||||
|
<button
|
||||||
|
onClick={handlePickGameFolder}
|
||||||
|
disabled={isLoading.game}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
<Folder size={20} />
|
||||||
|
{isLoading.game ? 'Selecting...' : 'Select Game Folder'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{gameDir && (
|
||||||
|
<div className="rounded-lg p-2">
|
||||||
|
<p className="font-mono text-sm px-3 py-2 rounded border truncate max-w-full overflow-hidden whitespace-nowrap">
|
||||||
|
{gameDir}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{folderCheckResult && (
|
||||||
|
<div className={`flex items-center gap-2 p-3 rounded-lg ${folderCheckResult === 'success'
|
||||||
|
? 'bg-success/5 text-success border border-success'
|
||||||
|
: 'bg-error/5 text-error border border-error'
|
||||||
|
}`}>
|
||||||
|
{folderCheckResult === 'success' ? (
|
||||||
|
<>
|
||||||
|
<Check size={20} />
|
||||||
|
<span>Valid game directory found!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X size={20} />
|
||||||
|
<span>Game directory not found. Please select the correct folder.</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Folder Selection Section */}
|
||||||
|
<div className="pb-2">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<File className="text-primary" size={24} />
|
||||||
|
Diff file Directory
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-2 items-center'>
|
||||||
|
<button
|
||||||
|
onClick={handlePickDiffFile}
|
||||||
|
disabled={isLoading.diff}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
<File size={20} />
|
||||||
|
{isLoading.diff ? 'Selecting...' : 'Select Diff file Folder'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{diffDir && (
|
||||||
|
<div className="rounded-lg p-2">
|
||||||
|
<p className="font-mono text-sm px-3 py-2 rounded border truncate max-w-full overflow-hidden whitespace-nowrap">
|
||||||
|
{diffDir}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{diffCheckResult && (
|
||||||
|
<div className={`flex items-center gap-2 p-3 mt-2 rounded-lg ${diffCheckResult === 'success'
|
||||||
|
? 'bg-success/5 text-success border border-success'
|
||||||
|
: 'bg-error/5 text-error border border-error'
|
||||||
|
}`}>
|
||||||
|
{diffCheckResult === 'success' ? (
|
||||||
|
<>
|
||||||
|
<Check size={20} />
|
||||||
|
<span>Valid diff file found!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X size={20} />
|
||||||
|
<span>Diff file not found. Please select the correct file.</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apply Button */}
|
||||||
|
<div className="mt-6 flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={handleUpdateGame}
|
||||||
|
disabled={!diffDir || !gameDir || isLoading.game || isLoading.diff}
|
||||||
|
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
|
||||||
|
>
|
||||||
|
<Settings size={20} />
|
||||||
|
{isDiffLoading ? 'Updating...' : 'Update Game'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDiffLoading && (
|
||||||
|
<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-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 text-center">
|
||||||
|
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
|
||||||
|
Update Game
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="w-full p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-center items-center text-sm text-white/80">
|
||||||
|
<span className="font-bold text-lg text-warning">{stageType}:</span>
|
||||||
|
<div className="flex items-center gap-4 ml-2">
|
||||||
|
{stageType !== 'Cut Data' && <span className="text-white font-bold">{progressUpdate.toFixed(0)} / {maxProgressUpdate.toFixed(0)}</span>}
|
||||||
|
{stageType === 'Cut Data' && <span className="text-white font-bold truncate max-w-full overflow-hidden whitespace-nowrap">{messageUpdate}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-white/20 rounded-full h-2 overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${(progressUpdate/maxProgressUpdate)*100}%` }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-lg text-white/60">
|
||||||
|
Please wait...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div className="bg-info/5 rounded-lg p-4 border border-info/30 mt-6">
|
||||||
|
<h3 className="font-medium text-error mb-2">📋 Instructions:</h3>
|
||||||
|
<ol className="text-sm text-error space-y-1">
|
||||||
|
<li>1. Click "Select Game Folder" and choose your game's root directory</li>
|
||||||
|
<li>2. Wait for the system to validate the game directory</li>
|
||||||
|
<li>3. Click "Select Diff file Folder" and choose your diff file's root directory</li>
|
||||||
|
<li>4. Wait for the system to validate the diff file directory</li>
|
||||||
|
<li>5. Click "Update Game" to save your changes</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
245
frontend/src/pages/howto/index.tsx
Normal file
245
frontend/src/pages/howto/index.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
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">
|
||||||
|
<h1 className="text-4xl font-bold text-primary text-center">How to Use</h1>
|
||||||
|
|
||||||
|
{/* Section 1: Launcher 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>Using the Launcher 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>Automatically update <span className="font-semibold text-amber-600">Firefly Go</span> and proxy tools when launching.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-green-600 text-lg">🎮</div>
|
||||||
|
<p>Launch the game directly through the launcher with correct parameters and runtime environment.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-green-600 text-lg">🌐</div>
|
||||||
|
<p>Support switching in-game language (e.g., EN, JP, ZH, KR) via{" "}
|
||||||
|
<a href="/language" className="link link-info font-mono">Language Tools</a>
|
||||||
|
</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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 2: FireflyGo Commands */}
|
||||||
|
<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>FireflyGo Chat Commands</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-blue-700 mb-4">
|
||||||
|
Below are in-game chat commands you can use. Some commands require you to enable{" "}
|
||||||
|
<span className="font-semibold text-warning">Theorycraft Mode</span>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Theorycraft Mode Warning */}
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-red-600 text-xl">🔒</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-red-800 mb-2">Theorycraft Mode Required</h3>
|
||||||
|
<p className="text-red-700 mb-2">The following commands are only available when <strong>Theorycraft Mode</strong> is enabled:</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/cycle</code>
|
||||||
|
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/hp</code>
|
||||||
|
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/log</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Commands List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
|
||||||
|
|
||||||
|
{/* Theorycraft Toggle */}
|
||||||
|
<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">Theorycraft Mode</h4>
|
||||||
|
<div className="space-y-1 text-blue-700">
|
||||||
|
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 1</code> — Enable Theorycraft Mode</p>
|
||||||
|
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 0</code> — Disable Theorycraft Mode</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cycle Command */}
|
||||||
|
<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">Cycle Control <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">/cycle N</code> — Set cycle count in battle</p>
|
||||||
|
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 30</code> sets battle to 30 cycles</p>
|
||||||
|
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 0</code> disables custom cycle</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HP Command */}
|
||||||
|
<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">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>
|
||||||
|
</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">
|
||||||
|
<div className="text-blue-600 text-lg">📝</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-blue-800 mb-1">Battle Log <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">/log 1</code> — Enable battle log output</p>
|
||||||
|
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 0</code> — Disable battle log</p>
|
||||||
|
<p className="text-sm">Output will be written as <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">.json</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skip Command */}
|
||||||
|
<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">Skip Nodes</h4>
|
||||||
|
<div className="space-y-1 text-blue-700">
|
||||||
|
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip N</code> — Skip nodes in MOC / AS / Pure Fiction</p>
|
||||||
|
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 2</code> skips node 2</p>
|
||||||
|
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 0</code> disables skipping</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ID Command */}
|
||||||
|
<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">Character Path Switch</h4>
|
||||||
|
<div className="space-y-1 text-blue-700">
|
||||||
|
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id CHAR_ID</code> — Switch path for multi-form characters</p>
|
||||||
|
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id 8008</code> to change MC (Trailblazer) form</p>
|
||||||
|
<p className="text-sm">Works with IDs like <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">8001 → 8008</code>, <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">1001 → 1224</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Update Command */}
|
||||||
|
<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">Refresh Data</h4>
|
||||||
|
<div className="space-y-1 text-blue-700">
|
||||||
|
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/update</code> — Refresh server data from current <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 3: Other Notes */}
|
||||||
|
<div className="bg-gray-50 border-l-4 border-gray-400 p-6 rounded-r-lg">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-2 mb-4">
|
||||||
|
<span>📌</span>
|
||||||
|
<span>Other Notes</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Administrator Note */}
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-yellow-600 text-xl">⚠️</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-yellow-800 mb-1">Administrator Rights</h3>
|
||||||
|
<p className="text-yellow-700">
|
||||||
|
Always run the launcher as Administrator for file permission access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Backup Note */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-blue-600 text-xl">💾</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-blue-800 mb-1">Backup Data</h3>
|
||||||
|
<p className="text-blue-700">
|
||||||
|
Backup your <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">config.json</code> and{' '}
|
||||||
|
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code> regularly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Voice Pack Note */}
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-green-600 text-xl">🎵</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-green-800 mb-2">Enable Voice Packs in Beta Client</h3>
|
||||||
|
<div className="space-y-3 text-green-700">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-green-600">1.</span>
|
||||||
|
<div>
|
||||||
|
<p className="mb-1">Copy the desired voice folder (e.g., <code className="bg-green-100 px-1 py-0.5 rounded text-sm">Japanese</code>, <code className="bg-green-100 px-1 py-0.5 rounded text-sm">English</code>) from:</p>
|
||||||
|
<code className="block bg-green-100 px-2 py-1 rounded text-sm mt-1">
|
||||||
|
Star Rail\Games\StarRail_Data\Persistent\Audio\AudioPackage\Windows
|
||||||
|
</code>
|
||||||
|
<p className="mt-1">to the beta folder by clicking <strong>"Open Voice Folder"</strong> on the Home tab.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-medium min-w-[20px] text-green-600">2.</span>
|
||||||
|
<p>When launching the game for the first time, it may delete the voice folder. If so, repeat step 1 to restore it.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center pt-4">
|
||||||
|
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
285
frontend/src/pages/language/index.tsx
Normal file
285
frontend/src/pages/language/index.tsx
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
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 { toast } from 'react-toastify'
|
||||||
|
import useSettingStore from '@/stores/settingStore'
|
||||||
|
|
||||||
|
export default function LanguagePage() {
|
||||||
|
const { gameDir, setGameDir } = useSettingStore()
|
||||||
|
const [folderCheckResult, setFolderCheckResult] = useState<'success' | 'error' | null>(null)
|
||||||
|
|
||||||
|
const [textLang, setTextLang] = useState('')
|
||||||
|
const [voiceLang, setVoiceLang] = useState('')
|
||||||
|
|
||||||
|
const [selectedTextLang, setSelectedTextLang] = useState('')
|
||||||
|
const [selectedVoiceLang, setSelectedVoiceLang] = useState('')
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isSettingLanguage, setIsSettingLanguage] = useState(false)
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'en', label: 'English', flag: '🇺🇸' },
|
||||||
|
{ value: 'cn', label: 'Chinese', flag: '🇨🇳' },
|
||||||
|
{ value: 'jp', label: 'Japanese', flag: '🇯🇵' },
|
||||||
|
{ value: 'kr', label: 'Korean', flag: '🇰🇷' }
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getLanguage = async () => {
|
||||||
|
if (gameDir) {
|
||||||
|
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
|
||||||
|
const fullPath = `${gameDir}/${subPath}`
|
||||||
|
|
||||||
|
const exists = await FSService.DirExists(fullPath)
|
||||||
|
if (exists) {
|
||||||
|
const [textLang, voiceLang] = await LanguageService.GetLanguage(fullPath)
|
||||||
|
setTextLang(textLang)
|
||||||
|
setVoiceLang(voiceLang)
|
||||||
|
setFolderCheckResult('success')
|
||||||
|
setSelectedTextLang(textLang)
|
||||||
|
setSelectedVoiceLang(voiceLang)
|
||||||
|
} else {
|
||||||
|
setTextLang('')
|
||||||
|
setVoiceLang('')
|
||||||
|
setSelectedTextLang('')
|
||||||
|
setSelectedVoiceLang('')
|
||||||
|
setFolderCheckResult('error')
|
||||||
|
setGameDir('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLanguage()
|
||||||
|
}, [gameDir])
|
||||||
|
|
||||||
|
const handlePickFolder = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const basePath = await FSService.PickFolder()
|
||||||
|
if (basePath) {
|
||||||
|
setGameDir(basePath)
|
||||||
|
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
|
||||||
|
const fullPath = `${basePath}/${subPath}`
|
||||||
|
const exists = await FSService.DirExists(fullPath)
|
||||||
|
setFolderCheckResult(exists ? 'success' : 'error')
|
||||||
|
setGameDir(exists ? basePath : "")
|
||||||
|
if (!exists) {
|
||||||
|
toast.error('Game directory not found. Please select the correct folder.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('No folder path selected')
|
||||||
|
setFolderCheckResult('error')
|
||||||
|
setGameDir('')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('PickFolder error:', err)
|
||||||
|
setFolderCheckResult('error')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetLanguage = async () => {
|
||||||
|
if (!gameDir) {
|
||||||
|
toast.error('No folder path selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsSettingLanguage(true)
|
||||||
|
const result = await LanguageService.SetLanguage(
|
||||||
|
`${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`,
|
||||||
|
selectedTextLang,
|
||||||
|
selectedVoiceLang
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
toast.success('Language set successfully')
|
||||||
|
setTextLang(selectedTextLang)
|
||||||
|
setVoiceLang(selectedVoiceLang)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Language set failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('SetLanguage error:', err)
|
||||||
|
} finally {
|
||||||
|
setIsSettingLanguage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLanguageLabel = (code: string) => {
|
||||||
|
const lang = languageOptions.find(l => l.value === code)
|
||||||
|
return lang ? `${lang.flag} ${lang.label}` : code
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-2 mx-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-2">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">
|
||||||
|
🎮 Game Language Manager
|
||||||
|
</h1>
|
||||||
|
<p className="">Manage text and voice language settings for your game</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="rounded-2xl p-2 space-y-4">
|
||||||
|
|
||||||
|
{/* Folder Selection Section */}
|
||||||
|
<div className="pb-2">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Folder className="text-primary" size={24} />
|
||||||
|
Game Directory
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-2 items-center'>
|
||||||
|
<button
|
||||||
|
onClick={handlePickFolder}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
<Folder size={20} />
|
||||||
|
{isLoading ? 'Selecting...' : 'Select Game Folder'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{gameDir && (
|
||||||
|
<div className="rounded-lg p-2">
|
||||||
|
<p className="font-mono text-sm px-3 py-2 rounded border truncate max-w-full overflow-hidden whitespace-nowrap">
|
||||||
|
{gameDir}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{folderCheckResult && (
|
||||||
|
<div className={`flex items-center gap-2 p-3 rounded-lg ${folderCheckResult === 'success'
|
||||||
|
? 'bg-success/5 text-success border border-success'
|
||||||
|
: 'bg-error/5 text-error border border-error'
|
||||||
|
}`}>
|
||||||
|
{folderCheckResult === 'success' ? (
|
||||||
|
<>
|
||||||
|
<Check size={20} />
|
||||||
|
<span>Valid game directory found!</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X size={20} />
|
||||||
|
<span>Game directory not found. Please select the correct folder.</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Language Display */}
|
||||||
|
{(textLang && voiceLang) && (
|
||||||
|
<div className="pb-2">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Globe className="text-primary" size={24} />
|
||||||
|
Current Languages
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-success/5 rounded-lg p-2 border border-success/30">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Globe size={20} className="text-success" />
|
||||||
|
<span className="font-bold text-success">Text Language</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-success">
|
||||||
|
{getLanguageLabel(textLang)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-warning/5 rounded-lg p-2 border border-warning/30">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Mic size={20} className="text-warning" />
|
||||||
|
<span className="font-bold text-warning">Voice Language</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-warning">
|
||||||
|
{getLanguageLabel(voiceLang)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Language Selection */}
|
||||||
|
<div className={`transition-opacity duration-300 ${gameDir === "" ? 'opacity-50 pointer-events-none' : ''
|
||||||
|
}`}>
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Settings className="text-primary" size={24} />
|
||||||
|
Language Settings
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{/* Text Language */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex text-sm font-medium text-success items-center gap-2">
|
||||||
|
<Globe size={16} />
|
||||||
|
Text Language
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedTextLang}
|
||||||
|
onChange={(e) => setSelectedTextLang(e.target.value)}
|
||||||
|
className="w-full select select-success"
|
||||||
|
>
|
||||||
|
<option value="">Select text language...</option>
|
||||||
|
{languageOptions.map(lang => (
|
||||||
|
<option key={lang.value} value={lang.value}>
|
||||||
|
{lang.flag} {lang.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Voice Language */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex text-sm font-medium text-warning items-center gap-2">
|
||||||
|
<Mic size={16} />
|
||||||
|
Voice Language
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedVoiceLang}
|
||||||
|
onChange={(e) => setSelectedVoiceLang(e.target.value)}
|
||||||
|
className="w-full select select-warning"
|
||||||
|
>
|
||||||
|
<option value="">Select voice language...</option>
|
||||||
|
{languageOptions.map(lang => (
|
||||||
|
<option key={lang.value} value={lang.value}>
|
||||||
|
{lang.flag} {lang.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apply Button */}
|
||||||
|
<div className="mt-6 flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={handleSetLanguage}
|
||||||
|
disabled={!selectedTextLang || !selectedVoiceLang || isSettingLanguage}
|
||||||
|
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
|
||||||
|
>
|
||||||
|
<Settings size={20} />
|
||||||
|
{isSettingLanguage ? 'Applying...' : 'Apply Language Settings'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div className="bg-info/5 rounded-lg p-4 border border-info/30 mt-6">
|
||||||
|
<h3 className="font-medium text-error mb-2">📋 Instructions:</h3>
|
||||||
|
<ol className="text-sm text-error space-y-1">
|
||||||
|
<li>1. Click "Select Game Folder" and choose your game's root directory</li>
|
||||||
|
<li>2. Wait for the system to validate the game directory</li>
|
||||||
|
<li>3. Select your preferred text and voice languages</li>
|
||||||
|
<li>4. Click "Apply Language Settings" to save your changes</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
498
frontend/src/pages/launcher/index.tsx
Normal file
498
frontend/src/pages/launcher/index.tsx
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
|
||||||
|
import { FSService, GitService } from '@bindings/firefly-launcher/internal';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import path from 'path-browserify'
|
||||||
|
import useSettingStore from '@/stores/settingStore';
|
||||||
|
import useModalStore from '@/stores/modalStore';
|
||||||
|
import useLauncherStore from '@/stores/launcherStore';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import { Link } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
export default function LauncherPage() {
|
||||||
|
const { gamePath,
|
||||||
|
setGamePath,
|
||||||
|
setGameDir,
|
||||||
|
serverPath,
|
||||||
|
proxyPath,
|
||||||
|
gameDir,
|
||||||
|
serverVersion,
|
||||||
|
proxyVersion,
|
||||||
|
setServerVersion,
|
||||||
|
setProxyVersion,
|
||||||
|
setServerPath,
|
||||||
|
setProxyPath
|
||||||
|
} = useSettingStore()
|
||||||
|
const { isOpenNotification, setIsOpenNotification } = useModalStore()
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
modelName,
|
||||||
|
downloadType,
|
||||||
|
serverReady,
|
||||||
|
proxyReady,
|
||||||
|
isDownloading,
|
||||||
|
serverRunning,
|
||||||
|
proxyRunning,
|
||||||
|
gameRunning,
|
||||||
|
progressDownload,
|
||||||
|
downloadSpeed,
|
||||||
|
setIsLoading,
|
||||||
|
setModelName,
|
||||||
|
setDownloadType,
|
||||||
|
setServerReady,
|
||||||
|
setProxyReady,
|
||||||
|
setIsDownloading,
|
||||||
|
setServerRunning,
|
||||||
|
setProxyRunning,
|
||||||
|
setGameRunning,
|
||||||
|
} = useLauncherStore()
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const check = async () => {
|
||||||
|
if (!serverPath || !proxyPath || !serverVersion || !proxyVersion) {
|
||||||
|
setServerReady(false)
|
||||||
|
setProxyReady(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverExists = await FSService.FileExists(serverPath)
|
||||||
|
const proxyExists = await FSService.FileExists(proxyPath)
|
||||||
|
setServerReady(serverExists)
|
||||||
|
setProxyReady(proxyExists)
|
||||||
|
}
|
||||||
|
|
||||||
|
check()
|
||||||
|
}, [serverPath, proxyPath, serverVersion, proxyVersion])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkStartUp = async (): Promise<void> => {
|
||||||
|
let isExists = false
|
||||||
|
if (serverPath && proxyPath && serverVersion && proxyVersion) {
|
||||||
|
isExists = true
|
||||||
|
setServerReady(true)
|
||||||
|
setProxyReady(true)
|
||||||
|
}
|
||||||
|
const dataUpdate = await handlerCheckUpdate()
|
||||||
|
const exitGame = await FSService.FileExists(gamePath)
|
||||||
|
if (!exitGame) {
|
||||||
|
setGameRunning(false)
|
||||||
|
setGamePath("")
|
||||||
|
setGameDir("")
|
||||||
|
}
|
||||||
|
if (!dataUpdate.server.isUpdate && !dataUpdate.proxy.isUpdate) {
|
||||||
|
setServerReady(true)
|
||||||
|
setProxyReady(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isExists) {
|
||||||
|
setServerReady(false)
|
||||||
|
setProxyReady(false)
|
||||||
|
}
|
||||||
|
setModelName(!isExists ? "Download Data" : "Update Data")
|
||||||
|
setIsOpenNotification(true)
|
||||||
|
}
|
||||||
|
checkStartUp()
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePickFile = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const basePath = await FSService.PickFile()
|
||||||
|
if (basePath.endsWith("StarRail.exe") || basePath.endsWith("launcher.exe")) {
|
||||||
|
const normalized = basePath.replace(/\\/g, '/')
|
||||||
|
const folderPath = path.dirname(normalized)
|
||||||
|
const fullPath = `${folderPath}/StarRail_Data/StreamingAssets/DesignData/Windows`
|
||||||
|
const exists = await FSService.DirExists(fullPath)
|
||||||
|
if (!exists) {
|
||||||
|
toast.error('Game directory not found. Please select the correct folder.')
|
||||||
|
} else {
|
||||||
|
setGamePath(basePath)
|
||||||
|
setGameDir(folderPath)
|
||||||
|
toast.success('Game path set successfully')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('Not valid file type')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('PickFolder error:', err)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStartGame = async () => {
|
||||||
|
if (!gamePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (gameRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
|
||||||
|
const resultProxy = await FSService.StartWithConsole(proxyPath)
|
||||||
|
if (!resultProxy) {
|
||||||
|
toast.error('Failed to start proxy')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setProxyRunning(true)
|
||||||
|
}
|
||||||
|
await sleep(500)
|
||||||
|
if (!serverRunning) {
|
||||||
|
const resultServer = await FSService.StartWithConsole(serverPath)
|
||||||
|
if (!resultServer) {
|
||||||
|
toast.error('Failed to start server')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setServerRunning(true)
|
||||||
|
}
|
||||||
|
await sleep(2000)
|
||||||
|
if (gamePath.endsWith("launcher.exe")) {
|
||||||
|
const resultGame = await FSService.StartWithConsole(gamePath)
|
||||||
|
if (!resultGame) {
|
||||||
|
toast.error('Failed to start game')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resultGame = await FSService.StartApp(gamePath)
|
||||||
|
if (!resultGame) {
|
||||||
|
toast.error('Failed to start game')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setGameRunning(true)
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error('StartGame error:', err)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerCheckUpdate = async (): Promise<Record<string, { isUpdate: boolean, version: string }>> => {
|
||||||
|
let isUpdateServer = false
|
||||||
|
let isUpdateProxy = false
|
||||||
|
const [serverOk, serverNewVersion, serverError] = await GitService.GetLatestServerVersion(serverVersion)
|
||||||
|
const serverExists = await FSService.FileExists(serverPath)
|
||||||
|
if (serverOk) {
|
||||||
|
if (serverNewVersion !== serverVersion || !serverExists) {
|
||||||
|
isUpdateServer = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Server error: " + serverError)
|
||||||
|
}
|
||||||
|
const [proxyOk, proxyNewVersion, proxyError] = await GitService.GetLatestProxyVersion(proxyVersion)
|
||||||
|
const proxyExists = await FSService.FileExists(proxyPath)
|
||||||
|
if (proxyOk) {
|
||||||
|
if (proxyNewVersion !== proxyVersion || !proxyExists) {
|
||||||
|
isUpdateProxy = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Proxy error: " + proxyError)
|
||||||
|
}
|
||||||
|
return { server: { isUpdate: isUpdateServer, version: serverNewVersion }, proxy: { isUpdate: isUpdateProxy, version: proxyNewVersion } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerUpdateServer = async (updateInfo: Record<string, { isUpdate: boolean, version: string }>) => {
|
||||||
|
setIsDownloading(true)
|
||||||
|
for (const [key, value] of Object.entries(updateInfo)) {
|
||||||
|
if (value.isUpdate) {
|
||||||
|
if (key === "server") {
|
||||||
|
setDownloadType("Downloading server...")
|
||||||
|
const [ok, error] = await GitService.DownloadServerProgress(value.version)
|
||||||
|
if (ok) {
|
||||||
|
setDownloadType("Unzipping server...")
|
||||||
|
GitService.UnzipServer()
|
||||||
|
setDownloadType("Download server successfully")
|
||||||
|
setServerVersion(value.version)
|
||||||
|
|
||||||
|
setServerPath("./server/firefly-go_win.exe")
|
||||||
|
} else {
|
||||||
|
toast.error(error)
|
||||||
|
setDownloadType("Download server failed")
|
||||||
|
}
|
||||||
|
} else if (key === "proxy") {
|
||||||
|
setDownloadType("Downloading proxy...")
|
||||||
|
const [ok, error] = await GitService.DownloadProxyProgress(value.version)
|
||||||
|
if (ok) {
|
||||||
|
setDownloadType("Unzipping proxy...")
|
||||||
|
GitService.UnzipProxy()
|
||||||
|
setDownloadType("Download proxy successfully")
|
||||||
|
setProxyVersion(value.version)
|
||||||
|
setProxyPath("./proxy/FireflyProxy.exe")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast.error(error)
|
||||||
|
setDownloadType("Download proxy failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDownloadType("")
|
||||||
|
setIsDownloading(false)
|
||||||
|
setServerReady(true)
|
||||||
|
setProxyReady(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ESC key to close modal
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsOpenNotification(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleEscKey);
|
||||||
|
return () => window.removeEventListener('keydown', handleEscKey);
|
||||||
|
}, [isOpenNotification]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-fit overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-0 w-full h-full"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/bg.jpg')",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat"
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<header className="hidden sm:flex fixed z-10 items-center justify-between p-6">
|
||||||
|
<div className="text-2xl font-bold text-white">Firefly GO</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<a
|
||||||
|
className="btn btn-circle btn-primary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://sranalysis.kain.id.vn/"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://icons.duckduckgo.com/ip3/sranalysis.kain.id.vn.ico"
|
||||||
|
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://icons.duckduckgo.com/ip3/srtools.kain.id.vn.ico"
|
||||||
|
alt="SRTools Logo"
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="tooltip tooltip-right" data-tip="Amazing's SRTools (Original UI)">
|
||||||
|
<a
|
||||||
|
className="btn btn-circle btn-primary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://srtools.pages.dev/"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://icons.duckduckgo.com/ip3/srtools.pages.dev.ico"
|
||||||
|
alt="SRTools Logo"
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="tooltip tooltip-right" data-tip="Amazing's SRTools (Modern UI)">
|
||||||
|
<a
|
||||||
|
className="btn btn-circle btn-secondary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://srtools.neonteam.dev/"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://icons.duckduckgo.com/ip3/srtools.neonteam.dev.ico"
|
||||||
|
alt="SRTools Logo"
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tooltip tooltip-right" data-tip="How to use all tools & commands">
|
||||||
|
<Link
|
||||||
|
to="/howto"
|
||||||
|
className="btn btn-warning btn-circle"
|
||||||
|
>
|
||||||
|
<MessageCircleQuestionMark className="w-6 h-6 font-bold rounded-full" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Bottom Panel */}
|
||||||
|
{serverReady && proxyReady && !isDownloading && (
|
||||||
|
<div className="fixed bottom-0 right-0 p-8 z-10">
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||||
|
{gamePath === "" ? (
|
||||||
|
<button
|
||||||
|
className="btn btn-accent btn-xl font-bold"
|
||||||
|
onClick={handlePickFile}
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-6 h-6" />
|
||||||
|
{isLoading ? 'Selecting...' : 'Select Game file'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-warning btn-xl font-bold"
|
||||||
|
onClick={handleStartGame}
|
||||||
|
>
|
||||||
|
<Play className="w-6 h-6" />
|
||||||
|
{isLoading ? 'Selecting...' : gameRunning ? 'Game is running' : 'Start Game'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="dropdown dropdown-top dropdown-end">
|
||||||
|
<div tabIndex={0} role="button" className="btn btn-black btn-circle btn-xl m-1">
|
||||||
|
<Menu className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
|
||||||
|
<li><button onClick={handlePickFile}>Change Game Path</button></li>
|
||||||
|
<li><button
|
||||||
|
onClick={async () => {
|
||||||
|
const updateInfo = await handlerCheckUpdate()
|
||||||
|
if (updateInfo.server.isUpdate || updateInfo.proxy.isUpdate) {
|
||||||
|
setModelName("Update Data")
|
||||||
|
setIsOpenNotification(true)
|
||||||
|
} else {
|
||||||
|
toast.success("No updates available")
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Check for Updates
|
||||||
|
</button></li>
|
||||||
|
<li><button disabled={!serverPath} onClick={() => {
|
||||||
|
if (serverPath) {
|
||||||
|
FSService.OpenFolder("./server")
|
||||||
|
}
|
||||||
|
}}>Open server folder</button></li>
|
||||||
|
<li><button disabled={!proxyPath} onClick={() => {
|
||||||
|
if (proxyPath) {
|
||||||
|
FSService.OpenFolder("./proxy")
|
||||||
|
}
|
||||||
|
}}>Open proxy folder</button></li>
|
||||||
|
<li><button disabled={!gameDir} onClick={() => {
|
||||||
|
if (gameDir) {
|
||||||
|
FSService.OpenFolder(gameDir + "/StarRail_Data/Persistent/Audio/AudioPackage/Windows")
|
||||||
|
}
|
||||||
|
}}>Open voice folder</button></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Downloading */}
|
||||||
|
{isDownloading && (
|
||||||
|
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="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-white font-bold">{progressDownload.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-white/20 rounded-full h-2 overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
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 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-xs text-white/60">
|
||||||
|
{progressDownload < 100 ? 'Please wait...' : 'Complete!'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Version Info */}
|
||||||
|
{serverReady && proxyReady && !isDownloading && (
|
||||||
|
<div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-black/20 backdrop-blur-sm rounded-lg p-2 shadow">
|
||||||
|
<p className="text-primary">Version server: {serverVersion}</p>
|
||||||
|
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{isOpenNotification && (
|
||||||
|
<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">
|
||||||
|
{modelName !== "Download Data" && (
|
||||||
|
<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={() => setIsOpenNotification(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">
|
||||||
|
{modelName}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<p className="text-warning text-lg">
|
||||||
|
{modelName === "Download Data"
|
||||||
|
? "Download required data!"
|
||||||
|
: "Do you want to update data?"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
{modelName !== "Download Data" && (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="btn btn-outline btn-error"
|
||||||
|
onClick={() => setIsOpenNotification(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</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 () => {
|
||||||
|
setIsOpenNotification(false)
|
||||||
|
const dataCheck = await handlerCheckUpdate()
|
||||||
|
await handlerUpdateServer(dataCheck)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
116
frontend/src/pages/srtools/index.tsx
Normal file
116
frontend/src/pages/srtools/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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 uses a self-hosted version of <span className="font-semibold text-success">SR Tools</span> available at{" "}
|
||||||
|
<a
|
||||||
|
href="https://srtools.kain.id.vn/"
|
||||||
|
className="link link-info"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
srtools.kain.id.vn
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
189
frontend/src/routeTree.gen.ts
Normal file
189
frontend/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
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 AnalysisRouteImport } from './routes/analysis'
|
||||||
|
import { Route as AboutRouteImport } from './routes/about'
|
||||||
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
|
const SrtoolsRoute = SrtoolsRouteImport.update({
|
||||||
|
id: '/srtools',
|
||||||
|
path: '/srtools',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const LanguageRoute = LanguageRouteImport.update({
|
||||||
|
id: '/language',
|
||||||
|
path: '/language',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const HowtoRoute = HowtoRouteImport.update({
|
||||||
|
id: '/howto',
|
||||||
|
path: '/howto',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const HdiffzRoute = HdiffzRouteImport.update({
|
||||||
|
id: '/hdiffz',
|
||||||
|
path: '/hdiffz',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AnalysisRoute = AnalysisRouteImport.update({
|
||||||
|
id: '/analysis',
|
||||||
|
path: '/analysis',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AboutRoute = AboutRouteImport.update({
|
||||||
|
id: '/about',
|
||||||
|
path: '/about',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const IndexRoute = IndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/about': typeof AboutRoute
|
||||||
|
'/analysis': typeof AnalysisRoute
|
||||||
|
'/hdiffz': typeof HdiffzRoute
|
||||||
|
'/howto': typeof HowtoRoute
|
||||||
|
'/language': typeof LanguageRoute
|
||||||
|
'/srtools': typeof SrtoolsRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/about': typeof AboutRoute
|
||||||
|
'/analysis': typeof AnalysisRoute
|
||||||
|
'/hdiffz': typeof HdiffzRoute
|
||||||
|
'/howto': typeof HowtoRoute
|
||||||
|
'/language': typeof LanguageRoute
|
||||||
|
'/srtools': typeof SrtoolsRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/about': typeof AboutRoute
|
||||||
|
'/analysis': typeof AnalysisRoute
|
||||||
|
'/hdiffz': typeof HdiffzRoute
|
||||||
|
'/howto': typeof HowtoRoute
|
||||||
|
'/language': typeof LanguageRoute
|
||||||
|
'/srtools': typeof SrtoolsRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/about'
|
||||||
|
| '/analysis'
|
||||||
|
| '/hdiffz'
|
||||||
|
| '/howto'
|
||||||
|
| '/language'
|
||||||
|
| '/srtools'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/about'
|
||||||
|
| '/analysis'
|
||||||
|
| '/hdiffz'
|
||||||
|
| '/howto'
|
||||||
|
| '/language'
|
||||||
|
| '/srtools'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/about'
|
||||||
|
| '/analysis'
|
||||||
|
| '/hdiffz'
|
||||||
|
| '/howto'
|
||||||
|
| '/language'
|
||||||
|
| '/srtools'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
AboutRoute: typeof AboutRoute
|
||||||
|
AnalysisRoute: typeof AnalysisRoute
|
||||||
|
HdiffzRoute: typeof HdiffzRoute
|
||||||
|
HowtoRoute: typeof HowtoRoute
|
||||||
|
LanguageRoute: typeof LanguageRoute
|
||||||
|
SrtoolsRoute: typeof SrtoolsRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/srtools': {
|
||||||
|
id: '/srtools'
|
||||||
|
path: '/srtools'
|
||||||
|
fullPath: '/srtools'
|
||||||
|
preLoaderRoute: typeof SrtoolsRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/language': {
|
||||||
|
id: '/language'
|
||||||
|
path: '/language'
|
||||||
|
fullPath: '/language'
|
||||||
|
preLoaderRoute: typeof LanguageRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/howto': {
|
||||||
|
id: '/howto'
|
||||||
|
path: '/howto'
|
||||||
|
fullPath: '/howto'
|
||||||
|
preLoaderRoute: typeof HowtoRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/hdiffz': {
|
||||||
|
id: '/hdiffz'
|
||||||
|
path: '/hdiffz'
|
||||||
|
fullPath: '/hdiffz'
|
||||||
|
preLoaderRoute: typeof HdiffzRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/analysis': {
|
||||||
|
id: '/analysis'
|
||||||
|
path: '/analysis'
|
||||||
|
fullPath: '/analysis'
|
||||||
|
preLoaderRoute: typeof AnalysisRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/about': {
|
||||||
|
id: '/about'
|
||||||
|
path: '/about'
|
||||||
|
fullPath: '/about'
|
||||||
|
preLoaderRoute: typeof AboutRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
AboutRoute: AboutRoute,
|
||||||
|
AnalysisRoute: AnalysisRoute,
|
||||||
|
HdiffzRoute: HdiffzRoute,
|
||||||
|
HowtoRoute: HowtoRoute,
|
||||||
|
LanguageRoute: LanguageRoute,
|
||||||
|
SrtoolsRoute: SrtoolsRoute,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
102
frontend/src/routes/__root.tsx
Normal file
102
frontend/src/routes/__root.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
|
||||||
|
import ThemeController from '../components/themeController'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
import { useGlobalEvents } from '@/hooks';
|
||||||
|
import useLauncherStore from '@/stores/launcherStore';
|
||||||
|
import useHdiffzStore from '@/stores/hdiffzStore';
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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="/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">
|
||||||
|
<h1 className="text-lg font-bold">
|
||||||
|
<span className="text-emerald-500">Firefly Lauc</span>
|
||||||
|
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||||
|
her
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500">By Kain</p>
|
||||||
|
</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>
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
frontend/src/routes/about.tsx
Normal file
8
frontend/src/routes/about.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import AboutPage from '@/pages/about'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/about')({
|
||||||
|
component: AboutPage,
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
7
frontend/src/routes/analysis.tsx
Normal file
7
frontend/src/routes/analysis.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import AnalysisPage from '@/pages/analysis'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/analysis')({
|
||||||
|
component: AnalysisPage,
|
||||||
|
})
|
||||||
|
|
||||||
7
frontend/src/routes/hdiffz.tsx
Normal file
7
frontend/src/routes/hdiffz.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import HdiffzPage from '@/pages/hdiffz'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/hdiffz')({
|
||||||
|
component: HdiffzPage,
|
||||||
|
})
|
||||||
|
|
||||||
6
frontend/src/routes/howto.tsx
Normal file
6
frontend/src/routes/howto.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import HowToPage from '@/pages/howto'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/howto')({
|
||||||
|
component: HowToPage,
|
||||||
|
})
|
||||||
7
frontend/src/routes/index.tsx
Normal file
7
frontend/src/routes/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import LauncherPage from '@/pages/launcher'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/')({
|
||||||
|
component: LauncherPage,
|
||||||
|
})
|
||||||
|
|
||||||
7
frontend/src/routes/language.tsx
Normal file
7
frontend/src/routes/language.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import LanguagePage from '@/pages/language'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/language')({
|
||||||
|
component: LanguagePage,
|
||||||
|
})
|
||||||
|
|
||||||
8
frontend/src/routes/srtools.tsx
Normal file
8
frontend/src/routes/srtools.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import SrToolsPage from '@/pages/srtools'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/srtools')({
|
||||||
|
component: SrToolsPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
46
frontend/src/stores/hdiffzStore.ts
Normal file
46
frontend/src/stores/hdiffzStore.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface LauncherState {
|
||||||
|
folderCheckResult: 'success' | 'error' | null,
|
||||||
|
isLoading: {game: boolean, diff: boolean},
|
||||||
|
diffDir: string,
|
||||||
|
diffCheckResult: 'success' | 'error' | null,
|
||||||
|
isDiffLoading: boolean,
|
||||||
|
progressUpdate: number,
|
||||||
|
maxProgressUpdate: number,
|
||||||
|
messageUpdate: string,
|
||||||
|
stageType: string,
|
||||||
|
setMessageUpdate: (value: string) => void,
|
||||||
|
setIsLoading: (value: {game: boolean, diff: boolean}) => void,
|
||||||
|
setFolderCheckResult: (value: 'success' | 'error' | null) => void,
|
||||||
|
setDiffDir: (value: string) => void,
|
||||||
|
setDiffCheckResult: (value: 'success' | 'error' | null) => void,
|
||||||
|
setIsDiffLoading: (value: boolean) => void,
|
||||||
|
setProgressUpdate: (value: number) => void,
|
||||||
|
setMaxProgressUpdate: (value: number) => void,
|
||||||
|
setStageType: (value: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const useLauncherStore = create<LauncherState>((set, get) => ({
|
||||||
|
isLoading: {game: false, diff: false},
|
||||||
|
folderCheckResult: null,
|
||||||
|
diffDir: "",
|
||||||
|
diffCheckResult: null,
|
||||||
|
isDiffLoading: false,
|
||||||
|
progressUpdate: 0,
|
||||||
|
maxProgressUpdate: 0,
|
||||||
|
messageUpdate: "",
|
||||||
|
stageType: "",
|
||||||
|
setIsLoading: (value: {game: boolean, diff: boolean}) => set({ isLoading: value }),
|
||||||
|
setFolderCheckResult: (value: 'success' | 'error' | null) => set({ folderCheckResult: value }),
|
||||||
|
setDiffDir: (value: string) => set({ diffDir: value }),
|
||||||
|
setDiffCheckResult: (value: 'success' | 'error' | null) => set({ diffCheckResult: value }),
|
||||||
|
setIsDiffLoading: (value: boolean) => set({ isDiffLoading: value }),
|
||||||
|
setProgressUpdate: (value: number) => set({ progressUpdate: value }),
|
||||||
|
setMaxProgressUpdate: (value: number) => set({ maxProgressUpdate: value }),
|
||||||
|
setMessageUpdate: (value: string) => set({ messageUpdate: value }),
|
||||||
|
setStageType: (value: string) => set({ stageType: value }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useLauncherStore;
|
||||||
54
frontend/src/stores/launcherStore.ts
Normal file
54
frontend/src/stores/launcherStore.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface LauncherState {
|
||||||
|
modelName: string;
|
||||||
|
downloadType: string;
|
||||||
|
serverReady: boolean;
|
||||||
|
proxyReady: boolean;
|
||||||
|
isDownloading: boolean;
|
||||||
|
serverRunning: boolean;
|
||||||
|
proxyRunning: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
gameRunning: boolean;
|
||||||
|
progressDownload: number;
|
||||||
|
downloadSpeed: number;
|
||||||
|
setModelName: (value: string) => void;
|
||||||
|
setDownloadType: (value: string) => void;
|
||||||
|
setServerReady: (value: boolean) => void;
|
||||||
|
setProxyReady: (value: boolean) => void;
|
||||||
|
setIsDownloading: (value: boolean) => void;
|
||||||
|
setServerRunning: (value: boolean) => void;
|
||||||
|
setProxyRunning: (value: boolean) => void;
|
||||||
|
setIsLoading: (value: boolean) => void;
|
||||||
|
setGameRunning: (value: boolean) => void;
|
||||||
|
setProgressDownload: (value: number) => void;
|
||||||
|
setDownloadSpeed: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useLauncherStore = create<LauncherState>((set, get) => ({
|
||||||
|
isLoading: false,
|
||||||
|
modelName: "Download Data",
|
||||||
|
downloadType: "",
|
||||||
|
serverReady: false,
|
||||||
|
proxyReady: false,
|
||||||
|
isDownloading: false,
|
||||||
|
serverRunning: false,
|
||||||
|
proxyRunning: false,
|
||||||
|
gameRunning: false,
|
||||||
|
progressDownload: 0,
|
||||||
|
downloadSpeed: 0,
|
||||||
|
setIsLoading: (value: boolean) => set({ isLoading: value }),
|
||||||
|
setModelName: (value: string) => set({ modelName: value }),
|
||||||
|
setDownloadType: (value: string) => set({ downloadType: value }),
|
||||||
|
setServerReady: (value: boolean) => set({ serverReady: value }),
|
||||||
|
setProxyReady: (value: boolean) => set({ proxyReady: value }),
|
||||||
|
setIsDownloading: (value: boolean) => set({ isDownloading: value }),
|
||||||
|
setServerRunning: (value: boolean) => set({ serverRunning: value }),
|
||||||
|
setProxyRunning: (value: boolean) => set({ proxyRunning: value }),
|
||||||
|
setGameRunning: (value: boolean) => set({ gameRunning: value }),
|
||||||
|
setProgressDownload: (value: number) => set({ progressDownload: value }),
|
||||||
|
setDownloadSpeed: (value: number) => set({ downloadSpeed: value }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useLauncherStore;
|
||||||
14
frontend/src/stores/modalStore.ts
Normal file
14
frontend/src/stores/modalStore.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface ModalState {
|
||||||
|
isOpenNotification: boolean;
|
||||||
|
setIsOpenNotification: (modal: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useModalStore = create<ModalState>((set, get) => ({
|
||||||
|
isOpenNotification: false,
|
||||||
|
setIsOpenNotification: (modal: boolean) => set({ isOpenNotification: modal }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useModalStore;
|
||||||
47
frontend/src/stores/settingStore.ts
Normal file
47
frontend/src/stores/settingStore.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
|
||||||
|
interface SettingState {
|
||||||
|
locale: string;
|
||||||
|
gamePath: string;
|
||||||
|
gameDir: string;
|
||||||
|
serverPath: string;
|
||||||
|
proxyPath: string;
|
||||||
|
serverVersion: string;
|
||||||
|
proxyVersion: string;
|
||||||
|
setLocale: (newLocale: string) => void;
|
||||||
|
setGamePath: (newGamePath: string) => void;
|
||||||
|
setGameDir: (newGameDir: string) => void;
|
||||||
|
setServerPath: (newServerPath: string) => void;
|
||||||
|
setProxyPath: (newProxyPath: string) => void;
|
||||||
|
setServerVersion: (newServerVersion: string) => void;
|
||||||
|
setProxyVersion: (newProxyVersion: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSettingStore = create<SettingState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
locale: "en",
|
||||||
|
gamePath: "",
|
||||||
|
gameDir: "",
|
||||||
|
serverPath: "",
|
||||||
|
proxyPath: "",
|
||||||
|
serverVersion: "",
|
||||||
|
proxyVersion: "",
|
||||||
|
setLocale: (newLocale: string) => set({ locale: newLocale }),
|
||||||
|
setGamePath: (newGamePath: string) => set({ gamePath: newGamePath }),
|
||||||
|
setGameDir: (newGameDir: string) => set({ gameDir: newGameDir }),
|
||||||
|
setServerPath: (newServerPath: string) => set({ serverPath: newServerPath }),
|
||||||
|
setProxyPath: (newProxyPath: string) => set({ proxyPath: newProxyPath }),
|
||||||
|
setServerVersion: (newServerVersion: string) => set({ serverVersion: newServerVersion }),
|
||||||
|
setProxyVersion: (newProxyVersion: string) => set({ proxyVersion: newProxyVersion }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'setting-storage',
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useSettingStore;
|
||||||
5
frontend/src/styles/index.css
Normal file
5
frontend/src/styles/index.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: night --default, night --prefersdark, cupcake;
|
||||||
|
}
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
30
frontend/tsconfig.json
Normal file
30
frontend/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@bindings/*": ["bindings/*"],
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src", "bindings"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
27
frontend/vite.config.ts
Normal file
27
frontend/vite.config.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: '/',
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
tanstackRouter({
|
||||||
|
target: 'react',
|
||||||
|
routesDirectory: 'src/routes',
|
||||||
|
autoCodeSplitting: false,
|
||||||
|
}),
|
||||||
|
tailwindcss(),
|
||||||
|
react(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@bindings': path.resolve(__dirname, 'bindings'),
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
54
go.mod
Normal file
54
go.mod
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
module firefly-launcher
|
||||||
|
|
||||||
|
go 1.22.4
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
|
||||||
|
golang.org/x/sys v0.28.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
|
github.com/adrg/xdg v0.5.0 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/cloudflare/circl v1.3.8 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
|
||||||
|
github.com/ebitengine/purego v0.4.0-alpha.4 // 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.0 // indirect
|
||||||
|
github.com/go-git/go-git/v5 v5.12.0 // 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-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
|
github.com/leaanthony/u v1.1.0 // indirect
|
||||||
|
github.com/lmittmann/tint v1.0.4 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/samber/lo v1.38.1 // indirect
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||||
|
golang.org/x/mod v0.19.0 // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
|
golang.org/x/sync v0.9.0 // indirect
|
||||||
|
golang.org/x/tools v0.23.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
)
|
||||||
194
go.sum
Normal file
194
go.sum
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
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.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
|
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||||
|
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
|
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
|
||||||
|
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
|
||||||
|
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.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.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
|
||||||
|
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
|
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.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||||
|
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
|
||||||
|
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.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||||
|
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||||
|
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=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
|
github.com/google/uuid v1.4.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-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/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=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
|
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
|
||||||
|
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
|
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
|
||||||
|
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
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.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||||
|
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||||
|
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.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||||
|
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.19/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.9 h1:b8CfRrhPno8Fra0xFp4Ifyj+ogmXBc35rsQWvcrHtsI=
|
||||||
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.9/go.mod h1:dSv6s722nSWaUyUiapAM1DHc5HKggNGY1a79shO85/g=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||||
|
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
|
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||||
|
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||||
|
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
142
internal/fs-service.go
Normal file
142
internal/fs-service.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"firefly-launcher/pkg/sevenzip"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FSService struct{}
|
||||||
|
|
||||||
|
func (f *FSService) PickFolder() (string, error) {
|
||||||
|
dialog := application.OpenFileDialog().
|
||||||
|
CanChooseDirectories(true).
|
||||||
|
CanCreateDirectories(true).
|
||||||
|
ResolvesAliases(true)
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
dialog.SetMessage("Select a file/directory")
|
||||||
|
} else {
|
||||||
|
dialog.SetTitle("Select a file/directory")
|
||||||
|
}
|
||||||
|
if path, err := dialog.PromptForSingleSelection(); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) PickFile() (string, error) {
|
||||||
|
dialog := application.OpenFileDialog().
|
||||||
|
CanChooseFiles(true).
|
||||||
|
ResolvesAliases(true)
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
dialog.SetMessage("Select a file/directory")
|
||||||
|
} else {
|
||||||
|
dialog.SetTitle("Select a file/directory")
|
||||||
|
}
|
||||||
|
if path, err := dialog.PromptForSingleSelection(); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) DirExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) FileExists(path string) bool {
|
||||||
|
if info, err := os.Stat(path); err == nil {
|
||||||
|
return info.Mode().IsRegular()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) StartApp(path string) (bool, error) {
|
||||||
|
cmd := exec.Command(path)
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, "StarRail.exe") {
|
||||||
|
go func() {
|
||||||
|
_ = cmd.Wait()
|
||||||
|
application.Get().EmitEvent("game:exit")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) StartWithConsole(path string) (bool, error) {
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||||
|
return false, fmt.Errorf("file not found: %s", absPath)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(absPath)
|
||||||
|
cmd.Dir = filepath.Dir(absPath)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
|
||||||
|
cmd.SysProcAttr = &windows.SysProcAttr{
|
||||||
|
CreationFlags: windows.CREATE_NEW_CONSOLE |
|
||||||
|
windows.CREATE_BREAKAWAY_FROM_JOB,
|
||||||
|
NoInheritHandles: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = cmd.Wait()
|
||||||
|
if strings.HasSuffix(path, "launcher.exe") {
|
||||||
|
application.Get().EmitEvent("game:exit")
|
||||||
|
} else if strings.HasSuffix(path, "firefly-go_win.exe") {
|
||||||
|
application.Get().EmitEvent("server:exit")
|
||||||
|
} else if strings.HasSuffix(path, "FireflyProxy.exe") {
|
||||||
|
application.Get().EmitEvent("proxy:exit")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) OpenFolder(path string) (bool, string) {
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, "failed to resolve absolute path: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.DirExists(absPath) {
|
||||||
|
return false, "directory not found: " + absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "file:///" + filepath.ToSlash(absPath)
|
||||||
|
application.Get().BrowserOpenURL(url)
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, string) {
|
||||||
|
exists, err := sevenzip.IsFileIn7z(archivePath, fileInside)
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
return exists, ""
|
||||||
|
}
|
||||||
203
internal/git-service.go
Normal file
203
internal/git-service.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"firefly-launcher/pkg/constant"
|
||||||
|
"firefly-launcher/pkg/models"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GitService struct{}
|
||||||
|
|
||||||
|
func (g *GitService) GetLatestServerVersion(oldVersion string) (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().EmitEvent("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(oldVersion string) (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().EmitEvent("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))
|
||||||
|
}
|
||||||
212
internal/hdiffz-service.go
Normal file
212
internal/hdiffz-service.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"firefly-launcher/pkg/constant"
|
||||||
|
"firefly-launcher/pkg/models"
|
||||||
|
"firefly-launcher/pkg/sevenzip"
|
||||||
|
"firefly-launcher/pkg/verifier"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HdiffzService struct{}
|
||||||
|
|
||||||
|
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) (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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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().EmitEvent("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()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.RemoveAll(constant.TempUrl)
|
||||||
|
|
||||||
|
return true, "cut completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
|
||||||
|
data, err := os.ReadFile(filepath.Join(gamePath, "hdiffmap.json"))
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData struct {
|
||||||
|
DiffMap []*models.DiffMapType `json:"diff_map"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, entry := range jsonData.DiffMap {
|
||||||
|
application.Get().EmitEvent(
|
||||||
|
"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)
|
||||||
|
|
||||||
|
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(constant.ToolHPatchzExe.String(), sourceFile, patchFile, targetFile)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
_, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, file := range deleteFiles {
|
||||||
|
os.Remove(filepath.Join(gamePath, file))
|
||||||
|
application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
146
internal/language-service.go
Normal file
146
internal/language-service.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LanguageService struct{}
|
||||||
|
|
||||||
|
func isValidLang(lang string) bool {
|
||||||
|
valid := []string{"en", "jp", "cn", "kr"}
|
||||||
|
for _, v := range valid {
|
||||||
|
if lang == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LanguageService) GetLanguage(path string) (string, string, error) {
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filePath := filepath.Join(path, file.Name())
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf")
|
||||||
|
idx := bytes.Index(content, patternToFind)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern := []byte("Korean")
|
||||||
|
idx = bytes.Index(content, pattern)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to os text language
|
||||||
|
idx += 10
|
||||||
|
idx += 4
|
||||||
|
osText := string(content[idx : idx+2])
|
||||||
|
idx += 3 * 4
|
||||||
|
|
||||||
|
// Move to cn voice language
|
||||||
|
idx += 1
|
||||||
|
idx += 5
|
||||||
|
cnVoice := string(content[idx : idx+2])
|
||||||
|
idx += 3 * 2 // skip 2 entries
|
||||||
|
|
||||||
|
// Move to os voice language
|
||||||
|
idx += 1
|
||||||
|
idx += 5
|
||||||
|
osVoice := string(content[idx : idx+2])
|
||||||
|
idx += 3 * 5 // skip 5 entries
|
||||||
|
|
||||||
|
// Move to cn text language
|
||||||
|
idx += 1
|
||||||
|
idx += 4
|
||||||
|
cnText := string(content[idx : idx+2])
|
||||||
|
|
||||||
|
textLang := osText
|
||||||
|
voiceLang := osVoice
|
||||||
|
if !isValidLang(textLang) {
|
||||||
|
textLang = cnText
|
||||||
|
}
|
||||||
|
if !isValidLang(voiceLang) {
|
||||||
|
voiceLang = cnVoice
|
||||||
|
}
|
||||||
|
|
||||||
|
return textLang, voiceLang, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("couldn't find file to read language from")
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceBytes(content []byte, idx int, choice string, param int) int {
|
||||||
|
for i := 0; i < param; i++ {
|
||||||
|
copy(content[idx:idx+2], []byte(choice))
|
||||||
|
idx += 3
|
||||||
|
}
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, error) {
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filePath := filepath.Join(path, file.Name())
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf")
|
||||||
|
idx := bytes.Index(content, patternToFind)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern := []byte("Korean")
|
||||||
|
idx = bytes.Index(content, pattern)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idx += 10
|
||||||
|
idx += 4
|
||||||
|
idx = replaceBytes(content, idx, text, 4)
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
idx += 5
|
||||||
|
idx = replaceBytes(content, idx, voice, 2)
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
idx += 5
|
||||||
|
idx = replaceBytes(content, idx, voice, 5)
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
idx += 4
|
||||||
|
_ = replaceBytes(content, idx, text, 2)
|
||||||
|
|
||||||
|
err = os.WriteFile(filePath, content, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("couldn't find file to patch. Make sure this file is placed in the correct folder")
|
||||||
|
}
|
||||||
192
internal/util.go
Normal file
192
internal/util.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
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.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
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
|
||||||
|
}
|
||||||
106
main.go
Normal file
106
main.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
_ "embed"
|
||||||
|
"firefly-launcher/internal"
|
||||||
|
"firefly-launcher/pkg/constant"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wails uses Go's `embed` package to embed the frontend files into the binary.
|
||||||
|
// Any files in the frontend/dist folder will be embedded into the binary and
|
||||||
|
// made available to the frontend.
|
||||||
|
// See https://pkg.go.dev/embed for more information.
|
||||||
|
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
//go:embed all:assets
|
||||||
|
var tools embed.FS
|
||||||
|
|
||||||
|
func fileExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFile(embedPath, outPath string) error {
|
||||||
|
data, err := tools.ReadFile(embedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't read file embed %s: %w", embedPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("can't create directory %s: %w", filepath.Dir(outPath), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(outPath, data, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// main function serves as the application's entry point. It initializes the application, creates a window,
|
||||||
|
// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and
|
||||||
|
// logs any error that might occur.
|
||||||
|
func main() {
|
||||||
|
for outPath, embedPath := range constant.RequiredFiles {
|
||||||
|
if !fileExists(outPath.String()) {
|
||||||
|
err := extractFile(embedPath, outPath.String())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("can't copy file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create a new Wails application by providing the necessary options.
|
||||||
|
// Variables 'Name' and 'Description' are for application metadata.
|
||||||
|
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
|
||||||
|
// 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances.
|
||||||
|
// 'Mac' options tailor the application when running an macOS.
|
||||||
|
app := application.New(application.Options{
|
||||||
|
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{}),
|
||||||
|
},
|
||||||
|
Assets: application.AssetOptions{
|
||||||
|
Handler: application.AssetFileServerFS(assets),
|
||||||
|
|
||||||
|
},
|
||||||
|
Mac: application.MacOptions{
|
||||||
|
ApplicationShouldTerminateAfterLastWindowClosed: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a new window with the necessary options.
|
||||||
|
// 'Title' is the title of the window.
|
||||||
|
// 'Mac' options tailor the window when running on macOS.
|
||||||
|
// 'BackgroundColour' is the background colour of the window.
|
||||||
|
// 'URL' is the URL that will be loaded into the webview.
|
||||||
|
|
||||||
|
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||||
|
|
||||||
|
Title: "Firefly Launcher - Kain",
|
||||||
|
Mac: application.MacWindow{
|
||||||
|
InvisibleTitleBarHeight: 50,
|
||||||
|
Backdrop: application.MacBackdropTranslucent,
|
||||||
|
TitleBar: application.MacTitleBarHiddenInset,
|
||||||
|
},
|
||||||
|
|
||||||
|
BackgroundColour: application.NewRGB(27, 38, 54),
|
||||||
|
Width: 1200,
|
||||||
|
Height: 600,
|
||||||
|
URL: "/",
|
||||||
|
DevToolsEnabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := app.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
pkg/constant/constant.go
Normal file
35
pkg/constant/constant.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const ProxyGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Proxy/releases"
|
||||||
|
const ServerGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Local_Archive/releases"
|
||||||
|
const ServerStorageUrl = "./server"
|
||||||
|
const ProxyStorageUrl = "./proxy"
|
||||||
|
const ServerZipFile = "prebuild_win_x86.zip"
|
||||||
|
const ProxyZipFile = "64bit.zip"
|
||||||
|
const TempUrl = "./temp"
|
||||||
|
|
||||||
|
type ToolFile string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Tool7zaExe ToolFile = "bin/7za.exe"
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ToolFile) GetEmbedPath() string {
|
||||||
|
return RequiredFiles[t]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ToolFile) String() string {
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
73
pkg/models/binary-version.go
Normal file
73
pkg/models/binary-version.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BinaryVersion struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseBinaryVersion(path string) (*BinaryVersion, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
dashPos := strings.LastIndex(content, "-")
|
||||||
|
if dashPos == -1 {
|
||||||
|
return nil, errors.New("no dash found in version string")
|
||||||
|
}
|
||||||
|
|
||||||
|
start := dashPos - 6
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
versionSlice := content[start:]
|
||||||
|
end := strings.Index(versionSlice, "-")
|
||||||
|
if end == -1 {
|
||||||
|
end = len(versionSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := versionSlice[:end]
|
||||||
|
parts := strings.SplitN(versionStr, ".", 3)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, errors.New("invalid version format")
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patch, err := strconv.Atoi(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BinaryVersion{major, minor, patch}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BinaryVersion) String() string {
|
||||||
|
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BinaryVersion) ToInt() int {
|
||||||
|
return v.Major*100 + v.Minor*10 + v.Patch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BinaryVersion) Subtract(other BinaryVersion) int {
|
||||||
|
return v.ToInt() - other.ToInt()
|
||||||
|
}
|
||||||
15
pkg/models/hdiffmap.go
Normal file
15
pkg/models/hdiffmap.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type DiffMapType struct {
|
||||||
|
SourceFileName string `json:"source_file_name"`
|
||||||
|
SourceFileMD5 string `json:"source_file_md5"`
|
||||||
|
SourceFileSize int64 `json:"source_file_size"`
|
||||||
|
|
||||||
|
TargetFileName string `json:"target_file_name"`
|
||||||
|
TargetFileMD5 string `json:"target_file_md5"`
|
||||||
|
TargetFileSize int64 `json:"target_file_size"`
|
||||||
|
|
||||||
|
PatchFileName string `json:"patch_file_name"`
|
||||||
|
PatchFileMD5 string `json:"patch_file_md5"`
|
||||||
|
PatchFileSize int64 `json:"patch_file_size"`
|
||||||
|
}
|
||||||
56
pkg/models/release.go
Normal file
56
pkg/models/release.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type ReleaseType struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
TargetCommitish string `json:"target_commitish"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
TarballURL string `json:"tarball_url"`
|
||||||
|
ZipballURL string `json:"zipball_url"`
|
||||||
|
UploadURL string `json:"upload_url"`
|
||||||
|
Draft bool `json:"draft"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
PublishedAt string `json:"published_at"`
|
||||||
|
Author AuthorType `json:"author"`
|
||||||
|
Assets []AssetType `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorType struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
LoginName string `json:"login_name"`
|
||||||
|
SourceID int `json:"source_id"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
LastLogin string `json:"last_login"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Restricted bool `json:"restricted"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
ProhibitLogin bool `json:"prohibit_login"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
FollowersCount int `json:"followers_count"`
|
||||||
|
FollowingCount int `json:"following_count"`
|
||||||
|
StarredReposCount int `json:"starred_repos_count"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetType struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
DownloadCount int `json:"download_count"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
BrowserDownloadURL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
44
pkg/sevenzip/sevenzip.go
Normal file
44
pkg/sevenzip/sevenzip.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package sevenzip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"firefly-launcher/pkg/constant"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsFileIn7z(archivePath, fileInside string) (bool, error) {
|
||||||
|
cmd := exec.Command(constant.Tool7zaExe.String(), "l", archivePath)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &out
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false, fmt.Errorf("7za list failed: %v\nOutput: %s", err, out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, fileInside) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("%s not found in %s", fileInside, archivePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractAFileFromZip(archivePath, fileInside, outDir string) error {
|
||||||
|
cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractAllFilesFromZip(archivePath, outDir string) error {
|
||||||
|
cmd := exec.Command(constant.Tool7zaExe.String(), "x", archivePath, "-o"+outDir, "-y")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
98
pkg/verifier/verifier.go
Normal file
98
pkg/verifier/verifier.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package verifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"firefly-launcher/pkg/models"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Verifier struct {
|
||||||
|
GamePath string
|
||||||
|
HdiffPath string
|
||||||
|
DiffMapEntries []*models.DiffMapType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVerifier(gamePath, hdiffPath string) (*Verifier, error) {
|
||||||
|
data, err := os.ReadFile(hdiffPath + "/hdiffmap.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData struct {
|
||||||
|
DiffMap []*models.DiffMapType `json:"diff_map"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Verifier{
|
||||||
|
GamePath: gamePath,
|
||||||
|
HdiffPath: hdiffPath,
|
||||||
|
DiffMapEntries: jsonData.DiffMap,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Verifier) VerifyAll() error {
|
||||||
|
for i, entry := range v.DiffMapEntries {
|
||||||
|
application.Get().EmitEvent(
|
||||||
|
"hdiffz:progress", map[string]int{
|
||||||
|
"progress": i,
|
||||||
|
"maxProgress": len(v.DiffMapEntries),
|
||||||
|
})
|
||||||
|
if err := check(entry.SourceFileName, entry.SourceFileSize, entry.SourceFileMD5, v.GamePath); err != nil {
|
||||||
|
return fmt.Errorf("source_file failed: %w", err)
|
||||||
|
}
|
||||||
|
// if err := check(entry.PatchFileName, entry.PatchFileSize, entry.PatchFileMD5, v.HdiffPath); err != nil {
|
||||||
|
// return fmt.Errorf("patch_file failed: %w", err)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(relPath string, expectedSize int64, expectedMD5, base string) error {
|
||||||
|
fullPath := filepath.Join(base, relPath)
|
||||||
|
|
||||||
|
info, err := os.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file not found: %s", fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Size() != expectedSize {
|
||||||
|
return fmt.Errorf("file size mismatch for %s: expected %d, got %d",
|
||||||
|
fullPath, expectedSize, info.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
md5Hash, err := fileMD5(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading %s: %w", fullPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if md5Hash != expectedMD5 {
|
||||||
|
return fmt.Errorf("md5 mismatch for %s: expected %s, got %s",
|
||||||
|
fullPath, expectedMD5, md5Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileMD5(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := md5.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user