Compare commits

...

53 Commits

Author SHA1 Message Date
AMIAY
71974e031f Merge pull request #105 from gaurav-null/readme-tailscale-multiplayer
updated SERVER.md to add platform specification on radmin and add Tai…
2026-01-22 22:45:48 +01:00
guarav-null
5bd52f09db updated SERVER.md to add platform specification on radmin and add Tailscale guide for max compability 2026-01-23 03:05:29 +05:30
AMIAY
828d05ca33 Update README.md 2026-01-22 10:46:09 +01:00
Fazri Gading
1c779e0e41 fix v2.0.2b changelog in README.md file 2026-01-22 02:18:17 +08:00
Fazri Gading
bb474fe233 Update README.md v2.0.2b changelog 2026-01-22 02:17:41 +08:00
Fazri Gading
917f5f455b Merge branch 'develop' 2026-01-22 02:15:36 +08:00
Fazri Gading
1dd42bdc79 Merge pull request #88 from amiayweb/release
merge v2.0.2b release files to main branch
2026-01-22 02:01:01 +08:00
Fazri Gading
7cfe3edd32 Merge branch 'main' into release 2026-01-22 01:44:53 +08:00
Fazri Gading
eb22758ab9 adds v2.0.2b changelog 2026-01-22 00:53:10 +08:00
Fazri Gading
42fd51486a feat: auto-detect GPU for Windows and MacOS (#87) 2026-01-21 22:13:47 +08:00
xSamiVS
9ef05e8322 Added internationalization support (i18n) (#74)
* - Implemented i18n.
- Updated UI elements to use localized strings for various messages and confirmations.
- Added language selection functionality in settings with appropriate event handling.
- Created English localization file with translations for all new strings.
- Updated backend to save and load user-selected language preferences.

* Add Spanish localization for the GUI

* Add Portuguese (Brazil) localization for the GUI

* update main branch to release/v2.0.2b (#86)

* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b

* Update localization for game repair and GPU settings

Added new localization entries for game repair and GPU preferences.

* Update spanish localization for game repair and GPU settings

* Update portuguese (brazil) for game repair and GPU settings

* Update localization for system logs in English, Spanish, and Portuguese

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
2026-01-21 21:41:12 +08:00
Fazri Gading
4ac12e0e24 fix release version name 2026-01-21 21:01:32 +08:00
AMIAY
72a151930e Patch Discord invite URLs in client binary 2026-01-21 13:30:49 +01:00
Fazri Gading
a9644b8c64 remove v2 suffix from name and set consistent artifact name 2026-01-21 20:12:14 +08:00
Fazri Gading
9fc238e103 update executable name to be consistent with product name 2026-01-21 19:53:23 +08:00
Fazri Gading
b62e94a415 fix: package.json Module Not Found in 'Get version' step 2026-01-21 19:53:00 +08:00
Fazri Gading
3e82e8fadb add libarchive-tools for bsdtar in release workflow 2026-01-21 18:38:49 +08:00
Fazri Gading
a355133ccf update main branch to release/v2.0.2b (#86)
* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b
2026-01-21 16:39:18 +08:00
Fazri Gading
ff5acb5278 fix release build naming 2026-01-21 16:24:47 +08:00
Fazri Gading
5d75ca80aa update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap 2026-01-21 16:07:57 +08:00
Fazri Gading
281aa6fcde update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap; 2026-01-21 16:07:12 +08:00
Fazri Gading
158d0af820 update version to 2.0.2b also add deps for rpm and arch 2026-01-21 16:05:20 +08:00
Fazri Gading
503f304704 add package-lock.json to maintain stability development 2026-01-21 16:04:40 +08:00
Fazri Gading
234e1e1008 update .gitignore for local build 2026-01-21 16:04:17 +08:00
Fazri Gading
d002e831cd removed package-lock from gitignore 2026-01-21 07:42:03 +08:00
Fazri Gading
048f2040f1 add more linux pkgs, create auto-release and pre-release feature for Github Actions 2026-01-21 06:41:25 +08:00
Rahul Sahani
b05aeef66d feat: Add Repair Game button, UserData backup and cache clearing (#79)
* feat: Add Repair Game functionality including UserData backup and cache clearing

* feat: Add In-App Logs Viewer and Logs Folder shortcut

* feat: Add Open Logs feature

* disable dev tools

* Fix Settings UI

* fix reorder settings section in index.html

relocated sections in settings from most used to least:
1. game options (playername, opengamedir, repair, GPUpreference)
2. player uuid management
3. discord integration rich presence
4. custom java path

---------

Co-authored-by: Fazri Gading <super.fai700@gmail.com>
2026-01-21 06:27:33 +08:00
Fazri Gading
30265549cf feat: Implement GPU preference system with auto-detection and UI enhancements (#82)
* feat: add GPU options for launcher

- Add GPU preference setting (Auto/Integrated/Dedicated)
- Implement Linux GPU selection with DRI_PRIME and NVIDIA env vars
- Add GPU detection using Electron's app.getGPUInfo()
- Update settings UI with GPU preference dropdown
- Integrate GPU preference into game launch process

* feat: detailed GPU info in auto-detection feature on startup

* add gpudetection feature to launcher backend

* feat: detailed GPU info in auto-detection feature on startup

* feat: auto-detect dedicated GPU on hybrid laptops (iGPU+dGPU)

* add fallbacks to and option to use integrated GPU

* add package-lock and fix deps version

* changed 'Nvidia' string to 'NVIDIA'

* fix: selecting `dedicated` option while using nvidia GPU did not set its specific env variables

* remove unused `CONFIG_FILE` variable on launcher core modules

* fix: duplicated save-load gpu detection functions

* move game option settings to the top, while custom java to the bottom

* fix: settings-header margin-bottom from 3rem to 1rem and supress line-clamp warning
2026-01-21 05:54:17 +08:00
Terromur
5a3efba1d6 Update discription 2026-01-21 01:43:40 +05:00
Terromur
479f24e86f Update PKGBUILD 2026-01-21 01:28:53 +05:00
Fazri Gading
611b7a7084 Merge branch 'main' into main 2026-01-21 04:05:02 +08:00
Terromur
e472435927 Update PKGBUILD 2026-01-21 00:34:52 +05:00
AMIAY
f40d0105df Update package-lock.json 2026-01-20 18:35:26 +01:00
AMIAY
96db9adf68 Update package.json 2026-01-20 18:34:57 +01:00
Fazri Gading
a0f49f126c add libarchive-tools dep for pacman package 2026-01-21 00:53:47 +08:00
Fazri Gading
f8333c09cd Update package.json 2026-01-21 00:43:29 +08:00
Fazri Gading
bcf0326763 Merge branch 'main' of https://github.com/fazrigading/Hytale-F2P 2026-01-21 00:31:20 +08:00
Fazri Gading
0a5c3db710 add 3 package formats for Linux, descriptions, and update launcher version to 2.0.2 2026-01-21 00:27:11 +08:00
Fazri Gading
905a9d754c Merge GPU preference feature branch to main branch version 2.0.2 for testing (#3)
* modernized UI for GPU Preference option

* feat: auto-detect dedicated GPU on hybrid laptops (iGPU+dGPU)

* feat: detailed GPU info in auto-detection feature on startup

* feat: add GPU options for launcher

- Add GPU preference setting (Auto/Integrated/Dedicated)
- Implement Linux GPU selection with DRI_PRIME and NVIDIA env vars
- Add GPU detection using Electron's app.getGPUInfo()
- Update settings UI with GPU preference dropdown
- Integrate GPU preference into game launch process

* feat: auto-detect dedicated GPU on hybrid laptops (iGPU+dGPU)

* added fallbacks to and option to use integrated GPU.

* add package-lock and fix deps version

* changed 'Nvidia' string to 'NVIDIA'

* fix: selecting `dedicated` option while using nvidia GPU did not set its specific env variables

* remove unused `CONFIG_FILE` variable on launcher core modules

* fix: duplicated save-load gpu detection functions

* move game option settings to the top, while custom java to the bottom

* fix: settings-header margin-bottom from 3rem to 1rem and supress line-clamp warning
2026-01-20 23:45:38 +08:00
Fazri Gading
99f032e9ab Merge branch 'main' of https://github.com/amiayweb/Hytale-F2P 2026-01-20 19:37:54 +08:00
AMIAY
5e3506a9ac Update README.md 2026-01-20 09:57:21 +01:00
AMIAY
f2a05d2079 Update release.yml 2026-01-20 09:49:11 +01:00
AMIAY
cece338609 Update release.yml 2026-01-20 09:48:15 +01:00
AMIAY
261582a882 Update release.yml 2026-01-20 09:47:34 +01:00
AMIAY
0f0f0fa308 Update release.yml 2026-01-20 09:46:33 +01:00
AMIAY
5e6a07f0a6 Allow manual trigger for release workflow 2026-01-20 09:42:16 +01:00
AMIAY
713377fdc6 Merge pull request #71 from Rahul-Sahani04/feature/profile-system
Profile System & Mod Loading Fixes
2026-01-20 09:33:20 +01:00
Fazri Gading
2671a59f38 Revert "add save-load GPU preference handler"
This reverts commit b957a76aede8f9425b0a283d1444a32f55baa95a.
2026-01-20 16:26:28 +08:00
Fazri Gading
8e9af9c768 add save-load GPU preference handler 2026-01-20 16:26:28 +08:00
Rahul Sahani
e962a8880d Delete PR_DESCRIPTION.md 2026-01-20 11:57:23 +05:30
Rahul Sahani
c2d5536dd0 Improve comments in profileManager.js
Refactor comments for clarity and conciseness in profileManager.js.
2026-01-20 11:57:01 +05:30
Rahul Sahani
727be2ca5c Improve comments in saveModsToConfig function
Refactor comments in saveModsToConfig function for clarity.
2026-01-20 11:55:18 +05:30
Rahul-Sahani04
64892c81e9 Profile System & Mod Loading Fixes
Added a full profile system and fixed a few critical mod loading issues.

What changed

Profiles — Implemented proper profile management (create, switch, delete). 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 only affects the selected profile.

Critical Fix — Fixed a path bug where mods were being saved to ~/AppData/Local on macOS (Windows path) instead of ~/Library/Application Support. Mods now save to the correct location and load correctly in-game.

Stability — Added an auto-sync step before every launch to make sure the physical mods folder always matches the active profile.

UI — Added a profile selector dropdown and a profile management modal.
2026-01-20 11:52:36 +05:30
32 changed files with 3763 additions and 939 deletions

View File

@@ -3,7 +3,7 @@ name: Build and Release
on:
push:
branches:
- main
- release
tags:
- 'v*'
workflow_dispatch:
@@ -13,19 +13,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npx electron-builder --linux --publish never
- name: Build Linux Packages
run: |
npx electron-builder --linux --x64 --arm64 --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-builds
path: |
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.pacman
dist/latest.yml
build-windows:
runs-on: windows-latest
@@ -35,7 +46,6 @@ jobs:
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4
@@ -43,6 +53,7 @@ jobs:
name: windows-builds
path: |
dist/*.exe
dist/latest.yml
build-macos:
runs-on: macos-latest
@@ -52,7 +63,6 @@ jobs:
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4
@@ -66,12 +76,19 @@ jobs:
release:
needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
if: |
startsWith(github.ref, 'refs/tags/v') ||
github.ref == 'refs/heads/release' ||
github.event_name == 'workflow_dispatch'
permissions:
contents: write
steps:
# FIX: './package.json' Module Not Found in `Get version` step
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
@@ -80,13 +97,22 @@ jobs:
- name: Display structure of downloaded files
run: ls -R artifacts
- name: Get version from package.json
id: pkg_version
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v2
with:
# 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) }}
# 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) }}
files: |
artifacts/linux-builds/*
artifacts/windows-builds/*
artifacts/macos-builds/*
artifacts/linux-builds/**/*
artifacts/windows-builds/**/*
artifacts/macos-builds/**/*
generate_release_notes: true
draft: false
prerelease: false
draft: true
# DYNAMIC FLAGS: Mark as pre-release ONLY IF it's NOT a tag (meaning it's a branch push)
prerelease: ${{ github.ref_type != 'tag' }}

10
.gitignore vendored
View File

@@ -1,3 +1,11 @@
dist/*
node_modules/*
package-lock.json
bun.lock
# Build artifacts
src/
pkg/
# Package files
*.tar.zst
*.zst

View File

@@ -1,19 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P Launcher</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0">
<img src="https://i.imgur.com/Visrk66.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-[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" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/%3E%3C/filter%3E%3Crect width="100%25" height="100%25" filter="url(%23noiseFilter)" opacity="0.1"/%3E%3C/svg%3E')] opacity-20"></div>
<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"
baseFrequency="0.65" numOctaves="3" stitchTiles="stitch" /%3E%3C/filter%3E%3Crect width="100%25"
height="100%25" filter="url(%23noiseFilter)" opacity="0.1" /%3E%3C/svg%3E')] opacity-20"></div>
</div>
<div class="flex w-full h-screen relative z-10">
@@ -26,27 +33,31 @@
<div class="sidebar-nav">
<div class="nav-item active" data-page="play">
<i class="fas fa-play"></i>
<span class="nav-tooltip">Play</span>
<span class="nav-tooltip" data-i18n="nav.play">Play</span>
</div>
<div class="nav-item" data-page="mods">
<i class="fas fa-box"></i>
<span class="nav-tooltip">Mods</span>
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
</div>
<div class="nav-item" data-page="news">
<i class="fas fa-newspaper"></i>
<span class="nav-tooltip">News</span>
<span class="nav-tooltip" data-i18n="nav.news">News</span>
</div>
<div class="nav-item" data-page="chat">
<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 class="nav-item" data-page="settings">
<i class="fas fa-cog"></i>
<span class="nav-tooltip">Settings</span>
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
</div>
<div class="nav-item" data-page="skins">
<i class="fas fa-user"></i>
<span class="nav-tooltip">Skins</span>
<span class="nav-tooltip" data-i18n="nav.skins">Skins</span>
</div>
<div class="nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
<i class="fas fa-terminal"></i>
<span class="nav-tooltip">Logs</span>
</div>
</div>
@@ -57,10 +68,28 @@
<header class="header">
<div id="playersOnlineCounter" class="players-counter">
<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>
</div>
<div class="profile-selector" id="profileSelector">
<button class="profile-btn" onclick="toggleProfileDropdown()">
<i class="fas fa-user-circle"></i>
<span id="currentProfileName">Default</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="profile-dropdown" id="profileDropdown">
<div class="profile-list" id="profileList">
<!-- Profiles populated by JS -->
</div>
<div class="profile-divider"></div>
<div class="profile-action" onclick="openProfileManager()">
<i class="fas fa-cog"></i>
<span data-i18n="header.manageProfiles">Manage Profiles</span>
</div>
</div>
</div>
<div class="window-controls">
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
<i class="fas fa-minus"></i>
@@ -76,7 +105,7 @@
HY<span class="title-accent">TALE</span>
</h1>
<div class="game-tags">
<span class="tag">FREE TO PLAY</span>
<span class="tag" data-i18n="header.f2p">FREE TO PLAY</span>
</div>
</div>
@@ -87,26 +116,28 @@
<h1 class="install-title">
HYTA<span class="title-accent">LE</span>
</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 class="install-form">
<div class="form-group">
<label class="form-label">Player Name</label>
<input type="text" id="installPlayerName" placeholder="Enter your name" class="form-input" value="Player" />
<label class="form-label" data-i18n="install.playerName">Player Name</label>
<input type="text" id="installPlayerName" data-i18n-placeholder="install.playerNamePlaceholder"
class="form-input" value="Player" />
</div>
<div class="form-group">
<label class="checkbox-group">
<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>
<div id="installCustomOptions" class="custom-options">
<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">
<input type="text" id="installPath" placeholder="Default location" class="form-input" readonly />
<input type="text" id="installPath" data-i18n-placeholder="install.pathPlaceholder"
class="form-input" readonly />
<button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i>
</button>
@@ -117,7 +148,7 @@
<button id="installBtn" class="install-button" onclick="installGame()">
<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>
</div>
</div>
@@ -130,14 +161,14 @@
<div class="play-header">
<h2 class="play-title">
<i class="fas fa-play-circle mr-2"></i>
READY TO PLAY
<span data-i18n="play.ready">READY TO PLAY</span>
</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>
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
<i class="fas fa-play"></i>
<span>PLAY HYTALE</span>
<span data-i18n="play.playButton">PLAY HYTALE</span>
</button>
</div>
</div>
@@ -146,10 +177,10 @@
<div class="news-header">
<h2 class="news-title">
<i class="fas fa-star mr-2"></i>
LATEST NEWS
<span data-i18n="play.latestNews">LATEST NEWS</span>
</h2>
<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>
</div>
<div id="newsGrid" class="news-grid-horizontal"></div>
@@ -160,12 +191,12 @@
<div class="mods-header">
<div class="mods-search-container">
<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 class="mods-actions">
<button id="myModsBtn" class="mods-btn-primary">
<i class="fas fa-box"></i>
MY MODS
<span data-i18n="mods.myMods">MY MODS</span>
</button>
</div>
</div>
@@ -176,13 +207,13 @@
<div class="mods-pagination">
<button id="prevPage" class="pagination-btn">
<i class="fas fa-chevron-left"></i>
PREVIOUS
<span data-i18n="mods.previous">PREVIOUS</span>
</button>
<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>
<button id="nextPage" class="pagination-btn">
NEXT
<span data-i18n="mods.next">NEXT</span>
<i class="fas fa-chevron-right"></i>
</button>
</div>
@@ -192,7 +223,7 @@
<div class="news-header">
<h2 class="news-title">
<i class="fas fa-newspaper mr-2"></i>
ALL NEWS
<span data-i18n="news.title">ALL NEWS</span>
</h2>
</div>
<div id="allNewsGrid" class="news-grid-full"></div>
@@ -203,16 +234,16 @@
<div class="chat-header">
<h2 class="chat-title">
<i class="fas fa-comments mr-2"></i>
PLAYERS CHAT
<span data-i18n="chat.title">PLAYERS CHAT</span>
</h2>
<div class="chat-header-actions">
<button id="chatColorBtn" class="chat-color-btn">
<i class="fas fa-palette"></i>
<span>Color</span>
<span data-i18n="chat.pickColor">Color</span>
</button>
<div class="chat-online-badge">
<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>
@@ -224,13 +255,9 @@
<div class="chat-footer">
<div class="chat-input-container">
<textarea
id="chatInput"
class="chat-input"
placeholder="Type your message... (Links are automatically censored)"
rows="1"
maxlength="500"
></textarea>
<textarea id="chatInput" class="chat-input"
data-i18n-placeholder="chat.inputPlaceholder" rows="1"
maxlength="500"></textarea>
<button id="chatSendBtn" class="chat-send-btn">
<i class="fas fa-paper-plane"></i>
</button>
@@ -239,7 +266,7 @@
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
<span class="chat-warning-text">
<i class="fas fa-shield-alt"></i>
Secure chat - Links are censored
<span data-i18n="chat.secureChat">Secure chat - Links are censored</span>
</span>
</div>
</div>
@@ -251,133 +278,98 @@
<div class="settings-header">
<h2 class="settings-title">
<i class="fas fa-cog mr-2"></i>
SETTINGS
<span data-i18n="settings.title">SETTINGS</span>
</h2>
</div>
<div class="settings-content">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
Java Runtime
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div>
<div class="checkbox-description">Override the bundled Java runtime with your own installation</div>
</div>
</label>
</div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label>
<div class="settings-input-with-button">
<input
type="text"
id="customJavaPath"
class="settings-input"
placeholder="Select Java path..."
readonly
/>
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
Browse
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select the Java installation folder (supports Windows, Mac, Linux)
</p>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-gamepad"></i>
Game Options
<span data-i18n="settings.game">Game Options</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label">Player Name</label>
<input
type="text"
id="settingsPlayerName"
class="settings-input"
placeholder="Enter your player name"
maxlength="16"
/>
<label class="settings-input-label" data-i18n="settings.playerName">Player Name</label>
<input type="text" id="settingsPlayerName" class="settings-input"
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
<p class="settings-hint">
<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>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="openGameLocationBtn" class="settings-action-btn" onclick="openGameLocation()">
<button id="openGameLocationBtn" class="settings-action-btn"
onclick="openGameLocation()">
<i class="fas fa-folder-open"></i>
<div class="btn-content">
<div class="btn-title">Open Game Location</div>
<div class="btn-description">Open the game installation folder</div>
<div class="btn-title" data-i18n="settings.openGameLocation">Open Game Location</div>
<div class="btn-description" data-i18n="settings.openGameLocationDesc">Open the game installation folder</div>
</div>
</button>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="repairGameBtn" class="settings-action-btn"
onclick="repairGame()">
<i class="fas fa-tools"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.repairGame">Repair Game</div>
<div class="btn-description" data-i18n="settings.reinstallGame">Reinstall game files (preserves data)
</div>
</div>
</button>
</div>
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference" value="integrated">
<label for="gpu-integrated" data-i18n="settings.gpuIntegrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference" value="dedicated">
<label for="gpu-dedicated" data-i18n="settings.gpuDedicated">Dedicated</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux: affects DRI_PRIME)</span>
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i>
Player UUID Management
<span data-i18n="settings.account">Player UUID Management</span>
</h3>
<div class="settings-option">
<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">
<input
type="text"
id="currentUuid"
class="settings-input uuid-input"
readonly
placeholder="Loading UUID..."
/>
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn" title="Generate New UUID">
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<p class="settings-hint">
<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>
</div>
</div>
@@ -387,13 +379,85 @@
<button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i>
<div class="btn-content">
<div class="btn-title">Manage All UUIDs</div>
<div class="btn-description">View and manage all player UUIDs</div>
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All UUIDs</div>
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">View and manage all player UUIDs</div>
</div>
</button>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
<span data-i18n="settings.discord">Discord Integration</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable Discord Rich Presence</div>
<div class="checkbox-description" data-i18n="settings.discordDescription">Show your launcher activity on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
<span data-i18n="settings.java">Java Runtime</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use Custom Java Path</div>
<div class="checkbox-description" data-i18n="settings.javaDescription">Override the bundled Java runtime with
your own installation</div>
</div>
</label>
</div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.javaPath">Java Executable Path</label>
<div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input"
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
<span data-i18n="settings.javaBrowse">Browse</span>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.javaHint">Select the Java installation folder (supports Windows, Mac, Linux)</span>
</p>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-language"></i>
<span data-i18n="settings.language">Language</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.selectLanguage">Select Language</label>
<select id="languageSelect" class="settings-input">
<!-- Options populated by i18n.js -->
</select>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -401,8 +465,32 @@
<div id="skins-page" class="page">
<div class="placeholder-content">
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
<h2>Skins</h2>
<p>Skin customization coming soon...</p>
<h2 data-i18n="skins.title">Skins</h2>
<p data-i18n="skins.comingSoon">Skin customization coming soon...</p>
</div>
</div>
<div id="logs-page" class="page">
<div class="logs-container">
<div class="logs-header">
<h2 class="logs-title">
<i class="fas fa-terminal"></i>
<span data-i18n="settings.logs">SYSTEM LOGS</span>
</h2>
<div class="logs-actions">
<button class="logs-action-btn" onclick="copyLogs()">
<i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
</button>
<button class="logs-action-btn" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> <span data-i18n="settings.logsRefresh">Refresh</span>
</button>
<button class="logs-action-btn" onclick="openLogsFolder()">
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open Folder</span>
</button>
</div>
</div>
<div id="logsTerminal" class="logs-terminal">
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading logs...</div>
</div>
</div>
</div>
@@ -415,7 +503,7 @@
<div class="mods-modal-header">
<h2 class="mods-modal-title">
<i class="fas fa-box mr-2"></i>
MY MODS
<span data-i18n="mods.modalTitle">MY MODS</span>
</h2>
<button id="closeMyModsModal" class="mods-modal-close">
<i class="fas fa-times"></i>
@@ -431,7 +519,7 @@
<div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content">
<div class="progress-info">
<span id="progressText">Initializing...</span>
<span id="progressText" data-i18n="progress.initializing">Initializing...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
@@ -449,35 +537,29 @@
<div class="chat-username-modal-header">
<h2 class="chat-username-modal-title">
<i class="fas fa-comments mr-2"></i>
Join Chat
<span data-i18n="chat.joinChat">Join Chat</span>
</h2>
</div>
<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
</p>
<div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label">Username</label>
<input
type="text"
id="chatUsernameInput"
class="chat-username-input"
placeholder="Enter your username..."
maxlength="20"
autocomplete="off"
/>
<span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span>
<label for="chatUsernameInput" class="chat-username-label" data-i18n="chat.username">Username</label>
<input type="text" id="chatUsernameInput" class="chat-username-input"
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
<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>
</div>
</div>
<div class="chat-username-modal-footer">
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
<i class="fas fa-times"></i>
Cancel
<span data-i18n="common.cancel">Cancel</span>
</button>
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
<i class="fas fa-check"></i>
Join Chat
<span data-i18n="chat.joinButton">Join Chat</span>
</button>
</div>
</div>
@@ -489,7 +571,7 @@
<div class="uuid-modal-header">
<h2 class="uuid-modal-title">
<i class="fas fa-fingerprint mr-2"></i>
UUID Management
<span data-i18n="uuid.modalTitle">UUID Management</span>
</h2>
<button id="uuidModalClose" class="modal-close-btn">
<i class="fas fa-times"></i>
@@ -498,13 +580,14 @@
<div class="uuid-modal-body">
<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">
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn" title="Generate New UUID">
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
@@ -512,55 +595,87 @@
<div class="uuid-list-section">
<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">
<i class="fas fa-plus"></i>
Generate New UUID
<span data-i18n="uuid.generateNew">Generate New UUID</span>
</button>
</div>
<div id="uuidList" class="uuid-list">
<div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i>
Loading UUIDs...
<span data-i18n="uuid.loadingUUIDs">Loading UUIDs...</span>
</div>
</div>
</div>
<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">
<input
type="text"
id="customUuidInput"
class="uuid-input"
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
maxlength="36"
/>
<input type="text" id="customUuidInput" class="uuid-input"
data-i18n-placeholder="uuid.customPlaceholder"
maxlength="36" />
<button id="setCustomUuidBtn" class="uuid-set-btn">
<i class="fas fa-check"></i>
Set UUID
<span data-i18n="uuid.setUUID">Set UUID</span>
</button>
</div>
<p class="uuid-custom-hint">
<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>
</div>
</div>
</div>
</div>
<!-- Profile Manager Modal -->
<div id="profileManagerModal" class="profile-modal" style="display: none;">
<div class="profile-modal-content">
<div class="profile-modal-header">
<h2 class="profile-modal-title">
<i class="fas fa-users-cog mr-2"></i>
<span data-i18n="profiles.modalTitle">Manage Profiles</span>
</h2>
<button class="modal-close-btn" onclick="closeProfileManager()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="profile-modal-body">
<div class="profile-manager-list" id="managerProfileList">
<!-- Populated by JS -->
</div>
<div class="profile-create-section">
<input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder" class="profile-input"
maxlength="20">
<button class="profile-create-btn" onclick="createNewProfile()">
<i class="fas fa-plus"></i> <span data-i18n="profiles.createProfile">Create Profile</span>
</button>
</div>
</div>
</div>
</div>
<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">
<span>Made by <a href="https://github.com/amiayweb" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a href="https://github.com/Relyz1993" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
<span>Made by <a href="https://github.com/amiayweb" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a
href="https://github.com/Relyz1993" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
<span class="mx-2">|</span>
<span>Contributors:
<a href="https://github.com/chasem-dev" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
<a href="https://github.com/crimera" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
<a href="https://github.com/sanasol" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
<a href="https://github.com/Terromur" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
<a href="https://github.com/ericiskoolbeans" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
<a href="https://github.com/fazrigading" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
<a href="https://github.com/chasem-dev" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
<a href="https://github.com/crimera" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
<a href="https://github.com/sanasol" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
<a href="https://github.com/Terromur" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
<a href="https://github.com/ericiskoolbeans" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
<a href="https://github.com/fazrigading" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
</span>
</div>
</footer>
@@ -569,9 +684,10 @@
<div id="discordNotification" class="discord-notification">
<div class="notification-content">
<i class="fab fa-discord"></i>
<span class="notification-text">Join our Discord community!</span>
<button class="notification-action" onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
Join Discord
<span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span>
<button class="notification-action"
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
<span data-i18n="discord.joinButton">Join Discord</span>
</button>
</div>
<button class="notification-close" onclick="closeDiscordNotification()">
@@ -585,7 +701,7 @@
<div class="chat-color-modal-header">
<h3 class="chat-color-modal-title">
<i class="fas fa-palette"></i>
Customize Username Color
<span data-i18n="chat.colorModal.title">Customize Username Color</span>
</h3>
<button class="modal-close-btn" onclick="closeChatColorModal()">
<i class="fas fa-times"></i>
@@ -593,7 +709,7 @@
</div>
<div class="chat-color-modal-body">
<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="color-option" data-color="#3498db" style="background: #3498db;"></div>
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
@@ -605,24 +721,26 @@
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
</div>
<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">
</div>
</div>
<div class="color-preview">
<h4>Preview:</h4>
<div id="colorPreview" class="preview-username">YourUsername</div>
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">YourUsername</div>
</div>
</div>
<div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button>
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button>
<button class="btn-secondary" onclick="closeChatColorModal()"><span 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>
<script src="js/i18n.js"></script>
<script type="module" src="js/update.js"></script>
</body>
</html>

89
GUI/js/i18n.js Normal file
View 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;

View File

@@ -51,7 +51,7 @@ export async function installGame() {
isDownloading = true;
if (installBtn) {
installBtn.disabled = true;
installText.textContent = 'INSTALLING...';
installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...';
}
try {
@@ -59,8 +59,9 @@ export async function installGame() {
const result = await window.electronAPI.installGame(playerName, '', installPath);
if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
@@ -76,8 +77,9 @@ export async function installGame() {
simulateInstallation(playerName);
}
} catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` });
window.LauncherUI.updateProgress({ message: errorMsg });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetInstallButton();
@@ -92,10 +94,13 @@ function simulateInstallation(playerName) {
progress += Math.random() * 3;
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) {
window.LauncherUI.updateProgress({
percent: progress,
message: progress < 100 ? 'Installing game files...' : 'Installation complete!',
message: progress < 100 ? installingMsg : completeMsg,
speed: 1024 * 1024 * (5 + Math.random() * 10),
downloaded: progress * 1024 * 1024 * 20,
total: 1024 * 1024 * 2000
@@ -104,9 +109,10 @@ function simulateInstallation(playerName) {
if (progress >= 100) {
clearInterval(interval);
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);

View File

@@ -31,8 +31,166 @@ export function setupLauncher() {
}
});
}
// Initial Profile Load
loadProfiles();
// Close dropdown on outside click
document.addEventListener('click', (e) => {
const selector = document.getElementById('profileSelector');
if (selector && !selector.contains(e.target)) {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
}
});
}
// ==========================================
// PROFILE MANAGEMENT
// ==========================================
async function loadProfiles() {
try {
if (!window.electronAPI || !window.electronAPI.profile) return;
const profiles = await window.electronAPI.profile.list();
const activeProfile = await window.electronAPI.profile.getActive();
renderProfileList(profiles, activeProfile);
updateCurrentProfileUI(activeProfile);
} catch (error) {
console.error('Failed to load profiles:', error);
}
}
function renderProfileList(profiles, activeProfile) {
const list = document.getElementById('profileList');
const managerList = document.getElementById('managerProfileList');
if (!list) return;
// Dropdown List
list.innerHTML = profiles.map(p => `
<div class="profile-item ${p.id === activeProfile.id ? 'active' : ''}"
onclick="switchProfile('${p.id}')">
<span>${p.name}</span>
${p.id === activeProfile.id ? '<i class="fas fa-check ml-auto"></i>' : ''}
</div>
`).join('');
// Manager Modal List
if (managerList) {
managerList.innerHTML = profiles.map(p => `
<div class="profile-manager-item ${p.id === activeProfile.id ? 'active' : ''}">
<div class="flex items-center gap-3">
<i class="fas fa-user-circle text-xl text-gray-400"></i>
<div>
<div class="font-bold">${p.name}</div>
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
</div>
</div>
${p.id !== activeProfile.id ? `
<button class="profile-delete-btn" onclick="deleteProfile('${p.id}')" title="Delete Profile">
<i class="fas fa-trash"></i>
</button>
` : '<span class="text-xs text-green-500 font-bold px-2">ACTIVE</span>'}
</div>
`).join('');
}
}
function updateCurrentProfileUI(profile) {
const nameEl = document.getElementById('currentProfileName');
if (nameEl && profile) {
nameEl.textContent = profile.name;
}
}
window.toggleProfileDropdown = () => {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
}
};
window.openProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) {
modal.style.display = 'flex';
// Refresh list
loadProfiles();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
};
window.closeProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) modal.style.display = 'none';
};
window.createNewProfile = async () => {
const input = document.getElementById('newProfileName');
if (!input || !input.value.trim()) return;
try {
const name = input.value.trim();
await window.electronAPI.profile.create(name);
input.value = '';
await loadProfiles();
} catch (error) {
console.error('Failed to create profile:', error);
alert('Failed to create profile: ' + error.message);
}
};
window.deleteProfile = async (id) => {
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
try {
await window.electronAPI.profile.delete(id);
await loadProfiles();
} catch (error) {
console.error('Failed to delete profile:', error);
alert('Failed to delete profile: ' + error.message);
}
};
window.switchProfile = async (id) => {
try {
if (window.LauncherUI) window.LauncherUI.showProgress();
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);
// Refresh UI
await loadProfiles();
// Refresh Mods
if (window.modsManager) {
if (window.modsManager.loadInstalledMods) await window.modsManager.loadInstalledMods();
if (window.modsManager.loadBrowseMods) await window.modsManager.loadBrowseMods();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) {
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!';
window.LauncherUI.updateProgress({ message: switchedMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
}
} catch (error) {
console.error('Failed to switch profile:', error);
alert('Failed to switch profile: ' + error.message);
if (window.LauncherUI) window.LauncherUI.hideProgress();
}
};
export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return;
@@ -48,6 +206,15 @@ export async function launch() {
javaPath = window.SettingsAPI.getCurrentJavaPath();
}
let gpuPreference = 'auto';
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
gpuPreference = await window.electronAPI.loadGpuPreference();
}
} catch (error) {
console.error('Error loading GPU preference:', error);
}
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (playBtn) {
@@ -56,10 +223,11 @@ export async function launch() {
}
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) {
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
isDownloading = false;
@@ -96,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');
if (existingModal) {
existingModal.remove();
@@ -218,22 +391,28 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
}
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(
'Are you sure you want to uninstall Hytale? All game files will be deleted.',
'Uninstall Game',
message,
title,
async () => {
await performUninstall();
},
null,
'Uninstall',
'Cancel'
confirmBtn,
cancelBtn
);
}
async function performUninstall() {
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;
try {
@@ -241,8 +420,9 @@ async function performUninstall() {
const result = await window.electronAPI.uninstallGame();
if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false);
@@ -252,9 +432,10 @@ async function performUninstall() {
throw new Error(result.error || 'Uninstall failed');
}
} else {
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false);
@@ -263,8 +444,9 @@ async function performUninstall() {
}, 2000);
}
} catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.uninstallFailed').replace('{error}', error.message) : `Uninstall failed: ${error.message}`;
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` });
window.LauncherUI.updateProgress({ message: errorMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
}
} finally {
@@ -272,11 +454,54 @@ async function performUninstall() {
}
}
export async function repairGame() {
showCustomConfirm(
'Are you sure you want to repair Hytale? This will reinstall the game files but keep your data (saves, screenshots, etc.).',
'Repair Game',
async () => {
await performRepair();
},
null,
'Repair',
'Cancel'
);
}
async function performRepair() {
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Repairing game...' });
isDownloading = true;
try {
if (window.electronAPI && window.electronAPI.repairGame) {
const result = await window.electronAPI.repairGame();
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game repaired successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
}, 2000);
}
} else {
throw new Error(result.error || 'Repair failed');
}
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Repair failed: ${error.message}` });
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
}
} finally {
isDownloading = false;
}
}
function resetPlayButton() {
isDownloading = false;
if (playBtn) {
playBtn.disabled = false;
playText.textContent = 'PLAY';
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
}
}
@@ -364,5 +589,59 @@ async function loadCustomJavaPath() {
window.launch = launch;
window.uninstallGame = uninstallGame;
window.repairGame = repairGame;
window.openLogs = async () => {
if (window.LauncherUI) {
window.LauncherUI.showPage('logs-page');
window.LauncherUI.setActiveNav('logs');
}
await refreshLogs();
};
window.openLogsFolder = async () => {
try {
if (window.electronAPI && window.electronAPI.openLogsFolder) {
await window.electronAPI.openLogsFolder();
}
} catch (error) {
console.error('Failed to open logs folder:', error);
}
};
window.refreshLogs = async () => {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
try {
if (window.electronAPI && window.electronAPI.getRecentLogs) {
// Fetch up to MAX_LOG_LINES lines
const logs = await window.electronAPI.getRecentLogs(MAX_LOG_LINES);
if (logs) {
// Formatting for colors could be done here if needed
terminal.textContent = logs;
terminal.scrollTop = terminal.scrollHeight;
} else {
terminal.textContent = 'No logs available.';
}
}
} catch (error) {
terminal.textContent = 'Error loading logs: ' + error.message;
}
};
window.copyLogs = () => {
const terminal = document.getElementById('logsTerminal');
if (terminal) {
navigator.clipboard.writeText(terminal.textContent)
.then(() => alert('Logs copied to clipboard!'))
.catch(err => console.error('Failed to copy logs:', err));
}
};
window.repairGame = repairGame;
// Constants
const MAX_LOG_LINES = 500;
document.addEventListener('DOMContentLoaded', setupLauncher);

96
GUI/js/logs.js Normal file
View File

@@ -0,0 +1,96 @@
// Logs Page Logic
async function loadLogs() {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
terminal.innerHTML = '<div class="text-gray-500 text-center mt-10">Loading logs...</div>';
try {
const logs = await window.electronAPI.getRecentLogs(500); // Fetch last 500 lines
if (logs) {
// Escape HTML to prevent XSS and preserve format
const safeLogs = logs.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
terminal.innerHTML = `<pre class="logs-content">${safeLogs}</pre>`;
// Auto scroll to bottom
terminal.scrollTop = terminal.scrollHeight;
} else {
terminal.innerHTML = '<div class="text-gray-500 text-center mt-10">No logs found.</div>';
}
} catch (error) {
console.error('Failed to load logs:', error);
terminal.innerHTML = `<div class="text-red-500 text-center mt-10">Error loading logs: ${error.message}</div>`;
}
}
async function refreshLogs() {
const btn = document.querySelector('button[onclick="refreshLogs()"] i');
if (btn) btn.classList.add('fa-spin');
await loadLogs();
if (btn) setTimeout(() => btn.classList.remove('fa-spin'), 500);
}
async function copyLogs() {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
const content = terminal.innerText;
if (!content) return;
try {
await navigator.clipboard.writeText(content);
const btn = document.querySelector('button[onclick="copyLogs()"]');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
} catch (err) {
console.error('Failed to copy logs:', err);
}
}
async function openLogsFolder() {
await window.electronAPI.openLogsFolder();
}
function openLogs() {
// Navigation is handled by sidebar logic, but we can trigger a refresh
window.LauncherUI.showPage('logs-page');
window.LauncherUI.setActiveNav('logs');
refreshLogs();
}
// Expose functions globally
window.refreshLogs = refreshLogs;
window.copyLogs = copyLogs;
window.openLogsFolder = openLogsFolder;
window.openLogs = openLogs;
// Auto-load logs when the page becomes active
const logsObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.classList.contains('active') && mutation.target.id === 'logs-page') {
loadLogs();
}
});
});
document.addEventListener('DOMContentLoaded', () => {
const logsPage = document.getElementById('logs-page');
if (logsPage) {
logsObserver.observe(logsPage, { attributes: true, attributeFilter: ['class'] });
}
});

View File

@@ -113,10 +113,15 @@ function displayInstalledMods(mods) {
modsContainer.innerHTML = `
<div class=\"empty-installed-mods\">
<i class=\"fas fa-box-open\"></i>
<h4>No Mods Installed</h4>
<p>Add mods from CurseForge or import local files</p>
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
</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;
}
@@ -138,9 +143,9 @@ function displayInstalledMods(mods) {
function createInstalledModCard(mod) {
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 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';
return `
@@ -154,7 +159,7 @@ function createInstalledModCard(mod) {
<h4 class="installed-mod-name">${mod.name}</h4>
<span class="installed-mod-version">v${mod.version}</span>
</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 class="installed-mod-actions">
@@ -163,7 +168,7 @@ function createInstalledModCard(mod) {
${statusText}
</div>
<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>
</button>
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
@@ -180,7 +185,7 @@ async function loadBrowseMods() {
const browseContainer = document.getElementById('browseModsList');
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 {
if (!API_KEY || API_KEY.length < 10) {
@@ -264,10 +269,15 @@ function displayBrowseMods(mods) {
browseContainer.innerHTML = `
<div class=\"empty-browse-mods\">
<i class=\"fas fa-search\"></i>
<h4>No Mods Found</h4>
<p>Try adjusting your search</p>
<h4 data-i18n="mods.noModsFound">No Mods Found</h4>
<p data-i18n="mods.noModsFoundDesc">Try adjusting your search</p>
</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;
}
@@ -282,10 +292,17 @@ function displayBrowseMods(mods) {
}
function createBrowseModCard(mod) {
const isInstalled = installedMods.some(installed =>
installed.name.toLowerCase().includes(mod.name.toLowerCase()) ||
installed.curseForgeId == mod.id
);
const isInstalled = installedMods.some(installed => {
// Check by CurseForge ID (most reliable)
if (installed.curseForgeId && installed.curseForgeId.toString() === mod.id.toString()) {
return true;
}
// Check by exact name match for manually installed mods
if (installed.name.toLowerCase() === mod.name.toLowerCase()) {
return true;
}
return false;
});
return `
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
@@ -317,16 +334,16 @@ function createBrowseModCard(mod) {
<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})\">
<i class=\"fas fa-external-link-alt\"></i>
VIEW
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
</button>
${!isInstalled ?
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
<i class=\"fas fa-download\"></i>
INSTALL
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
<i class="fas fa-download"></i>
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
</button>` :
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
<i class=\"fas fa-check\"></i>
INSTALLED
`<button class="mod-btn-toggle bg-white/10 text-white" disabled>
<i class="fas fa-check"></i>
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
</button>`
}
</div>
@@ -336,7 +353,8 @@ function createBrowseModCard(mod) {
async function downloadAndInstallMod(modInfo) {
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);
@@ -360,20 +378,23 @@ async function downloadAndInstallMod(modInfo) {
await loadInstalledMods();
await loadBrowseMods();
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 {
throw new Error(result?.error || 'Failed to download mod');
}
} catch (error) {
console.error('Error downloading mod:', error);
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) {
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 result = await window.electronAPI?.toggleMod(modId, modsPath);
@@ -387,7 +408,8 @@ async function toggleMod(modId) {
} catch (error) {
console.error('Error toggling mod:', error);
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');
}
}
@@ -395,11 +417,16 @@ async function deleteMod(modId) {
const mod = installedMods.find(m => m.id === modId);
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(
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`,
confirmMsg,
async () => {
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 result = await window.electronAPI?.uninstallMod(modId, modsPath);
@@ -408,14 +435,16 @@ async function deleteMod(modId) {
await loadInstalledMods();
await loadBrowseMods();
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 {
throw new Error(result?.error || 'Failed to delete mod');
}
} catch (error) {
console.error('Error deleting mod:', error);
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');
}
}
);
@@ -564,7 +593,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
<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;">
<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 style="padding: 24px; color: #e5e7eb;">
@@ -580,7 +609,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">Cancel</button>
">${window.i18n ? window.i18n.t('common.cancel') : 'Cancel'}</button>
<button class="mod-confirm-delete" style="
background: #ef4444;
color: white;
@@ -590,7 +619,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">Delete</button>
">${window.i18n ? window.i18n.t('common.delete') : 'Delete'}</button>
</div>
`;
@@ -708,7 +737,8 @@ function viewModPage(modId) {
}
} else {
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');
}
}
@@ -717,7 +747,9 @@ window.modsManager = {
deleteMod,
openMyModsModal,
closeMyModsModal,
viewModPage
viewModPage,
loadInstalledMods,
loadBrowseMods
};
document.addEventListener('DOMContentLoaded', initModsManager);

View File

@@ -6,18 +6,57 @@ import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';
import './logs.js';
window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
// Initialize i18n immediately (before DOMContentLoaded)
let i18nInitialized = false;
(async () => {
const savedLang = await window.electronAPI?.loadLanguage();
await i18n.init(savedLang);
i18nInitialized = true;
// 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', () => {
// Populate language selector (wait for i18n if needed)
if (i18nInitialized) {
updateLanguageSelector();
}
// Discord notification
const notification = document.getElementById('discordNotification');
if (notification) {
const dismissed = localStorage.getItem('discordNotificationDismissed');
@@ -31,8 +70,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
const originalClose = window.closeDiscordNotification;
window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
}
localStorage.setItem('discordNotificationDismissed', 'true');
originalClose();
};

View File

@@ -5,6 +5,7 @@ let customJavaPath;
let browseJavaBtn;
let settingsPlayerName;
let discordRPCCheck;
let gpuPreferenceRadios;
// UUID Management elements
let currentUuidDisplay;
@@ -21,7 +22,12 @@ let uuidList;
let customUuidInput;
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');
if (existingModal) {
existingModal.remove();
@@ -142,6 +148,7 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
document.addEventListener('keydown', handleEscape);
}
export function initSettings() {
setupSettingsElements();
loadAllSettings();
@@ -154,6 +161,7 @@ function setupSettingsElements() {
browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
// UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid');
@@ -226,6 +234,15 @@ function setupSettingsElements() {
}
});
}
if (gpuPreferenceRadios) {
gpuPreferenceRadios.forEach(radio => {
radio.addEventListener('change', async () => {
await saveGpuPreference();
await updateGpuLabel();
});
});
}
}
function toggleCustomJava() {
@@ -301,9 +318,11 @@ async function saveDiscordRPC() {
// Feedback visuel pour l'utilisateur
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 {
showNotification('Discord Rich Presence disabled', 'success');
const msg = window.i18n ? window.i18n.t('notifications.discordDisabled') : 'Discord Rich Presence disabled';
showNotification(msg, 'success');
}
} else {
throw new Error('Failed to save Discord RPC setting');
@@ -311,7 +330,8 @@ async function saveDiscordRPC() {
}
} catch (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');
}
}
@@ -335,16 +355,19 @@ async function savePlayerName() {
const playerName = settingsPlayerName.value.trim();
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;
}
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) {
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');
}
}
@@ -361,11 +384,85 @@ async function loadPlayerName() {
}
}
async function saveGpuPreference() {
try {
if (window.electronAPI && window.electronAPI.saveGpuPreference && gpuPreferenceRadios) {
const gpuPreference = Array.from(gpuPreferenceRadios).find(radio => radio.checked)?.value || 'auto';
await window.electronAPI.saveGpuPreference(gpuPreference);
}
} catch (error) {
console.error('Error saving GPU preference:', error);
}
}
async function updateGpuLabel() {
const detectionInfo = document.getElementById('gpu-detection-info');
if (!detectionInfo) return;
if (gpuPreferenceRadios) {
const checked = Array.from(gpuPreferenceRadios).find(radio => radio.checked);
if (checked) {
try {
if (window.electronAPI && window.electronAPI.getDetectedGpu) {
const detected = await window.electronAPI.getDetectedGpu();
if (checked.value === 'auto') {
if (detected.dedicatedName) {
detectionInfo.textContent = `dGPU detected, using ${detected.dedicatedName}`;
} else {
detectionInfo.textContent = `dGPU not detected, using iGPU (${detected.integratedName}) instead`;
}
detectionInfo.style.display = 'block';
} else if (checked.value === 'integrated') {
detectionInfo.textContent = `Detected: ${detected.integratedName}`;
detectionInfo.style.display = 'block';
} else if (checked.value === 'dedicated') {
if (detected.dedicatedName) {
detectionInfo.textContent = `Detected: ${detected.dedicatedName}`;
} else {
detectionInfo.textContent = `No dedicated GPU detected`;
}
detectionInfo.style.display = 'block';
} else {
detectionInfo.style.display = 'none';
}
}
} catch (error) {
console.error('Error getting detected GPU:', error);
detectionInfo.style.display = 'none';
}
} else {
detectionInfo.style.display = 'none';
}
} else {
detectionInfo.style.display = 'none';
}
}
async function loadGpuPreference() {
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference && gpuPreferenceRadios) {
const savedPreference = await window.electronAPI.loadGpuPreference();
if (savedPreference) {
for (const radio of gpuPreferenceRadios) {
if (radio.value === savedPreference) {
radio.checked = true;
break;
}
}
await updateGpuLabel();
}
}
} catch (error) {
console.error('Error loading GPU preference:', error);
}
}
async function loadAllSettings() {
await loadCustomJavaPath();
await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
await loadGpuPreference();
}
async function openGameLocation() {
@@ -421,34 +518,43 @@ async function copyCurrentUuid() {
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
if (uuid && navigator.clipboard) {
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) {
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() {
try {
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(
'Are you sure you want to generate a new UUID? This will change your player identity.',
'Generate New UUID',
message,
title,
async () => {
await performRegenerateUuid();
},
null,
'Generate',
'Cancel'
confirmBtn,
cancelBtn
);
} else {
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) {
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');
}
}
@@ -458,7 +564,8 @@ async function performRegenerateUuid() {
if (result.success && result.uuid) {
if (currentUuidDisplay) currentUuidDisplay.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') {
await loadAllUuids();
@@ -468,7 +575,8 @@ async function performRegenerateUuid() {
}
} catch (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');
}
}
@@ -561,19 +669,22 @@ async function generateNewUuid() {
const newUuid = await window.electronAPI.generateNewUuid();
if (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) {
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() {
try {
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;
}
@@ -581,23 +692,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;
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;
}
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(
'Are you sure you want to set this custom UUID? This will change your player identity.',
'Set Custom UUID',
message,
title,
async () => {
await performSetCustomUuid(uuid);
},
null,
'Set UUID',
'Cancel'
confirmBtn,
cancelBtn
);
} catch (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');
}
}
@@ -612,7 +730,8 @@ async function setCustomUuid() {
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
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();
} else {
@@ -621,7 +740,8 @@ async function setCustomUuid() {
}
} catch (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');
}
}
@@ -629,29 +749,37 @@ window.copyUuid = async function(uuid) {
try {
if (navigator.clipboard) {
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) {
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) {
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(
`Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`,
'Delete UUID',
message,
title,
async () => {
await performDeleteUuid(username);
},
null,
'Delete',
'Cancel'
confirmBtn,
cancelBtn
);
} catch (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');
}
};
@@ -661,7 +789,8 @@ async function performDeleteUuid(username) {
const result = await window.electronAPI.deleteUuidForUser(username);
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();
} else {
throw new Error(result.error || 'Failed to delete UUID');
@@ -669,7 +798,8 @@ async function performDeleteUuid(username) {
}
} catch (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');
}
}

View File

@@ -49,6 +49,13 @@ function setupWindowControls() {
const windowControls = document.querySelector('.window-controls');
const header = document.querySelector('.header');
const profileSelector = document.querySelector('.profile-selector');
if (profileSelector) {
profileSelector.style.pointerEvents = 'auto';
profileSelector.style.zIndex = '10000';
}
if (windowControls) {
windowControls.style.pointerEvents = 'auto';
windowControls.style.zIndex = '10000';
@@ -59,6 +66,9 @@ function setupWindowControls() {
if (windowControls) {
windowControls.style.webkitAppRegion = 'no-drag';
}
if (profileSelector) {
profileSelector.style.webkitAppRegion = 'no-drag';
}
}
if (window.electronAPI) {
@@ -356,7 +366,7 @@ function lockPlayButton(locked) {
if (!playButton.getAttribute('data-original-text')) {
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');
@@ -367,9 +377,9 @@ function lockPlayButton(locked) {
playButton.removeAttribute('data-locked');
const spanElement = playButton.querySelector('span');
const originalText = playButton.getAttribute('data-original-text');
if (spanElement && originalText) {
spanElement.textContent = originalText;
if (spanElement) {
// Use i18n to get the current translation instead of restoring saved text
spanElement.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
playButton.removeAttribute('data-original-text');
}
@@ -383,7 +393,8 @@ async function acceptFirstLaunchUpdate() {
const existingGame = window.firstLaunchExistingGame;
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;
}
@@ -400,7 +411,8 @@ async function acceptFirstLaunchUpdate() {
try {
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);
@@ -414,10 +426,12 @@ async function acceptFirstLaunchUpdate() {
if (result.success) {
hideProgress();
showNotification('Game updated successfully! 🎉', 'success');
const successMsg = window.i18n ? window.i18n.t('notifications.gameUpdatedSuccess') : 'Game updated successfully! 🎉';
showNotification(successMsg, 'success');
} else {
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) {
if (modal) {
@@ -425,7 +439,8 @@ async function acceptFirstLaunchUpdate() {
}
lockPlayButton(false);
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');
}
}

234
GUI/locales/en.json Normal file
View File

@@ -0,0 +1,234 @@
{
"nav": {
"play": "Play",
"mods": "Mods",
"news": "News",
"chat": "Players Chat",
"settings": "Settings",
"skins": "Skins"
},
"header": {
"playersLabel": "Players:",
"manageProfiles": "Manage Profiles",
"defaultProfile": "Default",
"f2p": "FREE TO PLAY"
},
"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..."
},
"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"
},
"skins": {
"title": "Skins",
"comingSoon": "Skin customization coming soon..."
},
"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!"
}
}

234
GUI/locales/es.json Normal file
View File

@@ -0,0 +1,234 @@
{
"nav": {
"play": "Jugar",
"mods": "Mods",
"news": "Noticias",
"chat": "Chat de Jugadores",
"settings": "Configuración",
"skins": "Aspectos"
},
"header": {
"playersLabel": "Jugadores:",
"manageProfiles": "Gestionar Perfiles",
"defaultProfile": "Predeterminado",
"f2p": "FREE TO PLAY"
},
"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..."
},
"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"
},
"skins": {
"title": "Aspectos",
"comingSoon": "Personalización de aspectos próximamente..."
},
"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!"
}
}

234
GUI/locales/pt-BR.json Normal file
View File

@@ -0,0 +1,234 @@
{
"nav": {
"play": "Jogar",
"mods": "Mods",
"news": "Notícias",
"chat": "Chat de Jogadores",
"settings": "Configurações",
"skins": "Aparências"
},
"header": {
"playersLabel": "Jogadores:",
"manageProfiles": "Gerenciar Perfis",
"defaultProfile": "Padrão",
"f2p": "FREE TO PLAY"
},
"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..."
},
"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"
},
"skins": {
"title": "Aparências",
"comingSoon": "Personalização de aparências em breve..."
},
"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!"
}
}

View File

@@ -263,6 +263,7 @@ body {
.window-controls {
display: flex;
width: fit-content;
gap: 0.5rem;
z-index: 9999;
position: relative;
@@ -293,6 +294,7 @@ body {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
@@ -590,6 +592,7 @@ body {
flex-direction: column;
height: 100%;
}
.content-pages {
flex: 1;
overflow: hidden;
@@ -792,6 +795,96 @@ body {
}
.custom-java-hint i {
color: #3b82f6;
}
/* Logs Page Styles */
.logs-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem 2rem 2rem;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.logs-title {
font-size: 2rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
.logs-actions {
display: flex;
gap: 0.75rem;
}
.logs-action-btn {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #d1d5db;
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logs-action-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
border-color: rgba(255, 255, 255, 0.2);
}
.logs-terminal {
flex: 1;
background: #0d1117;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
color: #e5e7eb;
overflow-y: auto;
white-space: pre-wrap;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
/* Custom scrollbar */
scrollbar-width: thin;
scrollbar-color: rgba(147, 51, 234, 0.3) transparent;
}
.log-line {
margin-bottom: 0.25rem;
}
.log-line.error {
color: #ef4444;
}
.log-line.warn {
color: #f59e0b;
}
.log-line.info {
color: #3b82f6;
}
.news-section {
padding: 1rem 2rem;
flex: 1;
@@ -1002,6 +1095,7 @@ body {
transform: translateY(0.5rem);
transition: all 0.3s ease 0.1s;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -1430,6 +1524,7 @@ body {
inset 0 1px 0 rgba(255, 255, 255, 0.1);
border-color: rgba(147, 51, 234, 0.3);
}
100% {
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.6),
@@ -1463,8 +1558,15 @@ body {
}
@keyframes textPulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
0%,
100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
#progressPercent {
@@ -1476,10 +1578,13 @@ body {
}
@keyframes percentGlow {
0%, 100% {
0%,
100% {
text-shadow: 0 0 20px rgba(147, 51, 234, 0.8);
transform: scale(1);
}
50% {
text-shadow: 0 0 30px rgba(147, 51, 234, 1);
transform: scale(1.05);
@@ -1508,14 +1613,18 @@ body {
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.progress-bar-fill {
@@ -1525,8 +1634,7 @@ body {
#a855f7 25%,
#3b82f6 50%,
#06b6d4 75%,
#10b981 100%
);
#10b981 100%);
background-size: 200% 100%;
border-radius: 10px;
width: 0%;
@@ -1540,8 +1648,13 @@ body {
}
@keyframes progressFlow {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.progress-bar-fill::after {
@@ -1554,14 +1667,20 @@ body {
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
transparent 100%);
animation: progressPulse 1.5s ease-in-out infinite;
}
@keyframes progressPulse {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
.progress-details {
@@ -1798,8 +1917,13 @@ body {
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}
.mod-image img {
@@ -1855,6 +1979,7 @@ body {
line-height: 1.5;
margin: 0.75rem 0;
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -2028,8 +2153,13 @@ body {
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-mods span {
@@ -2637,6 +2767,7 @@ body {
color: rgba(255, 255, 255, 0.5);
line-height: 1.4;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -2879,6 +3010,7 @@ body {
from {
opacity: 0;
}
to {
opacity: 1;
}
@@ -2902,6 +3034,7 @@ body {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
@@ -3055,6 +3188,7 @@ body {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
@@ -3319,8 +3453,15 @@ body {
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@@ -3373,6 +3514,7 @@ body {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -3550,6 +3692,7 @@ body {
from {
opacity: 0;
}
to {
opacity: 1;
}
@@ -3571,6 +3714,7 @@ body {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -3708,7 +3852,7 @@ body {
}
.settings-header {
margin-bottom: 3rem;
margin-bottom: 1rem;
text-align: center;
}
@@ -3798,13 +3942,13 @@ body {
backdrop-filter: blur(10px);
}
.settings-checkbox input[type="checkbox"]:checked + .checkmark {
.settings-checkbox input[type="checkbox"]:checked+.checkmark {
background: linear-gradient(135deg, #9333ea, #3b82f6);
border-color: #9333ea;
box-shadow: 0 0 20px rgba(147, 51, 234, 0.4);
}
.settings-checkbox input[type="checkbox"]:checked + .checkmark::after {
.settings-checkbox input[type="checkbox"]:checked+.checkmark::after {
content: '✓';
position: absolute;
top: 50%;
@@ -3889,6 +4033,17 @@ body {
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 {
margin-bottom: 1rem;
}
@@ -3997,6 +4152,14 @@ body {
font-size: 0.75rem;
}
.gpu-detection-info {
margin-top: 0.5rem;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
display: none;
}
#settings-page {
opacity: 0;
@@ -4028,7 +4191,53 @@ body {
background: linear-gradient(135deg, #a855f7, #60a5fa);
}
.segmented-control {
display: flex;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
backdrop-filter: blur(10px);
overflow: hidden;
transition: all 0.3s ease;
}
.segmented-control input[type="radio"] {
display: none;
}
.segmented-control label {
flex: 1;
padding: 0.75rem 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
color: #d1d5db;
font-size: 0.875rem;
background: transparent;
border-right: 1px solid rgba(255, 255, 255, 0.1);
font-family: 'JetBrains Mono', monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.segmented-control label:last-child {
border-right: none;
}
.segmented-control input[type="radio"]:checked + label {
background: linear-gradient(135deg, #9333ea, #3b82f6);
color: white;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.segmented-control label:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.segmented-control input[type="radio"]:checked + label:hover {
background: linear-gradient(135deg, #7c3aed, #2563eb);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.segmented-control:focus-within {
border-color: rgba(147, 51, 234, 0.3);
box-shadow: 0 0 20px rgba(147, 51, 234, 0.1);
}
#update-popup-overlay {
@@ -4068,6 +4277,7 @@ body {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
@@ -4208,8 +4418,15 @@ body {
}
@keyframes updatePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
@@ -4330,6 +4547,7 @@ body {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
@@ -4773,16 +4991,27 @@ body {
/* Chat Badges et Animations */
@keyframes rainbow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes contributorGlow {
0%, 100% {
0%,
100% {
background-position: 0% 50%;
filter: brightness(1);
}
50% {
background-position: 100% 50%;
filter: brightness(1.2);
@@ -4929,12 +5158,12 @@ body {
transition: all 0.3s ease;
}
.color-type-option input[type="radio"]:checked + .radio-custom {
.color-type-option input[type="radio"]:checked+.radio-custom {
border-color: #9333ea;
background: rgba(147, 51, 234, 0.2);
}
.color-type-option input[type="radio"]:checked + .radio-custom::after {
.color-type-option input[type="radio"]:checked+.radio-custom::after {
content: '';
position: absolute;
top: 50%;
@@ -5090,6 +5319,7 @@ body {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(147, 51, 234, 0.4);
}
.color-picker-container {
display: flex;
flex-wrap: wrap;
@@ -5114,3 +5344,238 @@ body {
border-color: #fff;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
/* Profile Selector Styles */
.profile-selector {
position: relative;
pointer-events: auto;
margin-right: auto;
margin-left: 1rem;
z-index: 9999 !important;
}
.profile-btn {
display: flex;
align-items: center;
gap: 0.75rem;
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;
color: white;
cursor: pointer;
font-family: 'Space Grotesk', sans-serif;
font-weight: 600;
transition: all 0.2s ease;
z-index: 100000 !important;
pointer-events: auto !important;
}
.profile-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(147, 51, 234, 0.4);
}
.profile-btn i {
color: #9333ea;
}
.profile-dropdown {
position: absolute;
top: 100%;
left: 0;
margin-top: 0.5rem;
width: 200px;
background: rgba(20, 20, 20, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 0.5rem;
display: none;
/* Toggled by JS */
flex-direction: column;
z-index: 2000;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
animation: fadeIn 0.1s ease;
}
.profile-dropdown.show {
display: flex;
}
.profile-item {
padding: 0.6rem 0.8rem;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
color: #ccc;
transition: all 0.2s;
}
.profile-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.profile-item.active {
background: rgba(147, 51, 234, 0.2);
color: white;
font-weight: bold;
}
.profile-item.active::before {
content: '•';
color: #9333ea;
font-size: 1.2rem;
}
.profile-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 0.5rem 0;
}
.profile-action {
padding: 0.6rem 0.8rem;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
color: #9333ea;
font-weight: 600;
}
.profile-action:hover {
background: rgba(147, 51, 234, 0.1);
}
/* Profile Modal Styles */
.profile-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.2s ease;
}
.profile-modal-content {
background: #111;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
width: 500px;
max-width: 90%;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
}
.profile-modal-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.profile-modal-title {
margin: 0;
font-size: 1.5rem;
color: white;
}
.profile-modal-body {
padding: 1.5rem;
}
.profile-manager-list {
max-height: 300px;
overflow-y: auto;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.profile-manager-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid transparent;
}
.profile-manager-item:hover {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.08);
}
.profile-manager-item.active {
border-color: #9333ea;
background: rgba(147, 51, 234, 0.1);
}
.profile-delete-btn {
background: transparent;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 1rem;
padding: 0.4rem;
border-radius: 6px;
transition: all 0.2s;
}
.profile-delete-btn:hover {
background: rgba(239, 68, 68, 0.1);
}
.profile-create-section {
display: flex;
gap: 0.5rem;
}
.profile-input {
flex: 1;
padding: 0.8rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.5);
color: white;
}
.profile-create-btn {
padding: 0 1.2rem;
background: #9333ea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.profile-create-btn:hover {
background: #7c3aed;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -1,19 +1,20 @@
# Maintainer: Terromur <terromuroz@proton.me>
# Maintainer: Fazri Gading <fazrigading@gmail.com>
pkgname=Hytale-F2P-git
_pkgname=Hytale-F2P
pkgver=2.0.2.r90.g21f8527
pkgver=2.0.2b.r120.gb05aeef
pkgrel=1
pkgdesc="HyLauncher - unofficial Hytale Launcher for free to play gamers"
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
arch=('x86_64')
url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
makedepends=('npm')
makedepends=('npm' 'git' 'rpm-tools' 'libxcrypt-compat')
source=("git+$url.git" "Hytale-F2P.desktop")
sha256sums=('SKIP' '8c78a6931fade2b0501122980dc238e042b9f6f0292b5ca74c391d7b3c1543c0')
pkgver() {
cd "$_pkgname"
printf "2.0.2.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
printf "2.0.2b.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {

View File

@@ -6,7 +6,7 @@
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
**A modern, cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members)
@@ -78,7 +78,21 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
## 📋 Changelog
### 🆕 v2.0.2 *(Latest)*
### 🆕 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)*
- 🧑‍🚀 **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.
- 🚨 **Critical Path Fix** — Resolved a macOS bug where mods were being saved to a Windows path (`~/AppData/Local`) instead of `~/Library/Application Support`. Mods now save to the **correct location** and load properly in-game.
- 🛡️ **Stability Improvements** — Added an **auto-sync step before every launch** to ensure the physical mods folder always matches the active profile.
- 🎨 **UI Enhancements** — Added a **profile selector dropdown** and a **profile management modal**.
### 🆕 v2.0.2
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
- 🎨 **Chat Improvements** - Simplified chat color system

View File

@@ -89,8 +89,23 @@ Creates a virtual LAN - all players need to install it:
3. **Host via Online Play** - Use your Radmin VPN IP instead
4. **Friends connect** - They'll see you on the virtual LAN
Both options bypass all NAT/CGNAT issues.
Both options bypass all NAT/CGNAT issues. But for **Windows machines only!**
#### Option 3: Tailscale
It creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
1. All member's are required to download [Tailscale](https://tailscale.com/download) on your device.
[Once installed, Tailwind starts and live inside your hidden icon section in Windows, Mac and Linux]
2. Create a **common tailscale** account which will shared among your friends to log in.
3. Ask your **host to login in to thier tailscale client first**, then the other members.
##### Host
1. Open your singleplayer world
2. Go to Online Play settings
3. Re-save your settings to generate a new share code
##### Friends
1. Ensure Tailscale is connected
2. Use the new share code to connect
[To test your connection, ping the host's ipv4 mentioned in tailwind]
---
## Part 2: Dedicated Server (Advanced)

View File

@@ -14,6 +14,10 @@ function getAuthDomain() {
}
// Then check config file
const config = loadConfig();
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
// Allow profile to override auth domain if ever needed
// but for now stick to global or env
}
if (config.authDomain) {
return config.authDomain;
}
@@ -111,6 +115,16 @@ function saveJavaPath(javaPath) {
function loadJavaPath() {
const config = loadConfig();
// Prefer Active Profile's Java Path
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
const profile = config.profiles[config.activeProfileId];
if (profile.javaPath && profile.javaPath.trim().length > 0) {
return profile.javaPath;
}
}
// Fallback to global setting
return config.javaPath || '';
}
@@ -133,10 +147,30 @@ function loadDiscordRPC() {
return config.discordRPC !== undefined ? config.discordRPC : true;
}
function saveLanguage(language) {
saveConfig({ language: language || 'en' });
}
function loadLanguage() {
const config = loadConfig();
return config.language || 'en';
}
function saveModsToConfig(mods) {
try {
let config = loadConfig();
const config = loadConfig();
// Config migration handles structure, but mod saves must go to the ACTIVE profile.
// Global installedMods is kept mainly for reference/migration.
// The profile is the source of truth for enabled mods.
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
config.profiles[config.activeProfileId].mods = mods;
} else {
// Fallback for legacy or no-profile state
config.installedMods = mods;
}
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
@@ -153,6 +187,12 @@ function saveModsToConfig(mods) {
function loadModsFromConfig() {
try {
const config = loadConfig();
// Prefer Active Profile
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
return config.profiles[config.activeProfileId].mods || [];
}
return config.installedMods || [];
} catch (error) {
console.error('Error loading mods from config:', error);
@@ -246,6 +286,15 @@ function loadChatColor() {
return config.chatColor || '#3498db';
}
function saveGpuPreference(gpuPreference) {
saveConfig({ gpuPreference: gpuPreference || 'auto' });
}
function loadGpuPreference() {
const config = loadConfig();
return config.gpuPreference || 'auto';
}
module.exports = {
loadConfig,
saveConfig,
@@ -262,6 +311,8 @@ module.exports = {
loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveLanguage,
loadLanguage,
saveModsToConfig,
loadModsFromConfig,
isFirstLaunch,
@@ -277,5 +328,8 @@ module.exports = {
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
resetCurrentUserUuid,
// GPU Preference exports
saveGpuPreference,
loadGpuPreference
};

View File

@@ -152,10 +152,8 @@ async function getModsPath(customInstallPath = null) {
}
if (!installPath) {
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
installPath = path.join(localAppData, 'HytaleF2P');
} else {
installPath = path.join(installPath, 'HytaleF2P');
// Use the standard app directory logic which handles platforms correctly
installPath = getAppDir();
}
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');

View File

@@ -15,19 +15,23 @@ const {
loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveLanguage,
loadLanguage,
saveModsToConfig,
loadModsFromConfig,
getUuidForUser,
isFirstLaunch,
markAsLaunched,
CONFIG_FILE,
// UUID Management
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
resetCurrentUserUuid,
// GPU Preference
saveGpuPreference,
loadGpuPreference
} = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -38,7 +42,8 @@ const {
installGame,
uninstallGame,
updateGameFiles,
checkExistingGameInstallation
checkExistingGameInstallation,
repairGame
} = require('./managers/gameManager');
const {
@@ -77,6 +82,9 @@ const {
handleFirstLaunchCheck
} = require('./services/firstLaunch');
// Utils
const { detectGpu } = require('./utils/platformUtils');
// Re-export all functions to maintain backward compatibility
module.exports = {
// Game launch functions
@@ -88,6 +96,7 @@ module.exports = {
isGameInstalled,
uninstallGame,
updateGameFiles,
repairGame,
// User configuration functions
saveUsername,
@@ -111,6 +120,15 @@ module.exports = {
saveDiscordRPC,
loadDiscordRPC,
// Language functions
saveLanguage,
loadLanguage,
// GPU Preference functions
saveGpuPreference,
loadGpuPreference,
detectGpu,
// Version functions
getInstalledClientVersion,
getLatestClientVersion,

View File

@@ -6,11 +6,12 @@ const { promisify } = require('util');
const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment } = require('../utils/platformUtils');
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager');
const { syncModsForCurrentProfile } = require('./modManager');
// Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null;
@@ -100,7 +101,7 @@ function generateLocalTokens(uuid, name) {
};
}
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') {
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
@@ -192,9 +193,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
const serverDir = path.join(gameLatest, 'Server');
const signPath = async (targetPath, deep = false) => {
await execAsync(`xattr -cr "${targetPath}"`).catch(() => {});
await execAsync(`xattr -cr "${targetPath}"`).catch(() => { });
const deepFlag = deep ? '--deep ' : '';
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => {});
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => { });
};
if (fs.existsSync(appBundle)) {
@@ -216,8 +217,8 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
}
if (fs.existsSync(serverDir)) {
await execAsync(`xattr -cr "${serverDir}"`).catch(() => {});
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {});
await execAsync(`xattr -cr "${serverDir}"`).catch(() => { });
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => { });
console.log('Signed server binaries (after patching)');
}
@@ -260,6 +261,18 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
if (progressCallback) {
progressCallback('Starting game...', null, null, null, null);
}
// Ensure mods are synced for the active profile before launching
try {
console.log('Syncing mods for active profile before launch...');
if (progressCallback) progressCallback('Syncing mods...', null, null, null, null);
await syncModsForCurrentProfile();
} catch (syncError) {
console.error('Failed to sync mods before launch:', syncError);
// Continue anyway? Or fail?
// Warn user but continue might be safer to avoid blocking play if sync is just glitchy
}
console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
@@ -268,6 +281,9 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv);
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -339,7 +355,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') {
try {
if (progressCallback) {
progressCallback('Checking for updates...', 0, null, null, null);
@@ -390,7 +406,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
progressCallback('Launching game...', 80, null, null, null);
}
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride);
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference);
} catch (error) {
console.error('Error in version check and launch:', error);
if (progressCallback) {

View File

@@ -387,6 +387,102 @@ function checkExistingGameInstallation() {
}
}
async function repairGame(progressCallback) {
const appDir = getResolvedAppDir();
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
// Check if game exists
if (!fs.existsSync(gameDir)) {
throw new Error('Game directory not found. Cannot repair.');
}
// Locate UserData
const userDataPath = findUserDataRecursive(gameDir);
let userDataBackup = null;
if (progressCallback) {
progressCallback('Backing up user data...', 10, null, null, null);
}
// Backup UserData
if (userDataPath && fs.existsSync(userDataPath)) {
userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now());
console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`);
// Copy function
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataPath, userDataBackup);
}
if (progressCallback) {
progressCallback('Removing old game files...', 30, null, null, null);
}
// Delete Game and Cache Directory
console.log('Removing corrupted game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
const cacheDir = path.join(appDir, 'cache');
if (fs.existsSync(cacheDir)) {
console.log('Clearing cache directory...');
fs.rmSync(cacheDir, { recursive: true, force: true });
}
console.log('Reinstalling game files...');
// Passing null/undefined for overrides to use defaults/saved configs
// installGame calls progressCallback internally
await installGame('Player', progressCallback);
// Restore UserData
if (userDataBackup && fs.existsSync(userDataBackup)) {
if (progressCallback) {
progressCallback('Restoring user data...', 90, null, null, null);
}
// installGame creates: path.join(customGameDir, 'Client', 'UserData')
const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
const newUserDataPath = path.join(newGameDir, 'Client', 'UserData');
if (!fs.existsSync(newUserDataPath)) {
fs.mkdirSync(newUserDataPath, { recursive: true });
}
console.log(`Restoring UserData to ${newUserDataPath}`);
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataBackup, newUserDataPath);
// Cleanup Backup
console.log('Cleaning up repair backup...');
fs.rmSync(userDataBackup, { recursive: true, force: true });
}
if (progressCallback) {
progressCallback('Repair completed successfully!', 100, null, null, null);
}
return { success: true, repaired: true };
}
module.exports = {
downloadPWR,
applyPWR,
@@ -394,5 +490,9 @@ module.exports = {
isGameInstalled,
installGame,
uninstallGame,
checkExistingGameInstallation
isGameInstalled,
installGame,
uninstallGame,
checkExistingGameInstallation,
repairGame
};

View File

@@ -4,6 +4,7 @@ const crypto = require('crypto');
const axios = require('axios');
const { getModsPath } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager');
function generateModId(filename) {
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
@@ -26,77 +27,53 @@ function extractVersion(filename) {
return versionMatch ? versionMatch[1] : null;
}
// Helper to get mods from active profile
function getProfileMods() {
const profile = profileManager.getActiveProfile();
return profile ? (profile.mods || []) : [];
}
async function loadInstalledMods(modsPath) {
try {
const configMods = loadModsFromConfig();
const modsMap = new Map();
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) return [];
configMods.forEach(mod => {
modsMap.set(mod.fileName, mod);
});
const profileMods = activeProfile.mods || [];
const profileModFiles = new Set(profileMods.map(m => m.fileName));
if (fs.existsSync(modsPath)) {
const files = fs.readdirSync(modsPath);
for (const file of files) {
const filePath = path.join(modsPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
const configMod = modsMap.get(file);
const modInfo = {
id: configMod?.id || generateModId(file),
name: configMod?.name || extractModName(file),
version: configMod?.version || extractVersion(file) || '1.0.0',
description: configMod?.description || 'Installed mod',
author: configMod?.author || 'Unknown',
enabled: true,
filePath: filePath,
fileName: file,
fileSize: configMod?.fileSize || stats.size,
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
curseForgeId: configMod?.curseForgeId,
curseForgeFileId: configMod?.curseForgeFileId
};
modsMap.set(file, modInfo);
}
}
}
// We only return mods that are explicitly in the profile
// Check which ones are physically present (either in mods/ or DisabledMods/)
const physicalModsPath = modsPath; // .../mods
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (fs.existsSync(disabledModsPath)) {
const files = fs.readdirSync(disabledModsPath);
for (const file of files) {
const filePath = path.join(disabledModsPath, file);
const stats = fs.statSync(filePath);
const validMods = [];
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
const configMod = modsMap.get(file);
for (const modConfig of profileMods) {
// Check if file exists in either location
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
const modInfo = {
id: configMod?.id || generateModId(file),
name: configMod?.name || extractModName(file),
version: configMod?.version || extractVersion(file) || '1.0.0',
description: configMod?.description || 'Disabled mod',
author: configMod?.author || 'Unknown',
enabled: false,
filePath: filePath,
fileName: file,
fileSize: configMod?.fileSize || stats.size,
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
curseForgeId: configMod?.curseForgeId,
curseForgeFileId: configMod?.curseForgeFileId
};
modsMap.set(file, modInfo);
}
if (inEnabled || inDisabled) {
validMods.push({
...modConfig,
// Set filePath based on physical location
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
enabled: modConfig.enabled !== false // Default true
});
} else {
console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
// Include it so user can see it's missing or remove it
validMods.push({
...modConfig,
filePath: null,
missing: true,
enabled: modConfig.enabled !== false
});
}
}
return Array.from(modsMap.values());
return validMods;
} catch (error) {
console.error('Error loading installed mods:', error);
return [];
@@ -142,7 +119,9 @@ async function downloadMod(modInfo) {
return new Promise((resolve, reject) => {
writer.on('finish', () => {
const configMods = loadModsFromConfig();
// NEW: Update Active Profile instead of global config
const activeProfile = profileManager.getActiveProfile();
if (activeProfile) {
const newMod = {
id: modInfo.id || generateModId(fileName),
name: modInfo.name || extractModName(fileName),
@@ -157,8 +136,8 @@ async function downloadMod(modInfo) {
curseForgeFileId: modInfo.fileId
};
configMods.push(newMod);
saveModsToConfig(configMods);
const updatedMods = [...(activeProfile.mods || []), newMod];
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
resolve({
success: true,
@@ -166,6 +145,9 @@ async function downloadMod(modInfo) {
fileName: fileName,
modInfo: newMod
});
} else {
reject(new Error('No active profile to save mod to'));
}
});
writer.on('error', reject);
});
@@ -181,11 +163,14 @@ async function downloadMod(modInfo) {
async function uninstallMod(modId, modsPath) {
try {
const configMods = loadModsFromConfig();
const mod = configMods.find(m => m.id === modId);
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) throw new Error('No active profile');
const profileMods = activeProfile.mods || [];
const mod = profileMods.find(m => m.id === modId);
if (!mod) {
throw new Error('Mod not found in config');
throw new Error('Mod not found in profile');
}
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
@@ -193,23 +178,23 @@ async function uninstallMod(modId, modsPath) {
const disabledPath = path.join(disabledModsPath, mod.fileName);
let fileRemoved = false;
// Try to remove file from both locations to be safe
if (fs.existsSync(enabledPath)) {
fs.unlinkSync(enabledPath);
fileRemoved = true;
console.log('Removed mod from Mods folder:', enabledPath);
} else if (fs.existsSync(disabledPath)) {
fs.unlinkSync(disabledPath);
fileRemoved = true;
console.log('Removed mod from DisabledMods folder:', disabledPath);
}
if (fs.existsSync(disabledPath)) {
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { }
}
if (!fileRemoved) {
console.warn('Mod file not found on filesystem, removing from config anyway');
console.warn('Mod file not found on filesystem, removing from profile anyway');
}
const updatedMods = configMods.filter(m => m.id !== modId);
saveModsToConfig(updatedMods);
console.log('Mod removed from config.json');
const updatedMods = profileMods.filter(m => m.id !== modId);
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
console.log('Mod removed from profile');
return { success: true };
} catch (error) {
@@ -223,36 +208,51 @@ async function uninstallMod(modId, modsPath) {
async function toggleMod(modId, modsPath) {
try {
const mods = await loadInstalledMods(modsPath);
const mod = mods.find(m => m.id === modId);
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) throw new Error('No active profile');
if (!mod) {
throw new Error('Mod not found');
const profileMods = activeProfile.mods || [];
const modIndex = profileMods.findIndex(m => m.id === modId);
if (modIndex === -1) {
throw new Error('Mod not found in profile');
}
const mod = profileMods[modIndex];
const newEnabled = !mod.enabled; // Toggle
// Update Profile First
const updatedMods = [...profileMods];
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
// Manually move the file to reflect the new state
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
const currentPath = mod.filePath;
let newPath, newEnabled;
const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
if (mod.enabled) {
newPath = path.join(disabledModsPath, path.basename(currentPath));
newEnabled = false;
// Determine target paths
const targetDir = newEnabled ? modsPath : disabledModsPath;
const targetPath = path.join(targetDir, mod.fileName);
if (fs.existsSync(currentPath)) {
fs.renameSync(currentPath, targetPath);
} else {
newPath = path.join(modsPath, path.basename(currentPath));
newEnabled = true;
// Fallback: check if it's already in target?
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`);
} else {
// Try finding it
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName);
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
}
fs.renameSync(currentPath, newPath);
const configMods = loadModsFromConfig();
const configModIndex = configMods.findIndex(m => m.id === modId);
if (configModIndex !== -1) {
configMods[configModIndex].enabled = newEnabled;
saveModsToConfig(configMods);
}
return { success: true, enabled: newEnabled };
@@ -265,11 +265,69 @@ async function toggleMod(modId, modsPath) {
}
}
async function syncModsForCurrentProfile() {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) {
console.warn('No active profile found during mod sync');
return;
}
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
const modsPath = await getModsPath();
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
// Get all physical files from both folders
const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
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) {
const modConfig = profileMods.find(m => m.fileName === fileName);
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
// Logic:
// If it should be enabled -> Move to mods/
// 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);
if (path.dirname(currentPath) !== targetDir) {
console.log(`[Mod Sync] Moving ${fileName} to ${shouldBeEnabled ? 'Enabled' : 'Disabled'}`);
try {
fs.renameSync(currentPath, targetPath);
} catch (err) {
console.error(`Failed to move ${fileName}: ${err.message}`);
}
}
}
return { success: true };
} catch (error) {
console.error('[ModManager] Error syncing mods:', error);
return { success: false, error: error.message };
}
}
module.exports = {
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod,
syncModsForCurrentProfile,
generateModId,
extractModName,
extractVersion

View File

@@ -0,0 +1,206 @@
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const {
loadConfig,
saveConfig,
getModsPath
} = require('../core/config');
// Lazy-load modManager to avoid circular deps, or keep imports structured.
// For now, access mod paths directly or use helper helpers.
class ProfileManager {
constructor() {
this.initialized = false;
}
init() {
if (this.initialized) return;
const config = loadConfig();
// Migration: specific check to see if we have profiles yet
if (!config.profiles || Object.keys(config.profiles).length === 0) {
this.migrateLegacyConfig(config);
}
this.initialized = true;
console.log('[ProfileManager] Initialized');
}
migrateLegacyConfig(config) {
console.log('[ProfileManager] Migrating legacy config to profile system...');
// Create a default profile with current settings
const defaultProfileId = 'default';
const defaultProfile = {
id: defaultProfileId,
name: 'Default',
created: new Date().toISOString(),
lastUsed: new Date().toISOString(),
// settings specific to this profile
// If global settings existed, we copy them here
mods: config.installedMods || [], // Legacy mods are now part of default profile
javaPath: config.javaPath || '',
gameOptions: {
minMemory: '1G',
maxMemory: '4G',
args: []
}
};
const updates = {
profiles: {
[defaultProfileId]: defaultProfile
},
activeProfileId: defaultProfileId,
// Mods are currently treated as files on disk.
// The profile's `mods` array is the source of truth for enabled/known mods per profile.
};
saveConfig(updates);
console.log('[ProfileManager] Migration complete. Created Default profile.');
}
createProfile(name) {
if (!name || typeof name !== 'string') {
throw new Error('Invalid profile name');
}
const config = loadConfig();
const id = uuidv4();
const newProfile = {
id,
name: name.trim(),
created: new Date().toISOString(),
lastUsed: null,
mods: [], // Start with no mods enabled
javaPath: '',
gameOptions: {
minMemory: '1G',
maxMemory: '4G',
args: []
}
};
const profiles = config.profiles || {};
profiles[id] = newProfile;
saveConfig({ profiles });
console.log(`[ProfileManager] Created new profile: "${name}" (${id})`);
return newProfile;
}
getProfiles() {
const config = loadConfig();
return Object.values(config.profiles || {});
}
getProfile(id) {
const config = loadConfig();
return (config.profiles && config.profiles[id]) || null;
}
getActiveProfile() {
const config = loadConfig();
const activeId = config.activeProfileId;
if (!activeId || !config.profiles || !config.profiles[activeId]) {
// Fallback if something is corrupted
return this.getProfiles()[0] || null;
}
return config.profiles[activeId];
}
async activateProfile(id) {
const config = loadConfig();
if (!config.profiles || !config.profiles[id]) {
throw new Error(`Profile not found: ${id}`);
}
if (config.activeProfileId === id) {
console.log(`[ProfileManager] Profile ${id} is already active.`);
return config.profiles[id];
}
console.log(`[ProfileManager] Switching to profile: ${config.profiles[id].name} (${id})`);
// 1. Update config first
config.profiles[id].lastUsed = new Date().toISOString();
saveConfig({
activeProfileId: id,
profiles: config.profiles
});
// 2. Trigger Mod Sync
// We need to require this here to ensure it uses the *newly saved* active profile ID
const { syncModsForCurrentProfile } = require('./modManager');
await syncModsForCurrentProfile();
return config.profiles[id];
}
deleteProfile(id) {
const config = loadConfig();
const profiles = config.profiles || {};
if (!profiles[id]) {
throw new Error('Profile not found');
}
if (config.activeProfileId === id) {
throw new Error('Cannot delete the active profile');
}
// Don't allow deleting the last profile
if (Object.keys(profiles).length <= 1) {
throw new Error('Cannot delete the only remaining profile');
}
delete profiles[id];
saveConfig({ profiles });
console.log(`[ProfileManager] Deleted profile: ${id}`);
return true;
}
updateProfile(id, updates) {
const config = loadConfig();
const profiles = config.profiles || {};
if (!profiles[id]) {
throw new Error('Profile not found');
}
// Safety checks on updates
const allowedFields = ['name', 'javaPath', 'gameOptions', 'mods'];
const sanitizedUpdates = {};
Object.keys(updates).forEach(key => {
if (allowedFields.includes(key)) {
sanitizedUpdates[key] = updates[key];
}
});
profiles[id] = { ...profiles[id], ...sanitizedUpdates };
saveConfig({ profiles });
console.log(`[ProfileManager] Updated profile: ${id}`);
// If we updated mods for the *active* profile, we might need to sync immediately
if (config.activeProfileId === id && updates.mods) {
// Optionally trigger sync?
// Sync is usually triggered when toggling a single mod.
// For bulk updates, the caller should decide when to sync.
}
return profiles[id];
}
}
module.exports = new ProfileManager();

View File

@@ -143,6 +143,29 @@ class ClientPatcher {
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
*/
@@ -256,9 +279,12 @@ class ClientPatcher {
console.log('Patching domain references...');
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
if (count === 0) {
console.log('No occurrences of hytale.com found - binary may already be modified or has different format');
return { success: true, patchCount: 0, warning: 'No domain occurrences found' };
console.log('Patching Discord URLs...');
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
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) {
@@ -266,7 +292,7 @@ class ClientPatcher {
}
console.log('Writing patched binary...');
fs.writeFileSync(clientPath, patchedData);
fs.writeFileSync(clientPath, finalData);
this.markAsPatched(clientPath);
@@ -274,10 +300,10 @@ class ClientPatcher {
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 ===');
return { success: true, patchCount: count };
return { success: true, patchCount: count + discordCount };
}
/**

View File

@@ -138,6 +138,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
if (!isRetryable || attempt === maxRetries - 1) {
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
break;
}

View File

@@ -65,9 +65,177 @@ function setupWaylandEnvironment() {
return envVars;
}
function detectGpu() {
const platform = getOS();
try {
if (platform === 'linux') {
return detectGpuLinux();
} else if (platform === 'windows') {
return detectGpuWindows();
} else if (platform === 'darwin') {
return detectGpuMac();
} else {
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
} catch (error) {
console.warn('GPU detection failed, falling back to integrated:', error.message);
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
}
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) {
if (process.platform !== 'linux') {
return {};
}
let finalPreference = gpuPreference;
let detected = detectGpu();
if (gpuPreference === 'auto') {
finalPreference = detected.mode;
console.log(`Auto-detected GPU: ${detected.vendor} (${detected.mode})`);
}
console.log('Preferred GPU set to:', finalPreference);
const envVars = {};
if (finalPreference === 'dedicated') {
envVars.DRI_PRIME = '1';
if (detected.vendor === 'nvidia') {
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
envVars.__GL_SHADER_DISK_CACHE = '1';
envVars.__GL_SHADER_DISK_CACHE_PATH = '/tmp';
}
console.log('GPU environment variables:', envVars);
} else {
console.log('Using integrated GPU, no environment variables set');
}
return envVars;
}
module.exports = {
getOS,
getArch,
isWaylandSession,
setupWaylandEnvironment
setupWaylandEnvironment,
detectGpu,
setupGpuEnvironment
};

154
main.js
View File

@@ -1,9 +1,10 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path');
const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, 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, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager');
logger.interceptConsole();
@@ -119,8 +120,6 @@ function createWindow() {
mainWindow.webContents.send('show-update-popup', updateInfo);
}
}, 3000);
//mainWindow.webContents.openDevTools();
mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools();
@@ -160,6 +159,24 @@ app.whenReady().then(async () => {
console.log('Node.js version:', process.versions.node);
console.log('Log directory:', logger.getLogDirectory());
try {
const { loadGpuPreference, detectGpu } = require('./backend/launcher');
const savedPreference = loadGpuPreference();
if (savedPreference === 'auto') {
global.detectedGpu = detectGpu(); // if 'auto' selected = preload GPU detection
console.log('GPU auto-detection completed on startup:', global.detectedGpu);
} else {
console.log('GPU preference is manual, skipping auto-detection');
}
} catch (error) {
console.warn('Failed to preload GPU detection:', error.message);
global.detectedGpu = { mode: 'integrated', vendor: 'intel' };
}
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
createWindow();
setTimeout(async () => {
@@ -276,7 +293,7 @@ app.on('window-all-closed', () => {
}
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) => {
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -291,7 +308,7 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) =
}
};
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
return result;
} catch (error) {
@@ -388,6 +405,15 @@ ipcMain.handle('load-discord-rpc', () => {
return loadDiscordRPC();
});
ipcMain.handle('save-language', (event, language) => {
saveLanguage(language);
return { success: true };
});
ipcMain.handle('load-language', () => {
return loadLanguage();
});
ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
@@ -450,13 +476,35 @@ ipcMain.handle('is-game-installed', async () => {
ipcMain.handle('uninstall-game', async () => {
try {
await uninstallGame();
return { success: true };
} catch (error) {
console.error('Uninstall error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('repair-game', async () => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await repairGame(progressCallback);
return result;
} catch (error) {
console.error('Repair error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-hytale-news', async () => {
try {
const news = await getHytaleNews();
@@ -704,6 +752,35 @@ ipcMain.handle('get-update-info', async () => {
return updateManager.getUpdateInfo();
});
ipcMain.handle('get-gpu-info', () => {
try {
return app.getGPUInfo('complete');
} catch (error) {
console.error('Error getting GPU info:', error);
return {};
}
});
ipcMain.handle('save-gpu-preference', (event, gpuPreference) => {
const { saveGpuPreference } = require('./backend/launcher');
saveGpuPreference(gpuPreference);
return { success: true };
});
ipcMain.handle('load-gpu-preference', () => {
const { loadGpuPreference } = require('./backend/launcher');
return loadGpuPreference();
});
ipcMain.handle('get-detected-gpu', () => {
if (global.detectedGpu) {
return global.detectedGpu;
}
const { detectGpu } = require('./backend/launcher');
global.detectedGpu = detectGpu();
return global.detectedGpu;
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
@@ -802,10 +879,73 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
const content = fs.readFileSync(latestLogFile, 'utf8');
const lines = content.split('\n');
return lines.slice(-maxLines).join('\n');
let result = lines.slice(-maxLines).join('\n');
if (lines.length > maxLines) {
const truncatedMsg = `\n--- ⚠️ LOG TRUNCATED: Showing last ${maxLines} lines of ${lines.length}. Open Logs Folder for full history ---\n\n`;
return result + truncatedMsg;
}
return result;
} catch (error) {
console.error('Error reading logs:', error);
return null;
}
});
ipcMain.handle('open-logs-folder', async () => {
try {
const logDir = logger.getLogDirectory();
if (logDir && fs.existsSync(logDir)) {
await shell.openPath(logDir);
return { success: true };
}
return { success: false, error: 'Logs directory not found' };
} catch (error) {
console.error('Error opening logs folder:', error);
return { success: false, error: error.message };
}
});
// Profile Management IPC
ipcMain.handle('profile-create', async (event, name) => {
try {
return profileManager.createProfile(name);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-list', async () => {
return profileManager.getProfiles();
});
ipcMain.handle('profile-get-active', async () => {
return profileManager.getActiveProfile();
});
ipcMain.handle('profile-activate', async (event, id) => {
try {
return await profileManager.activateProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-delete', async (event, id) => {
try {
return profileManager.deleteProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-update', async (event, id, updates) => {
try {
return profileManager.updateProfile(id, updates);
} catch (error) {
return { error: error.message };
}
});

48
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "hytale-f2p-launcher",
"version": "2.0.1",
"name": "hytale-f2p-launcherv2",
"version": "2.0.2b",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hytale-f2p-launcher",
"version": "2.0.1",
"name": "hytale-f2p-launcherv2",
"version": "2.0.2b",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.10",
@@ -3459,15 +3459,17 @@
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
@@ -4504,6 +4506,18 @@
"node": ">=8"
}
},
"node_modules/tar/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/temp": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
@@ -4568,20 +4582,6 @@
"node": ">= 10.0.0"
}
},
"node_modules/temp/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/tiny-async-pool": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "hytale-f2p-launcherv2",
"version": "2.0.1",
"name": "hytale-f2p-launcher",
"version": "2.0.2b",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",
@@ -23,6 +23,17 @@
"auto-update",
"mod-manager",
"chat"
],
"maintainers": [
{
"name": "Terromur",
"url": "https://github.com/Terromur"
},
{
"name": "Fari Gading",
"email": "fazrigading@gmail.com",
"url": "https://github.com/fazrigading"
}
],
"author": {
"name": "AMIAY",
@@ -45,7 +56,8 @@
},
"build": {
"appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P",
"productName": "Hytale F2P Launcher",
"artifactName": "${name}_${version}_${arch}.${ext}",
"directories": {
"output": "dist"
},
@@ -58,54 +70,25 @@
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
"x64"
]
}
{ "target": "nsis", "arch": ["x64", "arm64"] },
{ "target": "portable", "arch": ["x64"] }
],
"icon": "icon.ico"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
{ "target": "AppImage", "arch": ["x64", "arm64"] },
{ "target": "deb", "arch": ["x64", "arm64"] },
{ "target": "rpm", "arch": ["x64", "arm64"] },
{ "target": "pacman", "arch": ["x64", "arm64"] }
],
"icon": "build/icon.png",
"category": "Game",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support"
"category": "Game"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
},
{
"target": "zip",
"arch": [
"universal"
]
}
{ "target": "dmg", "arch": ["universal"] },
{ "target": "zip", "arch": ["universal"] }
],
"icon": "build/icon.icns",
"category": "public.app-category.games"
@@ -118,6 +101,3 @@
}
}
}

View File

@@ -1,7 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
launchGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath),
launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference),
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
closeWindow: () => ipcRenderer.invoke('window-close'),
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
@@ -17,10 +17,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
saveLanguage: (language) => ipcRenderer.invoke('save-language', language),
loadLanguage: () => ipcRenderer.invoke('load-language'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
repairGame: () => ipcRenderer.invoke('repair-game'),
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
openExternal: (url) => ipcRenderer.invoke('open-external', url),
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
@@ -49,6 +52,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('show-update-popup', (event, data) => callback(data));
},
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),
saveGpuPreference: (gpuPreference) => ipcRenderer.invoke('save-gpu-preference', gpuPreference),
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),
getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'),
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
onFirstLaunchUpdate: (callback) => {
@@ -65,6 +73,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
// UUID Management methods
@@ -73,5 +82,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid),
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid')
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
// Profile API
profile: {
create: (name) => ipcRenderer.invoke('profile-create', name),
list: () => ipcRenderer.invoke('profile-list'),
getActive: () => ipcRenderer.invoke('profile-get-active'),
activate: (id) => ipcRenderer.invoke('profile-activate', id),
delete: (id) => ipcRenderer.invoke('profile-delete', id),
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
}
});