mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 16:21:49 -03:00
Compare commits
44 Commits
v2.0.2b
...
v2.0.11-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70fe4203ef | ||
|
|
f433120084 | ||
|
|
f4099acbed | ||
|
|
1ba6b22b74 | ||
|
|
a1bc88b754 | ||
|
|
24c2371b50 | ||
|
|
4c6e1a616e | ||
|
|
b54eb4e834 | ||
|
|
a1c74e4175 | ||
|
|
260e6c1126 | ||
|
|
6eb628559b | ||
|
|
052b5dc7dc | ||
|
|
7e9b5046df | ||
|
|
204d6b21f6 | ||
|
|
740d516cfe | ||
|
|
ce052add0d | ||
|
|
d7a904c641 | ||
|
|
d5d2f60c97 | ||
|
|
61433bfeea | ||
|
|
9eb5d1759c | ||
|
|
68d697576a | ||
|
|
a8da559e93 | ||
|
|
75f9403888 | ||
|
|
b61c94d348 | ||
|
|
c0109575d6 | ||
|
|
2a024b61dd | ||
|
|
1c39e8e4c6 | ||
|
|
753bd4fd61 | ||
|
|
cefb4c5575 | ||
|
|
1c779e0e41 | ||
|
|
bb474fe233 | ||
|
|
917f5f455b | ||
|
|
1dd42bdc79 | ||
|
|
7cfe3edd32 | ||
|
|
eb22758ab9 | ||
|
|
42fd51486a | ||
|
|
9ef05e8322 | ||
|
|
4ac12e0e24 | ||
|
|
72a151930e | ||
|
|
a9644b8c64 | ||
|
|
9fc238e103 | ||
|
|
b62e94a415 | ||
|
|
3e82e8fadb | ||
|
|
a355133ccf |
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CURSEFORGE_API_KEY=$1234asdxXXXXXXkQCXXXXXXXXXXASDb32
|
||||||
|
DISCORD_CLIENT_ID=561263XXXXXX
|
||||||
18
.github/README1.md
vendored
18
.github/README1.md
vendored
@@ -22,13 +22,25 @@ All builds run in parallel:
|
|||||||
|
|
||||||
### Creating a Release
|
### Creating a Release
|
||||||
|
|
||||||
1. Update version in `package.json`
|
**⚠️ IMPORTANT: Semantic Versioning Required**
|
||||||
|
|
||||||
|
This project uses **strict semantic versioning with numerical versions only**:
|
||||||
|
- ✅ **Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
|
||||||
|
- ❌ **Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`
|
||||||
|
|
||||||
|
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
|
||||||
|
|
||||||
|
The auto-update system requires semantic versioning for proper version comparison. Letter suffixes are not supported.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Update version in `package.json` (use numerical format only, e.g., `2.0.11`)
|
||||||
2. Commit and push to `main`
|
2. Commit and push to `main`
|
||||||
3. Create and push a version tag:
|
3. Create and push a version tag:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag v2.0.1
|
git tag v2.0.11
|
||||||
git push origin v2.0.1
|
git push origin v2.0.11
|
||||||
```
|
```
|
||||||
|
|
||||||
The workflow will:
|
The workflow will:
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -13,6 +13,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
# FIX Install bsdtar for Pacman builds
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libarchive-tools
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
@@ -27,10 +33,11 @@ jobs:
|
|||||||
name: linux-builds
|
name: linux-builds
|
||||||
path: |
|
path: |
|
||||||
dist/*.AppImage
|
dist/*.AppImage
|
||||||
|
dist/*.AppImage.blockmap
|
||||||
dist/*.deb
|
dist/*.deb
|
||||||
dist/*.rpm
|
dist/*.rpm
|
||||||
dist/*.pacman
|
dist/*.pacman
|
||||||
dist/latest.yml
|
dist/latest-linux.yml
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@@ -47,6 +54,7 @@ jobs:
|
|||||||
name: windows-builds
|
name: windows-builds
|
||||||
path: |
|
path: |
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
|
dist/*.exe.blockmap
|
||||||
dist/latest.yml
|
dist/latest.yml
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
@@ -79,6 +87,10 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
# FIX: './package.json' Module Not Found in `Get version` step
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -97,7 +109,7 @@ jobs:
|
|||||||
# If it's a tag, use the tag.
|
# If it's a tag, use the tag.
|
||||||
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
||||||
# If it's the 'release' branch, use 'v2.0.2-beta.r42'
|
# If it's the 'release' branch, use 'v2.0.2-beta.r42'
|
||||||
name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1})', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
||||||
files: |
|
files: |
|
||||||
artifacts/linux-builds/**/*
|
artifacts/linux-builds/**/*
|
||||||
artifacts/windows-builds/**/*
|
artifacts/windows-builds/**/*
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,7 @@ pkg/
|
|||||||
|
|
||||||
# Package files
|
# Package files
|
||||||
*.tar.zst
|
*.tar.zst
|
||||||
|
*.zst.DS_Store
|
||||||
*.zst
|
*.zst
|
||||||
|
bun.lockb
|
||||||
|
.env
|
||||||
|
|||||||
319
GUI/index.html
319
GUI/index.html
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
|
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
|
||||||
<div class="absolute inset-0 z-0">
|
<div class="absolute inset-0 z-0">
|
||||||
<img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" />
|
<img src="https://assets.authbp.xyz/bg.png" alt="Background" class="w-full h-full object-cover" />
|
||||||
<div class="absolute inset-0 bg-black/60"></div>
|
<div class="absolute inset-0 bg-black/60"></div>
|
||||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox=" 0 0 256 256"
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox=" 0 0 256 256"
|
||||||
xmlns="http://www.w3.org/2000/svg" %3E%3Cfilter id="noiseFilter" %3E%3CfeTurbulence type="fractalNoise"
|
xmlns="http://www.w3.org/2000/svg" %3E%3Cfilter id="noiseFilter" %3E%3CfeTurbulence type="fractalNoise"
|
||||||
@@ -33,29 +33,25 @@
|
|||||||
<div class="sidebar-nav">
|
<div class="sidebar-nav">
|
||||||
<div class="nav-item active" data-page="play">
|
<div class="nav-item active" data-page="play">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
<span class="nav-tooltip">Play</span>
|
<span class="nav-tooltip" data-i18n="nav.play">Play</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="mods">
|
<div class="nav-item" data-page="mods">
|
||||||
<i class="fas fa-box"></i>
|
<i class="fas fa-box"></i>
|
||||||
<span class="nav-tooltip">Mods</span>
|
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="news">
|
<div class="nav-item" data-page="news">
|
||||||
<i class="fas fa-newspaper"></i>
|
<i class="fas fa-newspaper"></i>
|
||||||
<span class="nav-tooltip">News</span>
|
<span class="nav-tooltip" data-i18n="nav.news">News</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="chat">
|
<div class="nav-item" data-page="chat">
|
||||||
<i class="fas fa-comments"></i>
|
<i class="fas fa-comments"></i>
|
||||||
<span class="nav-tooltip">Players Chat</span>
|
<span class="nav-tooltip" data-i18n="nav.chat">Players Chat</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="settings">
|
<div class="nav-item" data-page="settings">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
<span class="nav-tooltip">Settings</span>
|
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="skins">
|
<div class="nav-item logs-nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
<span class="nav-tooltip">Skins</span>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
|
|
||||||
<i class="fas fa-terminal"></i>
|
<i class="fas fa-terminal"></i>
|
||||||
<span class="nav-tooltip">Logs</span>
|
<span class="nav-tooltip">Logs</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +64,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div id="playersOnlineCounter" class="players-counter">
|
<div id="playersOnlineCounter" class="players-counter">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
<span class="counter-label">Players:</span>
|
<span class="counter-label" data-i18n="header.playersLabel">Players:</span>
|
||||||
<span id="onlineCount" class="counter-value">0</span>
|
<span id="onlineCount" class="counter-value">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,7 +81,7 @@
|
|||||||
<div class="profile-divider"></div>
|
<div class="profile-divider"></div>
|
||||||
<div class="profile-action" onclick="openProfileManager()">
|
<div class="profile-action" onclick="openProfileManager()">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
<span>Manage Profiles</span>
|
<span data-i18n="header.manageProfiles">Manage Profiles</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +90,9 @@
|
|||||||
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
|
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
|
||||||
<i class="fas fa-minus"></i>
|
<i class="fas fa-minus"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="control-btn maximize" onclick="toggleMaximize()">
|
||||||
|
<i class="fas fa-square"></i>
|
||||||
|
</button>
|
||||||
<button class="control-btn close" onclick="window.electronAPI?.closeWindow()">
|
<button class="control-btn close" onclick="window.electronAPI?.closeWindow()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -104,9 +103,6 @@
|
|||||||
<h1 class="game-title">
|
<h1 class="game-title">
|
||||||
HY<span class="title-accent">TALE</span>
|
HY<span class="title-accent">TALE</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="game-tags">
|
|
||||||
<span class="tag">FREE TO PLAY</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-pages">
|
<div class="content-pages">
|
||||||
@@ -114,30 +110,34 @@
|
|||||||
<div class="install-content">
|
<div class="install-content">
|
||||||
<div class="install-header">
|
<div class="install-header">
|
||||||
<h1 class="install-title">
|
<h1 class="install-title">
|
||||||
HYTA<span class="title-accent">LE</span>
|
HY<span class="title-accent">TALE</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="install-subtitle">FREE TO PLAY LAUNCHER</p>
|
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="install-form">
|
<div class="install-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Player Name</label>
|
<label class="form-label" data-i18n="install.playerName">Player Name</label>
|
||||||
<input type="text" id="installPlayerName" placeholder="Enter your name"
|
<input type="text" id="installPlayerName"
|
||||||
class="form-input" value="Player" />
|
data-i18n-placeholder="install.playerNamePlaceholder" class="form-input"
|
||||||
|
value="Player" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-group">
|
<label class="checkbox-group">
|
||||||
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
|
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
|
||||||
<span class="checkbox-label">Custom Installation</span>
|
<span class="checkbox-label" data-i18n="install.customInstallation">Custom
|
||||||
|
Installation</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div id="installCustomOptions" class="custom-options">
|
<div id="installCustomOptions" class="custom-options">
|
||||||
<div class="form-subgroup">
|
<div class="form-subgroup">
|
||||||
<label class="form-label">Installation Folder</label>
|
<label class="form-label" data-i18n="install.installationFolder">Installation
|
||||||
|
Folder</label>
|
||||||
<div class="input-with-button">
|
<div class="input-with-button">
|
||||||
<input type="text" id="installPath" placeholder="Default location"
|
<input type="text" id="installPath"
|
||||||
class="form-input" readonly />
|
data-i18n-placeholder="install.pathPlaceholder" class="form-input"
|
||||||
|
readonly />
|
||||||
<button onclick="browseInstallPath()" class="browse-btn">
|
<button onclick="browseInstallPath()" class="browse-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
|
|
||||||
<button id="installBtn" class="install-button" onclick="installGame()">
|
<button id="installBtn" class="install-button" onclick="installGame()">
|
||||||
<i class="fas fa-download mr-2"></i>
|
<i class="fas fa-download mr-2"></i>
|
||||||
<span id="installText">INSTALL HYTALE</span>
|
<span id="installText" data-i18n="install.installButton">INSTALL HYTALE</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,14 +161,15 @@
|
|||||||
<div class="play-header">
|
<div class="play-header">
|
||||||
<h2 class="play-title">
|
<h2 class="play-title">
|
||||||
<i class="fas fa-play-circle mr-2"></i>
|
<i class="fas fa-play-circle mr-2"></i>
|
||||||
READY TO PLAY
|
<span data-i18n="play.ready">READY TO PLAY</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="play-subtitle">Launch Hytale and enter the adventure</p>
|
<p class="play-subtitle" data-i18n="play.subtitle">Launch Hytale and enter the
|
||||||
|
adventure</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
|
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
<span>PLAY HYTALE</span>
|
<span data-i18n="play.playButton">PLAY HYTALE</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,10 +178,11 @@
|
|||||||
<div class="news-header">
|
<div class="news-header">
|
||||||
<h2 class="news-title">
|
<h2 class="news-title">
|
||||||
<i class="fas fa-star mr-2"></i>
|
<i class="fas fa-star mr-2"></i>
|
||||||
LATEST NEWS
|
<span data-i18n="play.latestNews">LATEST NEWS</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button class="view-all-btn" onclick="navigateToPage('news')">
|
<button class="view-all-btn" onclick="navigateToPage('news')">
|
||||||
VIEW ALL <i class="fas fa-arrow-right ml-1"></i>
|
<span data-i18n="play.viewAll">VIEW ALL</span> <i
|
||||||
|
class="fas fa-arrow-right ml-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="newsGrid" class="news-grid-horizontal"></div>
|
<div id="newsGrid" class="news-grid-horizontal"></div>
|
||||||
@@ -191,12 +193,13 @@
|
|||||||
<div class="mods-header">
|
<div class="mods-header">
|
||||||
<div class="mods-search-container">
|
<div class="mods-search-container">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<input type="text" id="modsSearch" placeholder="Search mods..." class="mods-search" />
|
<input type="text" id="modsSearch" data-i18n-placeholder="mods.searchPlaceholder"
|
||||||
|
class="mods-search" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mods-actions">
|
<div class="mods-actions">
|
||||||
<button id="myModsBtn" class="mods-btn-primary">
|
<button id="myModsBtn" class="mods-btn-primary">
|
||||||
<i class="fas fa-box"></i>
|
<i class="fas fa-box"></i>
|
||||||
MY MODS
|
<span data-i18n="mods.myMods">MY MODS</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,13 +210,14 @@
|
|||||||
<div class="mods-pagination">
|
<div class="mods-pagination">
|
||||||
<button id="prevPage" class="pagination-btn">
|
<button id="prevPage" class="pagination-btn">
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
PREVIOUS
|
<span data-i18n="mods.previous">PREVIOUS</span>
|
||||||
</button>
|
</button>
|
||||||
<span class="pagination-info">
|
<span class="pagination-info">
|
||||||
Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
|
<span data-i18n="mods.page">Page</span> <span id="currentPage">1</span> <span
|
||||||
|
data-i18n="mods.of">of</span> <span id="totalPages">1</span>
|
||||||
</span>
|
</span>
|
||||||
<button id="nextPage" class="pagination-btn">
|
<button id="nextPage" class="pagination-btn">
|
||||||
NEXT
|
<span data-i18n="mods.next">NEXT</span>
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +227,7 @@
|
|||||||
<div class="news-header">
|
<div class="news-header">
|
||||||
<h2 class="news-title">
|
<h2 class="news-title">
|
||||||
<i class="fas fa-newspaper mr-2"></i>
|
<i class="fas fa-newspaper mr-2"></i>
|
||||||
ALL NEWS
|
<span data-i18n="news.title">ALL NEWS</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="allNewsGrid" class="news-grid-full"></div>
|
<div id="allNewsGrid" class="news-grid-full"></div>
|
||||||
@@ -234,16 +238,16 @@
|
|||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<h2 class="chat-title">
|
<h2 class="chat-title">
|
||||||
<i class="fas fa-comments mr-2"></i>
|
<i class="fas fa-comments mr-2"></i>
|
||||||
PLAYERS CHAT
|
<span data-i18n="chat.title">PLAYERS CHAT</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="chat-header-actions">
|
<div class="chat-header-actions">
|
||||||
<button id="chatColorBtn" class="chat-color-btn">
|
<button id="chatColorBtn" class="chat-color-btn">
|
||||||
<i class="fas fa-palette"></i>
|
<i class="fas fa-palette"></i>
|
||||||
<span>Color</span>
|
<span data-i18n="chat.pickColor">Color</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="chat-online-badge">
|
<div class="chat-online-badge">
|
||||||
<i class="fas fa-circle"></i>
|
<i class="fas fa-circle"></i>
|
||||||
<span id="chatOnlineCount">0</span> online
|
<span id="chatOnlineCount">0</span> <span data-i18n="chat.online">online</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,7 +260,7 @@
|
|||||||
<div class="chat-footer">
|
<div class="chat-footer">
|
||||||
<div class="chat-input-container">
|
<div class="chat-input-container">
|
||||||
<textarea id="chatInput" class="chat-input"
|
<textarea id="chatInput" class="chat-input"
|
||||||
placeholder="Type your message... (Links are automatically censored)" rows="1"
|
data-i18n-placeholder="chat.inputPlaceholder" rows="1"
|
||||||
maxlength="500"></textarea>
|
maxlength="500"></textarea>
|
||||||
<button id="chatSendBtn" class="chat-send-btn">
|
<button id="chatSendBtn" class="chat-send-btn">
|
||||||
<i class="fas fa-paper-plane"></i>
|
<i class="fas fa-paper-plane"></i>
|
||||||
@@ -266,7 +270,7 @@
|
|||||||
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
|
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
|
||||||
<span class="chat-warning-text">
|
<span class="chat-warning-text">
|
||||||
<i class="fas fa-shield-alt"></i>
|
<i class="fas fa-shield-alt"></i>
|
||||||
Secure chat - Links are censored
|
<span data-i18n="chat.secureChat">Secure chat - Links are censored</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,26 +282,27 @@
|
|||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2 class="settings-title">
|
<h2 class="settings-title">
|
||||||
<i class="fas fa-cog mr-2"></i>
|
<i class="fas fa-cog mr-2"></i>
|
||||||
SETTINGS
|
<span data-i18n="settings.title">SETTINGS</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-gamepad"></i>
|
<i class="fas fa-gamepad"></i>
|
||||||
Game Options
|
<span data-i18n="settings.game">Game Options</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Player Name</label>
|
<label class="settings-input-label" data-i18n="settings.playerName">Player
|
||||||
|
Name</label>
|
||||||
<input type="text" id="settingsPlayerName" class="settings-input"
|
<input type="text" id="settingsPlayerName" class="settings-input"
|
||||||
placeholder="Enter your player name" maxlength="16" />
|
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
This name will be used in-game (1-16 characters)
|
<span data-i18n="settings.playerNameHint">This name will be used in-game
|
||||||
|
(1-16 characters)</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,8 +313,11 @@
|
|||||||
onclick="openGameLocation()">
|
onclick="openGameLocation()">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<div class="btn-content">
|
<div class="btn-content">
|
||||||
<div class="btn-title">Open Game Location</div>
|
<div class="btn-title" data-i18n="settings.openGameLocation">Open
|
||||||
<div class="btn-description">Open the game installation folder</div>
|
Game Location</div>
|
||||||
|
<div class="btn-description"
|
||||||
|
data-i18n="settings.openGameLocationDesc">Open the game
|
||||||
|
installation folder</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -321,8 +329,10 @@
|
|||||||
onclick="repairGame()">
|
onclick="repairGame()">
|
||||||
<i class="fas fa-tools"></i>
|
<i class="fas fa-tools"></i>
|
||||||
<div class="btn-content">
|
<div class="btn-content">
|
||||||
<div class="btn-title">Repair Game</div>
|
<div class="btn-title" data-i18n="settings.repairGame">Repair Game
|
||||||
<div class="btn-description">Reinstall game files (preserves data)
|
</div>
|
||||||
|
<div class="btn-description" data-i18n="settings.reinstallGame">
|
||||||
|
Reinstall game files (preserves data)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -330,21 +340,25 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">GPU Preference</label>
|
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
|
||||||
|
Preference</label>
|
||||||
<div class="segmented-control">
|
<div class="segmented-control">
|
||||||
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto"
|
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto"
|
||||||
checked>
|
checked>
|
||||||
<label for="gpu-auto">Auto</label>
|
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
|
||||||
<input type="radio" id="gpu-integrated" name="gpuPreference"
|
<input type="radio" id="gpu-integrated" name="gpuPreference"
|
||||||
value="integrated">
|
value="integrated">
|
||||||
<label for="gpu-integrated">Integrated</label>
|
<label for="gpu-integrated"
|
||||||
|
data-i18n="settings.gpuIntegrated">Integrated</label>
|
||||||
<input type="radio" id="gpu-dedicated" name="gpuPreference"
|
<input type="radio" id="gpu-dedicated" name="gpuPreference"
|
||||||
value="dedicated">
|
value="dedicated">
|
||||||
<label for="gpu-dedicated">Dedicated</label>
|
<label for="gpu-dedicated"
|
||||||
|
data-i18n="settings.gpuDedicated">Dedicated</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Select your preferred GPU (Linux: affects DRI_PRIME)
|
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux:
|
||||||
|
affects DRI_PRIME)</span>
|
||||||
</p>
|
</p>
|
||||||
<div id="gpu-detection-info" class="gpu-detection-info"></div>
|
<div id="gpu-detection-info" class="gpu-detection-info"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,15 +368,16 @@
|
|||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-fingerprint"></i>
|
<i class="fas fa-fingerprint"></i>
|
||||||
Player UUID Management
|
<span data-i18n="settings.account">Player UUID Management</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Current UUID</label>
|
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
|
||||||
|
UUID</label>
|
||||||
<div class="uuid-display-container">
|
<div class="uuid-display-container">
|
||||||
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
||||||
readonly placeholder="Loading UUID..." />
|
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
|
||||||
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -373,7 +388,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Your unique player identifier for this username
|
<span data-i18n="settings.uuidHint">Your unique player identifier for
|
||||||
|
this username</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,9 +399,10 @@
|
|||||||
<button id="manageUuidsBtn" class="settings-action-btn">
|
<button id="manageUuidsBtn" class="settings-action-btn">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
<div class="btn-content">
|
<div class="btn-content">
|
||||||
<div class="btn-title">Manage All UUIDs</div>
|
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
|
||||||
<div class="btn-description">View and manage all player UUIDs
|
UUIDs</div>
|
||||||
</div>
|
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
|
||||||
|
View and manage all player UUIDs</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,7 +412,7 @@
|
|||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fab fa-discord"></i>
|
<i class="fab fa-discord"></i>
|
||||||
Discord Integration
|
<span data-i18n="settings.discord">Discord Integration</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
@@ -403,18 +420,42 @@
|
|||||||
<input type="checkbox" id="discordRPCCheck" checked />
|
<input type="checkbox" id="discordRPCCheck" checked />
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
<div class="checkbox-content">
|
<div class="checkbox-content">
|
||||||
<div class="checkbox-title">Enable Discord Rich Presence</div>
|
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
|
||||||
<div class="checkbox-description">Show your launcher activity on Discord
|
Discord Rich Presence</div>
|
||||||
|
<div class="checkbox-description"
|
||||||
|
data-i18n="settings.discordDescription">Show your launcher activity
|
||||||
|
on Discord
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fas fa-window-close"></i>
|
||||||
|
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<label class="settings-checkbox">
|
||||||
|
<input type="checkbox" id="closeLauncherCheck" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<div class="checkbox-content">
|
||||||
|
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher on game start</div>
|
||||||
|
<div class="checkbox-description" data-i18n="settings.closeOnStartDescription">
|
||||||
|
Automatically close the launcher after Hytale has launched
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-coffee"></i>
|
<i class="fas fa-coffee"></i>
|
||||||
Java Runtime
|
<span data-i18n="settings.java">Java Runtime</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
@@ -422,8 +463,10 @@
|
|||||||
<input type="checkbox" id="customJavaCheck" />
|
<input type="checkbox" id="customJavaCheck" />
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
<div class="checkbox-content">
|
<div class="checkbox-content">
|
||||||
<div class="checkbox-title">Use Custom Java Path</div>
|
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use
|
||||||
<div class="checkbox-description">Override the bundled Java runtime with
|
Custom Java Path</div>
|
||||||
|
<div class="checkbox-description" data-i18n="settings.javaDescription">
|
||||||
|
Override the bundled Java runtime with
|
||||||
your own installation</div>
|
your own installation</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -431,32 +474,42 @@
|
|||||||
|
|
||||||
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
|
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Java Executable Path</label>
|
<label class="settings-input-label" data-i18n="settings.javaPath">Java
|
||||||
|
Executable Path</label>
|
||||||
<div class="settings-input-with-button">
|
<div class="settings-input-with-button">
|
||||||
<input type="text" id="customJavaPath" class="settings-input"
|
<input type="text" id="customJavaPath" class="settings-input"
|
||||||
placeholder="Select Java path..." readonly />
|
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
|
||||||
<button id="browseJavaBtn" class="settings-browse-btn">
|
<button id="browseJavaBtn" class="settings-browse-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
Browse
|
<span data-i18n="settings.javaBrowse">Browse</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Select the Java installation folder (supports Windows, Mac, Linux)
|
<span data-i18n="settings.javaHint">Select the Java installation folder
|
||||||
|
(supports Windows, Mac, Linux)</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="settings-section">
|
||||||
</div>
|
<h3 class="settings-section-title">
|
||||||
</div>
|
<i class="fas fa-language"></i>
|
||||||
|
<span data-i18n="settings.language">Language</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div id="skins-page" class="page">
|
<div class="settings-option">
|
||||||
<div class="placeholder-content">
|
<div class="settings-input-group">
|
||||||
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
|
<label class="settings-input-label"
|
||||||
<h2>Skins</h2>
|
data-i18n="settings.selectLanguage">Select Language</label>
|
||||||
<p>Skin customization coming soon...</p>
|
<select id="languageSelect" class="settings-input">
|
||||||
|
<!-- Options populated by i18n.js -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -465,22 +518,25 @@
|
|||||||
<div class="logs-header">
|
<div class="logs-header">
|
||||||
<h2 class="logs-title">
|
<h2 class="logs-title">
|
||||||
<i class="fas fa-terminal"></i>
|
<i class="fas fa-terminal"></i>
|
||||||
SYSTEM LOGS
|
<span data-i18n="settings.logs">SYSTEM LOGS</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="logs-actions">
|
<div class="logs-actions">
|
||||||
<button class="logs-action-btn" onclick="copyLogs()">
|
<button class="logs-action-btn" onclick="copyLogs()">
|
||||||
<i class="fas fa-copy"></i> Copy
|
<i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="logs-action-btn" onclick="refreshLogs()">
|
<button class="logs-action-btn" onclick="refreshLogs()">
|
||||||
<i class="fas fa-sync-alt"></i> Refresh
|
<i class="fas fa-sync-alt"></i> <span
|
||||||
|
data-i18n="settings.logsRefresh">Refresh</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="logs-action-btn" onclick="openLogsFolder()">
|
<button class="logs-action-btn" onclick="openLogsFolder()">
|
||||||
<i class="fas fa-folder-open"></i> Open Folder
|
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
||||||
|
Folder</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="logsTerminal" class="logs-terminal">
|
<div id="logsTerminal" class="logs-terminal">
|
||||||
<div class="text-gray-500 text-center mt-10">Loading logs...</div>
|
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading
|
||||||
|
logs...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -493,7 +549,7 @@
|
|||||||
<div class="mods-modal-header">
|
<div class="mods-modal-header">
|
||||||
<h2 class="mods-modal-title">
|
<h2 class="mods-modal-title">
|
||||||
<i class="fas fa-box mr-2"></i>
|
<i class="fas fa-box mr-2"></i>
|
||||||
MY MODS
|
<span data-i18n="mods.modalTitle">MY MODS</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button id="closeMyModsModal" class="mods-modal-close">
|
<button id="closeMyModsModal" class="mods-modal-close">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -509,7 +565,7 @@
|
|||||||
<div id="progressOverlay" class="progress-overlay" style="display: none;">
|
<div id="progressOverlay" class="progress-overlay" style="display: none;">
|
||||||
<div class="progress-content">
|
<div class="progress-content">
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
<span id="progressText">Initializing...</span>
|
<span id="progressText" data-i18n="progress.initializing">Initializing...</span>
|
||||||
<span id="progressPercent">0%</span>
|
<span id="progressPercent">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container">
|
<div class="progress-bar-container">
|
||||||
@@ -522,34 +578,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Installation effects overlay -->
|
||||||
|
<div id="installationEffects" class="installation-effects" style="display: none;">
|
||||||
|
<div class="space-effects">
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
<div class="warp-line"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
|
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
|
||||||
<div class="chat-username-modal-content">
|
<div class="chat-username-modal-content">
|
||||||
<div class="chat-username-modal-header">
|
<div class="chat-username-modal-header">
|
||||||
<h2 class="chat-username-modal-title">
|
<h2 class="chat-username-modal-title">
|
||||||
<i class="fas fa-comments mr-2"></i>
|
<i class="fas fa-comments mr-2"></i>
|
||||||
Join Chat
|
<span data-i18n="chat.joinChat">Join Chat</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-username-modal-body">
|
<div class="chat-username-modal-body">
|
||||||
<p class="chat-username-modal-description">
|
<p class="chat-username-modal-description" data-i18n="chat.chooseUsername">
|
||||||
Choose a username to join the Players Chat
|
Choose a username to join the Players Chat
|
||||||
</p>
|
</p>
|
||||||
<div class="chat-username-input-group">
|
<div class="chat-username-input-group">
|
||||||
<label for="chatUsernameInput" class="chat-username-label">Username</label>
|
<label for="chatUsernameInput" class="chat-username-label"
|
||||||
|
data-i18n="chat.username">Username</label>
|
||||||
<input type="text" id="chatUsernameInput" class="chat-username-input"
|
<input type="text" id="chatUsernameInput" class="chat-username-input"
|
||||||
placeholder="Enter your username..." maxlength="20" autocomplete="off" />
|
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
|
||||||
<span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span>
|
<span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, -
|
||||||
|
and _ only</span>
|
||||||
<span id="chatUsernameError" class="chat-username-error"></span>
|
<span id="chatUsernameError" class="chat-username-error"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-username-modal-footer">
|
<div class="chat-username-modal-footer">
|
||||||
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
|
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
Cancel
|
<span data-i18n="common.cancel">Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
|
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
Join Chat
|
<span data-i18n="chat.joinButton">Join Chat</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -561,7 +633,7 @@
|
|||||||
<div class="uuid-modal-header">
|
<div class="uuid-modal-header">
|
||||||
<h2 class="uuid-modal-title">
|
<h2 class="uuid-modal-title">
|
||||||
<i class="fas fa-fingerprint mr-2"></i>
|
<i class="fas fa-fingerprint mr-2"></i>
|
||||||
UUID Management
|
<span data-i18n="uuid.modalTitle">UUID Management</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button id="uuidModalClose" class="modal-close-btn">
|
<button id="uuidModalClose" class="modal-close-btn">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -570,7 +642,7 @@
|
|||||||
|
|
||||||
<div class="uuid-modal-body">
|
<div class="uuid-modal-body">
|
||||||
<div class="uuid-current-section">
|
<div class="uuid-current-section">
|
||||||
<h3 class="uuid-section-title">Current User UUID</h3>
|
<h3 class="uuid-section-title" data-i18n="uuid.currentUserUUID">Current User UUID</h3>
|
||||||
<div class="uuid-current-display">
|
<div class="uuid-current-display">
|
||||||
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
|
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
|
||||||
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
|
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
|
||||||
@@ -585,34 +657,34 @@
|
|||||||
|
|
||||||
<div class="uuid-list-section">
|
<div class="uuid-list-section">
|
||||||
<div class="uuid-list-header">
|
<div class="uuid-list-header">
|
||||||
<h3 class="uuid-section-title">All Player UUIDs</h3>
|
<h3 class="uuid-section-title" data-i18n="uuid.allPlayerUUIDs">All Player UUIDs</h3>
|
||||||
<button id="generateNewUuidBtn" class="uuid-generate-btn">
|
<button id="generateNewUuidBtn" class="uuid-generate-btn">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
Generate New UUID
|
<span data-i18n="uuid.generateNew">Generate New UUID</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="uuidList" class="uuid-list">
|
<div id="uuidList" class="uuid-list">
|
||||||
<div class="uuid-loading">
|
<div class="uuid-loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
Loading UUIDs...
|
<span data-i18n="uuid.loadingUUIDs">Loading UUIDs...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uuid-custom-section">
|
<div class="uuid-custom-section">
|
||||||
<h3 class="uuid-section-title">Set Custom UUID</h3>
|
<h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
|
||||||
<div class="uuid-custom-form">
|
<div class="uuid-custom-form">
|
||||||
<input type="text" id="customUuidInput" class="uuid-input"
|
<input type="text" id="customUuidInput" class="uuid-input"
|
||||||
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
|
data-i18n-placeholder="uuid.customPlaceholder" maxlength="36" />
|
||||||
maxlength="36" />
|
|
||||||
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
Set UUID
|
<span data-i18n="uuid.setUUID">Set UUID</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="uuid-custom-hint">
|
<p class="uuid-custom-hint">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
Warning: Setting a custom UUID will change your current player identity
|
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player
|
||||||
|
identity</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -625,7 +697,7 @@
|
|||||||
<div class="profile-modal-header">
|
<div class="profile-modal-header">
|
||||||
<h2 class="profile-modal-title">
|
<h2 class="profile-modal-title">
|
||||||
<i class="fas fa-users-cog mr-2"></i>
|
<i class="fas fa-users-cog mr-2"></i>
|
||||||
Manage Profiles
|
<span data-i18n="profiles.modalTitle">Manage Profiles</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button class="modal-close-btn" onclick="closeProfileManager()">
|
<button class="modal-close-btn" onclick="closeProfileManager()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -636,16 +708,21 @@
|
|||||||
<!-- Populated by JS -->
|
<!-- Populated by JS -->
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-create-section">
|
<div class="profile-create-section">
|
||||||
<input type="text" id="newProfileName" placeholder="New Profile Name" class="profile-input"
|
<input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder"
|
||||||
maxlength="20">
|
class="profile-input" maxlength="20">
|
||||||
<button class="profile-create-btn" onclick="createNewProfile()">
|
<button class="profile-create-btn" onclick="createNewProfile()">
|
||||||
<i class="fas fa-plus"></i> Create Profile
|
<i class="fas fa-plus"></i> <span data-i18n="profiles.createProfile">Create Profile</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="version-display-bottom">
|
||||||
|
<i class="fas fa-code-branch"></i>
|
||||||
|
<span id="launcherVersion">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
||||||
<div class="flex items-center justify-center text-xs text-gray-400">
|
<div class="flex items-center justify-center text-xs text-gray-400">
|
||||||
<span>Made by <a href="https://github.com/amiayweb" target="_blank"
|
<span>Made by <a href="https://github.com/amiayweb" target="_blank"
|
||||||
@@ -674,10 +751,10 @@
|
|||||||
<div id="discordNotification" class="discord-notification">
|
<div id="discordNotification" class="discord-notification">
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
<i class="fab fa-discord"></i>
|
<i class="fab fa-discord"></i>
|
||||||
<span class="notification-text">Join our Discord community!</span>
|
<span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span>
|
||||||
<button class="notification-action"
|
<button class="notification-action"
|
||||||
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
||||||
Join Discord
|
<span data-i18n="discord.joinButton">Join Discord</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="notification-close" onclick="closeDiscordNotification()">
|
<button class="notification-close" onclick="closeDiscordNotification()">
|
||||||
@@ -691,7 +768,7 @@
|
|||||||
<div class="chat-color-modal-header">
|
<div class="chat-color-modal-header">
|
||||||
<h3 class="chat-color-modal-title">
|
<h3 class="chat-color-modal-title">
|
||||||
<i class="fas fa-palette"></i>
|
<i class="fas fa-palette"></i>
|
||||||
Customize Username Color
|
<span data-i18n="chat.colorModal.title">Customize Username Color</span>
|
||||||
</h3>
|
</h3>
|
||||||
<button class="modal-close-btn" onclick="closeChatColorModal()">
|
<button class="modal-close-btn" onclick="closeChatColorModal()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -699,7 +776,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="chat-color-modal-body">
|
<div class="chat-color-modal-body">
|
||||||
<div id="solidColorSection" class="color-section">
|
<div id="solidColorSection" class="color-section">
|
||||||
<h4>Choose a solid color:</h4>
|
<h4 data-i18n="chat.colorModal.chooseSolid">Choose a solid color:</h4>
|
||||||
<div class="predefined-colors">
|
<div class="predefined-colors">
|
||||||
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
|
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
|
||||||
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
|
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
|
||||||
@@ -711,23 +788,27 @@
|
|||||||
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
|
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-color-input">
|
<div class="custom-color-input">
|
||||||
<label for="customColor">Custom color:</label>
|
<label for="customColor" data-i18n="chat.colorModal.customColor">Custom color:</label>
|
||||||
<input type="color" id="customColor" value="#3498db">
|
<input type="color" id="customColor" value="#3498db">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="color-preview">
|
<div class="color-preview">
|
||||||
<h4>Preview:</h4>
|
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
|
||||||
<div id="colorPreview" class="preview-username">YourUsername</div>
|
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">
|
||||||
|
YourUsername</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-color-modal-footer">
|
<div class="chat-color-modal-footer">
|
||||||
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button>
|
<button class="btn-secondary" onclick="closeChatColorModal()"><span
|
||||||
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button>
|
data-i18n="common.cancel">Cancel</span></button>
|
||||||
|
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply
|
||||||
|
Color</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="js/i18n.js"></script>
|
||||||
<script type="module" src="js/update.js"></script>
|
<script type="module" src="js/update.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
89
GUI/js/i18n.js
Normal file
89
GUI/js/i18n.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Minimal i18n system - optimized async loading
|
||||||
|
const i18n = (() => {
|
||||||
|
let currentLang = 'en';
|
||||||
|
let translations = {};
|
||||||
|
const availableLanguages = [
|
||||||
|
{ code: 'en', name: 'English' },
|
||||||
|
{ code: 'es', name: 'Español' },
|
||||||
|
{ code: 'pt-BR', name: 'Português (Brasil)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load single language file
|
||||||
|
async function loadLanguage(lang) {
|
||||||
|
if (translations[lang]) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`locales/${lang}.json`);
|
||||||
|
if (response.ok) {
|
||||||
|
translations[lang] = await response.json();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load language: ${lang}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get translation by key
|
||||||
|
function t(key) {
|
||||||
|
const keys = key.split('.');
|
||||||
|
let value = translations[currentLang];
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (value && value[k] !== undefined) {
|
||||||
|
value = value[k];
|
||||||
|
} else {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set language
|
||||||
|
async function setLanguage(lang) {
|
||||||
|
await loadLanguage(lang);
|
||||||
|
if (translations[lang]) {
|
||||||
|
currentLang = lang;
|
||||||
|
updateDOM();
|
||||||
|
window.electronAPI?.saveLanguage(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all elements with data-i18n attribute
|
||||||
|
function updateDOM() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
el.textContent = t(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-placeholder');
|
||||||
|
el.placeholder = t(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-title');
|
||||||
|
el.title = t(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize - load saved language only
|
||||||
|
async function init(savedLang) {
|
||||||
|
const lang = savedLang || 'en';
|
||||||
|
await loadLanguage(lang);
|
||||||
|
currentLang = lang;
|
||||||
|
updateDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init,
|
||||||
|
t,
|
||||||
|
setLanguage,
|
||||||
|
getAvailableLanguages: () => availableLanguages,
|
||||||
|
getCurrentLanguage: () => currentLang
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Make i18n globally available
|
||||||
|
window.i18n = i18n;
|
||||||
@@ -39,6 +39,19 @@ export function setupInstallation() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup installation effects listeners
|
||||||
|
if (window.electronAPI && window.electronAPI.onInstallationStart) {
|
||||||
|
window.electronAPI.onInstallationStart(() => {
|
||||||
|
showInstallationEffects();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI && window.electronAPI.onInstallationEnd) {
|
||||||
|
window.electronAPI.onInstallationEnd(() => {
|
||||||
|
hideInstallationEffects();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installGame() {
|
export async function installGame() {
|
||||||
@@ -51,7 +64,7 @@ export async function installGame() {
|
|||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
if (installBtn) {
|
if (installBtn) {
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
installText.textContent = 'INSTALLING...';
|
installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -59,8 +72,9 @@ export async function installGame() {
|
|||||||
const result = await window.electronAPI.installGame(playerName, '', installPath);
|
const result = await window.electronAPI.installGame(playerName, '', installPath);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
@@ -76,12 +90,20 @@ export async function installGame() {
|
|||||||
simulateInstallation(playerName);
|
simulateInstallation(playerName);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
|
||||||
|
|
||||||
|
// Hide installation effects on error
|
||||||
|
if (window.hideInstallationEffects) {
|
||||||
|
window.hideInstallationEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button state on error
|
||||||
|
resetInstallButton();
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` });
|
window.LauncherUI.updateProgress({ message: errorMsg });
|
||||||
setTimeout(() => {
|
// Don't hide progress bar, just update the message
|
||||||
window.LauncherUI.hideProgress();
|
// User can see the error and close it manually
|
||||||
resetInstallButton();
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,10 +114,13 @@ function simulateInstallation(playerName) {
|
|||||||
progress += Math.random() * 3;
|
progress += Math.random() * 3;
|
||||||
if (progress > 100) progress = 100;
|
if (progress > 100) progress = 100;
|
||||||
|
|
||||||
|
const installingMsg = window.i18n ? window.i18n.t('progress.installingGameFiles') : 'Installing game files...';
|
||||||
|
const completeMsg = window.i18n ? window.i18n.t('progress.installComplete') : 'Installation complete!';
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({
|
window.LauncherUI.updateProgress({
|
||||||
percent: progress,
|
percent: progress,
|
||||||
message: progress < 100 ? 'Installing game files...' : 'Installation complete!',
|
message: progress < 100 ? installingMsg : completeMsg,
|
||||||
speed: 1024 * 1024 * (5 + Math.random() * 10),
|
speed: 1024 * 1024 * (5 + Math.random() * 10),
|
||||||
downloaded: progress * 1024 * 1024 * 20,
|
downloaded: progress * 1024 * 1024 * 20,
|
||||||
total: 1024 * 1024 * 2000
|
total: 1024 * 1024 * 2000
|
||||||
@@ -104,9 +129,10 @@ function simulateInstallation(playerName) {
|
|||||||
|
|
||||||
if (progress >= 100) {
|
if (progress >= 100) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
|||||||
@@ -160,7 +160,8 @@ window.deleteProfile = async (id) => {
|
|||||||
window.switchProfile = async (id) => {
|
window.switchProfile = async (id) => {
|
||||||
try {
|
try {
|
||||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Switching Profile...' });
|
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching Profile...';
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: switchingMsg });
|
||||||
|
|
||||||
await window.electronAPI.profile.activate(id);
|
await window.electronAPI.profile.activate(id);
|
||||||
|
|
||||||
@@ -178,7 +179,8 @@ window.switchProfile = async (id) => {
|
|||||||
if (dropdown) dropdown.classList.remove('show');
|
if (dropdown) dropdown.classList.remove('show');
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Profile Switched!' });
|
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!';
|
||||||
|
window.LauncherUI.updateProgress({ message: switchedMsg });
|
||||||
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
|
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +223,8 @@ export async function launch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' });
|
const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.launchGame) {
|
if (window.electronAPI && window.electronAPI.launchGame) {
|
||||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
||||||
@@ -261,7 +264,12 @@ export async function launch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
|
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
|
||||||
|
// Apply defaults with i18n support
|
||||||
|
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
||||||
|
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
|
||||||
|
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
|
||||||
|
|
||||||
const existingModal = document.querySelector('.custom-confirm-modal');
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.remove();
|
existingModal.remove();
|
||||||
@@ -383,22 +391,28 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallGame() {
|
export async function uninstallGame() {
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.uninstallGameMessage') : 'Are you sure you want to uninstall Hytale? All game files will be deleted.';
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.uninstallGameTitle') : 'Uninstall Game';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.uninstallGameButton') : 'Uninstall';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
'Are you sure you want to uninstall Hytale? All game files will be deleted.',
|
message,
|
||||||
'Uninstall Game',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performUninstall();
|
await performUninstall();
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Uninstall',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performUninstall() {
|
async function performUninstall() {
|
||||||
|
|
||||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
|
const uninstallingMsg = window.i18n ? window.i18n.t('progress.uninstallingGame') : 'Uninstalling game...';
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: uninstallingMsg });
|
||||||
if (uninstallBtn) uninstallBtn.disabled = true;
|
if (uninstallBtn) uninstallBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -406,8 +420,9 @@ async function performUninstall() {
|
|||||||
const result = await window.electronAPI.uninstallGame();
|
const result = await window.electronAPI.uninstallGame();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(false);
|
window.LauncherUI.showLauncherOrInstall(false);
|
||||||
@@ -417,9 +432,10 @@ async function performUninstall() {
|
|||||||
throw new Error(result.error || 'Uninstall failed');
|
throw new Error(result.error || 'Uninstall failed');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(false);
|
window.LauncherUI.showLauncherOrInstall(false);
|
||||||
@@ -428,8 +444,9 @@ async function performUninstall() {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMsg = window.i18n ? window.i18n.t('progress.uninstallFailed').replace('{error}', error.message) : `Uninstall failed: ${error.message}`;
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` });
|
window.LauncherUI.updateProgress({ message: errorMsg });
|
||||||
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
|
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -484,7 +501,7 @@ function resetPlayButton() {
|
|||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.disabled = false;
|
playBtn.disabled = false;
|
||||||
playText.textContent = 'PLAY';
|
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32';
|
let API_KEY = null;
|
||||||
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
|
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
|
||||||
const HYTALE_GAME_ID = 70216;
|
const HYTALE_GAME_ID = 70216;
|
||||||
|
|
||||||
@@ -11,6 +11,15 @@ let modsPageSize = 20;
|
|||||||
let modsTotalPages = 1;
|
let modsTotalPages = 1;
|
||||||
|
|
||||||
export async function initModsManager() {
|
export async function initModsManager() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.getEnvVar) {
|
||||||
|
API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY');
|
||||||
|
console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load API Key:', err);
|
||||||
|
}
|
||||||
|
|
||||||
setupModsEventListeners();
|
setupModsEventListeners();
|
||||||
await loadInstalledMods();
|
await loadInstalledMods();
|
||||||
await loadBrowseMods();
|
await loadBrowseMods();
|
||||||
@@ -113,10 +122,15 @@ function displayInstalledMods(mods) {
|
|||||||
modsContainer.innerHTML = `
|
modsContainer.innerHTML = `
|
||||||
<div class=\"empty-installed-mods\">
|
<div class=\"empty-installed-mods\">
|
||||||
<i class=\"fas fa-box-open\"></i>
|
<i class=\"fas fa-box-open\"></i>
|
||||||
<h4>No Mods Installed</h4>
|
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
|
||||||
<p>Add mods from CurseForge or import local files</p>
|
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (window.i18n) {
|
||||||
|
const container = modsContainer.querySelector('.empty-installed-mods');
|
||||||
|
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
|
||||||
|
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +152,9 @@ function displayInstalledMods(mods) {
|
|||||||
|
|
||||||
function createInstalledModCard(mod) {
|
function createInstalledModCard(mod) {
|
||||||
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
|
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
|
||||||
const statusText = mod.enabled ? 'ACTIVE' : 'DISABLED';
|
const statusText = mod.enabled ? (window.i18n ? window.i18n.t('mods.active') : 'ACTIVE') : (window.i18n ? window.i18n.t('mods.disabled') : 'DISABLED');
|
||||||
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
|
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
|
||||||
const toggleBtnText = mod.enabled ? 'DISABLE' : 'ENABLE';
|
const toggleBtnText = mod.enabled ? (window.i18n ? window.i18n.t('mods.disable') : 'DISABLE') : (window.i18n ? window.i18n.t('mods.enable') : 'ENABLE');
|
||||||
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
|
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -154,7 +168,7 @@ function createInstalledModCard(mod) {
|
|||||||
<h4 class="installed-mod-name">${mod.name}</h4>
|
<h4 class="installed-mod-name">${mod.name}</h4>
|
||||||
<span class="installed-mod-version">v${mod.version}</span>
|
<span class="installed-mod-version">v${mod.version}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="installed-mod-description">${mod.description || 'No description available'}</p>
|
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="installed-mod-actions">
|
<div class="installed-mod-actions">
|
||||||
@@ -163,7 +177,7 @@ function createInstalledModCard(mod) {
|
|||||||
${statusText}
|
${statusText}
|
||||||
</div>
|
</div>
|
||||||
<div class="installed-mod-buttons">
|
<div class="installed-mod-buttons">
|
||||||
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="Delete mod">
|
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="${window.i18n ? window.i18n.t('mods.delete') : 'Delete mod'}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
|
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
|
||||||
@@ -180,7 +194,7 @@ async function loadBrowseMods() {
|
|||||||
const browseContainer = document.getElementById('browseModsList');
|
const browseContainer = document.getElementById('browseModsList');
|
||||||
if (!browseContainer) return;
|
if (!browseContainer) return;
|
||||||
|
|
||||||
browseContainer.innerHTML = '<div class=\"loading-mods\"><div class=\"loading-spinner\"></div><span>Loading mods from CurseForge...</span></div>';
|
browseContainer.innerHTML = `<div class="loading-mods"><div class="loading-spinner"></div><span>${window.i18n ? window.i18n.t('mods.loadingMods') : 'Loading mods from CurseForge...'}</span></div>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!API_KEY || API_KEY.length < 10) {
|
if (!API_KEY || API_KEY.length < 10) {
|
||||||
@@ -264,10 +278,15 @@ function displayBrowseMods(mods) {
|
|||||||
browseContainer.innerHTML = `
|
browseContainer.innerHTML = `
|
||||||
<div class=\"empty-browse-mods\">
|
<div class=\"empty-browse-mods\">
|
||||||
<i class=\"fas fa-search\"></i>
|
<i class=\"fas fa-search\"></i>
|
||||||
<h4>No Mods Found</h4>
|
<h4 data-i18n="mods.noModsFound">No Mods Found</h4>
|
||||||
<p>Try adjusting your search</p>
|
<p data-i18n="mods.noModsFoundDesc">Try adjusting your search</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (window.i18n) {
|
||||||
|
const container = browseContainer.querySelector('.empty-browse-mods');
|
||||||
|
container.querySelector('h4').textContent = window.i18n.t('mods.noModsFound');
|
||||||
|
container.querySelector('p').textContent = window.i18n.t('mods.noModsFoundDesc');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,16 +343,16 @@ function createBrowseModCard(mod) {
|
|||||||
<div class=\"mod-actions\">
|
<div class=\"mod-actions\">
|
||||||
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
|
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
|
||||||
<i class=\"fas fa-external-link-alt\"></i>
|
<i class=\"fas fa-external-link-alt\"></i>
|
||||||
VIEW
|
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
|
||||||
</button>
|
</button>
|
||||||
${!isInstalled ?
|
${!isInstalled ?
|
||||||
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
|
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
|
||||||
<i class=\"fas fa-download\"></i>
|
<i class="fas fa-download"></i>
|
||||||
INSTALL
|
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
|
||||||
</button>` :
|
</button>` :
|
||||||
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
|
`<button class="mod-btn-toggle bg-white/10 text-white" disabled>
|
||||||
<i class=\"fas fa-check\"></i>
|
<i class="fas fa-check"></i>
|
||||||
INSTALLED
|
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
|
||||||
</button>`
|
</button>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -343,7 +362,8 @@ function createBrowseModCard(mod) {
|
|||||||
|
|
||||||
async function downloadAndInstallMod(modInfo) {
|
async function downloadAndInstallMod(modInfo) {
|
||||||
try {
|
try {
|
||||||
window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`);
|
const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`;
|
||||||
|
window.LauncherUI?.showProgress(downloadMsg);
|
||||||
|
|
||||||
const result = await window.electronAPI?.downloadMod(modInfo);
|
const result = await window.electronAPI?.downloadMod(modInfo);
|
||||||
|
|
||||||
@@ -367,20 +387,23 @@ async function downloadAndInstallMod(modInfo) {
|
|||||||
await loadInstalledMods();
|
await loadInstalledMods();
|
||||||
await loadBrowseMods();
|
await loadBrowseMods();
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification(`${modInfo.name} installed successfully! 🎉`, 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.modsInstalledSuccess').replace('{name}', modInfo.name) : `${modInfo.name} installed successfully! 🎉`;
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.error || 'Failed to download mod');
|
throw new Error(result?.error || 'Failed to download mod');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading mod:', error);
|
console.error('Error downloading mod:', error);
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification('Failed to download mod: ' + error.message, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsDownloadFailed').replace('{error}', error.message) : 'Failed to download mod: ' + error.message;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleMod(modId) {
|
async function toggleMod(modId) {
|
||||||
try {
|
try {
|
||||||
window.LauncherUI?.showProgress('Toggling mod...');
|
const toggleMsg = window.i18n ? window.i18n.t('notifications.modsTogglingMod') : 'Toggling mod...';
|
||||||
|
window.LauncherUI?.showProgress(toggleMsg);
|
||||||
|
|
||||||
const modsPath = await window.electronAPI?.getModsPath();
|
const modsPath = await window.electronAPI?.getModsPath();
|
||||||
const result = await window.electronAPI?.toggleMod(modId, modsPath);
|
const result = await window.electronAPI?.toggleMod(modId, modsPath);
|
||||||
@@ -394,7 +417,8 @@ async function toggleMod(modId) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling mod:', error);
|
console.error('Error toggling mod:', error);
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification('Failed to toggle mod: ' + error.message, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsToggleFailed').replace('{error}', error.message) : 'Failed to toggle mod: ' + error.message;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,11 +426,16 @@ async function deleteMod(modId) {
|
|||||||
const mod = installedMods.find(m => m.id === modId);
|
const mod = installedMods.find(m => m.id === modId);
|
||||||
if (!mod) return;
|
if (!mod) return;
|
||||||
|
|
||||||
|
const confirmMsg = window.i18n ?
|
||||||
|
window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') :
|
||||||
|
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`;
|
||||||
|
|
||||||
showConfirmModal(
|
showConfirmModal(
|
||||||
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`,
|
confirmMsg,
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
window.LauncherUI?.showProgress('Deleting mod...');
|
const deleteMsg = window.i18n ? window.i18n.t('notifications.modsDeletingMod') : 'Deleting mod...';
|
||||||
|
window.LauncherUI?.showProgress(deleteMsg);
|
||||||
|
|
||||||
const modsPath = await window.electronAPI?.getModsPath();
|
const modsPath = await window.electronAPI?.getModsPath();
|
||||||
const result = await window.electronAPI?.uninstallMod(modId, modsPath);
|
const result = await window.electronAPI?.uninstallMod(modId, modsPath);
|
||||||
@@ -415,14 +444,16 @@ async function deleteMod(modId) {
|
|||||||
await loadInstalledMods();
|
await loadInstalledMods();
|
||||||
await loadBrowseMods();
|
await loadBrowseMods();
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification(`"${mod.name}" deleted successfully`, 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.modsDeletedSuccess').replace('{name}', mod.name) : `"${mod.name}" deleted successfully`;
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.error || 'Failed to delete mod');
|
throw new Error(result?.error || 'Failed to delete mod');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting mod:', error);
|
console.error('Error deleting mod:', error);
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification('Failed to delete mod: ' + error.message, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsDeleteFailed').replace('{error}', error.message) : 'Failed to delete mod: ' + error.message;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -571,7 +602,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
|
|||||||
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||||
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
|
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
|
||||||
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
||||||
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Confirm Deletion</h3>
|
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${window.i18n ? window.i18n.t('mods.confirmDeletion') : 'Confirm Deletion'}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 24px; color: #e5e7eb;">
|
<div style="padding: 24px; color: #e5e7eb;">
|
||||||
@@ -587,7 +618,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
">Cancel</button>
|
">${window.i18n ? window.i18n.t('common.cancel') : 'Cancel'}</button>
|
||||||
<button class="mod-confirm-delete" style="
|
<button class="mod-confirm-delete" style="
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -597,7 +628,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
">Delete</button>
|
">${window.i18n ? window.i18n.t('common.delete') : 'Delete'}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -715,7 +746,8 @@ function viewModPage(modId) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Mod not found with ID:', modId);
|
console.error('Mod not found with ID:', modId);
|
||||||
showNotification('Mod information not found', 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsModNotFound') : 'Mod information not found';
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,17 +8,55 @@ import './chat.js';
|
|||||||
import './settings.js';
|
import './settings.js';
|
||||||
import './logs.js';
|
import './logs.js';
|
||||||
|
|
||||||
window.closeDiscordNotification = function () {
|
// Initialize i18n immediately (before DOMContentLoaded)
|
||||||
const notification = document.getElementById('discordNotification');
|
let i18nInitialized = false;
|
||||||
if (notification) {
|
(async () => {
|
||||||
notification.classList.add('hidden');
|
const savedLang = await window.electronAPI?.loadLanguage();
|
||||||
setTimeout(() => {
|
await i18n.init(savedLang);
|
||||||
notification.style.display = 'none';
|
i18nInitialized = true;
|
||||||
}, 300);
|
|
||||||
|
// Update language selector if DOM is already loaded
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
updateLanguageSelector();
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
|
function updateLanguageSelector() {
|
||||||
|
const langSelect = document.getElementById('languageSelect');
|
||||||
|
if (langSelect) {
|
||||||
|
// Clear existing options
|
||||||
|
langSelect.innerHTML = '';
|
||||||
|
|
||||||
|
const languages = i18n.getAvailableLanguages();
|
||||||
|
const currentLang = i18n.getCurrentLanguage();
|
||||||
|
|
||||||
|
languages.forEach(lang => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = lang.code;
|
||||||
|
option.textContent = lang.name;
|
||||||
|
if (lang.code === currentLang) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
langSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle language change (add listener only once)
|
||||||
|
if (!langSelect.hasAttribute('data-listener-added')) {
|
||||||
|
langSelect.addEventListener('change', async (e) => {
|
||||||
|
await i18n.setLanguage(e.target.value);
|
||||||
|
});
|
||||||
|
langSelect.setAttribute('data-listener-added', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Populate language selector (wait for i18n if needed)
|
||||||
|
if (i18nInitialized) {
|
||||||
|
updateLanguageSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord notification
|
||||||
const notification = document.getElementById('discordNotification');
|
const notification = document.getElementById('discordNotification');
|
||||||
if (notification) {
|
if (notification) {
|
||||||
const dismissed = localStorage.getItem('discordNotificationDismissed');
|
const dismissed = localStorage.getItem('discordNotificationDismissed');
|
||||||
@@ -32,8 +70,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const originalClose = window.closeDiscordNotification;
|
window.closeDiscordNotification = function() {
|
||||||
window.closeDiscordNotification = function () {
|
const notification = document.getElementById('discordNotification');
|
||||||
|
if (notification) {
|
||||||
|
notification.classList.add('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
localStorage.setItem('discordNotificationDismissed', 'true');
|
localStorage.setItem('discordNotificationDismissed', 'true');
|
||||||
originalClose();
|
|
||||||
};
|
};
|
||||||
@@ -5,8 +5,10 @@ let customJavaPath;
|
|||||||
let browseJavaBtn;
|
let browseJavaBtn;
|
||||||
let settingsPlayerName;
|
let settingsPlayerName;
|
||||||
let discordRPCCheck;
|
let discordRPCCheck;
|
||||||
|
let closeLauncherCheck;
|
||||||
let gpuPreferenceRadios;
|
let gpuPreferenceRadios;
|
||||||
|
|
||||||
|
|
||||||
// UUID Management elements
|
// UUID Management elements
|
||||||
let currentUuidDisplay;
|
let currentUuidDisplay;
|
||||||
let copyUuidBtn;
|
let copyUuidBtn;
|
||||||
@@ -22,7 +24,12 @@ let uuidList;
|
|||||||
let customUuidInput;
|
let customUuidInput;
|
||||||
let setCustomUuidBtn;
|
let setCustomUuidBtn;
|
||||||
|
|
||||||
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
|
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
|
||||||
|
// Apply defaults with i18n support
|
||||||
|
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
||||||
|
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
|
||||||
|
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
|
||||||
|
|
||||||
const existingModal = document.querySelector('.custom-confirm-modal');
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.remove();
|
existingModal.remove();
|
||||||
@@ -156,8 +163,10 @@ function setupSettingsElements() {
|
|||||||
browseJavaBtn = document.getElementById('browseJavaBtn');
|
browseJavaBtn = document.getElementById('browseJavaBtn');
|
||||||
settingsPlayerName = document.getElementById('settingsPlayerName');
|
settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
discordRPCCheck = document.getElementById('discordRPCCheck');
|
discordRPCCheck = document.getElementById('discordRPCCheck');
|
||||||
|
closeLauncherCheck = document.getElementById('closeLauncherCheck');
|
||||||
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
|
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
|
||||||
|
|
||||||
|
|
||||||
// UUID Management elements
|
// UUID Management elements
|
||||||
currentUuidDisplay = document.getElementById('currentUuid');
|
currentUuidDisplay = document.getElementById('currentUuid');
|
||||||
copyUuidBtn = document.getElementById('copyUuidBtn');
|
copyUuidBtn = document.getElementById('copyUuidBtn');
|
||||||
@@ -189,6 +198,11 @@ function setupSettingsElements() {
|
|||||||
discordRPCCheck.addEventListener('change', saveDiscordRPC);
|
discordRPCCheck.addEventListener('change', saveDiscordRPC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (closeLauncherCheck) {
|
||||||
|
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// UUID event listeners
|
// UUID event listeners
|
||||||
if (copyUuidBtn) {
|
if (copyUuidBtn) {
|
||||||
copyUuidBtn.addEventListener('click', copyCurrentUuid);
|
copyUuidBtn.addEventListener('click', copyCurrentUuid);
|
||||||
@@ -313,9 +327,11 @@ async function saveDiscordRPC() {
|
|||||||
|
|
||||||
// Feedback visuel pour l'utilisateur
|
// Feedback visuel pour l'utilisateur
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
showNotification('Discord Rich Presence enabled', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
|
||||||
|
showNotification(msg, 'success');
|
||||||
} else {
|
} else {
|
||||||
showNotification('Discord Rich Presence disabled', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.discordDisabled') : 'Discord Rich Presence disabled';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to save Discord RPC setting');
|
throw new Error('Failed to save Discord RPC setting');
|
||||||
@@ -323,7 +339,8 @@ async function saveDiscordRPC() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving Discord RPC setting:', error);
|
console.error('Error saving Discord RPC setting:', error);
|
||||||
showNotification('Failed to save Discord setting', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.discordSaveFailed') : 'Failed to save Discord setting';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +357,31 @@ async function loadDiscordRPC() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCloseLauncher() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
|
||||||
|
const enabled = closeLauncherCheck.checked;
|
||||||
|
await window.electronAPI.saveCloseLauncher(enabled);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving close launcher setting:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCloseLauncher() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
|
||||||
|
const enabled = await window.electronAPI.loadCloseLauncher();
|
||||||
|
if (closeLauncherCheck) {
|
||||||
|
closeLauncherCheck.checked = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading close launcher setting:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function savePlayerName() {
|
async function savePlayerName() {
|
||||||
try {
|
try {
|
||||||
if (!window.electronAPI || !settingsPlayerName) return;
|
if (!window.electronAPI || !settingsPlayerName) return;
|
||||||
@@ -347,16 +389,19 @@ async function savePlayerName() {
|
|||||||
const playerName = settingsPlayerName.value.trim();
|
const playerName = settingsPlayerName.value.trim();
|
||||||
|
|
||||||
if (!playerName) {
|
if (!playerName) {
|
||||||
showNotification('Please enter a valid player name', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
|
||||||
|
showNotification(msg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.electronAPI.saveUsername(playerName);
|
await window.electronAPI.saveUsername(playerName);
|
||||||
showNotification('Player name saved successfully', 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving player name:', error);
|
console.error('Error saving player name:', error);
|
||||||
showNotification('Failed to save player name', 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,9 +496,11 @@ async function loadAllSettings() {
|
|||||||
await loadPlayerName();
|
await loadPlayerName();
|
||||||
await loadCurrentUuid();
|
await loadCurrentUuid();
|
||||||
await loadDiscordRPC();
|
await loadDiscordRPC();
|
||||||
|
await loadCloseLauncher();
|
||||||
await loadGpuPreference();
|
await loadGpuPreference();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function openGameLocation() {
|
async function openGameLocation() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.openGameLocation) {
|
if (window.electronAPI && window.electronAPI.openGameLocation) {
|
||||||
@@ -507,34 +554,43 @@ async function copyCurrentUuid() {
|
|||||||
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
|
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
|
||||||
if (uuid && navigator.clipboard) {
|
if (uuid && navigator.clipboard) {
|
||||||
await navigator.clipboard.writeText(uuid);
|
await navigator.clipboard.writeText(uuid);
|
||||||
showNotification('UUID copied to clipboard!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying UUID:', error);
|
console.error('Error copying UUID:', error);
|
||||||
showNotification('Failed to copy UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function regenerateCurrentUuid() {
|
async function regenerateCurrentUuid() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
|
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
'Are you sure you want to generate a new UUID? This will change your player identity.',
|
message,
|
||||||
'Generate New UUID',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performRegenerateUuid();
|
await performRegenerateUuid();
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Generate',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error('electronAPI.resetCurrentUserUuid not available');
|
console.error('electronAPI.resetCurrentUserUuid not available');
|
||||||
showNotification('UUID regeneration not available', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenNotAvailable') : 'UUID regeneration not available';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in regenerateCurrentUuid:', error);
|
console.error('Error in regenerateCurrentUuid:', error);
|
||||||
showNotification('Failed to regenerate UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed') : 'Failed to regenerate UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +600,8 @@ async function performRegenerateUuid() {
|
|||||||
if (result.success && result.uuid) {
|
if (result.success && result.uuid) {
|
||||||
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
|
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
|
||||||
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
||||||
showNotification('New UUID generated successfully!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
if (uuidModal && uuidModal.style.display !== 'none') {
|
if (uuidModal && uuidModal.style.display !== 'none') {
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
@@ -554,7 +611,8 @@ async function performRegenerateUuid() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error regenerating UUID:', error);
|
console.error('Error regenerating UUID:', error);
|
||||||
showNotification(`Failed to regenerate UUID: ${error.message}`, 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed').replace('{error}', error.message) : `Failed to regenerate UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,19 +705,22 @@ async function generateNewUuid() {
|
|||||||
const newUuid = await window.electronAPI.generateNewUuid();
|
const newUuid = await window.electronAPI.generateNewUuid();
|
||||||
if (newUuid) {
|
if (newUuid) {
|
||||||
if (customUuidInput) customUuidInput.value = newUuid;
|
if (customUuidInput) customUuidInput.value = newUuid;
|
||||||
showNotification('New UUID generated!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating new UUID:', error);
|
console.error('Error generating new UUID:', error);
|
||||||
showNotification('Failed to generate new UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setCustomUuid() {
|
async function setCustomUuid() {
|
||||||
try {
|
try {
|
||||||
if (!customUuidInput || !customUuidInput.value.trim()) {
|
if (!customUuidInput || !customUuidInput.value.trim()) {
|
||||||
showNotification('Please enter a UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRequired') : 'Please enter a UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,23 +728,30 @@ async function setCustomUuid() {
|
|||||||
|
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
if (!uuidRegex.test(uuid)) {
|
if (!uuidRegex.test(uuid)) {
|
||||||
showNotification('Invalid UUID format', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
||||||
|
showNotification(msg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.setCustomUuidMessage') : 'Are you sure you want to set this custom UUID? This will change your player identity.';
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.setCustomUuidTitle') : 'Set Custom UUID';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.setCustomUuidButton') : 'Set UUID';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
'Are you sure you want to set this custom UUID? This will change your player identity.',
|
message,
|
||||||
'Set Custom UUID',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performSetCustomUuid(uuid);
|
await performSetCustomUuid(uuid);
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Set UUID',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in setCustomUuid:', error);
|
console.error('Error in setCustomUuid:', error);
|
||||||
showNotification('Failed to set custom UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed') : 'Failed to set custom UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +766,8 @@ async function setCustomUuid() {
|
|||||||
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
||||||
if (customUuidInput) customUuidInput.value = '';
|
if (customUuidInput) customUuidInput.value = '';
|
||||||
|
|
||||||
showNotification('Custom UUID set successfully!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
} else {
|
} else {
|
||||||
@@ -707,7 +776,8 @@ async function setCustomUuid() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting custom UUID:', error);
|
console.error('Error setting custom UUID:', error);
|
||||||
showNotification(`Failed to set custom UUID: ${error.message}`, 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,29 +785,37 @@ window.copyUuid = async function(uuid) {
|
|||||||
try {
|
try {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
await navigator.clipboard.writeText(uuid);
|
await navigator.clipboard.writeText(uuid);
|
||||||
showNotification('UUID copied to clipboard!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying UUID:', error);
|
console.error('Error copying UUID:', error);
|
||||||
showNotification('Failed to copy UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.deleteUuid = async function(username) {
|
window.deleteUuid = async function(username) {
|
||||||
try {
|
try {
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
`Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`,
|
message,
|
||||||
'Delete UUID',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performDeleteUuid(username);
|
await performDeleteUuid(username);
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Delete',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in deleteUuid:', error);
|
console.error('Error in deleteUuid:', error);
|
||||||
showNotification('Failed to delete UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed') : 'Failed to delete UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -747,7 +825,8 @@ async function performDeleteUuid(username) {
|
|||||||
const result = await window.electronAPI.deleteUuidForUser(username);
|
const result = await window.electronAPI.deleteUuidForUser(username);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showNotification('UUID deleted successfully!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || 'Failed to delete UUID');
|
throw new Error(result.error || 'Failed to delete UUID');
|
||||||
@@ -755,7 +834,8 @@ async function performDeleteUuid(username) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting UUID:', error);
|
console.error('Error deleting UUID:', error);
|
||||||
showNotification(`Failed to delete UUID: ${error.message}`, 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
GUI/js/ui.js
129
GUI/js/ui.js
@@ -366,7 +366,7 @@ function lockPlayButton(locked) {
|
|||||||
if (!playButton.getAttribute('data-original-text')) {
|
if (!playButton.getAttribute('data-original-text')) {
|
||||||
playButton.setAttribute('data-original-text', spanElement.textContent);
|
playButton.setAttribute('data-original-text', spanElement.textContent);
|
||||||
}
|
}
|
||||||
spanElement.textContent = 'CHECKING...';
|
spanElement.textContent = window.i18n ? window.i18n.t('play.checking') : 'CHECKING...';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Play button locked');
|
console.log('Play button locked');
|
||||||
@@ -377,9 +377,9 @@ function lockPlayButton(locked) {
|
|||||||
playButton.removeAttribute('data-locked');
|
playButton.removeAttribute('data-locked');
|
||||||
|
|
||||||
const spanElement = playButton.querySelector('span');
|
const spanElement = playButton.querySelector('span');
|
||||||
const originalText = playButton.getAttribute('data-original-text');
|
if (spanElement) {
|
||||||
if (spanElement && originalText) {
|
// Use i18n to get the current translation instead of restoring saved text
|
||||||
spanElement.textContent = originalText;
|
spanElement.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
|
||||||
playButton.removeAttribute('data-original-text');
|
playButton.removeAttribute('data-original-text');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +393,8 @@ async function acceptFirstLaunchUpdate() {
|
|||||||
const existingGame = window.firstLaunchExistingGame;
|
const existingGame = window.firstLaunchExistingGame;
|
||||||
|
|
||||||
if (!existingGame) {
|
if (!existingGame) {
|
||||||
showNotification('Error: Game data not found', 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.gameDataNotFound') : 'Error: Game data not found';
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +411,8 @@ async function acceptFirstLaunchUpdate() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
showProgress();
|
showProgress();
|
||||||
updateProgress({ message: 'Starting mandatory game update...', percent: 0 });
|
const updateMsg = window.i18n ? window.i18n.t('progress.startingUpdate') : 'Starting mandatory game update...';
|
||||||
|
updateProgress({ message: updateMsg, percent: 0 });
|
||||||
|
|
||||||
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
|
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
|
||||||
|
|
||||||
@@ -424,10 +426,12 @@ async function acceptFirstLaunchUpdate() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showNotification('Game updated successfully! 🎉', 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.gameUpdatedSuccess') : 'Game updated successfully! 🎉';
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
} else {
|
} else {
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showNotification(`Update failed: ${result.error}`, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.updateFailed').replace('{error}', result.error) : `Update failed: ${result.error}`;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -435,7 +439,8 @@ async function acceptFirstLaunchUpdate() {
|
|||||||
}
|
}
|
||||||
lockPlayButton(false);
|
lockPlayButton(false);
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showNotification(`Update error: ${error.message}`, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.updateError').replace('{error}', error.message) : `Update error: ${error.message}`;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +479,9 @@ function setupUI() {
|
|||||||
progressSpeed = document.getElementById('progressSpeed');
|
progressSpeed = document.getElementById('progressSpeed');
|
||||||
progressSize = document.getElementById('progressSize');
|
progressSize = document.getElementById('progressSize');
|
||||||
|
|
||||||
|
// Setup draggable progress bar
|
||||||
|
setupProgressDrag();
|
||||||
|
|
||||||
lockPlayButton(true);
|
lockPlayButton(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -492,10 +500,26 @@ function setupUI() {
|
|||||||
setupSidebarLogo();
|
setupSidebarLogo();
|
||||||
setupAnimations();
|
setupAnimations();
|
||||||
setupFirstLaunchHandlers();
|
setupFirstLaunchHandlers();
|
||||||
|
loadLauncherVersion();
|
||||||
|
|
||||||
document.body.focus();
|
document.body.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load launcher version from package.json
|
||||||
|
async function loadLauncherVersion() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.getVersion) {
|
||||||
|
const version = await window.electronAPI.getVersion();
|
||||||
|
const versionElement = document.getElementById('launcherVersion');
|
||||||
|
if (versionElement) {
|
||||||
|
versionElement.textContent = `v${version}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load launcher version:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.LauncherUI = {
|
window.LauncherUI = {
|
||||||
showPage,
|
showPage,
|
||||||
setActiveNav,
|
setActiveNav,
|
||||||
@@ -505,4 +529,91 @@ window.LauncherUI = {
|
|||||||
updateProgress
|
updateProgress
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Make installation effects globally available
|
||||||
|
window.showInstallationEffects = showInstallationEffects;
|
||||||
|
window.hideInstallationEffects = hideInstallationEffects;
|
||||||
|
|
||||||
|
// Draggable progress bar functionality
|
||||||
|
function setupProgressDrag() {
|
||||||
|
if (!progressOverlay) return;
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let offsetX;
|
||||||
|
let offsetY;
|
||||||
|
|
||||||
|
progressOverlay.addEventListener('mousedown', dragStart);
|
||||||
|
document.addEventListener('mousemove', drag);
|
||||||
|
document.addEventListener('mouseup', dragEnd);
|
||||||
|
|
||||||
|
function dragStart(e) {
|
||||||
|
// Only drag if clicking on the overlay itself, not on buttons or inputs
|
||||||
|
if (e.target.closest('.progress-bar-fill')) return;
|
||||||
|
|
||||||
|
if (e.target === progressOverlay || e.target.closest('.progress-content')) {
|
||||||
|
isDragging = true;
|
||||||
|
progressOverlay.classList.add('dragging');
|
||||||
|
|
||||||
|
// Get the current position of the progress overlay
|
||||||
|
const rect = progressOverlay.getBoundingClientRect();
|
||||||
|
offsetX = e.clientX - rect.left - progressOverlay.offsetWidth / 2;
|
||||||
|
offsetY = e.clientY - rect.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(e) {
|
||||||
|
if (isDragging) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Calculate new position
|
||||||
|
const newX = e.clientX - offsetX - progressOverlay.offsetWidth / 2;
|
||||||
|
const newY = e.clientY - offsetY;
|
||||||
|
|
||||||
|
// Get window bounds
|
||||||
|
const maxX = window.innerWidth - progressOverlay.offsetWidth;
|
||||||
|
const maxY = window.innerHeight - progressOverlay.offsetHeight;
|
||||||
|
const minX = 0;
|
||||||
|
const minY = 0;
|
||||||
|
|
||||||
|
// Constrain to window bounds
|
||||||
|
const constrainedX = Math.max(minX, Math.min(newX, maxX));
|
||||||
|
const constrainedY = Math.max(minY, Math.min(newY, maxY));
|
||||||
|
|
||||||
|
progressOverlay.style.left = constrainedX + 'px';
|
||||||
|
progressOverlay.style.bottom = 'auto';
|
||||||
|
progressOverlay.style.top = constrainedY + 'px';
|
||||||
|
progressOverlay.style.transform = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragEnd() {
|
||||||
|
isDragging = false;
|
||||||
|
progressOverlay.classList.remove('dragging');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide installation effects
|
||||||
|
function showInstallationEffects() {
|
||||||
|
const installationEffects = document.getElementById('installationEffects');
|
||||||
|
if (installationEffects) {
|
||||||
|
installationEffects.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideInstallationEffects() {
|
||||||
|
const installationEffects = document.getElementById('installationEffects');
|
||||||
|
if (installationEffects) {
|
||||||
|
installationEffects.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle maximize/restore window function
|
||||||
|
function toggleMaximize() {
|
||||||
|
if (window.electronAPI && window.electronAPI.maximizeWindow) {
|
||||||
|
window.electronAPI.maximizeWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make toggleMaximize globally available
|
||||||
|
window.toggleMaximize = toggleMaximize;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', setupUI);
|
document.addEventListener('DOMContentLoaded', setupUI);
|
||||||
|
|||||||
216
GUI/js/update.js
216
GUI/js/update.js
@@ -10,6 +10,23 @@ class ClientUpdateManager {
|
|||||||
this.showUpdatePopup(updateInfo);
|
this.showUpdatePopup(updateInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for electron-updater events
|
||||||
|
window.electronAPI.onUpdateAvailable((updateInfo) => {
|
||||||
|
this.showUpdatePopup(updateInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateDownloadProgress((progress) => {
|
||||||
|
this.updateDownloadProgress(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateDownloaded((updateInfo) => {
|
||||||
|
this.showUpdateDownloaded(updateInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateError((errorInfo) => {
|
||||||
|
this.handleUpdateError(errorInfo);
|
||||||
|
});
|
||||||
|
|
||||||
this.checkForUpdatesOnDemand();
|
this.checkForUpdatesOnDemand();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,23 +50,46 @@ class ClientUpdateManager {
|
|||||||
<div class="update-popup-versions">
|
<div class="update-popup-versions">
|
||||||
<div class="version-row">
|
<div class="version-row">
|
||||||
<span class="version-label">Current Version:</span>
|
<span class="version-label">Current Version:</span>
|
||||||
<span class="version-current">${updateInfo.currentVersion}</span>
|
<span class="version-current">${updateInfo.currentVersion || updateInfo.version || 'Unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-row">
|
<div class="version-row">
|
||||||
<span class="version-label">New Version:</span>
|
<span class="version-label">New Version:</span>
|
||||||
<span class="version-new">${updateInfo.newVersion}</span>
|
<span class="version-new">${updateInfo.newVersion || updateInfo.version || 'Unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="update-popup-message">
|
<div class="update-popup-message">
|
||||||
A new version of Hytale F2P Launcher is available.<br>
|
A new version of Hytale F2P Launcher is available.<br>
|
||||||
Please download the latest version to continue using the launcher.
|
<span id="update-status-text">Downloading update automatically...</span>
|
||||||
|
<div id="update-error-message" style="display: none; margin-top: 0.75rem; padding: 0.75rem; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; color: #fca5a5; font-size: 0.875rem;">
|
||||||
|
<i class="fas fa-exclamation-triangle" style="margin-right: 0.5rem;"></i>
|
||||||
|
<span id="update-error-text"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="update-download-btn" class="update-download-btn">
|
<div id="update-progress-container" style="display: none; margin-bottom: 1rem;">
|
||||||
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.75rem; color: #9ca3af;">
|
||||||
Download Update
|
<span id="update-progress-percent">0%</span>
|
||||||
</button>
|
<span id="update-progress-speed">0 KB/s</span>
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
|
||||||
|
<div id="update-progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #3b82f6, #9333ea); transition: width 0.3s ease;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #9ca3af; text-align: center;">
|
||||||
|
<span id="update-progress-size">0 MB / 0 MB</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="update-buttons-container" style="display: none;">
|
||||||
|
<button id="update-install-btn" class="update-download-btn">
|
||||||
|
<i class="fas fa-check" style="margin-right: 0.5rem;"></i>
|
||||||
|
Install & Restart
|
||||||
|
</button>
|
||||||
|
<button id="update-download-btn" class="update-download-btn update-download-btn-secondary" style="margin-top: 0.75rem;">
|
||||||
|
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
|
||||||
|
Manually Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="update-popup-footer">
|
<div class="update-popup-footer">
|
||||||
This popup cannot be closed until you update the launcher
|
This popup cannot be closed until you update the launcher
|
||||||
@@ -62,6 +102,31 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
this.blockInterface();
|
this.blockInterface();
|
||||||
|
|
||||||
|
// Show progress container immediately (auto-download is enabled)
|
||||||
|
const progressContainer = document.getElementById('update-progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
const installBtn = document.getElementById('update-install-btn');
|
||||||
|
if (installBtn) {
|
||||||
|
installBtn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
installBtn.disabled = true;
|
||||||
|
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electronAPI.quitAndInstallUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error installing update:', error);
|
||||||
|
installBtn.disabled = false;
|
||||||
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const downloadBtn = document.getElementById('update-download-btn');
|
const downloadBtn = document.getElementById('update-download-btn');
|
||||||
if (downloadBtn) {
|
if (downloadBtn) {
|
||||||
downloadBtn.addEventListener('click', async (e) => {
|
downloadBtn.addEventListener('click', async (e) => {
|
||||||
@@ -80,7 +145,7 @@ class ClientUpdateManager {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error opening download page:', error);
|
console.error('❌ Error opening download page:', error);
|
||||||
downloadBtn.disabled = false;
|
downloadBtn.disabled = false;
|
||||||
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update';
|
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Manually Download';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,6 +164,134 @@ class ClientUpdateManager {
|
|||||||
console.log('🔔 Update popup displayed with new style');
|
console.log('🔔 Update popup displayed with new style');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateDownloadProgress(progress) {
|
||||||
|
const progressBar = document.getElementById('update-progress-bar');
|
||||||
|
const progressPercent = document.getElementById('update-progress-percent');
|
||||||
|
const progressSpeed = document.getElementById('update-progress-speed');
|
||||||
|
const progressSize = document.getElementById('update-progress-size');
|
||||||
|
|
||||||
|
if (progressBar && progress) {
|
||||||
|
const percent = Math.round(progress.percent || 0);
|
||||||
|
progressBar.style.width = `${percent}%`;
|
||||||
|
|
||||||
|
if (progressPercent) {
|
||||||
|
progressPercent.textContent = `${percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressSpeed && progress.bytesPerSecond) {
|
||||||
|
const speedMBps = (progress.bytesPerSecond / 1024 / 1024).toFixed(2);
|
||||||
|
progressSpeed.textContent = `${speedMBps} MB/s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressSize && progress.transferred && progress.total) {
|
||||||
|
const transferredMB = (progress.transferred / 1024 / 1024).toFixed(2);
|
||||||
|
const totalMB = (progress.total / 1024 / 1024).toFixed(2);
|
||||||
|
progressSize.textContent = `${transferredMB} MB / ${totalMB} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't update status text here - it's already set and the progress bar shows the percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showUpdateDownloaded(updateInfo) {
|
||||||
|
const statusText = document.getElementById('update-status-text');
|
||||||
|
const progressContainer = document.getElementById('update-progress-container');
|
||||||
|
const buttonsContainer = document.getElementById('update-buttons-container');
|
||||||
|
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = 'Update downloaded! Ready to install.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonsContainer) {
|
||||||
|
buttonsContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Update downloaded, ready to install');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdateError(errorInfo) {
|
||||||
|
console.error('Update error:', errorInfo);
|
||||||
|
|
||||||
|
// If manual download is required, update the UI (this will handle status text)
|
||||||
|
if (errorInfo.requiresManualDownload) {
|
||||||
|
this.showManualDownloadRequired(errorInfo);
|
||||||
|
return; // Don't do anything else, showManualDownloadRequired handles everything
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-critical errors, just show error message without changing status
|
||||||
|
const errorMessage = document.getElementById('update-error-message');
|
||||||
|
const errorText = document.getElementById('update-error-text');
|
||||||
|
|
||||||
|
if (errorMessage && errorText) {
|
||||||
|
let message = errorInfo.message || 'An error occurred during the update process.';
|
||||||
|
if (errorInfo.isMacSigningError) {
|
||||||
|
message = 'Auto-update requires code signing. Please download manually.';
|
||||||
|
}
|
||||||
|
errorText.textContent = message;
|
||||||
|
errorMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showManualDownloadRequired(errorInfo) {
|
||||||
|
const statusText = document.getElementById('update-status-text');
|
||||||
|
const progressContainer = document.getElementById('update-progress-container');
|
||||||
|
const buttonsContainer = document.getElementById('update-buttons-container');
|
||||||
|
const installBtn = document.getElementById('update-install-btn');
|
||||||
|
const downloadBtn = document.getElementById('update-download-btn');
|
||||||
|
const errorMessage = document.getElementById('update-error-message');
|
||||||
|
const errorText = document.getElementById('update-error-text');
|
||||||
|
|
||||||
|
// Hide progress and install button
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installBtn) {
|
||||||
|
installBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status message (only once, don't change it again)
|
||||||
|
if (statusText && !statusText.dataset.manualMode) {
|
||||||
|
statusText.textContent = 'Please download and install the update manually.';
|
||||||
|
statusText.dataset.manualMode = 'true'; // Mark that we've set manual mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message with details
|
||||||
|
if (errorMessage && errorText) {
|
||||||
|
let message = 'Auto-update is not available. ';
|
||||||
|
if (errorInfo.isMacSigningError) {
|
||||||
|
message = 'This app requires code signing for automatic updates.';
|
||||||
|
} else if (errorInfo.isLinuxInstallError) {
|
||||||
|
message = 'Auto-installation requires root privileges. Please download and install the update manually using your package manager.';
|
||||||
|
} else if (errorInfo.message) {
|
||||||
|
message = errorInfo.message;
|
||||||
|
} else {
|
||||||
|
message = 'An error occurred during the update process.';
|
||||||
|
}
|
||||||
|
errorText.textContent = message;
|
||||||
|
errorMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show and enable the manual download button (make it primary since it's the only option)
|
||||||
|
if (downloadBtn) {
|
||||||
|
downloadBtn.style.display = 'block';
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
downloadBtn.classList.remove('update-download-btn-secondary');
|
||||||
|
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update Manually';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show buttons container if not already visible
|
||||||
|
if (buttonsContainer) {
|
||||||
|
buttonsContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('⚠️ Manual download required due to update error');
|
||||||
|
}
|
||||||
|
|
||||||
blockInterface() {
|
blockInterface() {
|
||||||
const mainContent = document.querySelector('.flex.w-full.h-screen');
|
const mainContent = document.querySelector('.flex.w-full.h-screen');
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
@@ -144,7 +337,12 @@ class ClientUpdateManager {
|
|||||||
async checkForUpdatesOnDemand() {
|
async checkForUpdatesOnDemand() {
|
||||||
try {
|
try {
|
||||||
const updateInfo = await window.electronAPI.checkForUpdates();
|
const updateInfo = await window.electronAPI.checkForUpdates();
|
||||||
if (updateInfo.updateAvailable) {
|
|
||||||
|
// Double-check that versions are actually different before showing popup
|
||||||
|
if (updateInfo.updateAvailable &&
|
||||||
|
updateInfo.newVersion &&
|
||||||
|
updateInfo.currentVersion &&
|
||||||
|
updateInfo.newVersion !== updateInfo.currentVersion) {
|
||||||
this.showUpdatePopup(updateInfo);
|
this.showUpdatePopup(updateInfo);
|
||||||
}
|
}
|
||||||
return updateInfo;
|
return updateInfo;
|
||||||
|
|||||||
231
GUI/locales/en.json
Normal file
231
GUI/locales/en.json
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Play",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "News",
|
||||||
|
"chat": "Players Chat",
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Players:",
|
||||||
|
"manageProfiles": "Manage Profiles",
|
||||||
|
"defaultProfile": "Default"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "FREE TO PLAY LAUNCHER",
|
||||||
|
"playerName": "Player Name",
|
||||||
|
"playerNamePlaceholder": "Enter your name",
|
||||||
|
"customInstallation": "Custom Installation",
|
||||||
|
"installationFolder": "Installation Folder",
|
||||||
|
"pathPlaceholder": "Default location",
|
||||||
|
"browse": "Browse",
|
||||||
|
"installButton": "INSTALL HYTALE",
|
||||||
|
"installing": "INSTALLING..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "READY TO PLAY",
|
||||||
|
"subtitle": "Launch Hytale and enter the adventure",
|
||||||
|
"playButton": "PLAY HYTALE",
|
||||||
|
"latestNews": "LATEST NEWS",
|
||||||
|
"viewAll": "VIEW ALL",
|
||||||
|
"checking": "CHECKING...",
|
||||||
|
"play": "PLAY"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Search mods...",
|
||||||
|
"myMods": "MY MODS",
|
||||||
|
"previous": "PREVIOUS",
|
||||||
|
"next": "NEXT",
|
||||||
|
"page": "Page",
|
||||||
|
"of": "of",
|
||||||
|
"modalTitle": "MY MODS",
|
||||||
|
"noModsFound": "No Mods Found",
|
||||||
|
"noModsFoundDesc": "Try adjusting your search",
|
||||||
|
"noModsInstalled": "No Mods Installed",
|
||||||
|
"noModsInstalledDesc": "Add mods from CurseForge or import local files",
|
||||||
|
"view": "VIEW",
|
||||||
|
"install": "INSTALL",
|
||||||
|
"installed": "INSTALLED",
|
||||||
|
"enable": "ENABLE",
|
||||||
|
"disable": "DISABLE",
|
||||||
|
"active": "ACTIVE",
|
||||||
|
"disabled": "DISABLED",
|
||||||
|
"delete": "Delete mod",
|
||||||
|
"noDescription": "No description available",
|
||||||
|
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "This action cannot be undone.",
|
||||||
|
"confirmDeletion": "Confirm Deletion"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "ALL NEWS",
|
||||||
|
"readMore": "Read More"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "PLAYERS CHAT",
|
||||||
|
"pickColor": "Color",
|
||||||
|
"inputPlaceholder": "Type your message...",
|
||||||
|
"send": "Send",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Secure chat - Links are censored",
|
||||||
|
"joinChat": "Join Chat",
|
||||||
|
"chooseUsername": "Choose a username to join the Players Chat",
|
||||||
|
"username": "Username",
|
||||||
|
"usernamePlaceholder": "Enter your username...",
|
||||||
|
"usernameHint": "3-20 characters, letters, numbers, - and _ only",
|
||||||
|
"joinButton": "Join Chat",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Customize Username Color",
|
||||||
|
"chooseSolid": "Choose a solid color:",
|
||||||
|
"customColor": "Custom color:",
|
||||||
|
"preview": "Preview:",
|
||||||
|
"previewUsername": "Username",
|
||||||
|
"apply": "Apply Color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "SETTINGS",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Use Custom Java Path",
|
||||||
|
"javaDescription": "Override the bundled Java runtime with your own installation",
|
||||||
|
"javaPath": "Java Executable Path",
|
||||||
|
"javaPathPlaceholder": "Select Java path...",
|
||||||
|
"javaBrowse": "Browse",
|
||||||
|
"javaHint": "Select the Java installation folder (supports Windows, Mac, Linux)",
|
||||||
|
"discord": "Discord Integration",
|
||||||
|
"enableRPC": "Enable Discord Rich Presence",
|
||||||
|
"discordDescription": "Show your launcher activity on Discord",
|
||||||
|
"game": "Game Options",
|
||||||
|
"playerName": "Player Name",
|
||||||
|
"playerNamePlaceholder": "Enter your player name",
|
||||||
|
"playerNameHint": "This name will be used in-game (1-16 characters)",
|
||||||
|
"openGameLocation": "Open Game Location",
|
||||||
|
"openGameLocationDesc": "Open the game installation folder",
|
||||||
|
"account": "Player UUID Management",
|
||||||
|
"currentUUID": "Current UUID",
|
||||||
|
"uuidPlaceholder": "Loading UUID...",
|
||||||
|
"copyUUID": "Copy UUID",
|
||||||
|
"regenerateUUID": "Regenerate UUID",
|
||||||
|
"uuidHint": "Your unique player identifier for this username",
|
||||||
|
"manageUUIDs": "Manage All UUIDs",
|
||||||
|
"manageUUIDsDesc": "View and manage all player UUIDs",
|
||||||
|
"language": "Language",
|
||||||
|
"selectLanguage": "Select Language",
|
||||||
|
"repairGame": "Repair Game",
|
||||||
|
"reinstallGame": "Reinstall game files (preserves data)",
|
||||||
|
"gpuPreference": "GPU Preference",
|
||||||
|
"gpuHint": "Select your preferred GPU (Linux: affects DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Integrated",
|
||||||
|
"gpuDedicated": "Dedicated",
|
||||||
|
"logs": "SYSTEM LOGS",
|
||||||
|
"logsCopy": "Copy",
|
||||||
|
"logsRefresh": "Refresh",
|
||||||
|
"logsFolder": "Open Folder",
|
||||||
|
"logsLoading": "Loading logs...",
|
||||||
|
"closeLauncher": "Launcher Behavior",
|
||||||
|
"closeOnStart": "Close Launcher on game start",
|
||||||
|
"closeOnStartDescription": "Automatically close the launcher after Hytale has launched"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "UUID Management",
|
||||||
|
"currentUserUUID": "Current User UUID",
|
||||||
|
"allPlayerUUIDs": "All Player UUIDs",
|
||||||
|
"generateNew": "Generate New UUID",
|
||||||
|
"loadingUUIDs": "Loading UUIDs...",
|
||||||
|
"setCustomUUID": "Set Custom UUID",
|
||||||
|
"customPlaceholder": "Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Set UUID",
|
||||||
|
"warning": "Warning: Setting a custom UUID will change your current player identity",
|
||||||
|
"copyTooltip": "Copy UUID",
|
||||||
|
"regenerateTooltip": "Generate New UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Manage Profiles",
|
||||||
|
"newProfilePlaceholder": "New Profile Name",
|
||||||
|
"createProfile": "Create Profile"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Join our Discord community!",
|
||||||
|
"joinButton": "Join Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Save",
|
||||||
|
"close": "Close",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"apply": "Apply"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Error: Game data not found",
|
||||||
|
"gameUpdatedSuccess": "Game updated successfully! 🎉",
|
||||||
|
"updateFailed": "Update failed: {error}",
|
||||||
|
"updateError": "Update error: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence enabled",
|
||||||
|
"discordDisabled": "Discord Rich Presence disabled",
|
||||||
|
"discordSaveFailed": "Failed to save Discord setting",
|
||||||
|
"playerNameRequired": "Please enter a valid player name",
|
||||||
|
"playerNameSaved": "Player name saved successfully",
|
||||||
|
"playerNameSaveFailed": "Failed to save player name",
|
||||||
|
"uuidCopied": "UUID copied to clipboard!",
|
||||||
|
"uuidCopyFailed": "Failed to copy UUID",
|
||||||
|
"uuidRegenNotAvailable": "UUID regeneration not available",
|
||||||
|
"uuidRegenFailed": "Failed to regenerate UUID",
|
||||||
|
"uuidGenerated": "New UUID generated successfully!",
|
||||||
|
"uuidGeneratedShort": "New UUID generated!",
|
||||||
|
"uuidGenerateFailed": "Failed to generate new UUID",
|
||||||
|
"uuidRequired": "Please enter a UUID",
|
||||||
|
"uuidInvalidFormat": "Invalid UUID format",
|
||||||
|
"uuidSetFailed": "Failed to set custom UUID",
|
||||||
|
"uuidSetSuccess": "Custom UUID set successfully!",
|
||||||
|
"uuidDeleteFailed": "Failed to delete UUID",
|
||||||
|
"uuidDeleteSuccess": "UUID deleted successfully!",
|
||||||
|
"modsDownloading": "Downloading {name}...",
|
||||||
|
"modsTogglingMod": "Toggling mod...",
|
||||||
|
"modsDeletingMod": "Deleting mod...",
|
||||||
|
"modsLoadingMods": "Loading mods from CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} installed successfully! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} deleted successfully",
|
||||||
|
"modsDownloadFailed": "Failed to download mod: {error}",
|
||||||
|
"modsToggleFailed": "Failed to toggle mod: {error}",
|
||||||
|
"modsDeleteFailed": "Failed to delete mod: {error}",
|
||||||
|
"modsModNotFound": "Mod information not found"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Confirm action",
|
||||||
|
"regenerateUuidTitle": "Generate new UUID",
|
||||||
|
"regenerateUuidMessage": "Are you sure you want to generate a new UUID? This will change your player identity.",
|
||||||
|
"regenerateUuidButton": "Generate",
|
||||||
|
"setCustomUuidTitle": "Set custom UUID",
|
||||||
|
"setCustomUuidMessage": "Are you sure you want to set this custom UUID? This will change your player identity.",
|
||||||
|
"setCustomUuidButton": "Set UUID",
|
||||||
|
"deleteUuidTitle": "Delete UUID",
|
||||||
|
"deleteUuidMessage": "Are you sure you want to delete the UUID for \"{username}\"? This action cannot be undone.",
|
||||||
|
"deleteUuidButton": "Delete",
|
||||||
|
"uninstallGameTitle": "Uninstall game",
|
||||||
|
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
|
||||||
|
"uninstallGameButton": "Uninstall"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Initializing...",
|
||||||
|
"downloading": "Downloading...",
|
||||||
|
"installing": "Installing...",
|
||||||
|
"extracting": "Extracting...",
|
||||||
|
"verifying": "Verifying...",
|
||||||
|
"switchingProfile": "Switching profile...",
|
||||||
|
"profileSwitched": "Profile switched!",
|
||||||
|
"startingGame": "Starting game...",
|
||||||
|
"launching": "LAUNCHING...",
|
||||||
|
"uninstallingGame": "Uninstalling game...",
|
||||||
|
"gameUninstalled": "Game uninstalled successfully!",
|
||||||
|
"uninstallFailed": "Uninstall failed: {error}",
|
||||||
|
"startingUpdate": "Starting mandatory game update...",
|
||||||
|
"installationComplete": "Installation completed successfully!",
|
||||||
|
"installationFailed": "Installation failed: {error}",
|
||||||
|
"installingGameFiles": "Installing game files...",
|
||||||
|
"installComplete": "Installation complete!"
|
||||||
|
}
|
||||||
|
}
|
||||||
231
GUI/locales/es.json
Normal file
231
GUI/locales/es.json
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Jugar",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "Noticias",
|
||||||
|
"chat": "Chat de Jugadores",
|
||||||
|
"settings": "Configuración"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Jugadores:",
|
||||||
|
"manageProfiles": "Gestionar Perfiles",
|
||||||
|
"defaultProfile": "Predeterminado"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "LAUNCHER GRATUITO",
|
||||||
|
"playerName": "Nombre del Jugador",
|
||||||
|
"playerNamePlaceholder": "Ingresa tu nombre",
|
||||||
|
"customInstallation": "Instalación Personalizada",
|
||||||
|
"installationFolder": "Carpeta de Instalación",
|
||||||
|
"pathPlaceholder": "Ubicación predeterminada",
|
||||||
|
"browse": "Examinar",
|
||||||
|
"installButton": "INSTALAR HYTALE",
|
||||||
|
"installing": "INSTALANDO..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "LISTO PARA JUGAR",
|
||||||
|
"subtitle": "Inicia Hytale y entra en la aventura",
|
||||||
|
"playButton": "JUGAR HYTALE",
|
||||||
|
"latestNews": "ÚLTIMAS NOTICIAS",
|
||||||
|
"viewAll": "VER TODO",
|
||||||
|
"checking": "VERIFICANDO...",
|
||||||
|
"play": "JUGAR"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Buscar mods...",
|
||||||
|
"myMods": "MIS MODS",
|
||||||
|
"previous": "ANTERIOR",
|
||||||
|
"next": "SIGUIENTE",
|
||||||
|
"page": "Página",
|
||||||
|
"of": "de",
|
||||||
|
"modalTitle": "MIS MODS",
|
||||||
|
"noModsFound": "No se encontraron mods",
|
||||||
|
"noModsFoundDesc": "Intenta ajustar tu búsqueda",
|
||||||
|
"noModsInstalled": "No hay mods instalados",
|
||||||
|
"noModsInstalledDesc": "Añade mods desde CurseForge o importa archivos locales",
|
||||||
|
"view": "VER",
|
||||||
|
"install": "INSTALAR",
|
||||||
|
"installed": "INSTALADO",
|
||||||
|
"enable": "ACTIVAR",
|
||||||
|
"disable": "DESACTIVAR",
|
||||||
|
"active": "ACTIVO",
|
||||||
|
"disabled": "DESACTIVADO",
|
||||||
|
"delete": "Eliminar mod",
|
||||||
|
"noDescription": "Sin descripción disponible",
|
||||||
|
"confirmDelete": "¿Estás seguro de que quieres eliminar \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
|
||||||
|
"confirmDeletion": "Confirmar eliminación"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "TODAS LAS NOTICIAS",
|
||||||
|
"readMore": "Leer más"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "CHAT DE JUGADORES",
|
||||||
|
"pickColor": "Color",
|
||||||
|
"inputPlaceholder": "Escribe tu mensaje...",
|
||||||
|
"send": "Enviar",
|
||||||
|
"online": "en línea",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Chat seguro - Los enlaces están censurados",
|
||||||
|
"joinChat": "Unirse al chat",
|
||||||
|
"chooseUsername": "Elige un nombre de usuario para unirte al chat de jugadores",
|
||||||
|
"username": "Nombre de usuario",
|
||||||
|
"usernamePlaceholder": "Ingresa tu nombre de usuario...",
|
||||||
|
"usernameHint": "3-20 caracteres, letras, números, - y _ solamente",
|
||||||
|
"joinButton": "Unirse al Chat",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Personalizar color del nombre",
|
||||||
|
"chooseSolid": "Elige un color sólido:",
|
||||||
|
"customColor": "Color personalizado:",
|
||||||
|
"preview": "Vista previa:",
|
||||||
|
"previewUsername": "Nombre de usuario",
|
||||||
|
"apply": "Aplicar color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "CONFIGURACIÓN",
|
||||||
|
"java": "Entorno Java",
|
||||||
|
"useCustomJava": "Usar ruta de Java personalizada",
|
||||||
|
"javaDescription": "Reemplaza el entorno Java incluido con tu propia instalación",
|
||||||
|
"javaPath": "Ruta del ejecutable Java",
|
||||||
|
"javaPathPlaceholder": "Selecciona la ruta de Java...",
|
||||||
|
"javaBrowse": "Examinar",
|
||||||
|
"javaHint": "Selecciona la carpeta de instalación de Java (compatible con Windows, Mac, Linux)",
|
||||||
|
"discord": "Integración con Discord",
|
||||||
|
"enableRPC": "Habilitar Discord Rich Presence",
|
||||||
|
"discordDescription": "Muestra tu actividad del launcher en Discord",
|
||||||
|
"game": "Opciones del juego",
|
||||||
|
"playerName": "Nombre del jugador",
|
||||||
|
"playerNamePlaceholder": "Ingresa tu nombre de jugador",
|
||||||
|
"playerNameHint": "Este nombre se usará en el juego (1-16 caracteres)",
|
||||||
|
"openGameLocation": "Abrir ubicación del juego",
|
||||||
|
"openGameLocationDesc": "Abre la carpeta de instalación del juego",
|
||||||
|
"account": "Gestión de UUID del jugador",
|
||||||
|
"currentUUID": "UUID actual",
|
||||||
|
"uuidPlaceholder": "Cargando UUID...",
|
||||||
|
"copyUUID": "Copiar UUID",
|
||||||
|
"regenerateUUID": "Regenerar UUID",
|
||||||
|
"uuidHint": "Tu identificador único de jugador para este nombre de usuario",
|
||||||
|
"manageUUIDs": "Gestionar todos los UUIDs",
|
||||||
|
"manageUUIDsDesc": "Ver y gestionar todos los UUIDs de jugadores",
|
||||||
|
"language": "Idioma",
|
||||||
|
"selectLanguage": "Seleccionar idioma",
|
||||||
|
"repairGame": "Reparar juego",
|
||||||
|
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
|
||||||
|
"gpuPreference": "Preferencia de GPU",
|
||||||
|
"gpuHint": "Selecciona tu GPU preferida (Linux: afecta DRI_PRIME)",
|
||||||
|
"gpuAuto": "Automático",
|
||||||
|
"gpuIntegrated": "Integrada",
|
||||||
|
"gpuDedicated": "Dedicada",
|
||||||
|
"logs": "REGISTROS DEL SISTEMA",
|
||||||
|
"logsCopy": "Copiar",
|
||||||
|
"logsRefresh": "Actualizar",
|
||||||
|
"logsFolder": "Abrir Carpeta",
|
||||||
|
"logsLoading": "Cargando registros...",
|
||||||
|
"closeLauncher": "Comportamiento del Launcher",
|
||||||
|
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
||||||
|
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Gestión de UUID",
|
||||||
|
"currentUserUUID": "UUID del usuario actual",
|
||||||
|
"allPlayerUUIDs": "Todos los UUIDs de jugadores",
|
||||||
|
"generateNew": "Generar nuevo UUID",
|
||||||
|
"loadingUUIDs": "Cargando UUIDs...",
|
||||||
|
"setCustomUUID": "Establecer UUID personalizado",
|
||||||
|
"customPlaceholder": "Ingresa un UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Establecer UUID",
|
||||||
|
"warning": "Advertencia: Establecer un UUID personalizado cambiará tu identidad de jugador actual",
|
||||||
|
"copyTooltip": "Copiar UUID",
|
||||||
|
"regenerateTooltip": "Generar nuevo UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Gestionar perfiles",
|
||||||
|
"newProfilePlaceholder": "Nombre del nuevo perfil",
|
||||||
|
"createProfile": "Crear perfil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "¡Únete a nuestra comunidad de Discord!",
|
||||||
|
"joinButton": "Unirse a Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"apply": "Aplicar"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Error: No se encontraron datos del juego",
|
||||||
|
"gameUpdatedSuccess": "¡Juego actualizado con éxito! 🎉",
|
||||||
|
"updateFailed": "Actualización fallida: {error}",
|
||||||
|
"updateError": "Error de actualización: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence habilitado",
|
||||||
|
"discordDisabled": "Discord Rich Presence deshabilitado",
|
||||||
|
"discordSaveFailed": "Error al guardar la configuración de Discord",
|
||||||
|
"playerNameRequired": "Por favor ingresa un nombre de jugador válido",
|
||||||
|
"playerNameSaved": "Nombre de jugador guardado con éxito",
|
||||||
|
"playerNameSaveFailed": "Error al guardar el nombre de jugador",
|
||||||
|
"uuidCopied": "¡UUID copiado al portapapeles!",
|
||||||
|
"uuidCopyFailed": "Error al copiar UUID",
|
||||||
|
"uuidRegenNotAvailable": "Regeneración de UUID no disponible",
|
||||||
|
"uuidRegenFailed": "Error al regenerar UUID",
|
||||||
|
"uuidGenerated": "¡Nuevo UUID generado con éxito!",
|
||||||
|
"uuidGeneratedShort": "¡Nuevo UUID generado!",
|
||||||
|
"uuidGenerateFailed": "Error al generar nuevo UUID",
|
||||||
|
"uuidRequired": "Por favor ingresa un UUID",
|
||||||
|
"uuidInvalidFormat": "Formato de UUID inválido",
|
||||||
|
"uuidSetFailed": "Error al establecer UUID personalizado",
|
||||||
|
"uuidSetSuccess": "¡UUID personalizado establecido con éxito!",
|
||||||
|
"uuidDeleteFailed": "Error al eliminar UUID",
|
||||||
|
"uuidDeleteSuccess": "¡UUID eliminado con éxito!",
|
||||||
|
"modsDownloading": "Descargando {name}...",
|
||||||
|
"modsTogglingMod": "Alternando mod...",
|
||||||
|
"modsDeletingMod": "Eliminando mod...",
|
||||||
|
"modsLoadingMods": "Cargando mods desde CurseForge...",
|
||||||
|
"modsInstalledSuccess": "¡{name} instalado con éxito! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} eliminado con éxito",
|
||||||
|
"modsDownloadFailed": "Error al descargar mod: {error}",
|
||||||
|
"modsToggleFailed": "Error al alternar mod: {error}",
|
||||||
|
"modsDeleteFailed": "Error al eliminar mod: {error}",
|
||||||
|
"modsModNotFound": "Información del mod no encontrada"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Confirmar acción",
|
||||||
|
"regenerateUuidTitle": "Generar nuevo UUID",
|
||||||
|
"regenerateUuidMessage": "¿Estás seguro de que quieres generar un nuevo UUID? Esto cambiará tu identidad de jugador.",
|
||||||
|
"regenerateUuidButton": "Generar",
|
||||||
|
"setCustomUuidTitle": "Establecer UUID personalizado",
|
||||||
|
"setCustomUuidMessage": "¿Estás seguro de que quieres establecer este UUID personalizado? Esto cambiará tu identidad de jugador.",
|
||||||
|
"setCustomUuidButton": "Establecer UUID",
|
||||||
|
"deleteUuidTitle": "Eliminar UUID",
|
||||||
|
"deleteUuidMessage": "¿Estás seguro de que quieres eliminar el UUID de \"{username}\"? Esta acción no se puede deshacer.",
|
||||||
|
"deleteUuidButton": "Eliminar",
|
||||||
|
"uninstallGameTitle": "Desinstalar juego",
|
||||||
|
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
|
||||||
|
"uninstallGameButton": "Desinstalar"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Inicializando...",
|
||||||
|
"downloading": "Descargando...",
|
||||||
|
"installing": "Instalando...",
|
||||||
|
"extracting": "Extrayendo...",
|
||||||
|
"verifying": "Verificando...",
|
||||||
|
"switchingProfile": "Cambiando perfil...",
|
||||||
|
"profileSwitched": "¡Perfil cambiado!",
|
||||||
|
"startingGame": "Iniciando juego...",
|
||||||
|
"launching": "INICIANDO...",
|
||||||
|
"uninstallingGame": "Desinstalando juego...",
|
||||||
|
"gameUninstalled": "¡Juego desinstalado con éxito!",
|
||||||
|
"uninstallFailed": "Desinstalación fallida: {error}",
|
||||||
|
"startingUpdate": "Iniciando actualización obligatoria del juego...",
|
||||||
|
"installationComplete": "¡Instalación completada con éxito!",
|
||||||
|
"installationFailed": "Instalación fallida: {error}",
|
||||||
|
"installingGameFiles": "Instalando archivos del juego...",
|
||||||
|
"installComplete": "¡Instalación completa!"
|
||||||
|
}
|
||||||
|
}
|
||||||
232
GUI/locales/pt-BR.json
Normal file
232
GUI/locales/pt-BR.json
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Jogar",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "Notícias",
|
||||||
|
"chat": "Chat de Jogadores",
|
||||||
|
"settings": "Configurações"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Jogadores:",
|
||||||
|
"manageProfiles": "Gerenciar Perfis",
|
||||||
|
"defaultProfile": "Padrão"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "LANÇADOR JOGO GRATUITO",
|
||||||
|
"playerName": "Nome do Jogador",
|
||||||
|
"playerNamePlaceholder": "Digite seu nome",
|
||||||
|
"customInstallation": "Instalação Personalizada",
|
||||||
|
"installationFolder": "Pasta de Instalação",
|
||||||
|
"pathPlaceholder": "Local padrão",
|
||||||
|
"browse": "Procurar",
|
||||||
|
"installButton": "INSTALAR HYTALE",
|
||||||
|
"installing": "INSTALANDO..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "PRONTO PARA JOGAR",
|
||||||
|
"subtitle": "Inicie Hytale e entre na aventura",
|
||||||
|
"playButton": "JOGAR HYTALE",
|
||||||
|
"latestNews": "ÚLTIMAS NOTÍCIAS",
|
||||||
|
"viewAll": "VER TUDO",
|
||||||
|
"checking": "VERIFICANDO...",
|
||||||
|
"play": "JOGAR"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Pesquisar mods...",
|
||||||
|
"myMods": "MEUS MODS",
|
||||||
|
"previous": "ANTERIOR",
|
||||||
|
"next": "PRÓXIMO",
|
||||||
|
"page": "Página",
|
||||||
|
"of": "de",
|
||||||
|
"modalTitle": "MEUS MODS",
|
||||||
|
"noModsFound": "Nenhum mod encontrado",
|
||||||
|
"noModsFoundDesc": "Tente ajustar sua pesquisa",
|
||||||
|
"noModsInstalled": "Nenhum mod instalado",
|
||||||
|
"noModsInstalledDesc": "Adicione mods do CurseForge ou importe arquivos locais",
|
||||||
|
"view": "VER",
|
||||||
|
"install": "INSTALAR",
|
||||||
|
"installed": "INSTALADO",
|
||||||
|
"enable": "ATIVAR",
|
||||||
|
"disable": "DESATIVAR",
|
||||||
|
"active": "ATIVO",
|
||||||
|
"disabled": "DESATIVADO",
|
||||||
|
"delete": "Excluir mod",
|
||||||
|
"noDescription": "Nenhuma descrição disponível",
|
||||||
|
"confirmDelete": "Tem certeza de que deseja excluir \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
|
||||||
|
"confirmDeletion": "Confirmar exclusão"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "TODAS AS NOTÍCIAS",
|
||||||
|
"readMore": "Leia mais"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "CHAT DE JOGADORES",
|
||||||
|
"pickColor": "Cor",
|
||||||
|
"inputPlaceholder": "Digite sua mensagem...",
|
||||||
|
"send": "Enviar",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Chat seguro - Links são censurados",
|
||||||
|
"joinChat": "Entrar no chat",
|
||||||
|
"chooseUsername": "Escolha um nome de usuário para entrar no chat de jogadores",
|
||||||
|
"username": "Nome de usuário",
|
||||||
|
"usernamePlaceholder": "Digite seu nome de usuário...",
|
||||||
|
"usernameHint": "3-20 caracteres, letras, números, - e _ apenas",
|
||||||
|
"joinButton": "Entrar no Chat",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Personalizar cor do nome de usuário",
|
||||||
|
"chooseSolid": "Escolha uma cor sólida:",
|
||||||
|
"customColor": "Cor personalizada:",
|
||||||
|
"preview": "Visualização:",
|
||||||
|
"previewUsername": "Nome de usuário",
|
||||||
|
"apply": "Aplicar cor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "CONFIGURAÇÕES",
|
||||||
|
"java": "Tempo de execução Java",
|
||||||
|
"useCustomJava": "Usar caminho personalizado do Java",
|
||||||
|
"javaDescription": "Substitua o tempo de execução Java incluído pela sua própria instalação",
|
||||||
|
"javaPath": "Caminho do executável Java",
|
||||||
|
"javaPathPlaceholder": "Selecione o caminho do Java...",
|
||||||
|
"javaBrowse": "Procurar",
|
||||||
|
"javaHint": "Selecione a pasta de instalação do Java (suporta Windows, Mac, Linux)",
|
||||||
|
"discord": "Integração do Discord",
|
||||||
|
"enableRPC": "Ativar Discord Rich Presence",
|
||||||
|
"discordDescription": "Mostre sua atividade do lançador no Discord",
|
||||||
|
"game": "Opções do jogo",
|
||||||
|
"playerName": "Nome do jogador",
|
||||||
|
"playerNamePlaceholder": "Digite seu nome de jogador",
|
||||||
|
"playerNameHint": "Este nome será usado no jogo (1-16 caracteres)",
|
||||||
|
"openGameLocation": "Abrir local do jogo",
|
||||||
|
"openGameLocationDesc": "Abra a pasta de instalação do jogo",
|
||||||
|
"account": "Gerenciamento de UUID do jogador",
|
||||||
|
"currentUUID": "UUID atual",
|
||||||
|
"uuidPlaceholder": "Carregando UUID...",
|
||||||
|
"copyUUID": "Copiar UUID",
|
||||||
|
"regenerateUUID": "Regenerar UUID",
|
||||||
|
"uuidHint": "Seu identificador único de jogador para este nome de usuário",
|
||||||
|
"manageUUIDs": "Gerenciar todos os UUIDs",
|
||||||
|
"manageUUIDsDesc": "Ver e gerenciar todos os UUIDs de jogadores",
|
||||||
|
"language": "Idioma",
|
||||||
|
"selectLanguage": "Selecionar idioma",
|
||||||
|
"repairGame": "Reparar jogo",
|
||||||
|
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
|
||||||
|
"gpuPreference": "Preferência de GPU",
|
||||||
|
"gpuHint": "Selecione sua GPU preferida (Linux: afeta o DRI_PRIME)",
|
||||||
|
"gpuAuto": "Automático",
|
||||||
|
"gpuIntegrated": "Integrada",
|
||||||
|
"gpuDedicated": "Dedicada",
|
||||||
|
"logs": "REGISTROS DO SISTEMA",
|
||||||
|
"logsCopy": "Copiar",
|
||||||
|
"logsRefresh": "Atualizar",
|
||||||
|
"logsFolder": "Abrir Pasta",
|
||||||
|
"logsLoading": "Carregando registros...",
|
||||||
|
"closeLauncher": "Comportamento do Lançador",
|
||||||
|
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
||||||
|
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Gerenciamento de UUID",
|
||||||
|
"currentUserUUID": "UUID do usuário atual",
|
||||||
|
"allPlayerUUIDs": "Todos os UUIDs de jogadores",
|
||||||
|
"generateNew": "Gerar novo UUID",
|
||||||
|
"loadingUUIDs": "Carregando UUIDs...",
|
||||||
|
"setCustomUUID": "Definir UUID personalizado",
|
||||||
|
"customPlaceholder": "Digite um UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Definir UUID",
|
||||||
|
"warning": "Aviso: Definir um UUID personalizado alterará sua identidade de jogador atual",
|
||||||
|
"copyTooltip": "Copiar UUID",
|
||||||
|
"regenerateTooltip": "Gerar novo UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Gerenciar perfis",
|
||||||
|
"newProfilePlaceholder": "Nome do novo perfil",
|
||||||
|
"createProfile": "Criar perfil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Junte-se à nossa comunidade do Discord!",
|
||||||
|
"joinButton": "Entrar no Discord"
|
||||||
|
},
|
||||||
|
|
||||||
|
"common": {
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"save": "Salvar",
|
||||||
|
"close": "Fechar",
|
||||||
|
"delete": "Excluir",
|
||||||
|
"edit": "Editar",
|
||||||
|
"loading": "Carregando...",
|
||||||
|
"apply": "Aplicar"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Erro: Dados do jogo não encontrados",
|
||||||
|
"gameUpdatedSuccess": "Jogo atualizado com sucesso! 🎉",
|
||||||
|
"updateFailed": "Falha na atualização: {error}",
|
||||||
|
"updateError": "Erro de atualização: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence ativado",
|
||||||
|
"discordDisabled": "Discord Rich Presence desativado",
|
||||||
|
"discordSaveFailed": "Falha ao salvar configuração do Discord",
|
||||||
|
"playerNameRequired": "Por favor, digite um nome de jogador válido",
|
||||||
|
"playerNameSaved": "Nome do jogador salvo com sucesso",
|
||||||
|
"playerNameSaveFailed": "Falha ao salvar o nome do jogador",
|
||||||
|
"uuidCopied": "UUID copiado para a área de transferência!",
|
||||||
|
"uuidCopyFailed": "Falha ao copiar UUID",
|
||||||
|
"uuidRegenNotAvailable": "Regeneração de UUID não disponível",
|
||||||
|
"uuidRegenFailed": "Falha ao regenerar UUID",
|
||||||
|
"uuidGenerated": "Novo UUID gerado com sucesso!",
|
||||||
|
"uuidGeneratedShort": "Novo UUID gerado!",
|
||||||
|
"uuidGenerateFailed": "Falha ao gerar novo UUID",
|
||||||
|
"uuidRequired": "Por favor, digite um UUID",
|
||||||
|
"uuidInvalidFormat": "Formato de UUID inválido",
|
||||||
|
"uuidSetFailed": "Falha ao definir UUID personalizado",
|
||||||
|
"uuidSetSuccess": "UUID personalizado definido com sucesso!",
|
||||||
|
"uuidDeleteFailed": "Falha ao excluir UUID",
|
||||||
|
"uuidDeleteSuccess": "UUID excluído com sucesso!",
|
||||||
|
"modsDownloading": "Baixando {name}...",
|
||||||
|
"modsTogglingMod": "Alternando mod...",
|
||||||
|
"modsDeletingMod": "Excluindo mod...",
|
||||||
|
"modsLoadingMods": "Carregando mods do CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} instalado com sucesso! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} excluído com sucesso",
|
||||||
|
"modsDownloadFailed": "Falha ao baixar mod: {error}",
|
||||||
|
"modsToggleFailed": "Falha ao alternar mod: {error}",
|
||||||
|
"modsDeleteFailed": "Falha ao excluir mod: {error}",
|
||||||
|
"modsModNotFound": "Informações do mod não encontradas"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Confirmar ação",
|
||||||
|
"regenerateUuidTitle": "Gerar novo UUID",
|
||||||
|
"regenerateUuidMessage": "Tem certeza de que deseja gerar um novo UUID? Isso alterará sua identidade de jogador.",
|
||||||
|
"regenerateUuidButton": "Gerar",
|
||||||
|
"setCustomUuidTitle": "Definir UUID personalizado",
|
||||||
|
"setCustomUuidMessage": "Tem certeza de que deseja definir este UUID personalizado? Isso alterará sua identidade de jogador.",
|
||||||
|
"setCustomUuidButton": "Definir UUID",
|
||||||
|
"deleteUuidTitle": "Excluir UUID",
|
||||||
|
"deleteUuidMessage": "Tem certeza de que deseja excluir o UUID de \"{username}\"? Esta ação não pode ser desfeita.",
|
||||||
|
"deleteUuidButton": "Excluir",
|
||||||
|
"uninstallGameTitle": "Desinstalar jogo",
|
||||||
|
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
|
||||||
|
"uninstallGameButton": "Desinstalar"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Inicializando...",
|
||||||
|
"downloading": "Baixando...",
|
||||||
|
"installing": "Instalando...",
|
||||||
|
"extracting": "Extraindo...",
|
||||||
|
"verifying": "Verificando...",
|
||||||
|
"switchingProfile": "Alternando perfil...",
|
||||||
|
"profileSwitched": "Perfil alternado!",
|
||||||
|
"startingGame": "Iniciando jogo...",
|
||||||
|
"launching": "INICIANDO...",
|
||||||
|
"uninstallingGame": "Desinstalando jogo...",
|
||||||
|
"gameUninstalled": "Jogo desinstalado com sucesso!",
|
||||||
|
"uninstallFailed": "Falha na desinstalação: {error}",
|
||||||
|
"startingUpdate": "Iniciando atualização obrigatória do jogo...",
|
||||||
|
"installationComplete": "Instalação concluída com sucesso!",
|
||||||
|
"installationFailed": "Falha na instalação: {error}",
|
||||||
|
"installingGameFiles": "Instalando arquivos do jogo...",
|
||||||
|
"installComplete": "Instalação concluída!"
|
||||||
|
}
|
||||||
|
}
|
||||||
178
GUI/splash.html
Normal file
178
GUI/splash.html
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hytale F2P</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
text-align: center;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(0 0 30px rgba(147, 51, 234, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-accent {
|
||||||
|
background: linear-gradient(135deg, #9333ea, #a855f7, #c084fc);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 200px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(147, 51, 234, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #9333ea, #a855f7, #c084fc);
|
||||||
|
animation: loading 1.5s ease-in-out infinite;
|
||||||
|
box-shadow: 0 0 20px rgba(147, 51, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
animation: blink 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="background">
|
||||||
|
<img src="https://assets.authbp.xyz/bg.png" alt="Background">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="splash-container">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="./icon.png" alt="Hytale Logo">
|
||||||
|
</div>
|
||||||
|
<h1 class="title">
|
||||||
|
HY<span class="title-accent">TALE</span>
|
||||||
|
</h1>
|
||||||
|
<p class="subtitle">FREE TO PLAY LAUNCHER</p>
|
||||||
|
<div class="loader"></div>
|
||||||
|
<p class="loading-text">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
289
GUI/style.css
289
GUI/style.css
@@ -26,7 +26,7 @@ body {
|
|||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 20;
|
z-index: 45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
@@ -109,6 +109,12 @@ body {
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Allow logs navigation during installation */
|
||||||
|
.logs-nav-item {
|
||||||
|
z-index: 100;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-tooltip {
|
.nav-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
left: 100%;
|
||||||
@@ -210,6 +216,63 @@ body {
|
|||||||
border-color: rgba(147, 51, 234, 0.3);
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-display i {
|
||||||
|
color: #9333ea;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-display:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-display-bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 3rem;
|
||||||
|
right: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
z-index: 45;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-display-bottom i {
|
||||||
|
color: #9333ea;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-display-bottom:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -374,10 +437,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.control-btn {
|
.control-btn {
|
||||||
width: 20px;
|
width: 28px;
|
||||||
height: 20px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 6px;
|
||||||
border: none;
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
@@ -386,24 +449,36 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100000 !important;
|
z-index: 100000 !important;
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn i {
|
.control-btn i {
|
||||||
font-size: 0.5rem;
|
font-size: 0.75rem;
|
||||||
opacity: 0;
|
opacity: 0.7;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn:hover i {
|
.control-btn:hover i {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maximize {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.maximize:hover {
|
||||||
|
background: rgba(34, 197, 94, 0.4);
|
||||||
|
border-color: rgba(34, 197, 94, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.minimize {
|
.minimize {
|
||||||
background: rgba(251, 191, 36, 0.2);
|
background: rgba(251, 191, 36, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.minimize:hover {
|
.minimize:hover {
|
||||||
background: #fbbf24;
|
background: rgba(251, 191, 36, 0.4);
|
||||||
|
border-color: rgba(251, 191, 36, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
@@ -411,7 +486,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.close:hover {
|
.close:hover {
|
||||||
background: #ef4444;
|
background: rgba(239, 68, 68, 0.4);
|
||||||
|
border-color: rgba(239, 68, 68, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -429,7 +505,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title-accent {
|
.title-accent {
|
||||||
color: #9333ea;
|
color: #bf84f7;
|
||||||
text-shadow: 0 0 20px rgba(147, 51, 234, 0.5);
|
text-shadow: 0 0 20px rgba(147, 51, 234, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,15 +1004,22 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
.news-grid-horizontal {
|
.news-grid-horizontal {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
grid-auto-rows: minmax(200px, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow-x: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(147, 51, 234, 0.3) transparent;
|
scrollbar-color: rgba(147, 51, 234, 0.3) transparent;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-grid-horizontal::-webkit-scrollbar {
|
.news-grid-horizontal::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -954,9 +1037,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.news-grid-horizontal .news-item {
|
.news-grid-horizontal .news-item {
|
||||||
min-width: 300px;
|
width: 100%;
|
||||||
max-width: 300px;
|
min-width: 0;
|
||||||
height: 200px;
|
max-width: none;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -997,6 +1082,12 @@ body {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style spécifique pour LATEST NEWS (Play tab) */
|
||||||
|
.news-grid-horizontal .news-card {
|
||||||
|
aspect-ratio: unset;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.news-card:hover {
|
.news-card:hover {
|
||||||
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
|
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
|
||||||
border-color: rgba(147, 51, 234, 0.3);
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
@@ -1500,44 +1591,55 @@ body {
|
|||||||
|
|
||||||
.progress-overlay {
|
.progress-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1rem;
|
bottom: 1.5rem;
|
||||||
left: 1rem;
|
left: 50%;
|
||||||
right: 1rem;
|
transform: translateX(-50%);
|
||||||
background: rgba(0, 0, 0, 0.85);
|
width: 400px;
|
||||||
backdrop-filter: blur(30px);
|
background: rgba(15, 23, 42, 0.95);
|
||||||
border: 2px solid rgba(147, 51, 234, 0.3);
|
backdrop-filter: blur(20px);
|
||||||
border-radius: 16px;
|
border: 1px solid rgba(147, 51, 234, 0.3);
|
||||||
padding: 2rem;
|
border-radius: 12px;
|
||||||
z-index: 50;
|
padding: 1.25rem;
|
||||||
|
z-index: 60;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px rgba(0, 0, 0, 0.6),
|
0 4px 16px rgba(0, 0, 0, 0.5),
|
||||||
0 0 40px rgba(147, 51, 234, 0.1),
|
0 0 30px rgba(147, 51, 234, 0.15),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
animation: progressGlow 3s ease-in-out infinite alternate;
|
animation: progressGlow 3s ease-in-out infinite alternate;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-overlay.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgba(0, 0, 0, 0.7),
|
||||||
|
0 0 50px rgba(147, 51, 234, 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progressGlow {
|
@keyframes progressGlow {
|
||||||
0% {
|
0% {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px rgba(0, 0, 0, 0.6),
|
0 4px 16px rgba(0, 0, 0, 0.5),
|
||||||
0 0 40px rgba(147, 51, 234, 0.1),
|
0 0 30px rgba(147, 51, 234, 0.15),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
border-color: rgba(147, 51, 234, 0.3);
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px rgba(0, 0, 0, 0.6),
|
0 4px 16px rgba(0, 0, 0, 0.5),
|
||||||
0 0 60px rgba(147, 51, 234, 0.3),
|
0 0 40px rgba(147, 51, 234, 0.25),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
border-color: rgba(147, 51, 234, 0.5);
|
border-color: rgba(147, 51, 234, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-content {
|
.progress-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-info {
|
.progress-info {
|
||||||
@@ -1548,7 +1650,7 @@ body {
|
|||||||
|
|
||||||
.progress-info span {
|
.progress-info span {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progressText {
|
#progressText {
|
||||||
@@ -1572,8 +1674,8 @@ body {
|
|||||||
#progressPercent {
|
#progressPercent {
|
||||||
color: #9333ea;
|
color: #9333ea;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 2rem;
|
font-size: 1.25rem;
|
||||||
text-shadow: 0 0 20px rgba(147, 51, 234, 0.8);
|
text-shadow: 0 0 15px rgba(147, 51, 234, 0.6);
|
||||||
animation: percentGlow 1.5s ease-in-out infinite;
|
animation: percentGlow 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1592,15 +1694,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
height: 16px;
|
height: 10px;
|
||||||
background: linear-gradient(90deg, #1f2937, #374151);
|
background: linear-gradient(90deg, #1f2937, #374151);
|
||||||
border: 2px solid rgba(147, 51, 234, 0.2);
|
border: 1px solid rgba(147, 51, 234, 0.2);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 2px 4px rgba(0, 0, 0, 0.5),
|
inset 0 2px 4px rgba(0, 0, 0, 0.5),
|
||||||
0 0 20px rgba(147, 51, 234, 0.1);
|
0 0 15px rgba(147, 51, 234, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container::before {
|
.progress-bar-container::before {
|
||||||
@@ -1636,15 +1738,15 @@ body {
|
|||||||
#06b6d4 75%,
|
#06b6d4 75%,
|
||||||
#10b981 100%);
|
#10b981 100%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
border-radius: 10px;
|
border-radius: 6px;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: progressFlow 3s linear infinite;
|
animation: progressFlow 3s linear infinite;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 30px rgba(147, 51, 234, 0.6),
|
0 0 20px rgba(147, 51, 234, 0.5),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progressFlow {
|
@keyframes progressFlow {
|
||||||
@@ -1692,6 +1794,71 @@ body {
|
|||||||
text-shadow: 0 0 5px rgba(156, 163, 175, 0.3);
|
text-shadow: 0 0 5px rgba(156, 163, 175, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Installation effects */
|
||||||
|
.installation-effects {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 80px;
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 40;
|
||||||
|
pointer-events: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-effects {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warp-line {
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(180deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(147, 51, 234, 0.8) 50%,
|
||||||
|
transparent 100%);
|
||||||
|
box-shadow: 0 0 10px rgba(147, 51, 234, 0.8),
|
||||||
|
0 0 20px rgba(147, 51, 234, 0.4);
|
||||||
|
animation: warpSpeed 1.5s linear infinite;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warp-line:nth-child(1) { left: 10%; animation-delay: 0s; }
|
||||||
|
.warp-line:nth-child(2) { left: 25%; animation-delay: 0.2s; }
|
||||||
|
.warp-line:nth-child(3) { left: 40%; animation-delay: 0.4s; }
|
||||||
|
.warp-line:nth-child(4) { left: 55%; animation-delay: 0.6s; }
|
||||||
|
.warp-line:nth-child(5) { left: 70%; animation-delay: 0.8s; }
|
||||||
|
.warp-line:nth-child(6) { left: 85%; animation-delay: 1s; }
|
||||||
|
.warp-line:nth-child(7) { left: 15%; animation-delay: 0.3s; }
|
||||||
|
.warp-line:nth-child(8) { left: 60%; animation-delay: 0.7s; }
|
||||||
|
|
||||||
|
@keyframes warpSpeed {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100%) scaleY(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0%) scaleY(1);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100%) scaleY(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.mods-manager {
|
.mods-manager {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4033,6 +4200,17 @@ body {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Language selector styling */
|
||||||
|
select.settings-input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.settings-input option {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-button-group {
|
.settings-button-group {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@@ -4393,6 +4571,27 @@ body {
|
|||||||
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
|
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-download-btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-download-btn-secondary:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 255, 255, 0.15) !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-download-btn-secondary:active:not(:disabled) {
|
||||||
|
transform: translateY(0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.update-popup-footer {
|
.update-popup-footer {
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -76,8 +76,34 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📌 Versioning Policy
|
||||||
|
|
||||||
|
**⚠️ Important: Semantic Versioning Required**
|
||||||
|
|
||||||
|
This project follows **strict semantic versioning** with **numerical versions only**:
|
||||||
|
|
||||||
|
- ✅ **Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
|
||||||
|
- ❌ **Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`, `v2.0.2b`
|
||||||
|
|
||||||
|
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
|
||||||
|
|
||||||
|
- **MAJOR**: Breaking changes
|
||||||
|
- **MINOR**: New features (backward compatible)
|
||||||
|
- **PATCH**: Bug fixes (backward compatible)
|
||||||
|
|
||||||
|
**Why?** The auto-update system requires semantic versioning for proper version comparison. Letter suffixes (like `2.0.2b`) are not supported and will cause update detection issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📋 Changelog
|
## 📋 Changelog
|
||||||
|
|
||||||
|
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)*
|
||||||
|
- 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.**
|
||||||
|
- 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*.
|
||||||
|
- 👨💻 **In-App Logging** — Reporting bugs and issues to `Github Issues` tab or `Open A Ticket` channel in our Discord Server has been made easier for players, no more finding logs file manually.
|
||||||
|
- 🛠️ **Repair Button** — Your game's broken? One button will fix them, go to Settings pane to Repair your game in one-click, **without losing any data**. If doing so did not fix your issue, please report it to us immediately!
|
||||||
|
- 🐛 **Fixed Bugs** — Fixed issue [#84](https://github.com/amiayweb/Hytale-F2P/issues/84) where mods disappearing when game starts in previous launcher (v2.0.2a).
|
||||||
|
|
||||||
### 🆕 v2.0.2a *(Minor Update)*
|
### 🆕 v2.0.2a *(Minor Update)*
|
||||||
- 🧑🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
|
- 🧑🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
|
||||||
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.
|
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.
|
||||||
|
|||||||
379
backend/appUpdater.js
Normal file
379
backend/appUpdater.js
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
const { app } = require('electron');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
class AppUpdater {
|
||||||
|
constructor(mainWindow) {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
this.autoUpdateAvailable = true; // Track if auto-update is possible
|
||||||
|
this.updateAvailable = false; // Track if an update was detected
|
||||||
|
this.updateVersion = null; // Store the available update version
|
||||||
|
this.setupAutoUpdater();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAutoUpdater() {
|
||||||
|
// Enable dev mode for testing (reads dev-app-update.yml)
|
||||||
|
// Only enable in development, not in production builds
|
||||||
|
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||||
|
autoUpdater.forceDevUpdateConfig = true;
|
||||||
|
console.log('Dev update mode enabled - using dev-app-update.yml');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure logger for electron-updater
|
||||||
|
// Create a compatible logger interface
|
||||||
|
autoUpdater.logger = {
|
||||||
|
info: (...args) => logger.info(...args),
|
||||||
|
warn: (...args) => logger.warn(...args),
|
||||||
|
error: (...args) => logger.error(...args),
|
||||||
|
debug: (...args) => logger.log(...args)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto download updates
|
||||||
|
autoUpdater.autoDownload = true;
|
||||||
|
// Auto install on quit (after download)
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
console.log('Checking for updates...');
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-checking');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
console.log('Update available:', info.version);
|
||||||
|
const currentVersion = app.getVersion();
|
||||||
|
const newVersion = info.version;
|
||||||
|
|
||||||
|
// Only proceed if the new version is actually different from current
|
||||||
|
if (newVersion === currentVersion) {
|
||||||
|
console.log('Update version matches current version, ignoring update-available event');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateAvailable = true;
|
||||||
|
this.updateVersion = newVersion;
|
||||||
|
this.autoUpdateAvailable = true; // Reset flag when new update is available
|
||||||
|
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-available', {
|
||||||
|
version: newVersion,
|
||||||
|
newVersion: newVersion,
|
||||||
|
currentVersion: currentVersion,
|
||||||
|
releaseName: info.releaseName,
|
||||||
|
releaseNotes: info.releaseNotes
|
||||||
|
});
|
||||||
|
// Also send to the old popup handler for compatibility
|
||||||
|
this.mainWindow.webContents.send('show-update-popup', {
|
||||||
|
currentVersion: currentVersion,
|
||||||
|
newVersion: newVersion,
|
||||||
|
version: newVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-not-available', (info) => {
|
||||||
|
console.log('Update not available. Current version is latest.');
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-not-available', {
|
||||||
|
version: info.version
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('error', (err) => {
|
||||||
|
console.error('Error in auto-updater:', err);
|
||||||
|
|
||||||
|
// Check if this is a network error (not critical, don't show UI)
|
||||||
|
const errorMessage = err.message?.toLowerCase() || '';
|
||||||
|
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
|
||||||
|
errorMessage.includes('network') ||
|
||||||
|
errorMessage.includes('connection') ||
|
||||||
|
errorMessage.includes('timeout') ||
|
||||||
|
errorMessage.includes('enotfound');
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
console.warn('Network error in auto-updater - will retry later. Not showing error UI.');
|
||||||
|
return; // Don't show error UI for network issues
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SHA512 checksum mismatch - this can happen during updates, just retry
|
||||||
|
const isChecksumError = err.code === 'ERR_CHECKSUM_MISMATCH' ||
|
||||||
|
errorMessage.includes('sha512') ||
|
||||||
|
errorMessage.includes('checksum') ||
|
||||||
|
errorMessage.includes('mismatch');
|
||||||
|
|
||||||
|
if (isChecksumError) {
|
||||||
|
console.warn('SHA512 checksum mismatch detected - clearing cache and will retry automatically. This is normal during updates.');
|
||||||
|
// Clear the update cache and let it re-download
|
||||||
|
this.clearUpdateCache();
|
||||||
|
|
||||||
|
// Don't show error UI - just log and let it retry automatically on next check
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this is a critical error that prevents auto-update
|
||||||
|
const isCriticalError = this.isCriticalUpdateError(err);
|
||||||
|
|
||||||
|
if (isCriticalError) {
|
||||||
|
this.autoUpdateAvailable = false;
|
||||||
|
console.warn('Auto-update failed. Manual download required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle missing metadata files (platform-specific builds)
|
||||||
|
if (err.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND') {
|
||||||
|
const platform = process.platform === 'darwin' ? 'macOS' :
|
||||||
|
process.platform === 'win32' ? 'Windows' : 'Linux';
|
||||||
|
const missingFile = process.platform === 'darwin' ? 'latest-mac.yml' :
|
||||||
|
process.platform === 'win32' ? 'latest.yml' : 'latest-linux.yml';
|
||||||
|
console.warn(`${platform} update metadata file (${missingFile}) not found in release.`);
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-error', {
|
||||||
|
message: `Update metadata file for ${platform} not found in release. Please download manually.`,
|
||||||
|
code: err.code,
|
||||||
|
requiresManualDownload: true,
|
||||||
|
updateVersion: this.updateVersion,
|
||||||
|
isMissingMetadata: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux-specific: Handle installation permission errors
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
const errorMessage = err.message?.toLowerCase() || '';
|
||||||
|
const errorStack = err.stack?.toLowerCase() || '';
|
||||||
|
const isInstallError = errorMessage.includes('pkexec') ||
|
||||||
|
errorMessage.includes('gksudo') ||
|
||||||
|
errorMessage.includes('kdesudo') ||
|
||||||
|
errorMessage.includes('setuid root') ||
|
||||||
|
errorMessage.includes('exited with code 127') ||
|
||||||
|
errorStack.includes('pacmanupdater') ||
|
||||||
|
errorStack.includes('doinstall') ||
|
||||||
|
errorMessage.includes('installation failed');
|
||||||
|
|
||||||
|
if (isInstallError) {
|
||||||
|
console.warn('Linux installation error: Package installation requires root privileges. Manual installation required.');
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-error', {
|
||||||
|
message: 'Auto-installation requires root privileges. Please download and install the update manually.',
|
||||||
|
code: err.code || 'ERR_LINUX_INSTALL_PERMISSION',
|
||||||
|
isLinuxInstallError: true,
|
||||||
|
requiresManualDownload: true,
|
||||||
|
updateVersion: this.updateVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS-specific: Handle unsigned app errors gracefully
|
||||||
|
if (process.platform === 'darwin' && err.code === 2) {
|
||||||
|
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-error', {
|
||||||
|
message: 'Auto-update requires code signing. Please download manually from GitHub.',
|
||||||
|
code: err.code,
|
||||||
|
isMacSigningError: true,
|
||||||
|
requiresManualDownload: true,
|
||||||
|
updateVersion: this.updateVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-error', {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
requiresManualDownload: isCriticalError,
|
||||||
|
updateVersion: this.updateVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('download-progress', (progressObj) => {
|
||||||
|
const message = `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`;
|
||||||
|
console.log(message);
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-download-progress', {
|
||||||
|
percent: progressObj.percent,
|
||||||
|
bytesPerSecond: progressObj.bytesPerSecond,
|
||||||
|
transferred: progressObj.transferred,
|
||||||
|
total: progressObj.total
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
|
console.log('Update downloaded:', info.version);
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('update-downloaded', {
|
||||||
|
version: info.version,
|
||||||
|
releaseName: info.releaseName,
|
||||||
|
releaseNotes: info.releaseNotes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpdatesAndNotify() {
|
||||||
|
// Check for updates and notify if available
|
||||||
|
autoUpdater.checkForUpdatesAndNotify().catch(err => {
|
||||||
|
console.error('Failed to check for updates:', err);
|
||||||
|
|
||||||
|
// Network errors are not critical - just log and continue
|
||||||
|
const errorMessage = err.message?.toLowerCase() || '';
|
||||||
|
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
|
||||||
|
errorMessage.includes('network') ||
|
||||||
|
errorMessage.includes('connection') ||
|
||||||
|
errorMessage.includes('timeout') ||
|
||||||
|
errorMessage.includes('enotfound');
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
console.warn('Network error checking for updates - will retry later. This is not critical.');
|
||||||
|
return; // Don't show error UI for network issues
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCritical = this.isCriticalUpdateError(err);
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed() && isCritical) {
|
||||||
|
this.mainWindow.webContents.send('update-error', {
|
||||||
|
message: err.message || 'Failed to check for updates',
|
||||||
|
code: err.code,
|
||||||
|
requiresManualDownload: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpdates() {
|
||||||
|
// Manual check for updates (returns promise)
|
||||||
|
return autoUpdater.checkForUpdates().catch(err => {
|
||||||
|
console.error('Failed to check for updates:', err);
|
||||||
|
|
||||||
|
// Network errors are not critical - just return no update available
|
||||||
|
const errorMessage = err.message?.toLowerCase() || '';
|
||||||
|
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
|
||||||
|
errorMessage.includes('network') ||
|
||||||
|
errorMessage.includes('connection') ||
|
||||||
|
errorMessage.includes('timeout') ||
|
||||||
|
errorMessage.includes('enotfound');
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
console.warn('Network error - update check unavailable');
|
||||||
|
return { updateInfo: null }; // Return empty result for network errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCritical = this.isCriticalUpdateError(err);
|
||||||
|
if (isCritical) {
|
||||||
|
this.autoUpdateAvailable = false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
quitAndInstall() {
|
||||||
|
// Quit and install the update
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdateInfo() {
|
||||||
|
return {
|
||||||
|
currentVersion: app.getVersion(),
|
||||||
|
updateAvailable: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearUpdateCache() {
|
||||||
|
try {
|
||||||
|
// Get the cache directory based on platform
|
||||||
|
const cacheDir = process.platform === 'darwin'
|
||||||
|
? path.join(os.homedir(), 'Library', 'Caches', `${app.getName()}-updater`)
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? path.join(os.homedir(), 'AppData', 'Local', `${app.getName()}-updater`)
|
||||||
|
: path.join(os.homedir(), '.cache', `${app.getName()}-updater`);
|
||||||
|
|
||||||
|
if (fs.existsSync(cacheDir)) {
|
||||||
|
fs.rmSync(cacheDir, { recursive: true, force: true });
|
||||||
|
console.log('Update cache cleared successfully');
|
||||||
|
} else {
|
||||||
|
console.log('Update cache directory does not exist');
|
||||||
|
}
|
||||||
|
} catch (cacheError) {
|
||||||
|
console.warn('Could not clear update cache:', cacheError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isCriticalUpdateError(err) {
|
||||||
|
// Check for errors that prevent auto-update
|
||||||
|
const errorMessage = err.message?.toLowerCase() || '';
|
||||||
|
const errorCode = err.code;
|
||||||
|
|
||||||
|
// Missing update metadata files (platform-specific)
|
||||||
|
if (errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' ||
|
||||||
|
errorMessage.includes('cannot find latest') ||
|
||||||
|
errorMessage.includes('latest-linux.yml') ||
|
||||||
|
errorMessage.includes('latest-mac.yml') ||
|
||||||
|
errorMessage.includes('latest.yml')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS code signing errors
|
||||||
|
if (process.platform === 'darwin' && (errorCode === 2 || errorMessage.includes('shipit'))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download failures
|
||||||
|
if (errorMessage.includes('download') && errorMessage.includes('fail')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network errors that prevent download (but we handle these separately as non-critical)
|
||||||
|
// Installation errors
|
||||||
|
if (errorMessage.includes('install') && errorMessage.includes('fail')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission errors
|
||||||
|
if (errorMessage.includes('permission') || errorMessage.includes('access denied')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux installation errors (pkexec, sudo issues)
|
||||||
|
if (process.platform === 'linux' && (
|
||||||
|
errorMessage.includes('pkexec') ||
|
||||||
|
errorMessage.includes('setuid root') ||
|
||||||
|
errorMessage.includes('exited with code 127') ||
|
||||||
|
errorMessage.includes('gksudo') ||
|
||||||
|
errorMessage.includes('kdesudo'))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File system errors (but not "not found" for metadata files - handled above)
|
||||||
|
if (errorMessage.includes('enoent') || errorMessage.includes('cannot find')) {
|
||||||
|
// Only if it's not about metadata files
|
||||||
|
if (!errorMessage.includes('latest') && !errorMessage.includes('.yml')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic critical error codes (but not checksum errors - those are handled separately)
|
||||||
|
if (errorCode && (errorCode >= 100 ||
|
||||||
|
errorCode === 'ERR_UPDATER_INVALID_RELEASE_FEED' ||
|
||||||
|
errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND')) {
|
||||||
|
// Don't treat checksum errors as critical - they're handled separately
|
||||||
|
if (errorCode === 'ERR_CHECKSUM_MISMATCH') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AppUpdater;
|
||||||
@@ -147,6 +147,24 @@ function loadDiscordRPC() {
|
|||||||
return config.discordRPC !== undefined ? config.discordRPC : true;
|
return config.discordRPC !== undefined ? config.discordRPC : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveLanguage(language) {
|
||||||
|
saveConfig({ language: language || 'en' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLanguage() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.language || 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCloseLauncherOnStart(enabled) {
|
||||||
|
saveConfig({ closeLauncherOnStart: !!enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCloseLauncherOnStart() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false;
|
||||||
|
}
|
||||||
|
|
||||||
function saveModsToConfig(mods) {
|
function saveModsToConfig(mods) {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@@ -302,6 +320,8 @@ module.exports = {
|
|||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
@@ -320,5 +340,8 @@ module.exports = {
|
|||||||
resetCurrentUserUuid,
|
resetCurrentUserUuid,
|
||||||
// GPU Preference exports
|
// GPU Preference exports
|
||||||
saveGpuPreference,
|
saveGpuPreference,
|
||||||
loadGpuPreference
|
loadGpuPreference,
|
||||||
|
// Close Launcher export
|
||||||
|
saveCloseLauncherOnStart,
|
||||||
|
loadCloseLauncherOnStart
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,13 +162,18 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
|
|
||||||
const modsPath = path.join(userDataPath, 'Mods');
|
const modsPath = path.join(userDataPath, 'Mods');
|
||||||
const disabledModsPath = path.join(userDataPath, 'DisabledMods');
|
const disabledModsPath = path.join(userDataPath, 'DisabledMods');
|
||||||
|
const profilesPath = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
if (!fs.existsSync(modsPath)) {
|
if (!fs.existsSync(modsPath)) {
|
||||||
|
// Ensure the Mods directory exists
|
||||||
fs.mkdirSync(modsPath, { recursive: true });
|
fs.mkdirSync(modsPath, { recursive: true });
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(disabledModsPath)) {
|
if (!fs.existsSync(disabledModsPath)) {
|
||||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
if (!fs.existsSync(profilesPath)) {
|
||||||
|
fs.mkdirSync(profilesPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
return modsPath;
|
return modsPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -177,6 +182,34 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProfilesDir(customInstallPath = null) {
|
||||||
|
try {
|
||||||
|
// get UserData path
|
||||||
|
let installPath = customInstallPath;
|
||||||
|
if (!installPath) {
|
||||||
|
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
|
||||||
|
if (fs.existsSync(configFile)) {
|
||||||
|
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||||
|
installPath = config.installPath || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!installPath) installPath = getAppDir();
|
||||||
|
|
||||||
|
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
|
||||||
|
const userDataPath = findUserDataPath(gameLatest);
|
||||||
|
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
|
if (!fs.existsSync(profilesDir)) {
|
||||||
|
fs.mkdirSync(profilesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return profilesDir;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error getting profiles dir:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAppDir,
|
getAppDir,
|
||||||
getResolvedAppDir,
|
getResolvedAppDir,
|
||||||
@@ -191,5 +224,6 @@ module.exports = {
|
|||||||
findClientPath,
|
findClientPath,
|
||||||
findUserDataPath,
|
findUserDataPath,
|
||||||
findUserDataRecursive,
|
findUserDataRecursive,
|
||||||
getModsPath
|
getModsPath,
|
||||||
|
getProfilesDir
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const {
|
|||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
|
saveCloseLauncherOnStart,
|
||||||
|
loadCloseLauncherOnStart,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
@@ -118,6 +122,14 @@ module.exports = {
|
|||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
|
||||||
|
// Language functions
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
|
|
||||||
|
// Close Launcher functions
|
||||||
|
saveCloseLauncherOnStart,
|
||||||
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
// GPU Preference functions
|
// GPU Preference functions
|
||||||
saveGpuPreference,
|
saveGpuPreference,
|
||||||
loadGpuPreference,
|
loadGpuPreference,
|
||||||
|
|||||||
@@ -2,10 +2,30 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { getModsPath } = require('../core/paths');
|
const { getModsPath, getProfilesDir } = require('../core/paths');
|
||||||
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
||||||
const profileManager = require('./profileManager');
|
const profileManager = require('./profileManager');
|
||||||
|
|
||||||
|
const API_KEY = process.env.CURSEFORGE_API_KEY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the physical mods path for a specific profile.
|
||||||
|
* Each profile now has its own 'mods' folder.
|
||||||
|
*/
|
||||||
|
function getProfileModsPath(profileId) {
|
||||||
|
const profilesDir = getProfilesDir();
|
||||||
|
if (!profilesDir) return null;
|
||||||
|
|
||||||
|
const profileDir = path.join(profilesDir, profileId);
|
||||||
|
const modsDir = path.join(profileDir, 'mods');
|
||||||
|
|
||||||
|
if (!fs.existsSync(modsDir)) {
|
||||||
|
fs.mkdirSync(modsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return modsDir;
|
||||||
|
}
|
||||||
|
|
||||||
function generateModId(filename) {
|
function generateModId(filename) {
|
||||||
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
|
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
|
||||||
}
|
}
|
||||||
@@ -35,30 +55,33 @@ function getProfileMods() {
|
|||||||
|
|
||||||
async function loadInstalledMods(modsPath) {
|
async function loadInstalledMods(modsPath) {
|
||||||
try {
|
try {
|
||||||
|
// Sync first to ensure we detect any manually added mods and paths are correct
|
||||||
|
await syncModsForCurrentProfile();
|
||||||
|
|
||||||
const activeProfile = profileManager.getActiveProfile();
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
if (!activeProfile) return [];
|
if (!activeProfile) return [];
|
||||||
|
|
||||||
const profileMods = activeProfile.mods || [];
|
const profileMods = activeProfile.mods || [];
|
||||||
const profileModFiles = new Set(profileMods.map(m => m.fileName));
|
|
||||||
|
|
||||||
// We only return mods that are explicitly in the profile
|
// Use profile-specific paths
|
||||||
// Check which ones are physically present (either in mods/ or DisabledMods/)
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
||||||
|
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
||||||
|
|
||||||
const physicalModsPath = modsPath; // .../mods
|
if (!fs.existsSync(profileModsPath)) fs.mkdirSync(profileModsPath, { recursive: true });
|
||||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
if (!fs.existsSync(profileDisabledModsPath)) fs.mkdirSync(profileDisabledModsPath, { recursive: true });
|
||||||
|
|
||||||
const validMods = [];
|
const validMods = [];
|
||||||
|
|
||||||
for (const modConfig of profileMods) {
|
for (const modConfig of profileMods) {
|
||||||
// Check if file exists in either location
|
// Check if file exists in either location
|
||||||
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
|
const inEnabled = fs.existsSync(path.join(profileModsPath, modConfig.fileName));
|
||||||
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
|
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, modConfig.fileName));
|
||||||
|
|
||||||
if (inEnabled || inDisabled) {
|
if (inEnabled || inDisabled) {
|
||||||
validMods.push({
|
validMods.push({
|
||||||
...modConfig,
|
...modConfig,
|
||||||
// Set filePath based on physical location
|
// Set filePath based on physical location
|
||||||
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
|
filePath: inEnabled ? path.join(profileModsPath, modConfig.fileName) : path.join(profileDisabledModsPath, modConfig.fileName),
|
||||||
enabled: modConfig.enabled !== false // Default true
|
enabled: modConfig.enabled !== false // Default true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -82,7 +105,11 @@ async function loadInstalledMods(modsPath) {
|
|||||||
|
|
||||||
async function downloadMod(modInfo) {
|
async function downloadMod(modInfo) {
|
||||||
try {
|
try {
|
||||||
const modsPath = await getModsPath();
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
|
if (!activeProfile) throw new Error('No active profile to save mod to');
|
||||||
|
|
||||||
|
const modsPath = getProfileModsPath(activeProfile.id);
|
||||||
|
if (!modsPath) throw new Error('Could not determine profile mods path');
|
||||||
|
|
||||||
if (!modInfo.downloadUrl && !modInfo.fileId) {
|
if (!modInfo.downloadUrl && !modInfo.fileId) {
|
||||||
throw new Error('No download URL or file ID provided');
|
throw new Error('No download URL or file ID provided');
|
||||||
@@ -91,9 +118,9 @@ async function downloadMod(modInfo) {
|
|||||||
let downloadUrl = modInfo.downloadUrl;
|
let downloadUrl = modInfo.downloadUrl;
|
||||||
|
|
||||||
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
|
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
|
||||||
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
|
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId || modInfo.curseForgeId}/files/${modInfo.fileId || modInfo.curseForgeFileId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-api-key': modInfo.apiKey,
|
'x-api-key': modInfo.apiKey || API_KEY,
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -119,35 +146,30 @@ async function downloadMod(modInfo) {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
writer.on('finish', () => {
|
writer.on('finish', () => {
|
||||||
// NEW: Update Active Profile instead of global config
|
// Update Active Profile
|
||||||
const activeProfile = profileManager.getActiveProfile();
|
const newMod = {
|
||||||
if (activeProfile) {
|
id: modInfo.id || generateModId(fileName),
|
||||||
const newMod = {
|
name: modInfo.name || extractModName(fileName),
|
||||||
id: modInfo.id || generateModId(fileName),
|
version: modInfo.version || '1.0.0',
|
||||||
name: modInfo.name || extractModName(fileName),
|
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
|
||||||
version: modInfo.version || '1.0.0',
|
author: modInfo.author || 'Unknown',
|
||||||
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
|
enabled: true,
|
||||||
author: modInfo.author || 'Unknown',
|
fileName: fileName,
|
||||||
enabled: true,
|
fileSize: fs.statSync(filePath).size,
|
||||||
fileName: fileName,
|
dateInstalled: new Date().toISOString(),
|
||||||
fileSize: fs.statSync(filePath).size,
|
curseForgeId: modInfo.modId,
|
||||||
dateInstalled: new Date().toISOString(),
|
curseForgeFileId: modInfo.fileId
|
||||||
curseForgeId: modInfo.modId,
|
};
|
||||||
curseForgeFileId: modInfo.fileId
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedMods = [...(activeProfile.mods || []), newMod];
|
const updatedMods = [...(activeProfile.mods || []), newMod];
|
||||||
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
success: true,
|
success: true,
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
modInfo: newMod
|
modInfo: newMod
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
reject(new Error('No active profile to save mod to'));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
writer.on('error', reject);
|
writer.on('error', reject);
|
||||||
});
|
});
|
||||||
@@ -173,8 +195,11 @@ async function uninstallMod(modId, modsPath) {
|
|||||||
throw new Error('Mod not found in profile');
|
throw new Error('Mod not found in profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
// Use profile paths
|
||||||
const enabledPath = path.join(modsPath, mod.fileName);
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
||||||
|
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
||||||
|
|
||||||
|
const enabledPath = path.join(profileModsPath, mod.fileName);
|
||||||
const disabledPath = path.join(disabledModsPath, mod.fileName);
|
const disabledPath = path.join(disabledModsPath, mod.fileName);
|
||||||
|
|
||||||
let fileRemoved = false;
|
let fileRemoved = false;
|
||||||
@@ -226,31 +251,25 @@ async function toggleMod(modId, modsPath) {
|
|||||||
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
|
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
|
||||||
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||||
|
|
||||||
// Manually move the file to reflect the new state
|
// Move file between Profile/Mods and Profile/DisabledMods
|
||||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
||||||
|
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
||||||
|
|
||||||
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
|
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||||
|
|
||||||
const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
|
const currentPath = mod.enabled ? path.join(profileModsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
|
||||||
|
const targetDir = newEnabled ? profileModsPath : disabledModsPath;
|
||||||
// Determine target paths
|
|
||||||
|
|
||||||
const targetDir = newEnabled ? modsPath : disabledModsPath;
|
|
||||||
const targetPath = path.join(targetDir, mod.fileName);
|
const targetPath = path.join(targetDir, mod.fileName);
|
||||||
|
|
||||||
if (fs.existsSync(currentPath)) {
|
if (fs.existsSync(currentPath)) {
|
||||||
fs.renameSync(currentPath, targetPath);
|
fs.renameSync(currentPath, targetPath);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: check if it's already in target?
|
// Fallback: check if it's already in target?
|
||||||
|
|
||||||
|
|
||||||
if (fs.existsSync(targetPath)) {
|
if (fs.existsSync(targetPath)) {
|
||||||
// It's already there, maybe just state was wrong.
|
|
||||||
|
|
||||||
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
|
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Try finding it
|
// Try finding it
|
||||||
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName);
|
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(profileModsPath, mod.fileName);
|
||||||
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
|
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,35 +292,166 @@ async function syncModsForCurrentProfile() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
|
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
|
||||||
|
|
||||||
const modsPath = await getModsPath();
|
// 1. Resolve Paths
|
||||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
// globalModsPath is the one the game uses (symlink target)
|
||||||
|
const globalModsPath = await getModsPath();
|
||||||
|
// profileModsPath is the real storage for this profile
|
||||||
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
||||||
|
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
||||||
|
|
||||||
if (!fs.existsSync(disabledModsPath)) {
|
if (!fs.existsSync(profileDisabledModsPath)) {
|
||||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all physical files from both folders
|
// 2. Symlink / Migration Logic
|
||||||
const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
let needsLink = false;
|
||||||
const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
|
||||||
|
|
||||||
|
if (fs.existsSync(globalModsPath)) {
|
||||||
|
const stats = fs.lstatSync(globalModsPath);
|
||||||
|
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
const linkTarget = fs.readlinkSync(globalModsPath);
|
||||||
|
// Normalize paths for comparison
|
||||||
|
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
|
||||||
|
console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`);
|
||||||
|
fs.unlinkSync(globalModsPath);
|
||||||
|
needsLink = true;
|
||||||
|
}
|
||||||
|
} else if (stats.isDirectory()) {
|
||||||
|
// MIGRATION: It's a real directory. Move contents to profile.
|
||||||
|
console.log('[ModManager] Migrating global mods folder to profile folder...');
|
||||||
|
const files = fs.readdirSync(globalModsPath);
|
||||||
|
for (const file of files) {
|
||||||
|
const src = path.join(globalModsPath, file);
|
||||||
|
const dest = path.join(profileModsPath, file);
|
||||||
|
// Only move if dest doesn't exist to avoid overwriting
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.renameSync(src, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also migrate DisabledMods if it exists globally
|
||||||
|
const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods');
|
||||||
|
if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) {
|
||||||
|
const dFiles = fs.readdirSync(globalDisabledPath);
|
||||||
|
for (const file of dFiles) {
|
||||||
|
const src = path.join(globalDisabledPath, file);
|
||||||
|
const dest = path.join(profileDisabledModsPath, file);
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.renameSync(src, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We can remove global DisabledMods now, as it's not used by game
|
||||||
|
try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the directory so we can link it
|
||||||
|
try {
|
||||||
|
fs.rmSync(globalModsPath, { recursive: true, force: true });
|
||||||
|
needsLink = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to remove global mods dir:', e);
|
||||||
|
// Throw error to stop.
|
||||||
|
throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needsLink = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsLink) {
|
||||||
|
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
|
||||||
|
try {
|
||||||
|
// 'junction' is key for Windows without admin
|
||||||
|
fs.symlinkSync(profileModsPath, globalModsPath, 'junction');
|
||||||
|
} catch (err) {
|
||||||
|
// If we can't create the symlink, try creating the directory first
|
||||||
|
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
|
||||||
|
console.error(err.message);
|
||||||
|
|
||||||
|
// Fallback: create a real directory so the game still works
|
||||||
|
if (!fs.existsSync(globalModsPath)) {
|
||||||
|
fs.mkdirSync(globalModsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Auto-Repair (Download missing mods)
|
||||||
|
const profileModsSnapshot = activeProfile.mods || [];
|
||||||
|
for (const mod of profileModsSnapshot) {
|
||||||
|
if (mod.enabled && !mod.manual) {
|
||||||
|
const inEnabled = fs.existsSync(path.join(profileModsPath, mod.fileName));
|
||||||
|
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, mod.fileName));
|
||||||
|
|
||||||
|
if (!inEnabled && !inDisabled) {
|
||||||
|
if (mod.curseForgeId && (mod.curseForgeFileId || mod.fileId)) {
|
||||||
|
console.log(`[ModManager] Auto-repair: Re-downloading missing mod "${mod.name}"...`);
|
||||||
|
try {
|
||||||
|
await downloadMod({
|
||||||
|
...mod,
|
||||||
|
modId: mod.curseForgeId,
|
||||||
|
fileId: mod.curseForgeFileId || mod.fileId,
|
||||||
|
apiKey: API_KEY
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ModManager] Auto-repair failed for "${mod.name}": ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Auto-Import (Detect manual drops in the profile folder)
|
||||||
|
const enabledFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||||
|
|
||||||
|
let profileMods = activeProfile.mods || [];
|
||||||
|
let profileUpdated = false;
|
||||||
|
|
||||||
|
|
||||||
|
// Anything in this folder belongs to this profile.
|
||||||
|
|
||||||
|
for (const file of enabledFiles) {
|
||||||
|
const isKnown = profileMods.some(m => m.fileName === file);
|
||||||
|
|
||||||
|
if (!isKnown) {
|
||||||
|
console.log(`[ModManager] Auto-importing manual mod: ${file}`);
|
||||||
|
const newMod = {
|
||||||
|
id: generateModId(file),
|
||||||
|
name: extractModName(file),
|
||||||
|
version: 'Unknown',
|
||||||
|
description: 'Manually installed',
|
||||||
|
author: 'Local',
|
||||||
|
enabled: true,
|
||||||
|
fileName: file,
|
||||||
|
fileSize: 0,
|
||||||
|
dateInstalled: new Date().toISOString(),
|
||||||
|
manual: true
|
||||||
|
};
|
||||||
|
profileMods.push(newMod);
|
||||||
|
profileUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileUpdated) {
|
||||||
|
profileManager.updateProfile(activeProfile.id, { mods: profileMods });
|
||||||
|
const updatedProfile = profileManager.getActiveProfile();
|
||||||
|
profileMods = updatedProfile ? (updatedProfile.mods || []) : profileMods;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
|
||||||
|
// Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game.
|
||||||
|
|
||||||
|
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||||
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
||||||
|
|
||||||
// Profile.mods contains the list of ALL mods for that profile, with their enabled state.
|
|
||||||
|
|
||||||
const profileMods = activeProfile.mods || [];
|
|
||||||
|
|
||||||
for (const fileName of allFiles) {
|
for (const fileName of allFiles) {
|
||||||
const modConfig = profileMods.find(m => m.fileName === fileName);
|
const modConfig = profileMods.find(m => m.fileName === fileName);
|
||||||
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
|
const shouldBeEnabled = modConfig && modConfig.enabled !== false;
|
||||||
|
|
||||||
// Logic:
|
const currentPath = enabledFiles.includes(fileName) ? path.join(profileModsPath, fileName) : path.join(profileDisabledModsPath, fileName);
|
||||||
// If it should be enabled -> Move to mods/
|
const targetDir = shouldBeEnabled ? profileModsPath : profileDisabledModsPath;
|
||||||
// If it should be disabled -> Move to DisabledMods/
|
|
||||||
|
|
||||||
const currentPath = enabledFiles.includes(fileName) ? path.join(modsPath, fileName) : path.join(disabledModsPath, fileName);
|
|
||||||
const targetDir = shouldBeEnabled ? modsPath : disabledModsPath;
|
|
||||||
const targetPath = path.join(targetDir, fileName);
|
const targetPath = path.join(targetDir, fileName);
|
||||||
|
|
||||||
if (path.dirname(currentPath) !== targetDir) {
|
if (path.dirname(currentPath) !== targetDir) {
|
||||||
|
|||||||
@@ -143,6 +143,29 @@ class ClientPatcher {
|
|||||||
return { buffer: result, count };
|
return { buffer: result, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
||||||
|
*/
|
||||||
|
patchDiscordUrl(data) {
|
||||||
|
let count = 0;
|
||||||
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
|
const oldUrl = '.gg/hytale';
|
||||||
|
const newUrl = '.gg/MHkEjepMQ7';
|
||||||
|
|
||||||
|
const oldUtf16 = this.stringToUtf16LE(oldUrl);
|
||||||
|
const newUtf16 = this.stringToUtf16LE(newUrl);
|
||||||
|
|
||||||
|
const positions = this.findAllOccurrences(result, oldUtf16);
|
||||||
|
|
||||||
|
for (const pos of positions) {
|
||||||
|
newUtf16.copy(result, pos);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buffer: result, count };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the client binary has already been patched
|
* Check if the client binary has already been patched
|
||||||
*/
|
*/
|
||||||
@@ -256,9 +279,12 @@ class ClientPatcher {
|
|||||||
console.log('Patching domain references...');
|
console.log('Patching domain references...');
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
||||||
|
|
||||||
if (count === 0) {
|
console.log('Patching Discord URLs...');
|
||||||
console.log('No occurrences of hytale.com found - binary may already be modified or has different format');
|
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||||
return { success: true, patchCount: 0, warning: 'No domain occurrences found' };
|
|
||||||
|
if (count === 0 && discordCount === 0) {
|
||||||
|
console.log('No occurrences found - binary may already be modified or has different format');
|
||||||
|
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -266,7 +292,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Writing patched binary...');
|
console.log('Writing patched binary...');
|
||||||
fs.writeFileSync(clientPath, patchedData);
|
fs.writeFileSync(clientPath, finalData);
|
||||||
|
|
||||||
this.markAsPatched(clientPath);
|
this.markAsPatched(clientPath);
|
||||||
|
|
||||||
@@ -274,10 +300,10 @@ class ClientPatcher {
|
|||||||
progressCallback('Patching complete', 100);
|
progressCallback('Patching complete', 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully patched ${count} occurrences`);
|
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
||||||
console.log('=== Patching Complete ===');
|
console.log('=== Patching Complete ===');
|
||||||
|
|
||||||
return { success: true, patchCount: count };
|
return { success: true, patchCount: count + discordCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
function getOS() {
|
function getOS() {
|
||||||
if (process.platform === 'win32') return 'windows';
|
if (process.platform === 'win32') return 'windows';
|
||||||
@@ -66,55 +67,17 @@ function setupWaylandEnvironment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectGpu() {
|
function detectGpu() {
|
||||||
if (process.platform !== 'linux') {
|
const platform = getOS();
|
||||||
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const output = execSync('lspci -nn | grep \'VGA\\|3D\'', { encoding: 'utf8' });
|
if (platform === 'linux') {
|
||||||
// console.log('GPU detection raw output:', output);
|
return detectGpuLinux();
|
||||||
const lines = output.split('\n').filter(line => line.trim());
|
} else if (platform === 'windows') {
|
||||||
// console.log('GPU detection parsed lines:', lines);
|
return detectGpuWindows();
|
||||||
|
} else if (platform === 'darwin') {
|
||||||
let integratedName = null;
|
return detectGpuMac();
|
||||||
let dedicatedName = null;
|
|
||||||
let hasNvidia = false;
|
|
||||||
let hasAmd = false;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// console.log('Checking line:', line);
|
|
||||||
if (line.includes('VGA') || line.includes('3D')) {
|
|
||||||
// console.log('Line contains VGA or 3D');
|
|
||||||
|
|
||||||
const match = line.match(/\[([^\]]+)\]/g);
|
|
||||||
let modelName = null;
|
|
||||||
if (match && match.length >= 2) {
|
|
||||||
modelName = match[1].slice(1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.includes('10de:') || line.toLowerCase().includes('nvidia')) {
|
|
||||||
hasNvidia = true;
|
|
||||||
dedicatedName = "NVIDIA " + modelName || 'NVIDIA GPU';
|
|
||||||
console.log('Detected NVIDIA GPU:', dedicatedName);
|
|
||||||
} else if (line.includes('1002:') || line.toLowerCase().includes('amd') || line.toLowerCase().includes('radeon')) {
|
|
||||||
hasAmd = true;
|
|
||||||
dedicatedName = "AMD " + modelName || 'AMD GPU';
|
|
||||||
console.log('Detected AMD GPU:', dedicatedName);
|
|
||||||
} else if (line.includes('8086:') || line.toLowerCase().includes('intel')) {
|
|
||||||
integratedName = "Intel " + modelName || 'Intel GPU';
|
|
||||||
console.log('Detected Intel GPU:', integratedName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('hasNvidia:', hasNvidia, 'hasAmd:', hasAmd, 'integratedName:', integratedName, 'dedicatedName:', dedicatedName);
|
|
||||||
|
|
||||||
if (hasNvidia) {
|
|
||||||
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
|
||||||
} else if (hasAmd) {
|
|
||||||
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
|
||||||
} else {
|
} else {
|
||||||
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
|
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('GPU detection failed, falling back to integrated:', error.message);
|
console.warn('GPU detection failed, falling back to integrated:', error.message);
|
||||||
@@ -122,6 +85,121 @@ function detectGpu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectGpuLinux() {
|
||||||
|
const output = execSync('lspci -nn | grep \'VGA\\|3D\'', { encoding: 'utf8' });
|
||||||
|
const lines = output.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
let integratedName = null;
|
||||||
|
let dedicatedName = null;
|
||||||
|
let hasNvidia = false;
|
||||||
|
let hasAmd = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('VGA') || line.includes('3D')) {
|
||||||
|
const match = line.match(/\[([^\]]+)\]/g);
|
||||||
|
let modelName = null;
|
||||||
|
if (match && match.length >= 2) {
|
||||||
|
modelName = match[1].slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes('10de:') || line.toLowerCase().includes('nvidia')) {
|
||||||
|
hasNvidia = true;
|
||||||
|
dedicatedName = "NVIDIA " + modelName || 'NVIDIA GPU';
|
||||||
|
console.log('Detected NVIDIA GPU:', dedicatedName);
|
||||||
|
} else if (line.includes('1002:') || line.toLowerCase().includes('amd') || line.toLowerCase().includes('radeon')) {
|
||||||
|
hasAmd = true;
|
||||||
|
dedicatedName = "AMD " + modelName || 'AMD GPU';
|
||||||
|
console.log('Detected AMD GPU:', dedicatedName);
|
||||||
|
} else if (line.includes('8086:') || line.toLowerCase().includes('intel')) {
|
||||||
|
integratedName = "Intel " + modelName || 'Intel GPU';
|
||||||
|
console.log('Detected Intel GPU:', integratedName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNvidia) {
|
||||||
|
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||||
|
} else if (hasAmd) {
|
||||||
|
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||||
|
} else {
|
||||||
|
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectGpuWindows() {
|
||||||
|
const output = execSync('wmic path win32_VideoController get name', { encoding: 'utf8' });
|
||||||
|
const lines = output.split('\n').map(line => line.trim()).filter(line => line && line !== 'Name');
|
||||||
|
|
||||||
|
let integratedName = null;
|
||||||
|
let dedicatedName = null;
|
||||||
|
let hasNvidia = false;
|
||||||
|
let hasAmd = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const lowerLine = line.toLowerCase();
|
||||||
|
if (lowerLine.includes('nvidia')) {
|
||||||
|
hasNvidia = true;
|
||||||
|
dedicatedName = line;
|
||||||
|
console.log('Detected NVIDIA GPU:', dedicatedName);
|
||||||
|
} else if (lowerLine.includes('amd') || lowerLine.includes('radeon')) {
|
||||||
|
hasAmd = true;
|
||||||
|
dedicatedName = line;
|
||||||
|
console.log('Detected AMD GPU:', dedicatedName);
|
||||||
|
} else if (lowerLine.includes('intel')) {
|
||||||
|
integratedName = line;
|
||||||
|
console.log('Detected Intel GPU:', integratedName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNvidia) {
|
||||||
|
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||||
|
} else if (hasAmd) {
|
||||||
|
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||||
|
} else {
|
||||||
|
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectGpuMac() {
|
||||||
|
const output = execSync('system_profiler SPDisplaysDataType', { encoding: 'utf8' });
|
||||||
|
const lines = output.split('\n');
|
||||||
|
|
||||||
|
let integratedName = null;
|
||||||
|
let dedicatedName = null;
|
||||||
|
let hasNvidia = false;
|
||||||
|
let hasAmd = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('Chipset Model:')) {
|
||||||
|
const gpuName = line.split('Chipset Model:')[1].trim();
|
||||||
|
const lowerGpu = gpuName.toLowerCase();
|
||||||
|
if (lowerGpu.includes('nvidia')) {
|
||||||
|
hasNvidia = true;
|
||||||
|
dedicatedName = gpuName;
|
||||||
|
console.log('Detected NVIDIA GPU:', dedicatedName);
|
||||||
|
} else if (lowerGpu.includes('amd') || lowerGpu.includes('radeon')) {
|
||||||
|
hasAmd = true;
|
||||||
|
dedicatedName = gpuName;
|
||||||
|
console.log('Detected AMD GPU:', dedicatedName);
|
||||||
|
} else if (lowerGpu.includes('intel') || lowerGpu.includes('iris') || lowerGpu.includes('uhd')) {
|
||||||
|
integratedName = gpuName;
|
||||||
|
console.log('Detected Intel GPU:', integratedName);
|
||||||
|
} else if (!dedicatedName && !integratedName) {
|
||||||
|
// Fallback for Apple Silicon or other
|
||||||
|
integratedName = gpuName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNvidia) {
|
||||||
|
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Integrated GPU', dedicatedName };
|
||||||
|
} else if (hasAmd) {
|
||||||
|
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Integrated GPU', dedicatedName };
|
||||||
|
} else {
|
||||||
|
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Integrated GPU', dedicatedName: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupGpuEnvironment(gpuPreference) {
|
function setupGpuEnvironment(gpuPreference) {
|
||||||
if (process.platform !== 'linux') {
|
if (process.platform !== 'linux') {
|
||||||
return {};
|
return {};
|
||||||
@@ -140,12 +218,17 @@ function setupGpuEnvironment(gpuPreference) {
|
|||||||
const envVars = {};
|
const envVars = {};
|
||||||
|
|
||||||
if (finalPreference === 'dedicated') {
|
if (finalPreference === 'dedicated') {
|
||||||
envVars.DRI_PRIME = '1';
|
|
||||||
if (detected.vendor === 'nvidia') {
|
if (detected.vendor === 'nvidia') {
|
||||||
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
|
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
|
||||||
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
|
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
|
||||||
envVars.__GL_SHADER_DISK_CACHE = '1';
|
const nvidiaEglFile = '/usr/share/glvnd/egl_vendor.d/10_nvidia.json';
|
||||||
envVars.__GL_SHADER_DISK_CACHE_PATH = '/tmp';
|
if (fs.existsSync(nvidiaEglFile)) {
|
||||||
|
envVars.__EGL_VENDOR_LIBRARY_FILENAMES = nvidiaEglFile;
|
||||||
|
} else {
|
||||||
|
console.warn('NVIDIA EGL vendor library file not found, not setting __EGL_VENDOR_LIBRARY_FILENAMES');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
envVars.DRI_PRIME = '1';
|
||||||
}
|
}
|
||||||
console.log('GPU environment variables:', envVars);
|
console.log('GPU environment variables:', envVars);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
provider: github
|
||||||
|
owner: amiayweb # Change to your own GitHub username
|
||||||
|
repo: Hytale-F2P
|
||||||
284
docs/AUTO-UPDATES.md
Normal file
284
docs/AUTO-UPDATES.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Auto-Updates System
|
||||||
|
|
||||||
|
This document explains how the automatic update system works in the Hytale F2P Launcher.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The launcher uses [electron-updater](https://www.electron.build/auto-update) to automatically check for, download, and install updates. When a new version is available, users are notified and the update is downloaded in the background.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### 1. Update Checking
|
||||||
|
|
||||||
|
- **Automatic Check**: The app automatically checks for updates 3 seconds after startup
|
||||||
|
- **Manual Check**: Users can manually check for updates through the UI
|
||||||
|
- **Update Source**: Updates are fetched from GitHub Releases
|
||||||
|
|
||||||
|
### 2. Update Process
|
||||||
|
|
||||||
|
1. **Check for Updates**: The app queries GitHub Releases for a newer version
|
||||||
|
2. **Notify User**: If an update is available, the user is notified via the UI
|
||||||
|
3. **Download**: The update is automatically downloaded in the background
|
||||||
|
4. **Progress Tracking**: Download progress is shown to the user
|
||||||
|
5. **Install**: When the download completes, the user can choose to install immediately or wait until the app restarts
|
||||||
|
|
||||||
|
### 3. Installation
|
||||||
|
|
||||||
|
- Updates are installed when the app quits (if `autoInstallOnAppQuit` is enabled)
|
||||||
|
- Users can also manually trigger installation through the UI
|
||||||
|
- The app will restart automatically after installation
|
||||||
|
|
||||||
|
## Version Detection & Comparison
|
||||||
|
|
||||||
|
### Current Version Source
|
||||||
|
|
||||||
|
The app's current version is read from `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "2.0.2b"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This version is embedded into the built application and is accessible via `app.getVersion()` in Electron. When the app is built, electron-builder also creates an internal `app-update.yml` file in the app's resources that contains this version information.
|
||||||
|
|
||||||
|
### How Version Detection Works
|
||||||
|
|
||||||
|
1. **Current Version**: The app knows its own version from `package.json`, which is:
|
||||||
|
- Read at build time
|
||||||
|
- Embedded in the application binary
|
||||||
|
- Stored in the app's metadata
|
||||||
|
|
||||||
|
2. **Fetching Latest Version**: When checking for updates, electron-updater:
|
||||||
|
- Queries the GitHub Releases API: `https://api.github.com/repos/amiayweb/Hytale-F2P/releases/latest`
|
||||||
|
- Or reads the update metadata file: `https://github.com/amiayweb/Hytale-F2P/releases/download/latest/latest.yml` (or `latest-mac.yml` for macOS)
|
||||||
|
- The metadata file contains:
|
||||||
|
```yaml
|
||||||
|
version: 2.0.3
|
||||||
|
releaseDate: '2024-01-15T10:30:00.000Z'
|
||||||
|
path: Hytale-F2P-Launcher-2.0.3-x64.exe
|
||||||
|
sha512: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Version Comparison**: electron-updater uses semantic versioning comparison:
|
||||||
|
- Compares the **current version** (from `package.json`) with the **latest version** (from GitHub Releases)
|
||||||
|
- Uses semantic versioning rules: `major.minor.patch` (e.g., `2.0.2` vs `2.0.3`)
|
||||||
|
- An update is available if the remote version is **greater than** the current version
|
||||||
|
- Examples:
|
||||||
|
- Current: `2.0.2` → Remote: `2.0.3` ✅ Update available
|
||||||
|
- Current: `2.0.2` → Remote: `2.0.2` ❌ No update (same version)
|
||||||
|
- Current: `2.0.3` → Remote: `2.0.2` ❌ No update (current is newer)
|
||||||
|
- Current: `2.0.2b` → Remote: `2.0.3` ✅ Update available (prerelease tags are handled)
|
||||||
|
|
||||||
|
4. **Version Format Handling**:
|
||||||
|
- **Semantic versions** (e.g., `1.0.0`, `2.1.3`) are compared numerically
|
||||||
|
- **Prerelease versions** (e.g., `2.0.2b`, `2.0.2-beta`) are compared with special handling
|
||||||
|
- **Non-semantic versions** may cause issues - it's recommended to use semantic versioning
|
||||||
|
|
||||||
|
### Update Metadata Files
|
||||||
|
|
||||||
|
When you build and publish a release, electron-builder generates platform-specific metadata files:
|
||||||
|
|
||||||
|
**Windows/Linux** (`latest.yml`):
|
||||||
|
```yaml
|
||||||
|
version: 2.0.3
|
||||||
|
files:
|
||||||
|
- url: Hytale-F2P-Launcher-2.0.3-x64.exe
|
||||||
|
sha512: abc123...
|
||||||
|
size: 12345678
|
||||||
|
path: Hytale-F2P-Launcher-2.0.3-x64.exe
|
||||||
|
sha512: abc123...
|
||||||
|
releaseDate: '2024-01-15T10:30:00.000Z'
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS** (`latest-mac.yml`):
|
||||||
|
```yaml
|
||||||
|
version: 2.0.3
|
||||||
|
files:
|
||||||
|
- url: Hytale-F2P-Launcher-2.0.3-arm64-mac.zip
|
||||||
|
sha512: def456...
|
||||||
|
size: 23456789
|
||||||
|
path: Hytale-F2P-Launcher-2.0.3-arm64-mac.zip
|
||||||
|
sha512: def456...
|
||||||
|
releaseDate: '2024-01-15T10:30:00.000Z'
|
||||||
|
```
|
||||||
|
|
||||||
|
These files are:
|
||||||
|
- Automatically generated during build
|
||||||
|
- Uploaded to GitHub Releases
|
||||||
|
- Fetched by electron-updater to check for updates
|
||||||
|
- Used to determine if an update is available and what to download
|
||||||
|
|
||||||
|
### The Check Process in Detail
|
||||||
|
|
||||||
|
When `appUpdater.checkForUpdatesAndNotify()` is called:
|
||||||
|
|
||||||
|
1. **Read Current Version**: Gets version from `app.getVersion()` (which reads from `package.json`)
|
||||||
|
2. **Fetch Update Info**:
|
||||||
|
- Makes HTTP request to GitHub Releases API or reads `latest.yml`
|
||||||
|
- Gets the version number from the metadata
|
||||||
|
3. **Compare Versions**:
|
||||||
|
- Uses semantic versioning comparison (e.g., `semver.gt(remoteVersion, currentVersion)`)
|
||||||
|
- If remote > current: update available
|
||||||
|
- If remote <= current: no update
|
||||||
|
4. **Emit Events**:
|
||||||
|
- `update-available` if newer version found
|
||||||
|
- `update-not-available` if already up to date
|
||||||
|
5. **Download if Available**: If `autoDownload` is enabled, starts downloading automatically
|
||||||
|
|
||||||
|
### Example Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
App Version: 2.0.2 (from package.json)
|
||||||
|
↓
|
||||||
|
Check GitHub Releases API
|
||||||
|
↓
|
||||||
|
Latest Release: 2.0.3
|
||||||
|
↓
|
||||||
|
Compare: 2.0.3 > 2.0.2? YES
|
||||||
|
↓
|
||||||
|
Emit: 'update-available' event
|
||||||
|
↓
|
||||||
|
Download update automatically
|
||||||
|
↓
|
||||||
|
Emit: 'update-downloaded' event
|
||||||
|
↓
|
||||||
|
User can install on next restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### AppUpdater Class (`backend/appUpdater.js`)
|
||||||
|
|
||||||
|
The main class that handles all update operations:
|
||||||
|
|
||||||
|
- **`checkForUpdatesAndNotify()`**: Checks for updates and shows a system notification if available
|
||||||
|
- **`checkForUpdates()`**: Manually checks for updates (returns a promise)
|
||||||
|
- **`quitAndInstall()`**: Quits the app and installs the downloaded update
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
The AppUpdater emits the following events that the UI can listen to:
|
||||||
|
|
||||||
|
- `update-checking`: Update check has started
|
||||||
|
- `update-available`: A new update is available
|
||||||
|
- `update-not-available`: App is up to date
|
||||||
|
- `update-download-progress`: Download progress updates
|
||||||
|
- `update-downloaded`: Update has finished downloading
|
||||||
|
- `update-error`: An error occurred during the update process
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Package.json
|
||||||
|
|
||||||
|
The publish configuration in `package.json` tells electron-builder where to publish updates:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"publish": {
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "amiayweb",
|
||||||
|
"repo": "Hytale-F2P"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means updates will be fetched from GitHub Releases for the `amiayweb/Hytale-F2P` repository.
|
||||||
|
|
||||||
|
## Publishing Updates
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
1. **Update Version**: Bump the version in `package.json` (e.g., `2.0.2b` → `2.0.3`)
|
||||||
|
|
||||||
|
2. **Build the App**: Run the build command for your platform:
|
||||||
|
```bash
|
||||||
|
npm run build:win # Windows
|
||||||
|
npm run build:mac # macOS
|
||||||
|
npm run build:linux # Linux
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Publish to GitHub**: When building with electron-builder, it will:
|
||||||
|
- Generate update metadata files (`latest.yml`, `latest-mac.yml`, etc.)
|
||||||
|
- Upload the built files to GitHub Releases (if configured with `GH_TOKEN`)
|
||||||
|
- Make them available for auto-update
|
||||||
|
|
||||||
|
4. **Release on GitHub**: Create a GitHub Release with the new version tag
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
- **macOS Code Signing**: macOS apps **must** be code-signed for auto-updates to work
|
||||||
|
- **Version Format**: Use semantic versioning (e.g., `1.0.0`, `2.0.1`) for best compatibility
|
||||||
|
- **Update Files**: electron-builder automatically generates the required metadata files (`latest.yml`, etc.)
|
||||||
|
|
||||||
|
## Testing Updates
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
To test updates during development, create a `dev-app-update.yml` file in the project root:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
owner: amiayweb
|
||||||
|
repo: Hytale-F2P
|
||||||
|
provider: github
|
||||||
|
```
|
||||||
|
|
||||||
|
Then enable dev mode in the code:
|
||||||
|
```javascript
|
||||||
|
autoUpdater.forceDevUpdateConfig = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Testing
|
||||||
|
|
||||||
|
For local testing, you can use a local server (like Minio) or a generic HTTP server to host update files.
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
|
||||||
|
### What Users See
|
||||||
|
|
||||||
|
1. **On Startup**: The app silently checks for updates in the background
|
||||||
|
2. **Update Available**: A notification appears if an update is found
|
||||||
|
3. **Downloading**: Progress bar shows download status
|
||||||
|
4. **Ready to Install**: User is notified when the update is ready
|
||||||
|
5. **Installation**: Update installs on app restart or when user clicks "Install Now"
|
||||||
|
|
||||||
|
### User Actions
|
||||||
|
|
||||||
|
- Users can manually check for updates through the settings/update menu
|
||||||
|
- Users can choose to install immediately or wait until next app launch
|
||||||
|
- Users can continue using the app while updates download in the background
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Updates Not Working
|
||||||
|
|
||||||
|
1. **Check GitHub Releases**: Ensure releases are published on GitHub
|
||||||
|
2. **Check Version**: Make sure the version in `package.json` is higher than the current release
|
||||||
|
3. **Check Logs**: Check the app logs for update-related errors
|
||||||
|
4. **Code Signing (macOS)**: Verify the app is properly code-signed
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
- **"Update not available"**: Version in `package.json` may not be higher than the current release
|
||||||
|
- **"Download failed"**: Network issues or GitHub API rate limits
|
||||||
|
- **"Installation failed"**: Permissions issue or app is running from an unsupported location
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Supported Platforms
|
||||||
|
|
||||||
|
- **Windows**: NSIS installer (auto-update supported)
|
||||||
|
- **macOS**: DMG + ZIP (auto-update supported, requires code signing)
|
||||||
|
- **Linux**: AppImage, DEB, RPM, Pacman (auto-update supported)
|
||||||
|
|
||||||
|
### Update Files Generated
|
||||||
|
|
||||||
|
When building, electron-builder generates:
|
||||||
|
- `latest.yml` (Windows/Linux)
|
||||||
|
- `latest-mac.yml` (macOS)
|
||||||
|
- `latest-linux.yml` (Linux)
|
||||||
|
|
||||||
|
These files contain metadata about the latest release and are automatically uploaded to GitHub Releases.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [electron-updater Documentation](https://www.electron.build/auto-update)
|
||||||
|
- [electron-builder Auto Update Guide](https://www.electron.build/auto-update)
|
||||||
78
docs/CLEAR-UPDATE-CACHE.md
Normal file
78
docs/CLEAR-UPDATE-CACHE.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Clearing Electron-Updater Cache
|
||||||
|
|
||||||
|
To force electron-updater to re-download an update file, you need to clear the cached download.
|
||||||
|
|
||||||
|
## Quick Method (Terminal)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
```bash
|
||||||
|
# Remove the entire cache directory
|
||||||
|
rm -rf ~/Library/Caches/hytale-f2p-launcher
|
||||||
|
|
||||||
|
# Or just remove pending downloads
|
||||||
|
rm -rf ~/Library/Caches/hytale-f2p-launcher/pending
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
```bash
|
||||||
|
# Remove the entire cache directory
|
||||||
|
rmdir /s "%LOCALAPPDATA%\hytale-f2p-launcher-updater"
|
||||||
|
|
||||||
|
# Or just remove pending downloads
|
||||||
|
rmdir /s "%LOCALAPPDATA%\hytale-f2p-launcher-updater\pending"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
```bash
|
||||||
|
# Remove the entire cache directory
|
||||||
|
rm -rf ~/.cache/hytale-f2p-launcher-updater
|
||||||
|
|
||||||
|
# Or just remove pending downloads
|
||||||
|
rm -rf ~/.cache/hytale-f2p-launcher-updater/pending
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Locations
|
||||||
|
|
||||||
|
electron-updater stores downloaded updates in:
|
||||||
|
|
||||||
|
- **macOS**: `~/Library/Caches/hytale-f2p-launcher/`
|
||||||
|
- **Windows**: `%LOCALAPPDATA%\hytale-f2p-launcher-updater\`
|
||||||
|
- **Linux**: `~/.cache/hytale-f2p-launcher-updater/`
|
||||||
|
|
||||||
|
The cache typically contains:
|
||||||
|
- `pending/` - Downloaded update files waiting to be installed
|
||||||
|
- Metadata files about available updates
|
||||||
|
|
||||||
|
## After Clearing
|
||||||
|
|
||||||
|
After clearing the cache:
|
||||||
|
1. Restart the launcher
|
||||||
|
2. It will check for updates again
|
||||||
|
3. The update will be re-downloaded from scratch
|
||||||
|
|
||||||
|
## Programmatic Method
|
||||||
|
|
||||||
|
You can also clear the cache programmatically by adding this to your code:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
function clearUpdateCache() {
|
||||||
|
const cacheDir = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
process.platform === 'win32'
|
||||||
|
? 'AppData/Local/hytale-f2p-launcher-updater'
|
||||||
|
: process.platform === 'darwin'
|
||||||
|
? 'Library/Caches/hytale-f2p-launcher'
|
||||||
|
: '.cache/hytale-f2p-launcher-updater'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(cacheDir)) {
|
||||||
|
fs.rmSync(cacheDir, { recursive: true, force: true });
|
||||||
|
console.log('Update cache cleared');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
196
docs/TESTING-UPDATES.md
Normal file
196
docs/TESTING-UPDATES.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# Testing Auto-Updates
|
||||||
|
|
||||||
|
This guide explains how to test the auto-update system during development.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Option 1: Test with GitHub Releases (Easiest)
|
||||||
|
|
||||||
|
1. **Set up dev-app-update.yml** (already done):
|
||||||
|
```yaml
|
||||||
|
provider: github
|
||||||
|
owner: amiayweb
|
||||||
|
repo: Hytale-F2P
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Lower your current version** in `package.json`:
|
||||||
|
- Change version to something lower than what's on GitHub (e.g., `2.0.1` if GitHub has `2.0.3`)
|
||||||
|
|
||||||
|
3. **Run the app in dev mode**:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **The app will check for updates** 3 seconds after startup
|
||||||
|
- If a newer version exists on GitHub, it will detect it
|
||||||
|
- Check the console logs for update messages
|
||||||
|
|
||||||
|
### Option 2: Test with Local HTTP Server
|
||||||
|
|
||||||
|
For more control, you can set up a local server:
|
||||||
|
|
||||||
|
1. **Create a test update server**:
|
||||||
|
```bash
|
||||||
|
# Create a test directory
|
||||||
|
mkdir -p test-updates
|
||||||
|
cd test-updates
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build a test version** with a higher version number:
|
||||||
|
```bash
|
||||||
|
# In package.json, set version to 2.0.4
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Copy the generated files** to your test server:
|
||||||
|
- Copy `dist/latest.yml` (or `latest-mac.yml` for macOS)
|
||||||
|
- Copy the built installer/package
|
||||||
|
|
||||||
|
4. **Start a simple HTTP server**:
|
||||||
|
```bash
|
||||||
|
# Using Python
|
||||||
|
python3 -m http.server 8080
|
||||||
|
|
||||||
|
# Or using Node.js http-server
|
||||||
|
npx http-server -p 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Update dev-app-update.yml** to point to local server:
|
||||||
|
```yaml
|
||||||
|
provider: generic
|
||||||
|
url: http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Run the app** and it will check your local server
|
||||||
|
|
||||||
|
## Testing Steps
|
||||||
|
|
||||||
|
### 1. Prepare Test Environment
|
||||||
|
|
||||||
|
**Current version**: `2.0.3` (in package.json)
|
||||||
|
**Test version**: `2.0.4` (on GitHub or local server)
|
||||||
|
|
||||||
|
### 2. Run the App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Watch for Update Events
|
||||||
|
|
||||||
|
The app will automatically check for updates 3 seconds after startup. Watch the console for:
|
||||||
|
|
||||||
|
```
|
||||||
|
Checking for updates...
|
||||||
|
Update available: 2.0.4
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Check Console Logs
|
||||||
|
|
||||||
|
Look for these messages:
|
||||||
|
- `Checking for updates...` - Update check started
|
||||||
|
- `Update available: 2.0.4` - New version found
|
||||||
|
- `Download speed: ...` - Download progress
|
||||||
|
- `Update downloaded: 2.0.4` - Download complete
|
||||||
|
|
||||||
|
### 5. Test UI Integration
|
||||||
|
|
||||||
|
The app sends these events to the renderer:
|
||||||
|
- `update-checking`
|
||||||
|
- `update-available` (with version info)
|
||||||
|
- `update-download-progress` (with progress data)
|
||||||
|
- `update-downloaded` (ready to install)
|
||||||
|
|
||||||
|
You can listen to these in your frontend code to show update notifications.
|
||||||
|
|
||||||
|
## Manual Testing
|
||||||
|
|
||||||
|
### Trigger Manual Update Check
|
||||||
|
|
||||||
|
You can also trigger a manual check via IPC:
|
||||||
|
```javascript
|
||||||
|
// In renderer process
|
||||||
|
const result = await window.electronAPI.invoke('check-for-updates');
|
||||||
|
console.log(result);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Update
|
||||||
|
|
||||||
|
After an update is downloaded:
|
||||||
|
```javascript
|
||||||
|
// In renderer process
|
||||||
|
await window.electronAPI.invoke('quit-and-install-update');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Update Available
|
||||||
|
1. Set `package.json` version to `2.0.1`
|
||||||
|
2. Ensure GitHub has version `2.0.3` or higher
|
||||||
|
3. Run app → Should detect update
|
||||||
|
|
||||||
|
### Scenario 2: Already Up to Date
|
||||||
|
1. Set `package.json` version to `2.0.3`
|
||||||
|
2. Ensure GitHub has version `2.0.3` or lower
|
||||||
|
3. Run app → Should show "no update available"
|
||||||
|
|
||||||
|
### Scenario 3: Prerelease Version
|
||||||
|
1. Set `package.json` version to `2.0.2b`
|
||||||
|
2. Ensure GitHub has version `2.0.3`
|
||||||
|
3. Run app → Should detect update (prerelease < release)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Update Not Detected
|
||||||
|
|
||||||
|
1. **Check dev-app-update.yml exists** in project root
|
||||||
|
2. **Verify dev mode is enabled** - Check console for "Dev update mode enabled"
|
||||||
|
3. **Check version numbers** - Remote version must be higher than current
|
||||||
|
4. **Check network** - App needs internet to reach GitHub/local server
|
||||||
|
5. **Check logs** - Look for error messages in console
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
- **"Cannot find module 'electron-updater'"**: Run `npm install`
|
||||||
|
- **"Update check failed"**: Check network connection or GitHub API access
|
||||||
|
- **"No update available"**: Version comparison issue - check versions
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable more verbose logging by checking the console output. The logger will show:
|
||||||
|
- Update check requests
|
||||||
|
- Version comparisons
|
||||||
|
- Download progress
|
||||||
|
- Any errors
|
||||||
|
|
||||||
|
## Testing with Real GitHub Releases
|
||||||
|
|
||||||
|
For the most realistic test:
|
||||||
|
|
||||||
|
1. **Create a test release on GitHub**:
|
||||||
|
- Build the app with version `2.0.4`
|
||||||
|
- Create a GitHub release with tag `v2.0.4`
|
||||||
|
- Upload the built files
|
||||||
|
|
||||||
|
2. **Lower your local version**:
|
||||||
|
- Set `package.json` to `2.0.3`
|
||||||
|
|
||||||
|
3. **Run the app**:
|
||||||
|
- It will check GitHub and find `2.0.4`
|
||||||
|
- Download and install the update
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Dev mode only works when app is NOT packaged** (`!app.isPackaged`)
|
||||||
|
- **Production builds** ignore `dev-app-update.yml` and use the built-in `app-update.yml`
|
||||||
|
- **macOS**: Code signing is required for updates to work in production
|
||||||
|
- **Windows**: NSIS installer is required for auto-updates
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Once testing is complete:
|
||||||
|
1. Remove or comment out `forceDevUpdateConfig` for production
|
||||||
|
2. Ensure proper code signing for macOS
|
||||||
|
3. Set up CI/CD to automatically publish releases
|
||||||
153
main.js
153
main.js
@@ -1,19 +1,36 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||||||
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
||||||
|
|
||||||
const UpdateManager = require('./backend/updateManager');
|
const UpdateManager = require('./backend/updateManager');
|
||||||
const logger = require('./backend/logger');
|
const logger = require('./backend/logger');
|
||||||
const profileManager = require('./backend/managers/profileManager');
|
const profileManager = require('./backend/managers/profileManager');
|
||||||
|
|
||||||
logger.interceptConsole();
|
logger.interceptConsole();
|
||||||
|
|
||||||
|
// Single instance lock
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
console.log('Another instance is already running. Quitting...');
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let updateManager;
|
let updateManager;
|
||||||
let discordRPC = null;
|
let discordRPC = null;
|
||||||
|
|
||||||
// Discord Rich Presence setup
|
// Discord Rich Presence setup
|
||||||
const DISCORD_CLIENT_ID = '1462244937868513373';
|
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
|
||||||
|
|
||||||
function initDiscordRPC() {
|
function initDiscordRPC() {
|
||||||
try {
|
try {
|
||||||
@@ -80,19 +97,47 @@ function toggleDiscordRPC(enabled) {
|
|||||||
console.log('Discord RPC disconnected successfully');
|
console.log('Discord RPC disconnected successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error disconnecting Discord RPC:', error.message);
|
console.error('Error disconnecting Discord RPC:', error.message);
|
||||||
discordRPC = null; // Force null même en cas d'erreur
|
discordRPC = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSplashScreen() {
|
||||||
|
const splashWindow = new BrowserWindow({
|
||||||
|
width: 500,
|
||||||
|
height: 350,
|
||||||
|
frame: false,
|
||||||
|
transparent: true,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
resizable: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
splashWindow.loadFile('GUI/splash.html');
|
||||||
|
splashWindow.center();
|
||||||
|
|
||||||
|
// close splash after 2.5s , need to implement a files check or whatever. just mock for now
|
||||||
|
setTimeout(() => {
|
||||||
|
splashWindow.close();
|
||||||
|
createWindow();
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 600,
|
||||||
frame: false,
|
frame: false,
|
||||||
resizable: false,
|
resizable: true,
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
backgroundColor: '#090909',
|
backgroundColor: '#090909',
|
||||||
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
@@ -104,6 +149,10 @@ function createWindow() {
|
|||||||
|
|
||||||
mainWindow.loadFile('GUI/index.html');
|
mainWindow.loadFile('GUI/index.html');
|
||||||
|
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
// Cleanup Discord RPC when window is closed
|
// Cleanup Discord RPC when window is closed
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
console.log('Main window closed, cleaning up Discord RPC...');
|
console.log('Main window closed, cleaning up Discord RPC...');
|
||||||
@@ -141,9 +190,20 @@ function createWindow() {
|
|||||||
if (input.key === 'F5') {
|
if (input.key === 'F5') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close application shortcuts
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
|
||||||
|
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
|
||||||
|
(!isMac && input.alt && input.key === 'F4');
|
||||||
|
|
||||||
|
if (quitShortcut) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mainWindow.webContents.on('context-menu', (e) => {
|
mainWindow.webContents.on('context-menu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
@@ -152,7 +212,9 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
const packageJson = require('./package.json');
|
||||||
console.log('=== HYTALE F2P LAUNCHER STARTED ===');
|
console.log('=== HYTALE F2P LAUNCHER STARTED ===');
|
||||||
|
console.log('Launcher version:', packageJson.version);
|
||||||
console.log('Platform:', process.platform);
|
console.log('Platform:', process.platform);
|
||||||
console.log('Architecture:', process.arch);
|
console.log('Architecture:', process.arch);
|
||||||
console.log('Electron version:', process.versions.electron);
|
console.log('Electron version:', process.versions.electron);
|
||||||
@@ -177,7 +239,7 @@ app.whenReady().then(async () => {
|
|||||||
// Initialize Profile Manager (runs migration if needed)
|
// Initialize Profile Manager (runs migration if needed)
|
||||||
profileManager.init();
|
profileManager.init();
|
||||||
|
|
||||||
createWindow();
|
createSplashScreen();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
let timeoutReached = false;
|
let timeoutReached = false;
|
||||||
@@ -288,11 +350,10 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
cleanupDiscordRPC();
|
cleanupDiscordRPC();
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
app.quit();
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
|
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
|
||||||
try {
|
try {
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||||
@@ -310,7 +371,18 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
|
|||||||
|
|
||||||
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
|
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
|
||||||
|
|
||||||
|
if (result.success && result.launched) {
|
||||||
|
const closeOnStart = loadCloseLauncherOnStart();
|
||||||
|
if (closeOnStart) {
|
||||||
|
console.log('Close Launcher on start enabled, quitting application...');
|
||||||
|
setTimeout(() => {
|
||||||
|
app.quit();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Launch error:', error);
|
console.error('Launch error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
@@ -327,6 +399,11 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
|
|||||||
|
|
||||||
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
|
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
|
||||||
try {
|
try {
|
||||||
|
// Signal installation start
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('installation-start');
|
||||||
|
}
|
||||||
|
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const data = {
|
const data = {
|
||||||
@@ -342,11 +419,21 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath)
|
|||||||
|
|
||||||
const result = await installGame(playerName, progressCallback, javaPath, installPath);
|
const result = await installGame(playerName, progressCallback, javaPath, installPath);
|
||||||
|
|
||||||
|
// Signal installation end
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('installation-end');
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Install error:', error);
|
console.error('Install error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
|
// Signal installation end on error too
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('installation-end');
|
||||||
|
}
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
return { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -405,7 +492,26 @@ ipcMain.handle('load-discord-rpc', () => {
|
|||||||
return loadDiscordRPC();
|
return loadDiscordRPC();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-language', (event, language) => {
|
||||||
|
saveLanguage(language);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-language', () => {
|
||||||
|
return loadLanguage();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-close-launcher', (event, enabled) => {
|
||||||
|
saveCloseLauncherOnStart(enabled);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-close-launcher', () => {
|
||||||
|
return loadCloseLauncherOnStart();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('select-install-path', async () => {
|
ipcMain.handle('select-install-path', async () => {
|
||||||
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ['openDirectory'],
|
properties: ['openDirectory'],
|
||||||
title: 'Select Installation Folder'
|
title: 'Select Installation Folder'
|
||||||
@@ -617,6 +723,10 @@ ipcMain.handle('get-local-app-data', async () => {
|
|||||||
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-env-var', async (event, key) => {
|
||||||
|
return process.env[key];
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-user-id', async () => {
|
ipcMain.handle('get-user-id', async () => {
|
||||||
try {
|
try {
|
||||||
const { getOrCreatePlayerId } = require('./backend/launcher');
|
const { getOrCreatePlayerId } = require('./backend/launcher');
|
||||||
@@ -727,11 +837,10 @@ ipcMain.handle('open-download-page', async () => {
|
|||||||
await shell.openExternal(updateManager.getDownloadUrl());
|
await shell.openExternal(updateManager.getDownloadUrl());
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
app.quit();
|
||||||
mainWindow.close();
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening download page:', error);
|
console.error('Error opening download page:', error);
|
||||||
@@ -773,17 +882,31 @@ ipcMain.handle('get-detected-gpu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('window-close', () => {
|
ipcMain.handle('window-close', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
app.quit();
|
||||||
mainWindow.close();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('window-minimize', () => {
|
ipcMain.handle('window-minimize', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.minimize();
|
mainWindow.minimize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-maximize', () => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-version', () => {
|
||||||
|
const packageJson = require('./package.json');
|
||||||
|
return packageJson.version;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-log-directory', () => {
|
ipcMain.handle('get-log-directory', () => {
|
||||||
return logger.getLogDirectory();
|
return logger.getLogDirectory();
|
||||||
});
|
});
|
||||||
|
|||||||
133
package-lock.json
generated
133
package-lock.json
generated
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcherv2",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.2b",
|
"version": "2.0.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hytale-f2p-launcherv2",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.2b",
|
"version": "2.0.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"electron-updater": "^6.7.3",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
@@ -1018,6 +1020,19 @@
|
|||||||
"electron-builder-squirrel-windows": "26.4.0"
|
"electron-builder-squirrel-windows": "26.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/fs-extra": {
|
"node_modules/app-builder-lib/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
@@ -1073,7 +1088,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/assert-plus": {
|
"node_modules/assert-plus": {
|
||||||
@@ -1283,7 +1297,6 @@
|
|||||||
"version": "9.5.1",
|
"version": "9.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
||||||
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -1711,7 +1724,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -1962,10 +1974,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "17.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -1990,6 +2001,19 @@
|
|||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv-expand/node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -2178,6 +2202,69 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-updater": {
|
||||||
|
"version": "6.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz",
|
||||||
|
"integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"builder-util-runtime": "9.5.1",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lazy-val": "^1.0.5",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"semver": "~7.7.3",
|
||||||
|
"tiny-typed-emitter": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/fs-extra": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/jsonfile": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-winstaller": {
|
"node_modules/electron-winstaller": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
||||||
@@ -2759,7 +2846,6 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
@@ -3082,7 +3168,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -3150,7 +3235,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
@@ -3160,6 +3244,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/log-symbols": {
|
"node_modules/log-symbols": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||||
@@ -3476,7 +3573,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
@@ -4131,7 +4227,6 @@
|
|||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
@@ -4602,6 +4697,12 @@
|
|||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-typed-emitter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|||||||
73
package.json
73
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcherv2",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.2b",
|
"version": "2.0.11",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"mod-manager",
|
"mod-manager",
|
||||||
"chat"
|
"chat"
|
||||||
],
|
],
|
||||||
"maintainers": [
|
"maintainers": [
|
||||||
{
|
{
|
||||||
"name": "Terromur",
|
"name": "Terromur",
|
||||||
"url": "https://github.com/Terromur"
|
"url": "https://github.com/Terromur"
|
||||||
@@ -48,6 +48,8 @@
|
|||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"electron-updater": "^6.7.3",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "com.hytalef2p.launcher",
|
"appId": "com.hytalef2p.launcher",
|
||||||
"productName": "Hytale F2P Launcher",
|
"productName": "Hytale F2P Launcher",
|
||||||
"executableName": "hytale-f2p-launcherv2",
|
"artifactName": "${name}_${version}_${arch}.${ext}",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
@@ -66,29 +68,69 @@
|
|||||||
"preload.js",
|
"preload.js",
|
||||||
"backend/**/*",
|
"backend/**/*",
|
||||||
"GUI/**/*",
|
"GUI/**/*",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
".env"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{ "target": "nsis", "arch": ["x64", "arm64"] },
|
{
|
||||||
{ "target": "portable", "arch": ["x64"] }
|
"target": "nsis",
|
||||||
|
"arch": [
|
||||||
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon.ico"
|
"icon": "icon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
{ "target": "AppImage", "arch": ["x64", "arm64"] },
|
{
|
||||||
{ "target": "deb", "arch": ["x64", "arm64"] },
|
"target": "AppImage",
|
||||||
{ "target": "rpm", "arch": ["x64", "arm64"] },
|
"arch": [
|
||||||
{ "target": "pacman", "arch": ["x64", "arm64"] }
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "deb",
|
||||||
|
"arch": [
|
||||||
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "rpm",
|
||||||
|
"arch": [
|
||||||
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "pacman",
|
||||||
|
"arch": [
|
||||||
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.png",
|
"icon": "build/icon.png",
|
||||||
"category": "Game"
|
"category": "Game"
|
||||||
},
|
},
|
||||||
"mac": {
|
"mac": {
|
||||||
"target": [
|
"target": [
|
||||||
{ "target": "dmg", "arch": ["universal"] },
|
{
|
||||||
{ "target": "zip", "arch": ["universal"] }
|
"target": "dmg",
|
||||||
|
"arch": [
|
||||||
|
"universal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "zip",
|
||||||
|
"arch": [
|
||||||
|
"universal"
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.icns",
|
"icon": "build/icon.icns",
|
||||||
"category": "public.app-category.games"
|
"category": "public.app-category.games"
|
||||||
@@ -98,6 +140,11 @@
|
|||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true
|
"createStartMenuShortcut": true
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "amiayweb",
|
||||||
|
"repo": "Hytale-F2P"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
preload.js
13
preload.js
@@ -5,6 +5,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
|
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
|
||||||
closeWindow: () => ipcRenderer.invoke('window-close'),
|
closeWindow: () => ipcRenderer.invoke('window-close'),
|
||||||
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
||||||
|
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
|
||||||
|
getVersion: () => ipcRenderer.invoke('get-version'),
|
||||||
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
|
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
|
||||||
loadUsername: () => ipcRenderer.invoke('load-username'),
|
loadUsername: () => ipcRenderer.invoke('load-username'),
|
||||||
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
|
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
|
||||||
@@ -17,6 +19,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
|
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
|
||||||
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
|
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
|
||||||
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
|
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
|
||||||
|
saveLanguage: (language) => ipcRenderer.invoke('save-language', language),
|
||||||
|
loadLanguage: () => ipcRenderer.invoke('load-language'),
|
||||||
|
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
|
||||||
|
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
|
||||||
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
||||||
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
||||||
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
||||||
@@ -28,6 +34,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
openGameLocation: () => ipcRenderer.invoke('open-game-location'),
|
openGameLocation: () => ipcRenderer.invoke('open-game-location'),
|
||||||
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
|
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
|
||||||
loadSettings: () => ipcRenderer.invoke('load-settings'),
|
loadSettings: () => ipcRenderer.invoke('load-settings'),
|
||||||
|
getEnvVar: (key) => ipcRenderer.invoke('get-env-var', key),
|
||||||
getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'),
|
getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'),
|
||||||
getModsPath: () => ipcRenderer.invoke('get-mods-path'),
|
getModsPath: () => ipcRenderer.invoke('get-mods-path'),
|
||||||
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
|
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
|
||||||
@@ -42,6 +49,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
onProgressComplete: (callback) => {
|
onProgressComplete: (callback) => {
|
||||||
ipcRenderer.on('progress-complete', () => callback());
|
ipcRenderer.on('progress-complete', () => callback());
|
||||||
},
|
},
|
||||||
|
onInstallationStart: (callback) => {
|
||||||
|
ipcRenderer.on('installation-start', () => callback());
|
||||||
|
},
|
||||||
|
onInstallationEnd: (callback) => {
|
||||||
|
ipcRenderer.on('installation-end', () => callback());
|
||||||
|
},
|
||||||
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||||
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
||||||
|
|||||||
Reference in New Issue
Block a user