mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 12:51:47 -03:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71974e031f | ||
|
|
5bd52f09db | ||
|
|
828d05ca33 | ||
|
|
1c779e0e41 | ||
|
|
bb474fe233 | ||
|
|
917f5f455b | ||
|
|
1dd42bdc79 | ||
|
|
7cfe3edd32 | ||
|
|
eb22758ab9 | ||
|
|
42fd51486a | ||
|
|
9ef05e8322 | ||
|
|
4ac12e0e24 | ||
|
|
72a151930e | ||
|
|
a9644b8c64 | ||
|
|
9fc238e103 | ||
|
|
b62e94a415 | ||
|
|
3e82e8fadb | ||
|
|
a355133ccf | ||
|
|
ff5acb5278 | ||
|
|
5d75ca80aa | ||
|
|
281aa6fcde | ||
|
|
158d0af820 | ||
|
|
503f304704 | ||
|
|
234e1e1008 | ||
|
|
d002e831cd | ||
|
|
048f2040f1 | ||
|
|
b05aeef66d | ||
|
|
30265549cf | ||
|
|
5a3efba1d6 | ||
|
|
479f24e86f | ||
|
|
611b7a7084 | ||
|
|
e472435927 | ||
|
|
f40d0105df | ||
|
|
96db9adf68 | ||
|
|
a0f49f126c | ||
|
|
f8333c09cd | ||
|
|
bcf0326763 | ||
|
|
0a5c3db710 | ||
|
|
905a9d754c | ||
|
|
99f032e9ab | ||
|
|
5e3506a9ac | ||
|
|
f2a05d2079 | ||
|
|
cece338609 | ||
|
|
261582a882 | ||
|
|
0f0f0fa308 | ||
|
|
5e6a07f0a6 | ||
|
|
713377fdc6 | ||
|
|
2671a59f38 | ||
|
|
8e9af9c768 | ||
|
|
e962a8880d | ||
|
|
c2d5536dd0 | ||
|
|
727be2ca5c | ||
|
|
64892c81e9 |
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
@@ -3,7 +3,7 @@ name: Build and Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- release
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -13,19 +13,30 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
# FIX Install bsdtar for Pacman builds
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libarchive-tools
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm install
|
|
||||||
- run: npm ci
|
- 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
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-builds
|
name: linux-builds
|
||||||
path: |
|
path: |
|
||||||
dist/*.AppImage
|
dist/*.AppImage
|
||||||
dist/*.deb
|
dist/*.deb
|
||||||
|
dist/*.rpm
|
||||||
|
dist/*.pacman
|
||||||
|
dist/latest.yml
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@@ -35,7 +46,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm install
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npx electron-builder --win --publish never
|
- run: npx electron-builder --win --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@@ -43,6 +53,7 @@ jobs:
|
|||||||
name: windows-builds
|
name: windows-builds
|
||||||
path: |
|
path: |
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
|
dist/latest.yml
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
@@ -52,7 +63,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm install
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npx electron-builder --mac --publish never
|
- run: npx electron-builder --mac --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@@ -66,12 +76,19 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
needs: [build-linux, build-windows, build-macos]
|
needs: [build-linux, build-windows, build-macos]
|
||||||
runs-on: ubuntu-latest
|
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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
# FIX: './package.json' Module Not Found in `Get version` step
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -80,13 +97,22 @@ jobs:
|
|||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
run: ls -R artifacts
|
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
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
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: |
|
files: |
|
||||||
artifacts/linux-builds/*
|
artifacts/linux-builds/**/*
|
||||||
artifacts/windows-builds/*
|
artifacts/windows-builds/**/*
|
||||||
artifacts/macos-builds/*
|
artifacts/macos-builds/**/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
draft: false
|
draft: true
|
||||||
prerelease: false
|
# 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
10
.gitignore
vendored
@@ -1,3 +1,11 @@
|
|||||||
dist/*
|
dist/*
|
||||||
node_modules/*
|
node_modules/*
|
||||||
package-lock.json
|
bun.lock
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
src/
|
||||||
|
pkg/
|
||||||
|
|
||||||
|
# Package files
|
||||||
|
*.tar.zst
|
||||||
|
*.zst
|
||||||
524
GUI/index.html
524
GUI/index.html
@@ -1,66 +1,95 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Hytale F2P Launcher</title>
|
<title>Hytale F2P Launcher</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
<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 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
|
||||||
<link rel="stylesheet" href="style.css">
|
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>
|
</head>
|
||||||
|
|
||||||
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
|
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
|
||||||
<div class="absolute inset-0 z-0">
|
<div class="absolute inset-0 z-0">
|
||||||
<img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" />
|
<img src="https://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-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>
|
||||||
|
|
||||||
<div class="flex w-full h-screen relative z-10">
|
<div class="flex w-full h-screen relative z-10">
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<div class="sidebar-logo">
|
<div class="sidebar-logo">
|
||||||
<img src="./icon.png" alt="Hytale Logo" />
|
<img src="./icon.png" alt="Hytale Logo" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-nav">
|
<div class="sidebar-nav">
|
||||||
<div class="nav-item active" data-page="play">
|
<div class="nav-item active" data-page="play">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
<span class="nav-tooltip">Play</span>
|
<span class="nav-tooltip" data-i18n="nav.play">Play</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="mods">
|
<div class="nav-item" data-page="mods">
|
||||||
<i class="fas fa-box"></i>
|
<i class="fas fa-box"></i>
|
||||||
<span class="nav-tooltip">Mods</span>
|
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="news">
|
<div class="nav-item" data-page="news">
|
||||||
<i class="fas fa-newspaper"></i>
|
<i class="fas fa-newspaper"></i>
|
||||||
<span class="nav-tooltip">News</span>
|
<span class="nav-tooltip" data-i18n="nav.news">News</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="chat">
|
<div class="nav-item" data-page="chat">
|
||||||
<i class="fas fa-comments"></i>
|
<i class="fas fa-comments"></i>
|
||||||
<span class="nav-tooltip">Players Chat</span>
|
<span class="nav-tooltip" data-i18n="nav.chat">Players Chat</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="settings">
|
<div class="nav-item" data-page="settings">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
<span class="nav-tooltip">Settings</span>
|
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="skins">
|
<div class="nav-item" data-page="skins">
|
||||||
<i class="fas fa-user"></i>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div id="playersOnlineCounter" class="players-counter">
|
<div id="playersOnlineCounter" class="players-counter">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
<span class="counter-label">Players:</span>
|
<span class="counter-label" data-i18n="header.playersLabel">Players:</span>
|
||||||
<span id="onlineCount" class="counter-value">0</span>
|
<span id="onlineCount" class="counter-value">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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">
|
<div class="window-controls">
|
||||||
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
|
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
|
||||||
<i class="fas fa-minus"></i>
|
<i class="fas fa-minus"></i>
|
||||||
@@ -76,7 +105,7 @@
|
|||||||
HY<span class="title-accent">TALE</span>
|
HY<span class="title-accent">TALE</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="game-tags">
|
<div class="game-tags">
|
||||||
<span class="tag">FREE TO PLAY</span>
|
<span class="tag" data-i18n="header.f2p">FREE TO PLAY</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,26 +116,28 @@
|
|||||||
<h1 class="install-title">
|
<h1 class="install-title">
|
||||||
HYTA<span class="title-accent">LE</span>
|
HYTA<span class="title-accent">LE</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="install-subtitle">FREE TO PLAY LAUNCHER</p>
|
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="install-form">
|
<div class="install-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Player Name</label>
|
<label class="form-label" data-i18n="install.playerName">Player Name</label>
|
||||||
<input type="text" id="installPlayerName" placeholder="Enter your name" class="form-input" value="Player" />
|
<input type="text" id="installPlayerName" data-i18n-placeholder="install.playerNamePlaceholder"
|
||||||
|
class="form-input" value="Player" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-group">
|
<label class="checkbox-group">
|
||||||
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
|
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
|
||||||
<span class="checkbox-label">Custom Installation</span>
|
<span class="checkbox-label" data-i18n="install.customInstallation">Custom Installation</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div id="installCustomOptions" class="custom-options">
|
<div id="installCustomOptions" class="custom-options">
|
||||||
<div class="form-subgroup">
|
<div class="form-subgroup">
|
||||||
<label class="form-label">Installation Folder</label>
|
<label class="form-label" data-i18n="install.installationFolder">Installation Folder</label>
|
||||||
<div class="input-with-button">
|
<div class="input-with-button">
|
||||||
<input type="text" id="installPath" placeholder="Default location" class="form-input" readonly />
|
<input type="text" id="installPath" data-i18n-placeholder="install.pathPlaceholder"
|
||||||
|
class="form-input" readonly />
|
||||||
<button onclick="browseInstallPath()" class="browse-btn">
|
<button onclick="browseInstallPath()" class="browse-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -114,10 +145,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="installBtn" class="install-button" onclick="installGame()">
|
<button id="installBtn" class="install-button" onclick="installGame()">
|
||||||
<i class="fas fa-download mr-2"></i>
|
<i class="fas fa-download mr-2"></i>
|
||||||
<span id="installText">INSTALL HYTALE</span>
|
<span id="installText" data-i18n="install.installButton">INSTALL HYTALE</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,14 +161,14 @@
|
|||||||
<div class="play-header">
|
<div class="play-header">
|
||||||
<h2 class="play-title">
|
<h2 class="play-title">
|
||||||
<i class="fas fa-play-circle mr-2"></i>
|
<i class="fas fa-play-circle mr-2"></i>
|
||||||
READY TO PLAY
|
<span data-i18n="play.ready">READY TO PLAY</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="play-subtitle">Launch Hytale and enter the adventure</p>
|
<p class="play-subtitle" data-i18n="play.subtitle">Launch Hytale and enter the adventure</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
|
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
<span>PLAY HYTALE</span>
|
<span data-i18n="play.playButton">PLAY HYTALE</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,10 +177,10 @@
|
|||||||
<div class="news-header">
|
<div class="news-header">
|
||||||
<h2 class="news-title">
|
<h2 class="news-title">
|
||||||
<i class="fas fa-star mr-2"></i>
|
<i class="fas fa-star mr-2"></i>
|
||||||
LATEST NEWS
|
<span data-i18n="play.latestNews">LATEST NEWS</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button class="view-all-btn" onclick="navigateToPage('news')">
|
<button class="view-all-btn" onclick="navigateToPage('news')">
|
||||||
VIEW ALL <i class="fas fa-arrow-right ml-1"></i>
|
<span data-i18n="play.viewAll">VIEW ALL</span> <i class="fas fa-arrow-right ml-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="newsGrid" class="news-grid-horizontal"></div>
|
<div id="newsGrid" class="news-grid-horizontal"></div>
|
||||||
@@ -160,12 +191,12 @@
|
|||||||
<div class="mods-header">
|
<div class="mods-header">
|
||||||
<div class="mods-search-container">
|
<div class="mods-search-container">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<input type="text" id="modsSearch" placeholder="Search mods..." class="mods-search" />
|
<input type="text" id="modsSearch" data-i18n-placeholder="mods.searchPlaceholder" class="mods-search" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mods-actions">
|
<div class="mods-actions">
|
||||||
<button id="myModsBtn" class="mods-btn-primary">
|
<button id="myModsBtn" class="mods-btn-primary">
|
||||||
<i class="fas fa-box"></i>
|
<i class="fas fa-box"></i>
|
||||||
MY MODS
|
<span data-i18n="mods.myMods">MY MODS</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,13 +207,13 @@
|
|||||||
<div class="mods-pagination">
|
<div class="mods-pagination">
|
||||||
<button id="prevPage" class="pagination-btn">
|
<button id="prevPage" class="pagination-btn">
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
PREVIOUS
|
<span data-i18n="mods.previous">PREVIOUS</span>
|
||||||
</button>
|
</button>
|
||||||
<span class="pagination-info">
|
<span class="pagination-info">
|
||||||
Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
|
<span data-i18n="mods.page">Page</span> <span id="currentPage">1</span> <span data-i18n="mods.of">of</span> <span id="totalPages">1</span>
|
||||||
</span>
|
</span>
|
||||||
<button id="nextPage" class="pagination-btn">
|
<button id="nextPage" class="pagination-btn">
|
||||||
NEXT
|
<span data-i18n="mods.next">NEXT</span>
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +223,7 @@
|
|||||||
<div class="news-header">
|
<div class="news-header">
|
||||||
<h2 class="news-title">
|
<h2 class="news-title">
|
||||||
<i class="fas fa-newspaper mr-2"></i>
|
<i class="fas fa-newspaper mr-2"></i>
|
||||||
ALL NEWS
|
<span data-i18n="news.title">ALL NEWS</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="allNewsGrid" class="news-grid-full"></div>
|
<div id="allNewsGrid" class="news-grid-full"></div>
|
||||||
@@ -203,34 +234,30 @@
|
|||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<h2 class="chat-title">
|
<h2 class="chat-title">
|
||||||
<i class="fas fa-comments mr-2"></i>
|
<i class="fas fa-comments mr-2"></i>
|
||||||
PLAYERS CHAT
|
<span data-i18n="chat.title">PLAYERS CHAT</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="chat-header-actions">
|
<div class="chat-header-actions">
|
||||||
<button id="chatColorBtn" class="chat-color-btn">
|
<button id="chatColorBtn" class="chat-color-btn">
|
||||||
<i class="fas fa-palette"></i>
|
<i class="fas fa-palette"></i>
|
||||||
<span>Color</span>
|
<span data-i18n="chat.pickColor">Color</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="chat-online-badge">
|
<div class="chat-online-badge">
|
||||||
<i class="fas fa-circle"></i>
|
<i class="fas fa-circle"></i>
|
||||||
<span id="chatOnlineCount">0</span> online
|
<span id="chatOnlineCount">0</span> <span data-i18n="chat.online">online</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-body">
|
<div class="chat-body">
|
||||||
<div id="chatMessages" class="chat-messages">
|
<div id="chatMessages" class="chat-messages">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-footer">
|
<div class="chat-footer">
|
||||||
<div class="chat-input-container">
|
<div class="chat-input-container">
|
||||||
<textarea
|
<textarea id="chatInput" class="chat-input"
|
||||||
id="chatInput"
|
data-i18n-placeholder="chat.inputPlaceholder" rows="1"
|
||||||
class="chat-input"
|
maxlength="500"></textarea>
|
||||||
placeholder="Type your message... (Links are automatically censored)"
|
|
||||||
rows="1"
|
|
||||||
maxlength="500"
|
|
||||||
></textarea>
|
|
||||||
<button id="chatSendBtn" class="chat-send-btn">
|
<button id="chatSendBtn" class="chat-send-btn">
|
||||||
<i class="fas fa-paper-plane"></i>
|
<i class="fas fa-paper-plane"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -239,7 +266,7 @@
|
|||||||
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
|
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
|
||||||
<span class="chat-warning-text">
|
<span class="chat-warning-text">
|
||||||
<i class="fas fa-shield-alt"></i>
|
<i class="fas fa-shield-alt"></i>
|
||||||
Secure chat - Links are censored
|
<span data-i18n="chat.secureChat">Secure chat - Links are censored</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,102 +278,71 @@
|
|||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2 class="settings-title">
|
<h2 class="settings-title">
|
||||||
<i class="fas fa-cog mr-2"></i>
|
<i class="fas fa-cog mr-2"></i>
|
||||||
SETTINGS
|
<span data-i18n="settings.title">SETTINGS</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-coffee"></i>
|
<i class="fas fa-gamepad"></i>
|
||||||
Java Runtime
|
<span data-i18n="settings.game">Game Options</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<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">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Java Executable Path</label>
|
<label class="settings-input-label" data-i18n="settings.playerName">Player Name</label>
|
||||||
<div class="settings-input-with-button">
|
<input type="text" id="settingsPlayerName" class="settings-input"
|
||||||
<input
|
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
|
||||||
type="text"
|
<p class="settings-hint">
|
||||||
id="customJavaPath"
|
<i class="fas fa-user"></i>
|
||||||
class="settings-input"
|
<span data-i18n="settings.playerNameHint">This name will be used in-game (1-16 characters)</span>
|
||||||
placeholder="Select Java path..."
|
</p>
|
||||||
readonly
|
</div>
|
||||||
/>
|
</div>
|
||||||
<button id="browseJavaBtn" class="settings-browse-btn">
|
|
||||||
<i class="fas fa-folder-open"></i>
|
<div class="settings-option">
|
||||||
Browse
|
<div class="settings-button-group">
|
||||||
</button>
|
<button id="openGameLocationBtn" class="settings-action-btn"
|
||||||
|
onclick="openGameLocation()">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
<div class="btn-content">
|
||||||
|
<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>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Select the Java installation folder (supports Windows, Mac, Linux)
|
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux: affects DRI_PRIME)</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div id="gpu-detection-info" class="gpu-detection-info"></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
|
|
||||||
</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"
|
|
||||||
/>
|
|
||||||
<p class="settings-hint">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
This name will be used in-game (1-16 characters)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-button-group">
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,30 +350,26 @@
|
|||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-fingerprint"></i>
|
<i class="fas fa-fingerprint"></i>
|
||||||
Player UUID Management
|
<span data-i18n="settings.account">Player UUID Management</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Current UUID</label>
|
<label class="settings-input-label" data-i18n="settings.currentUUID">Current UUID</label>
|
||||||
<div class="uuid-display-container">
|
<div class="uuid-display-container">
|
||||||
<input
|
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
||||||
type="text"
|
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
|
||||||
id="currentUuid"
|
|
||||||
class="settings-input uuid-input"
|
|
||||||
readonly
|
|
||||||
placeholder="Loading UUID..."
|
|
||||||
/>
|
|
||||||
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<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>
|
<i class="fas fa-sync-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
Your unique player identifier for this username
|
<span data-i18n="settings.uuidHint">Your unique player identifier for this username</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,13 +379,85 @@
|
|||||||
<button id="manageUuidsBtn" class="settings-action-btn">
|
<button id="manageUuidsBtn" class="settings-action-btn">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
<div class="btn-content">
|
<div class="btn-content">
|
||||||
<div class="btn-title">Manage All UUIDs</div>
|
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All UUIDs</div>
|
||||||
<div class="btn-description">View and manage all player UUIDs</div>
|
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">View and manage all player UUIDs</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,12 +465,36 @@
|
|||||||
<div id="skins-page" class="page">
|
<div id="skins-page" class="page">
|
||||||
<div class="placeholder-content">
|
<div class="placeholder-content">
|
||||||
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
|
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
|
||||||
<h2>Skins</h2>
|
<h2 data-i18n="skins.title">Skins</h2>
|
||||||
<p>Skin customization coming soon...</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -415,7 +503,7 @@
|
|||||||
<div class="mods-modal-header">
|
<div class="mods-modal-header">
|
||||||
<h2 class="mods-modal-title">
|
<h2 class="mods-modal-title">
|
||||||
<i class="fas fa-box mr-2"></i>
|
<i class="fas fa-box mr-2"></i>
|
||||||
MY MODS
|
<span data-i18n="mods.modalTitle">MY MODS</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button id="closeMyModsModal" class="mods-modal-close">
|
<button id="closeMyModsModal" class="mods-modal-close">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -431,7 +519,7 @@
|
|||||||
<div id="progressOverlay" class="progress-overlay" style="display: none;">
|
<div id="progressOverlay" class="progress-overlay" style="display: none;">
|
||||||
<div class="progress-content">
|
<div class="progress-content">
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
<span id="progressText">Initializing...</span>
|
<span id="progressText" data-i18n="progress.initializing">Initializing...</span>
|
||||||
<span id="progressPercent">0%</span>
|
<span id="progressPercent">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container">
|
<div class="progress-bar-container">
|
||||||
@@ -449,35 +537,29 @@
|
|||||||
<div class="chat-username-modal-header">
|
<div class="chat-username-modal-header">
|
||||||
<h2 class="chat-username-modal-title">
|
<h2 class="chat-username-modal-title">
|
||||||
<i class="fas fa-comments mr-2"></i>
|
<i class="fas fa-comments mr-2"></i>
|
||||||
Join Chat
|
<span data-i18n="chat.joinChat">Join Chat</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-username-modal-body">
|
<div class="chat-username-modal-body">
|
||||||
<p class="chat-username-modal-description">
|
<p class="chat-username-modal-description" data-i18n="chat.chooseUsername">
|
||||||
Choose a username to join the Players Chat
|
Choose a username to join the Players Chat
|
||||||
</p>
|
</p>
|
||||||
<div class="chat-username-input-group">
|
<div class="chat-username-input-group">
|
||||||
<label for="chatUsernameInput" class="chat-username-label">Username</label>
|
<label for="chatUsernameInput" class="chat-username-label" data-i18n="chat.username">Username</label>
|
||||||
<input
|
<input type="text" id="chatUsernameInput" class="chat-username-input"
|
||||||
type="text"
|
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
|
||||||
id="chatUsernameInput"
|
<span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, - and _ only</span>
|
||||||
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>
|
|
||||||
<span id="chatUsernameError" class="chat-username-error"></span>
|
<span id="chatUsernameError" class="chat-username-error"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-username-modal-footer">
|
<div class="chat-username-modal-footer">
|
||||||
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
|
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
Cancel
|
<span data-i18n="common.cancel">Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
|
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
Join Chat
|
<span data-i18n="chat.joinButton">Join Chat</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -489,22 +571,23 @@
|
|||||||
<div class="uuid-modal-header">
|
<div class="uuid-modal-header">
|
||||||
<h2 class="uuid-modal-title">
|
<h2 class="uuid-modal-title">
|
||||||
<i class="fas fa-fingerprint mr-2"></i>
|
<i class="fas fa-fingerprint mr-2"></i>
|
||||||
UUID Management
|
<span data-i18n="uuid.modalTitle">UUID Management</span>
|
||||||
</h2>
|
</h2>
|
||||||
<button id="uuidModalClose" class="modal-close-btn">
|
<button id="uuidModalClose" class="modal-close-btn">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uuid-modal-body">
|
<div class="uuid-modal-body">
|
||||||
<div class="uuid-current-section">
|
<div class="uuid-current-section">
|
||||||
<h3 class="uuid-section-title">Current User UUID</h3>
|
<h3 class="uuid-section-title" data-i18n="uuid.currentUserUUID">Current User UUID</h3>
|
||||||
<div class="uuid-current-display">
|
<div class="uuid-current-display">
|
||||||
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
|
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
|
||||||
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
|
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</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>
|
<i class="fas fa-sync-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,66 +595,99 @@
|
|||||||
|
|
||||||
<div class="uuid-list-section">
|
<div class="uuid-list-section">
|
||||||
<div class="uuid-list-header">
|
<div class="uuid-list-header">
|
||||||
<h3 class="uuid-section-title">All Player UUIDs</h3>
|
<h3 class="uuid-section-title" data-i18n="uuid.allPlayerUUIDs">All Player UUIDs</h3>
|
||||||
<button id="generateNewUuidBtn" class="uuid-generate-btn">
|
<button id="generateNewUuidBtn" class="uuid-generate-btn">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
Generate New UUID
|
<span data-i18n="uuid.generateNew">Generate New UUID</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="uuidList" class="uuid-list">
|
<div id="uuidList" class="uuid-list">
|
||||||
<div class="uuid-loading">
|
<div class="uuid-loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
Loading UUIDs...
|
<span data-i18n="uuid.loadingUUIDs">Loading UUIDs...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uuid-custom-section">
|
<div class="uuid-custom-section">
|
||||||
<h3 class="uuid-section-title">Set Custom UUID</h3>
|
<h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
|
||||||
<div class="uuid-custom-form">
|
<div class="uuid-custom-form">
|
||||||
<input
|
<input type="text" id="customUuidInput" class="uuid-input"
|
||||||
type="text"
|
data-i18n-placeholder="uuid.customPlaceholder"
|
||||||
id="customUuidInput"
|
maxlength="36" />
|
||||||
class="uuid-input"
|
|
||||||
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
|
|
||||||
maxlength="36"
|
|
||||||
/>
|
|
||||||
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
Set UUID
|
<span data-i18n="uuid.setUUID">Set UUID</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="uuid-custom-hint">
|
<p class="uuid-custom-hint">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
Warning: Setting a custom UUID will change your current player identity
|
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player identity</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
||||||
<div class="flex items-center justify-center text-xs text-gray-400">
|
<div class="flex items-center justify-center text-xs text-gray-400">
|
||||||
<span>Made by <a href="https://github.com/amiayweb" target="_blank" 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 class="mx-2">|</span>
|
||||||
<span>Contributors:
|
<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/chasem-dev" target="_blank"
|
||||||
<a href="https://github.com/crimera" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
|
class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</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/crimera" target="_blank"
|
||||||
<a href="https://github.com/Terromur" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
|
class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</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/sanasol" target="_blank"
|
||||||
<a href="https://github.com/fazrigading" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="js/script.js"></script> <!-- Discord Notification -->
|
<script type="module" src="js/script.js"></script> <!-- Discord Notification -->
|
||||||
<div id="discordNotification" class="discord-notification">
|
<div id="discordNotification" class="discord-notification">
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
<i class="fab fa-discord"></i>
|
<i class="fab fa-discord"></i>
|
||||||
<span class="notification-text">Join our Discord community!</span>
|
<span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span>
|
||||||
<button class="notification-action" onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
<button class="notification-action"
|
||||||
Join Discord
|
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
||||||
|
<span data-i18n="discord.joinButton">Join Discord</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="notification-close" onclick="closeDiscordNotification()">
|
<button class="notification-close" onclick="closeDiscordNotification()">
|
||||||
@@ -585,7 +701,7 @@
|
|||||||
<div class="chat-color-modal-header">
|
<div class="chat-color-modal-header">
|
||||||
<h3 class="chat-color-modal-title">
|
<h3 class="chat-color-modal-title">
|
||||||
<i class="fas fa-palette"></i>
|
<i class="fas fa-palette"></i>
|
||||||
Customize Username Color
|
<span data-i18n="chat.colorModal.title">Customize Username Color</span>
|
||||||
</h3>
|
</h3>
|
||||||
<button class="modal-close-btn" onclick="closeChatColorModal()">
|
<button class="modal-close-btn" onclick="closeChatColorModal()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
@@ -593,7 +709,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="chat-color-modal-body">
|
<div class="chat-color-modal-body">
|
||||||
<div id="solidColorSection" class="color-section">
|
<div id="solidColorSection" class="color-section">
|
||||||
<h4>Choose a solid color:</h4>
|
<h4 data-i18n="chat.colorModal.chooseSolid">Choose a solid color:</h4>
|
||||||
<div class="predefined-colors">
|
<div class="predefined-colors">
|
||||||
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
|
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
|
||||||
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
|
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
|
||||||
@@ -605,24 +721,26 @@
|
|||||||
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
|
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-color-input">
|
<div class="custom-color-input">
|
||||||
<label for="customColor">Custom color:</label>
|
<label for="customColor" data-i18n="chat.colorModal.customColor">Custom color:</label>
|
||||||
<input type="color" id="customColor" value="#3498db">
|
<input type="color" id="customColor" value="#3498db">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="color-preview">
|
<div class="color-preview">
|
||||||
<h4>Preview:</h4>
|
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
|
||||||
<div id="colorPreview" class="preview-username">YourUsername</div>
|
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">YourUsername</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-color-modal-footer">
|
<div class="chat-color-modal-footer">
|
||||||
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button>
|
<button class="btn-secondary" onclick="closeChatColorModal()"><span data-i18n="common.cancel">Cancel</span></button>
|
||||||
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button>
|
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply Color</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="js/i18n.js"></script>
|
||||||
<script type="module" src="js/update.js"></script>
|
<script type="module" src="js/update.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
89
GUI/js/i18n.js
Normal file
89
GUI/js/i18n.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Minimal i18n system - optimized async loading
|
||||||
|
const i18n = (() => {
|
||||||
|
let currentLang = 'en';
|
||||||
|
let translations = {};
|
||||||
|
const availableLanguages = [
|
||||||
|
{ code: 'en', name: 'English' },
|
||||||
|
{ code: 'es', name: 'Español' },
|
||||||
|
{ code: 'pt-BR', name: 'Português (Brasil)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load single language file
|
||||||
|
async function loadLanguage(lang) {
|
||||||
|
if (translations[lang]) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`locales/${lang}.json`);
|
||||||
|
if (response.ok) {
|
||||||
|
translations[lang] = await response.json();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load language: ${lang}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get translation by key
|
||||||
|
function t(key) {
|
||||||
|
const keys = key.split('.');
|
||||||
|
let value = translations[currentLang];
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
if (value && value[k] !== undefined) {
|
||||||
|
value = value[k];
|
||||||
|
} else {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set language
|
||||||
|
async function setLanguage(lang) {
|
||||||
|
await loadLanguage(lang);
|
||||||
|
if (translations[lang]) {
|
||||||
|
currentLang = lang;
|
||||||
|
updateDOM();
|
||||||
|
window.electronAPI?.saveLanguage(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all elements with data-i18n attribute
|
||||||
|
function updateDOM() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
el.textContent = t(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-placeholder');
|
||||||
|
el.placeholder = t(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-title');
|
||||||
|
el.title = t(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize - load saved language only
|
||||||
|
async function init(savedLang) {
|
||||||
|
const lang = savedLang || 'en';
|
||||||
|
await loadLanguage(lang);
|
||||||
|
currentLang = lang;
|
||||||
|
updateDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init,
|
||||||
|
t,
|
||||||
|
setLanguage,
|
||||||
|
getAvailableLanguages: () => availableLanguages,
|
||||||
|
getCurrentLanguage: () => currentLang
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Make i18n globally available
|
||||||
|
window.i18n = i18n;
|
||||||
@@ -51,7 +51,7 @@ export async function installGame() {
|
|||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
if (installBtn) {
|
if (installBtn) {
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
installText.textContent = 'INSTALLING...';
|
installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -59,8 +59,9 @@ export async function installGame() {
|
|||||||
const result = await window.electronAPI.installGame(playerName, '', installPath);
|
const result = await window.electronAPI.installGame(playerName, '', installPath);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
@@ -76,8 +77,9 @@ export async function installGame() {
|
|||||||
simulateInstallation(playerName);
|
simulateInstallation(playerName);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` });
|
window.LauncherUI.updateProgress({ message: errorMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
@@ -92,10 +94,13 @@ function simulateInstallation(playerName) {
|
|||||||
progress += Math.random() * 3;
|
progress += Math.random() * 3;
|
||||||
if (progress > 100) progress = 100;
|
if (progress > 100) progress = 100;
|
||||||
|
|
||||||
|
const installingMsg = window.i18n ? window.i18n.t('progress.installingGameFiles') : 'Installing game files...';
|
||||||
|
const completeMsg = window.i18n ? window.i18n.t('progress.installComplete') : 'Installation complete!';
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({
|
window.LauncherUI.updateProgress({
|
||||||
percent: progress,
|
percent: progress,
|
||||||
message: progress < 100 ? 'Installing game files...' : 'Installation complete!',
|
message: progress < 100 ? installingMsg : completeMsg,
|
||||||
speed: 1024 * 1024 * (5 + Math.random() * 10),
|
speed: 1024 * 1024 * (5 + Math.random() * 10),
|
||||||
downloaded: progress * 1024 * 1024 * 20,
|
downloaded: progress * 1024 * 1024 * 20,
|
||||||
total: 1024 * 1024 * 2000
|
total: 1024 * 1024 * 2000
|
||||||
@@ -104,9 +109,10 @@ function simulateInstallation(playerName) {
|
|||||||
|
|
||||||
if (progress >= 100) {
|
if (progress >= 100) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ export function setupLauncher() {
|
|||||||
uninstallBtn = document.getElementById('uninstallBtn');
|
uninstallBtn = document.getElementById('uninstallBtn');
|
||||||
playerNameInput = document.getElementById('playerName');
|
playerNameInput = document.getElementById('playerName');
|
||||||
javaPathInput = document.getElementById('javaPath');
|
javaPathInput = document.getElementById('javaPath');
|
||||||
|
|
||||||
if (playerNameInput) {
|
if (playerNameInput) {
|
||||||
playerNameInput.addEventListener('change', savePlayerName);
|
playerNameInput.addEventListener('change', savePlayerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (javaPathInput) {
|
if (javaPathInput) {
|
||||||
javaPathInput.addEventListener('change', saveJavaPath);
|
javaPathInput.addEventListener('change', saveJavaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
|
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
|
||||||
window.electronAPI.onProgressUpdate((data) => {
|
window.electronAPI.onProgressUpdate((data) => {
|
||||||
if (!isDownloading) return;
|
if (!isDownloading) return;
|
||||||
@@ -31,43 +31,211 @@ 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() {
|
export async function launch() {
|
||||||
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
||||||
|
|
||||||
let playerName = 'Player';
|
let playerName = 'Player';
|
||||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
|
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
|
||||||
playerName = window.SettingsAPI.getCurrentPlayerName();
|
playerName = window.SettingsAPI.getCurrentPlayerName();
|
||||||
} else if (playerNameInput && playerNameInput.value.trim()) {
|
} else if (playerNameInput && playerNameInput.value.trim()) {
|
||||||
playerName = playerNameInput.value.trim();
|
playerName = playerNameInput.value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
let javaPath = '';
|
let javaPath = '';
|
||||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
|
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
|
||||||
javaPath = window.SettingsAPI.getCurrentJavaPath();
|
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();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.disabled = true;
|
playBtn.disabled = true;
|
||||||
playText.textContent = 'LAUNCHING...';
|
playText.textContent = 'LAUNCHING...';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' });
|
const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.launchGame) {
|
if (window.electronAPI && window.electronAPI.launchGame) {
|
||||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
|
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
||||||
|
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
}
|
}
|
||||||
resetPlayButton();
|
resetPlayButton();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (window.electronAPI.minimizeWindow) {
|
if (window.electronAPI.minimizeWindow) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -79,7 +247,7 @@ export async function launch() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
}
|
}
|
||||||
@@ -87,7 +255,7 @@ export async function launch() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
}
|
}
|
||||||
@@ -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');
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.remove();
|
existingModal.remove();
|
||||||
@@ -218,31 +391,38 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallGame() {
|
export async function uninstallGame() {
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.uninstallGameMessage') : 'Are you sure you want to uninstall Hytale? All game files will be deleted.';
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.uninstallGameTitle') : 'Uninstall Game';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.uninstallGameButton') : 'Uninstall';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
'Are you sure you want to uninstall Hytale? All game files will be deleted.',
|
message,
|
||||||
'Uninstall Game',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performUninstall();
|
await performUninstall();
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Uninstall',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performUninstall() {
|
async function performUninstall() {
|
||||||
|
|
||||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
|
const uninstallingMsg = window.i18n ? window.i18n.t('progress.uninstallingGame') : 'Uninstalling game...';
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: uninstallingMsg });
|
||||||
if (uninstallBtn) uninstallBtn.disabled = true;
|
if (uninstallBtn) uninstallBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.uninstallGame) {
|
if (window.electronAPI && window.electronAPI.uninstallGame) {
|
||||||
const result = await window.electronAPI.uninstallGame();
|
const result = await window.electronAPI.uninstallGame();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(false);
|
window.LauncherUI.showLauncherOrInstall(false);
|
||||||
@@ -252,9 +432,10 @@ async function performUninstall() {
|
|||||||
throw new Error(result.error || 'Uninstall failed');
|
throw new Error(result.error || 'Uninstall failed');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
|
window.LauncherUI.updateProgress({ message: successMsg });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(false);
|
window.LauncherUI.showLauncherOrInstall(false);
|
||||||
@@ -263,8 +444,9 @@ async function performUninstall() {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMsg = window.i18n ? window.i18n.t('progress.uninstallFailed').replace('{error}', error.message) : `Uninstall failed: ${error.message}`;
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` });
|
window.LauncherUI.updateProgress({ message: errorMsg });
|
||||||
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
|
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -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() {
|
function resetPlayButton() {
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.disabled = false;
|
playBtn.disabled = false;
|
||||||
playText.textContent = 'PLAY';
|
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +529,7 @@ async function saveJavaPath() {
|
|||||||
|
|
||||||
function toggleCustomJava() {
|
function toggleCustomJava() {
|
||||||
if (!customJavaOptions) return;
|
if (!customJavaOptions) return;
|
||||||
|
|
||||||
if (customJavaCheck && customJavaCheck.checked) {
|
if (customJavaCheck && customJavaCheck.checked) {
|
||||||
customJavaOptions.style.display = 'block';
|
customJavaOptions.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
@@ -364,5 +589,59 @@ async function loadCustomJavaPath() {
|
|||||||
|
|
||||||
window.launch = launch;
|
window.launch = launch;
|
||||||
window.uninstallGame = uninstallGame;
|
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);
|
document.addEventListener('DOMContentLoaded', setupLauncher);
|
||||||
|
|||||||
96
GUI/js/logs.js
Normal file
96
GUI/js/logs.js
Normal 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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
|
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'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
182
GUI/js/mods.js
182
GUI/js/mods.js
@@ -22,10 +22,10 @@ function setupModsEventListeners() {
|
|||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
searchInput.addEventListener('input', (e) => {
|
searchInput.addEventListener('input', (e) => {
|
||||||
searchQuery = e.target.value.toLowerCase().trim();
|
searchQuery = e.target.value.toLowerCase().trim();
|
||||||
|
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
modsPage = 0;
|
modsPage = 0;
|
||||||
loadBrowseMods();
|
loadBrowseMods();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
@@ -52,7 +52,7 @@ function setupModsEventListeners() {
|
|||||||
|
|
||||||
const prevPageBtn = document.getElementById('prevPage');
|
const prevPageBtn = document.getElementById('prevPage');
|
||||||
const nextPageBtn = document.getElementById('nextPage');
|
const nextPageBtn = document.getElementById('nextPage');
|
||||||
|
|
||||||
if (prevPageBtn) {
|
if (prevPageBtn) {
|
||||||
prevPageBtn.addEventListener('click', () => {
|
prevPageBtn.addEventListener('click', () => {
|
||||||
if (modsPage > 0) {
|
if (modsPage > 0) {
|
||||||
@@ -61,7 +61,7 @@ function setupModsEventListeners() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextPageBtn) {
|
if (nextPageBtn) {
|
||||||
nextPageBtn.addEventListener('click', () => {
|
nextPageBtn.addEventListener('click', () => {
|
||||||
if (modsPage < modsTotalPages - 1) {
|
if (modsPage < modsTotalPages - 1) {
|
||||||
@@ -97,7 +97,7 @@ async function loadInstalledMods() {
|
|||||||
|
|
||||||
const mods = await window.electronAPI?.loadInstalledMods(modsPath);
|
const mods = await window.electronAPI?.loadInstalledMods(modsPath);
|
||||||
installedMods = mods || [];
|
installedMods = mods || [];
|
||||||
|
|
||||||
displayInstalledMods(installedMods);
|
displayInstalledMods(installedMods);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading installed mods:', error);
|
console.error('Error loading installed mods:', error);
|
||||||
@@ -113,10 +113,15 @@ function displayInstalledMods(mods) {
|
|||||||
modsContainer.innerHTML = `
|
modsContainer.innerHTML = `
|
||||||
<div class=\"empty-installed-mods\">
|
<div class=\"empty-installed-mods\">
|
||||||
<i class=\"fas fa-box-open\"></i>
|
<i class=\"fas fa-box-open\"></i>
|
||||||
<h4>No Mods Installed</h4>
|
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
|
||||||
<p>Add mods from CurseForge or import local files</p>
|
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (window.i18n) {
|
||||||
|
const container = modsContainer.querySelector('.empty-installed-mods');
|
||||||
|
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
|
||||||
|
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +143,9 @@ function displayInstalledMods(mods) {
|
|||||||
|
|
||||||
function createInstalledModCard(mod) {
|
function createInstalledModCard(mod) {
|
||||||
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
|
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
|
||||||
const statusText = mod.enabled ? 'ACTIVE' : 'DISABLED';
|
const statusText = mod.enabled ? (window.i18n ? window.i18n.t('mods.active') : 'ACTIVE') : (window.i18n ? window.i18n.t('mods.disabled') : 'DISABLED');
|
||||||
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
|
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
|
||||||
const toggleBtnText = mod.enabled ? 'DISABLE' : 'ENABLE';
|
const toggleBtnText = mod.enabled ? (window.i18n ? window.i18n.t('mods.disable') : 'DISABLE') : (window.i18n ? window.i18n.t('mods.enable') : 'ENABLE');
|
||||||
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
|
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -154,7 +159,7 @@ function createInstalledModCard(mod) {
|
|||||||
<h4 class="installed-mod-name">${mod.name}</h4>
|
<h4 class="installed-mod-name">${mod.name}</h4>
|
||||||
<span class="installed-mod-version">v${mod.version}</span>
|
<span class="installed-mod-version">v${mod.version}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="installed-mod-description">${mod.description || 'No description available'}</p>
|
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="installed-mod-actions">
|
<div class="installed-mod-actions">
|
||||||
@@ -163,7 +168,7 @@ function createInstalledModCard(mod) {
|
|||||||
${statusText}
|
${statusText}
|
||||||
</div>
|
</div>
|
||||||
<div class="installed-mod-buttons">
|
<div class="installed-mod-buttons">
|
||||||
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="Delete mod">
|
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="${window.i18n ? window.i18n.t('mods.delete') : 'Delete mod'}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
|
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
|
||||||
@@ -180,7 +185,7 @@ async function loadBrowseMods() {
|
|||||||
const browseContainer = document.getElementById('browseModsList');
|
const browseContainer = document.getElementById('browseModsList');
|
||||||
if (!browseContainer) return;
|
if (!browseContainer) return;
|
||||||
|
|
||||||
browseContainer.innerHTML = '<div class=\"loading-mods\"><div class=\"loading-spinner\"></div><span>Loading mods from CurseForge...</span></div>';
|
browseContainer.innerHTML = `<div class="loading-mods"><div class="loading-spinner"></div><span>${window.i18n ? window.i18n.t('mods.loadingMods') : 'Loading mods from CurseForge...'}</span></div>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!API_KEY || API_KEY.length < 10) {
|
if (!API_KEY || API_KEY.length < 10) {
|
||||||
@@ -196,13 +201,13 @@ async function loadBrowseMods() {
|
|||||||
|
|
||||||
const offset = modsPage * modsPageSize;
|
const offset = modsPage * modsPageSize;
|
||||||
let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`;
|
let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`;
|
||||||
|
|
||||||
if (searchQuery && searchQuery.length > 0) {
|
if (searchQuery && searchQuery.length > 0) {
|
||||||
url += `&searchFilter=${encodeURIComponent(searchQuery)}`;
|
url += `&searchFilter=${encodeURIComponent(searchQuery)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url);
|
console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-api-key': API_KEY,
|
'x-api-key': API_KEY,
|
||||||
@@ -221,7 +226,7 @@ async function loadBrowseMods() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('API Response data:', data);
|
console.log('API Response data:', data);
|
||||||
console.log('Total mods found:', data.data?.length || 0);
|
console.log('Total mods found:', data.data?.length || 0);
|
||||||
|
|
||||||
browseMods = (data.data || []).map(mod => ({
|
browseMods = (data.data || []).map(mod => ({
|
||||||
id: mod.id.toString(),
|
id: mod.id.toString(),
|
||||||
name: mod.name,
|
name: mod.name,
|
||||||
@@ -264,10 +269,15 @@ function displayBrowseMods(mods) {
|
|||||||
browseContainer.innerHTML = `
|
browseContainer.innerHTML = `
|
||||||
<div class=\"empty-browse-mods\">
|
<div class=\"empty-browse-mods\">
|
||||||
<i class=\"fas fa-search\"></i>
|
<i class=\"fas fa-search\"></i>
|
||||||
<h4>No Mods Found</h4>
|
<h4 data-i18n="mods.noModsFound">No Mods Found</h4>
|
||||||
<p>Try adjusting your search</p>
|
<p data-i18n="mods.noModsFoundDesc">Try adjusting your search</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (window.i18n) {
|
||||||
|
const container = browseContainer.querySelector('.empty-browse-mods');
|
||||||
|
container.querySelector('h4').textContent = window.i18n.t('mods.noModsFound');
|
||||||
|
container.querySelector('p').textContent = window.i18n.t('mods.noModsFoundDesc');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,18 +292,25 @@ function displayBrowseMods(mods) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createBrowseModCard(mod) {
|
function createBrowseModCard(mod) {
|
||||||
const isInstalled = installedMods.some(installed =>
|
const isInstalled = installedMods.some(installed => {
|
||||||
installed.name.toLowerCase().includes(mod.name.toLowerCase()) ||
|
// Check by CurseForge ID (most reliable)
|
||||||
installed.curseForgeId == mod.id
|
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 `
|
return `
|
||||||
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
|
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
|
||||||
<div class=\"mod-image\">
|
<div class=\"mod-image\">
|
||||||
${mod.thumbnailUrl ?
|
${mod.thumbnailUrl ?
|
||||||
`<img src=\"${mod.thumbnailUrl}\" alt=\"${mod.name}\" onerror=\"this.parentElement.innerHTML='<i class=\\\"fas fa-puzzle-piece\\\"></i>'\">` :
|
`<img src=\"${mod.thumbnailUrl}\" alt=\"${mod.name}\" onerror=\"this.parentElement.innerHTML='<i class=\\\"fas fa-puzzle-piece\\\"></i>'\">` :
|
||||||
`<i class=\"fas fa-puzzle-piece\"></i>`
|
`<i class=\"fas fa-puzzle-piece\"></i>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=\"mod-info\">
|
<div class=\"mod-info\">
|
||||||
@@ -317,18 +334,18 @@ function createBrowseModCard(mod) {
|
|||||||
<div class=\"mod-actions\">
|
<div class=\"mod-actions\">
|
||||||
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
|
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
|
||||||
<i class=\"fas fa-external-link-alt\"></i>
|
<i class=\"fas fa-external-link-alt\"></i>
|
||||||
VIEW
|
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
|
||||||
</button>
|
</button>
|
||||||
${!isInstalled ?
|
${!isInstalled ?
|
||||||
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
|
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
|
||||||
<i class=\"fas fa-download\"></i>
|
<i class="fas fa-download"></i>
|
||||||
INSTALL
|
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
|
||||||
</button>` :
|
</button>` :
|
||||||
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
|
`<button class="mod-btn-toggle bg-white/10 text-white" disabled>
|
||||||
<i class=\"fas fa-check\"></i>
|
<i class="fas fa-check"></i>
|
||||||
INSTALLED
|
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
|
||||||
</button>`
|
</button>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -336,10 +353,11 @@ function createBrowseModCard(mod) {
|
|||||||
|
|
||||||
async function downloadAndInstallMod(modInfo) {
|
async function downloadAndInstallMod(modInfo) {
|
||||||
try {
|
try {
|
||||||
window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`);
|
const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`;
|
||||||
|
window.LauncherUI?.showProgress(downloadMsg);
|
||||||
|
|
||||||
const result = await window.electronAPI?.downloadMod(modInfo);
|
const result = await window.electronAPI?.downloadMod(modInfo);
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
const newMod = {
|
const newMod = {
|
||||||
id: result.modInfo.id,
|
id: result.modInfo.id,
|
||||||
@@ -354,30 +372,33 @@ async function downloadAndInstallMod(modInfo) {
|
|||||||
curseForgeId: modInfo.modId,
|
curseForgeId: modInfo.modId,
|
||||||
curseForgeFileId: modInfo.fileId
|
curseForgeFileId: modInfo.fileId
|
||||||
};
|
};
|
||||||
|
|
||||||
installedMods.push(newMod);
|
installedMods.push(newMod);
|
||||||
|
|
||||||
await loadInstalledMods();
|
await loadInstalledMods();
|
||||||
await loadBrowseMods();
|
await loadBrowseMods();
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification(`${modInfo.name} installed successfully! 🎉`, 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.modsInstalledSuccess').replace('{name}', modInfo.name) : `${modInfo.name} installed successfully! 🎉`;
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.error || 'Failed to download mod');
|
throw new Error(result?.error || 'Failed to download mod');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading mod:', error);
|
console.error('Error downloading mod:', error);
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification('Failed to download mod: ' + error.message, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsDownloadFailed').replace('{error}', error.message) : 'Failed to download mod: ' + error.message;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleMod(modId) {
|
async function toggleMod(modId) {
|
||||||
try {
|
try {
|
||||||
window.LauncherUI?.showProgress('Toggling mod...');
|
const toggleMsg = window.i18n ? window.i18n.t('notifications.modsTogglingMod') : 'Toggling mod...';
|
||||||
|
window.LauncherUI?.showProgress(toggleMsg);
|
||||||
|
|
||||||
const modsPath = await window.electronAPI?.getModsPath();
|
const modsPath = await window.electronAPI?.getModsPath();
|
||||||
const result = await window.electronAPI?.toggleMod(modId, modsPath);
|
const result = await window.electronAPI?.toggleMod(modId, modsPath);
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
await loadInstalledMods();
|
await loadInstalledMods();
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
@@ -387,7 +408,8 @@ async function toggleMod(modId) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling mod:', error);
|
console.error('Error toggling mod:', error);
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification('Failed to toggle mod: ' + error.message, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsToggleFailed').replace('{error}', error.message) : 'Failed to toggle mod: ' + error.message;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,27 +417,34 @@ async function deleteMod(modId) {
|
|||||||
const mod = installedMods.find(m => m.id === modId);
|
const mod = installedMods.find(m => m.id === modId);
|
||||||
if (!mod) return;
|
if (!mod) return;
|
||||||
|
|
||||||
|
const confirmMsg = window.i18n ?
|
||||||
|
window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') :
|
||||||
|
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`;
|
||||||
|
|
||||||
showConfirmModal(
|
showConfirmModal(
|
||||||
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`,
|
confirmMsg,
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
window.LauncherUI?.showProgress('Deleting mod...');
|
const deleteMsg = window.i18n ? window.i18n.t('notifications.modsDeletingMod') : 'Deleting mod...';
|
||||||
|
window.LauncherUI?.showProgress(deleteMsg);
|
||||||
|
|
||||||
const modsPath = await window.electronAPI?.getModsPath();
|
const modsPath = await window.electronAPI?.getModsPath();
|
||||||
const result = await window.electronAPI?.uninstallMod(modId, modsPath);
|
const result = await window.electronAPI?.uninstallMod(modId, modsPath);
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
await loadInstalledMods();
|
await loadInstalledMods();
|
||||||
await loadBrowseMods();
|
await loadBrowseMods();
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification(`"${mod.name}" deleted successfully`, 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.modsDeletedSuccess').replace('{name}', mod.name) : `"${mod.name}" deleted successfully`;
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.error || 'Failed to delete mod');
|
throw new Error(result?.error || 'Failed to delete mod');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting mod:', error);
|
console.error('Error deleting mod:', error);
|
||||||
window.LauncherUI?.hideProgress();
|
window.LauncherUI?.hideProgress();
|
||||||
showNotification('Failed to delete mod: ' + error.message, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsDeleteFailed').replace('{error}', error.message) : 'Failed to delete mod: ' + error.message;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -436,21 +465,21 @@ function showNotification(message, type = 'info', duration = 4000) {
|
|||||||
|
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = `mod-notification ${type}`;
|
notification.className = `mod-notification ${type}`;
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
success: 'fa-check-circle',
|
success: 'fa-check-circle',
|
||||||
error: 'fa-exclamation-circle',
|
error: 'fa-exclamation-circle',
|
||||||
info: 'fa-info-circle',
|
info: 'fa-info-circle',
|
||||||
warning: 'fa-exclamation-triangle'
|
warning: 'fa-exclamation-triangle'
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
success: '#10b981',
|
success: '#10b981',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
info: '#3b82f6',
|
info: '#3b82f6',
|
||||||
warning: '#f59e0b'
|
warning: '#f59e0b'
|
||||||
};
|
};
|
||||||
|
|
||||||
notification.innerHTML = `
|
notification.innerHTML = `
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
<i class="fas ${icons[type]}"></i>
|
<i class="fas ${icons[type]}"></i>
|
||||||
@@ -460,7 +489,7 @@ function showNotification(message, type = 'info', duration = 4000) {
|
|||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
notification.style.cssText = `
|
notification.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@@ -481,14 +510,14 @@ function showNotification(message, type = 'info', duration = 4000) {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const contentStyle = `
|
const contentStyle = `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const closeStyle = `
|
const closeStyle = `
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -500,17 +529,17 @@ function showNotification(message, type = 'info', duration = 4000) {
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
notification.querySelector('.notification-content').style.cssText = contentStyle;
|
notification.querySelector('.notification-content').style.cssText = contentStyle;
|
||||||
notification.querySelector('.notification-close').style.cssText = closeStyle;
|
notification.querySelector('.notification-close').style.cssText = closeStyle;
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
// Animate in
|
// Animate in
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.style.transform = 'translateX(0)';
|
notification.style.transform = 'translateX(0)';
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
// Auto remove
|
// Auto remove
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (notification.parentElement) {
|
if (notification.parentElement) {
|
||||||
@@ -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="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||||
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
|
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
|
||||||
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
||||||
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Confirm Deletion</h3>
|
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${window.i18n ? window.i18n.t('mods.confirmDeletion') : 'Confirm Deletion'}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 24px; color: #e5e7eb;">
|
<div style="padding: 24px; color: #e5e7eb;">
|
||||||
@@ -580,7 +609,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
">Cancel</button>
|
">${window.i18n ? window.i18n.t('common.cancel') : 'Cancel'}</button>
|
||||||
<button class="mod-confirm-delete" style="
|
<button class="mod-confirm-delete" style="
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -590,7 +619,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
">Delete</button>
|
">${window.i18n ? window.i18n.t('common.delete') : 'Delete'}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -651,13 +680,13 @@ function updatePagination() {
|
|||||||
|
|
||||||
if (currentPageEl) currentPageEl.textContent = modsPage + 1;
|
if (currentPageEl) currentPageEl.textContent = modsPage + 1;
|
||||||
if (totalPagesEl) totalPagesEl.textContent = modsTotalPages;
|
if (totalPagesEl) totalPagesEl.textContent = modsTotalPages;
|
||||||
|
|
||||||
if (prevBtn) {
|
if (prevBtn) {
|
||||||
prevBtn.disabled = modsPage === 0;
|
prevBtn.disabled = modsPage === 0;
|
||||||
prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1';
|
prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1';
|
||||||
prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer';
|
prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextBtn) {
|
if (nextBtn) {
|
||||||
nextBtn.disabled = modsPage >= modsTotalPages - 1;
|
nextBtn.disabled = modsPage >= modsTotalPages - 1;
|
||||||
nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1';
|
nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1';
|
||||||
@@ -681,7 +710,7 @@ function showInstalledModsError(message) {
|
|||||||
function viewModPage(modId) {
|
function viewModPage(modId) {
|
||||||
console.log('Looking for mod with ID:', modId, 'Type:', typeof modId);
|
console.log('Looking for mod with ID:', modId, 'Type:', typeof modId);
|
||||||
console.log('Available mods:', browseMods.map(m => ({ id: m.id, name: m.name, type: typeof m.id })));
|
console.log('Available mods:', browseMods.map(m => ({ id: m.id, name: m.name, type: typeof m.id })));
|
||||||
|
|
||||||
const mod = browseMods.find(m => m.id.toString() === modId.toString());
|
const mod = browseMods.find(m => m.id.toString() === modId.toString());
|
||||||
if (mod) {
|
if (mod) {
|
||||||
console.log('Found mod:', mod.name);
|
console.log('Found mod:', mod.name);
|
||||||
@@ -694,9 +723,9 @@ function viewModPage(modId) {
|
|||||||
const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||||
modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`;
|
modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Opening URL:', modUrl);
|
console.log('Opening URL:', modUrl);
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.openExternalLink) {
|
if (window.electronAPI && window.electronAPI.openExternalLink) {
|
||||||
window.electronAPI.openExternalLink(modUrl);
|
window.electronAPI.openExternalLink(modUrl);
|
||||||
} else {
|
} else {
|
||||||
@@ -708,7 +737,8 @@ function viewModPage(modId) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Mod not found with ID:', modId);
|
console.error('Mod not found with ID:', modId);
|
||||||
showNotification('Mod information not found', 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.modsModNotFound') : 'Mod information not found';
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +747,9 @@ window.modsManager = {
|
|||||||
deleteMod,
|
deleteMod,
|
||||||
openMyModsModal,
|
openMyModsModal,
|
||||||
closeMyModsModal,
|
closeMyModsModal,
|
||||||
viewModPage
|
viewModPage,
|
||||||
|
loadInstalledMods,
|
||||||
|
loadBrowseMods
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initModsManager);
|
document.addEventListener('DOMContentLoaded', initModsManager);
|
||||||
|
|||||||
@@ -6,6 +6,69 @@ import './mods.js';
|
|||||||
import './players.js';
|
import './players.js';
|
||||||
import './chat.js';
|
import './chat.js';
|
||||||
import './settings.js';
|
import './settings.js';
|
||||||
|
import './logs.js';
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
if (!dismissed) {
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.display = 'flex';
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
notification.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.closeDiscordNotification = function() {
|
window.closeDiscordNotification = function() {
|
||||||
const notification = document.getElementById('discordNotification');
|
const notification = document.getElementById('discordNotification');
|
||||||
@@ -15,24 +78,5 @@ window.closeDiscordNotification = function() {
|
|||||||
notification.style.display = 'none';
|
notification.style.display = 'none';
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const notification = document.getElementById('discordNotification');
|
|
||||||
if (notification) {
|
|
||||||
const dismissed = localStorage.getItem('discordNotificationDismissed');
|
|
||||||
if (!dismissed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.style.display = 'flex';
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
notification.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalClose = window.closeDiscordNotification;
|
|
||||||
window.closeDiscordNotification = function() {
|
|
||||||
localStorage.setItem('discordNotificationDismissed', 'true');
|
localStorage.setItem('discordNotificationDismissed', 'true');
|
||||||
originalClose();
|
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,7 @@ let customJavaPath;
|
|||||||
let browseJavaBtn;
|
let browseJavaBtn;
|
||||||
let settingsPlayerName;
|
let settingsPlayerName;
|
||||||
let discordRPCCheck;
|
let discordRPCCheck;
|
||||||
|
let gpuPreferenceRadios;
|
||||||
|
|
||||||
// UUID Management elements
|
// UUID Management elements
|
||||||
let currentUuidDisplay;
|
let currentUuidDisplay;
|
||||||
@@ -21,7 +22,12 @@ let uuidList;
|
|||||||
let customUuidInput;
|
let customUuidInput;
|
||||||
let setCustomUuidBtn;
|
let setCustomUuidBtn;
|
||||||
|
|
||||||
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
|
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
|
||||||
|
// Apply defaults with i18n support
|
||||||
|
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
||||||
|
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
|
||||||
|
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
|
||||||
|
|
||||||
const existingModal = document.querySelector('.custom-confirm-modal');
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.remove();
|
existingModal.remove();
|
||||||
@@ -142,6 +148,7 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
|
|||||||
document.addEventListener('keydown', handleEscape);
|
document.addEventListener('keydown', handleEscape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function initSettings() {
|
export function initSettings() {
|
||||||
setupSettingsElements();
|
setupSettingsElements();
|
||||||
loadAllSettings();
|
loadAllSettings();
|
||||||
@@ -154,6 +161,7 @@ function setupSettingsElements() {
|
|||||||
browseJavaBtn = document.getElementById('browseJavaBtn');
|
browseJavaBtn = document.getElementById('browseJavaBtn');
|
||||||
settingsPlayerName = document.getElementById('settingsPlayerName');
|
settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
discordRPCCheck = document.getElementById('discordRPCCheck');
|
discordRPCCheck = document.getElementById('discordRPCCheck');
|
||||||
|
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
|
||||||
|
|
||||||
// UUID Management elements
|
// UUID Management elements
|
||||||
currentUuidDisplay = document.getElementById('currentUuid');
|
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() {
|
function toggleCustomJava() {
|
||||||
@@ -301,9 +318,11 @@ async function saveDiscordRPC() {
|
|||||||
|
|
||||||
// Feedback visuel pour l'utilisateur
|
// Feedback visuel pour l'utilisateur
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
showNotification('Discord Rich Presence enabled', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
|
||||||
|
showNotification(msg, 'success');
|
||||||
} else {
|
} else {
|
||||||
showNotification('Discord Rich Presence disabled', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.discordDisabled') : 'Discord Rich Presence disabled';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to save Discord RPC setting');
|
throw new Error('Failed to save Discord RPC setting');
|
||||||
@@ -311,7 +330,8 @@ async function saveDiscordRPC() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving Discord RPC setting:', error);
|
console.error('Error saving Discord RPC setting:', error);
|
||||||
showNotification('Failed to save Discord setting', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.discordSaveFailed') : 'Failed to save Discord setting';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,16 +355,19 @@ async function savePlayerName() {
|
|||||||
const playerName = settingsPlayerName.value.trim();
|
const playerName = settingsPlayerName.value.trim();
|
||||||
|
|
||||||
if (!playerName) {
|
if (!playerName) {
|
||||||
showNotification('Please enter a valid player name', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
|
||||||
|
showNotification(msg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.electronAPI.saveUsername(playerName);
|
await window.electronAPI.saveUsername(playerName);
|
||||||
showNotification('Player name saved successfully', 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving player name:', error);
|
console.error('Error saving player name:', error);
|
||||||
showNotification('Failed to save player name', 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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() {
|
async function loadAllSettings() {
|
||||||
await loadCustomJavaPath();
|
await loadCustomJavaPath();
|
||||||
await loadPlayerName();
|
await loadPlayerName();
|
||||||
await loadCurrentUuid();
|
await loadCurrentUuid();
|
||||||
await loadDiscordRPC();
|
await loadDiscordRPC();
|
||||||
|
await loadGpuPreference();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openGameLocation() {
|
async function openGameLocation() {
|
||||||
@@ -421,34 +518,43 @@ async function copyCurrentUuid() {
|
|||||||
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
|
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
|
||||||
if (uuid && navigator.clipboard) {
|
if (uuid && navigator.clipboard) {
|
||||||
await navigator.clipboard.writeText(uuid);
|
await navigator.clipboard.writeText(uuid);
|
||||||
showNotification('UUID copied to clipboard!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying UUID:', error);
|
console.error('Error copying UUID:', error);
|
||||||
showNotification('Failed to copy UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function regenerateCurrentUuid() {
|
async function regenerateCurrentUuid() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
|
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
'Are you sure you want to generate a new UUID? This will change your player identity.',
|
message,
|
||||||
'Generate New UUID',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performRegenerateUuid();
|
await performRegenerateUuid();
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Generate',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error('electronAPI.resetCurrentUserUuid not available');
|
console.error('electronAPI.resetCurrentUserUuid not available');
|
||||||
showNotification('UUID regeneration not available', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenNotAvailable') : 'UUID regeneration not available';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in regenerateCurrentUuid:', error);
|
console.error('Error in regenerateCurrentUuid:', error);
|
||||||
showNotification('Failed to regenerate UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed') : 'Failed to regenerate UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +564,8 @@ async function performRegenerateUuid() {
|
|||||||
if (result.success && result.uuid) {
|
if (result.success && result.uuid) {
|
||||||
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
|
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
|
||||||
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
||||||
showNotification('New UUID generated successfully!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
if (uuidModal && uuidModal.style.display !== 'none') {
|
if (uuidModal && uuidModal.style.display !== 'none') {
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
@@ -468,7 +575,8 @@ async function performRegenerateUuid() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error regenerating UUID:', error);
|
console.error('Error regenerating UUID:', error);
|
||||||
showNotification(`Failed to regenerate UUID: ${error.message}`, 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed').replace('{error}', error.message) : `Failed to regenerate UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,19 +669,22 @@ async function generateNewUuid() {
|
|||||||
const newUuid = await window.electronAPI.generateNewUuid();
|
const newUuid = await window.electronAPI.generateNewUuid();
|
||||||
if (newUuid) {
|
if (newUuid) {
|
||||||
if (customUuidInput) customUuidInput.value = newUuid;
|
if (customUuidInput) customUuidInput.value = newUuid;
|
||||||
showNotification('New UUID generated!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating new UUID:', error);
|
console.error('Error generating new UUID:', error);
|
||||||
showNotification('Failed to generate new UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setCustomUuid() {
|
async function setCustomUuid() {
|
||||||
try {
|
try {
|
||||||
if (!customUuidInput || !customUuidInput.value.trim()) {
|
if (!customUuidInput || !customUuidInput.value.trim()) {
|
||||||
showNotification('Please enter a UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRequired') : 'Please enter a UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
if (!uuidRegex.test(uuid)) {
|
if (!uuidRegex.test(uuid)) {
|
||||||
showNotification('Invalid UUID format', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
||||||
|
showNotification(msg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.setCustomUuidMessage') : 'Are you sure you want to set this custom UUID? This will change your player identity.';
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.setCustomUuidTitle') : 'Set Custom UUID';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.setCustomUuidButton') : 'Set UUID';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
'Are you sure you want to set this custom UUID? This will change your player identity.',
|
message,
|
||||||
'Set Custom UUID',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performSetCustomUuid(uuid);
|
await performSetCustomUuid(uuid);
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Set UUID',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in setCustomUuid:', error);
|
console.error('Error in setCustomUuid:', error);
|
||||||
showNotification('Failed to set custom UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed') : 'Failed to set custom UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +730,8 @@ async function setCustomUuid() {
|
|||||||
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
||||||
if (customUuidInput) customUuidInput.value = '';
|
if (customUuidInput) customUuidInput.value = '';
|
||||||
|
|
||||||
showNotification('Custom UUID set successfully!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
} else {
|
} else {
|
||||||
@@ -621,7 +740,8 @@ async function setCustomUuid() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting custom UUID:', error);
|
console.error('Error setting custom UUID:', error);
|
||||||
showNotification(`Failed to set custom UUID: ${error.message}`, 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,29 +749,37 @@ window.copyUuid = async function(uuid) {
|
|||||||
try {
|
try {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
await navigator.clipboard.writeText(uuid);
|
await navigator.clipboard.writeText(uuid);
|
||||||
showNotification('UUID copied to clipboard!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying UUID:', error);
|
console.error('Error copying UUID:', error);
|
||||||
showNotification('Failed to copy UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.deleteUuid = async function(username) {
|
window.deleteUuid = async function(username) {
|
||||||
try {
|
try {
|
||||||
|
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
`Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`,
|
message,
|
||||||
'Delete UUID',
|
title,
|
||||||
async () => {
|
async () => {
|
||||||
await performDeleteUuid(username);
|
await performDeleteUuid(username);
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
'Delete',
|
confirmBtn,
|
||||||
'Cancel'
|
cancelBtn
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in deleteUuid:', error);
|
console.error('Error in deleteUuid:', error);
|
||||||
showNotification('Failed to delete UUID', 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed') : 'Failed to delete UUID';
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -661,7 +789,8 @@ async function performDeleteUuid(username) {
|
|||||||
const result = await window.electronAPI.deleteUuidForUser(username);
|
const result = await window.electronAPI.deleteUuidForUser(username);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showNotification('UUID deleted successfully!', 'success');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || 'Failed to delete UUID');
|
throw new Error(result.error || 'Failed to delete UUID');
|
||||||
@@ -669,7 +798,8 @@ async function performDeleteUuid(username) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting UUID:', error);
|
console.error('Error deleting UUID:', error);
|
||||||
showNotification(`Failed to delete UUID: ${error.message}`, 'error');
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
125
GUI/js/ui.js
125
GUI/js/ui.js
@@ -45,22 +45,32 @@ function handleNavigation() {
|
|||||||
function setupWindowControls() {
|
function setupWindowControls() {
|
||||||
const minimizeBtn = document.querySelector('.window-controls .minimize');
|
const minimizeBtn = document.querySelector('.window-controls .minimize');
|
||||||
const closeBtn = document.querySelector('.window-controls .close');
|
const closeBtn = document.querySelector('.window-controls .close');
|
||||||
|
|
||||||
const windowControls = document.querySelector('.window-controls');
|
const windowControls = document.querySelector('.window-controls');
|
||||||
const header = document.querySelector('.header');
|
const header = document.querySelector('.header');
|
||||||
|
|
||||||
|
const profileSelector = document.querySelector('.profile-selector');
|
||||||
|
|
||||||
|
if (profileSelector) {
|
||||||
|
profileSelector.style.pointerEvents = 'auto';
|
||||||
|
profileSelector.style.zIndex = '10000';
|
||||||
|
}
|
||||||
|
|
||||||
if (windowControls) {
|
if (windowControls) {
|
||||||
windowControls.style.pointerEvents = 'auto';
|
windowControls.style.pointerEvents = 'auto';
|
||||||
windowControls.style.zIndex = '10000';
|
windowControls.style.zIndex = '10000';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
header.style.webkitAppRegion = 'drag';
|
header.style.webkitAppRegion = 'drag';
|
||||||
if (windowControls) {
|
if (windowControls) {
|
||||||
windowControls.style.webkitAppRegion = 'no-drag';
|
windowControls.style.webkitAppRegion = 'no-drag';
|
||||||
}
|
}
|
||||||
|
if (profileSelector) {
|
||||||
|
profileSelector.style.webkitAppRegion = 'no-drag';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.electronAPI) {
|
if (window.electronAPI) {
|
||||||
if (minimizeBtn) {
|
if (minimizeBtn) {
|
||||||
minimizeBtn.onclick = (e) => {
|
minimizeBtn.onclick = (e) => {
|
||||||
@@ -82,7 +92,7 @@ function showLauncherOrInstall(isInstalled) {
|
|||||||
const install = document.getElementById('install-page');
|
const install = document.getElementById('install-page');
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
const gameTitle = document.querySelector('.game-title-section');
|
const gameTitle = document.querySelector('.game-title-section');
|
||||||
|
|
||||||
if (isInstalled) {
|
if (isInstalled) {
|
||||||
if (launcher) launcher.style.display = '';
|
if (launcher) launcher.style.display = '';
|
||||||
if (install) install.style.display = 'none';
|
if (install) install.style.display = 'none';
|
||||||
@@ -137,14 +147,14 @@ function updateProgress(data) {
|
|||||||
if (data.message && progressText) {
|
if (data.message && progressText) {
|
||||||
progressText.textContent = data.message;
|
progressText.textContent = data.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.percent !== null && data.percent !== undefined) {
|
if (data.percent !== null && data.percent !== undefined) {
|
||||||
const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
|
const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
|
||||||
if (progressPercent) progressPercent.textContent = `${percent}%`;
|
if (progressPercent) progressPercent.textContent = `${percent}%`;
|
||||||
if (progressBarFill) progressBarFill.style.width = `${percent}%`;
|
if (progressBarFill) progressBarFill.style.width = `${percent}%`;
|
||||||
if (progressBar) progressBar.style.width = `${percent}%`;
|
if (progressBar) progressBar.style.width = `${percent}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.speed && data.downloaded && data.total) {
|
if (data.speed && data.downloaded && data.total) {
|
||||||
const speedMB = (data.speed / 1024 / 1024).toFixed(2);
|
const speedMB = (data.speed / 1024 / 1024).toFixed(2);
|
||||||
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
|
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
|
||||||
@@ -157,13 +167,13 @@ function updateProgress(data) {
|
|||||||
function setupAnimations() {
|
function setupAnimations() {
|
||||||
document.body.style.opacity = '0';
|
document.body.style.opacity = '0';
|
||||||
document.body.style.transform = 'translateY(20px)';
|
document.body.style.transform = 'translateY(20px)';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.style.transition = 'all 0.6s ease';
|
document.body.style.transition = 'all 0.6s ease';
|
||||||
document.body.style.opacity = '1';
|
document.body.style.opacity = '1';
|
||||||
document.body.style.transform = 'translateY(0)';
|
document.body.style.transform = 'translateY(0)';
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
@@ -182,7 +192,7 @@ function setupAnimations() {
|
|||||||
|
|
||||||
function setupFirstLaunchHandlers() {
|
function setupFirstLaunchHandlers() {
|
||||||
console.log('Setting up first launch handlers...');
|
console.log('Setting up first launch handlers...');
|
||||||
|
|
||||||
window.electronAPI.onFirstLaunchUpdate((data) => {
|
window.electronAPI.onFirstLaunchUpdate((data) => {
|
||||||
console.log('Received first launch update event:', data);
|
console.log('Received first launch update event:', data);
|
||||||
showFirstLaunchUpdateDialog(data);
|
showFirstLaunchUpdateDialog(data);
|
||||||
@@ -195,12 +205,12 @@ function setupFirstLaunchHandlers() {
|
|||||||
showProgress();
|
showProgress();
|
||||||
updateProgress(data);
|
updateProgress(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
let lockButtonTimeout = null;
|
let lockButtonTimeout = null;
|
||||||
|
|
||||||
window.electronAPI.onLockPlayButton((locked) => {
|
window.electronAPI.onLockPlayButton((locked) => {
|
||||||
lockPlayButton(locked);
|
lockPlayButton(locked);
|
||||||
|
|
||||||
if (locked) {
|
if (locked) {
|
||||||
if (lockButtonTimeout) {
|
if (lockButtonTimeout) {
|
||||||
clearTimeout(lockButtonTimeout);
|
clearTimeout(lockButtonTimeout);
|
||||||
@@ -221,12 +231,12 @@ function setupFirstLaunchHandlers() {
|
|||||||
|
|
||||||
function showFirstLaunchUpdateDialog(data) {
|
function showFirstLaunchUpdateDialog(data) {
|
||||||
console.log('Creating first launch modal...');
|
console.log('Creating first launch modal...');
|
||||||
|
|
||||||
const existingModal = document.querySelector('.first-launch-modal-overlay');
|
const existingModal = document.querySelector('.first-launch-modal-overlay');
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.remove();
|
existingModal.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const modalOverlay = document.createElement('div');
|
const modalOverlay = document.createElement('div');
|
||||||
modalOverlay.className = 'first-launch-modal-overlay';
|
modalOverlay.className = 'first-launch-modal-overlay';
|
||||||
modalOverlay.style.cssText = `
|
modalOverlay.style.cssText = `
|
||||||
@@ -243,7 +253,7 @@ function showFirstLaunchUpdateDialog(data) {
|
|||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
pointer-events: all !important;
|
pointer-events: all !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const modalDialog = document.createElement('div');
|
const modalDialog = document.createElement('div');
|
||||||
modalDialog.className = 'first-launch-modal-dialog';
|
modalDialog.className = 'first-launch-modal-dialog';
|
||||||
modalDialog.style.cssText = `
|
modalDialog.style.cssText = `
|
||||||
@@ -257,7 +267,7 @@ function showFirstLaunchUpdateDialog(data) {
|
|||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
animation: modalSlideIn 0.3s ease-out !important;
|
animation: modalSlideIn 0.3s ease-out !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
modalDialog.innerHTML = `
|
modalDialog.innerHTML = `
|
||||||
<div style="background: linear-gradient(135deg, rgba(147, 51, 234, 0.2), rgba(59, 130, 246, 0.2)); padding: 25px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
<div style="background: linear-gradient(135deg, rgba(147, 51, 234, 0.2), rgba(59, 130, 246, 0.2)); padding: 25px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||||
<h2 style="margin: 0; color: #fff; font-size: 1.5rem; font-weight: 600; text-align: center;">
|
<h2 style="margin: 0; color: #fff; font-size: 1.5rem; font-weight: 600; text-align: center;">
|
||||||
@@ -306,9 +316,9 @@ function showFirstLaunchUpdateDialog(data) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
modalOverlay.appendChild(modalDialog);
|
modalOverlay.appendChild(modalDialog);
|
||||||
|
|
||||||
modalOverlay.onclick = (e) => {
|
modalOverlay.onclick = (e) => {
|
||||||
if (e.target === modalOverlay) {
|
if (e.target === modalOverlay) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -316,7 +326,7 @@ function showFirstLaunchUpdateDialog(data) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', function preventEscape(e) {
|
document.addEventListener('keydown', function preventEscape(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -324,55 +334,55 @@ function showFirstLaunchUpdateDialog(data) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(modalOverlay);
|
document.body.appendChild(modalOverlay);
|
||||||
|
|
||||||
const updateBtn = document.getElementById('updateGameBtn');
|
const updateBtn = document.getElementById('updateGameBtn');
|
||||||
updateBtn.onclick = () => {
|
updateBtn.onclick = () => {
|
||||||
acceptFirstLaunchUpdate();
|
acceptFirstLaunchUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.firstLaunchExistingGame = data.existingGame;
|
window.firstLaunchExistingGame = data.existingGame;
|
||||||
|
|
||||||
console.log('First launch modal created and displayed');
|
console.log('First launch modal created and displayed');
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockPlayButton(locked) {
|
function lockPlayButton(locked) {
|
||||||
const playButton = document.getElementById('homePlayBtn');
|
const playButton = document.getElementById('homePlayBtn');
|
||||||
|
|
||||||
if (!playButton) {
|
if (!playButton) {
|
||||||
console.warn('Play button not found');
|
console.warn('Play button not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locked) {
|
if (locked) {
|
||||||
playButton.style.opacity = '0.5';
|
playButton.style.opacity = '0.5';
|
||||||
playButton.style.pointerEvents = 'none';
|
playButton.style.pointerEvents = 'none';
|
||||||
playButton.style.cursor = 'not-allowed';
|
playButton.style.cursor = 'not-allowed';
|
||||||
playButton.setAttribute('data-locked', 'true');
|
playButton.setAttribute('data-locked', 'true');
|
||||||
|
|
||||||
const spanElement = playButton.querySelector('span');
|
const spanElement = playButton.querySelector('span');
|
||||||
if (spanElement) {
|
if (spanElement) {
|
||||||
if (!playButton.getAttribute('data-original-text')) {
|
if (!playButton.getAttribute('data-original-text')) {
|
||||||
playButton.setAttribute('data-original-text', spanElement.textContent);
|
playButton.setAttribute('data-original-text', spanElement.textContent);
|
||||||
}
|
}
|
||||||
spanElement.textContent = 'CHECKING...';
|
spanElement.textContent = window.i18n ? window.i18n.t('play.checking') : 'CHECKING...';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Play button locked');
|
console.log('Play button locked');
|
||||||
} else {
|
} else {
|
||||||
playButton.style.opacity = '';
|
playButton.style.opacity = '';
|
||||||
playButton.style.pointerEvents = '';
|
playButton.style.pointerEvents = '';
|
||||||
playButton.style.cursor = '';
|
playButton.style.cursor = '';
|
||||||
playButton.removeAttribute('data-locked');
|
playButton.removeAttribute('data-locked');
|
||||||
|
|
||||||
const spanElement = playButton.querySelector('span');
|
const spanElement = playButton.querySelector('span');
|
||||||
const originalText = playButton.getAttribute('data-original-text');
|
if (spanElement) {
|
||||||
if (spanElement && originalText) {
|
// Use i18n to get the current translation instead of restoring saved text
|
||||||
spanElement.textContent = originalText;
|
spanElement.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
|
||||||
playButton.removeAttribute('data-original-text');
|
playButton.removeAttribute('data-original-text');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Play button unlocked');
|
console.log('Play button unlocked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,12 +391,13 @@ function lockPlayButton(locked) {
|
|||||||
|
|
||||||
async function acceptFirstLaunchUpdate() {
|
async function acceptFirstLaunchUpdate() {
|
||||||
const existingGame = window.firstLaunchExistingGame;
|
const existingGame = window.firstLaunchExistingGame;
|
||||||
|
|
||||||
if (!existingGame) {
|
if (!existingGame) {
|
||||||
showNotification('Error: Game data not found', 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.gameDataNotFound') : 'Error: Game data not found';
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = document.querySelector('.first-launch-modal-overlay');
|
const modal = document.querySelector('.first-launch-modal-overlay');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.style.pointerEvents = 'none';
|
modal.style.pointerEvents = 'none';
|
||||||
@@ -397,27 +408,30 @@ async function acceptFirstLaunchUpdate() {
|
|||||||
btn.textContent = '🔄 Updating...';
|
btn.textContent = '🔄 Updating...';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showProgress();
|
showProgress();
|
||||||
updateProgress({ message: 'Starting mandatory game update...', percent: 0 });
|
const updateMsg = window.i18n ? window.i18n.t('progress.startingUpdate') : 'Starting mandatory game update...';
|
||||||
|
updateProgress({ message: updateMsg, percent: 0 });
|
||||||
|
|
||||||
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
|
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
|
||||||
|
|
||||||
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
|
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.remove();
|
modal.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
lockPlayButton(false);
|
lockPlayButton(false);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showNotification('Game updated successfully! 🎉', 'success');
|
const successMsg = window.i18n ? window.i18n.t('notifications.gameUpdatedSuccess') : 'Game updated successfully! 🎉';
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
} else {
|
} else {
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showNotification(`Update failed: ${result.error}`, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.updateFailed').replace('{error}', result.error) : `Update failed: ${result.error}`;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -425,7 +439,8 @@ async function acceptFirstLaunchUpdate() {
|
|||||||
}
|
}
|
||||||
lockPlayButton(false);
|
lockPlayButton(false);
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showNotification(`Update error: ${error.message}`, 'error');
|
const errorMsg = window.i18n ? window.i18n.t('notifications.updateError').replace('{error}', error.message) : `Update error: ${error.message}`;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +449,7 @@ function dismissFirstLaunchDialog() {
|
|||||||
if (modal) {
|
if (modal) {
|
||||||
modal.remove();
|
modal.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
lockPlayButton(false);
|
lockPlayButton(false);
|
||||||
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
|
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
|
||||||
}
|
}
|
||||||
@@ -443,13 +458,13 @@ function showNotification(message, type = 'info') {
|
|||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = `notification notification-${type}`;
|
notification.className = `notification notification-${type}`;
|
||||||
notification.textContent = message;
|
notification.textContent = message;
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.classList.add('show');
|
notification.classList.add('show');
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.remove();
|
notification.remove();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
@@ -463,9 +478,9 @@ function setupUI() {
|
|||||||
progressPercent = document.getElementById('progressPercent');
|
progressPercent = document.getElementById('progressPercent');
|
||||||
progressSpeed = document.getElementById('progressSpeed');
|
progressSpeed = document.getElementById('progressSpeed');
|
||||||
progressSize = document.getElementById('progressSize');
|
progressSize = document.getElementById('progressSize');
|
||||||
|
|
||||||
lockPlayButton(true);
|
lockPlayButton(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const playButton = document.getElementById('homePlayBtn');
|
const playButton = document.getElementById('homePlayBtn');
|
||||||
if (playButton && playButton.getAttribute('data-locked') === 'true') {
|
if (playButton && playButton.getAttribute('data-locked') === 'true') {
|
||||||
@@ -476,13 +491,13 @@ function setupUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 25000);
|
}, 25000);
|
||||||
|
|
||||||
handleNavigation();
|
handleNavigation();
|
||||||
setupWindowControls();
|
setupWindowControls();
|
||||||
setupSidebarLogo();
|
setupSidebarLogo();
|
||||||
setupAnimations();
|
setupAnimations();
|
||||||
setupFirstLaunchHandlers();
|
setupFirstLaunchHandlers();
|
||||||
|
|
||||||
document.body.focus();
|
document.body.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
234
GUI/locales/en.json
Normal file
234
GUI/locales/en.json
Normal 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
234
GUI/locales/es.json
Normal 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
234
GUI/locales/pt-BR.json
Normal 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!"
|
||||||
|
}
|
||||||
|
}
|
||||||
625
GUI/style.css
625
GUI/style.css
File diff suppressed because it is too large
Load Diff
9
PKGBUILD
9
PKGBUILD
@@ -1,19 +1,20 @@
|
|||||||
# Maintainer: Terromur <terromuroz@proton.me>
|
# Maintainer: Terromur <terromuroz@proton.me>
|
||||||
|
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||||
pkgname=Hytale-F2P-git
|
pkgname=Hytale-F2P-git
|
||||||
_pkgname=Hytale-F2P
|
_pkgname=Hytale-F2P
|
||||||
pkgver=2.0.2.r90.g21f8527
|
pkgver=2.0.2b.r120.gb05aeef
|
||||||
pkgrel=1
|
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')
|
arch=('x86_64')
|
||||||
url="https://github.com/amiayweb/Hytale-F2P"
|
url="https://github.com/amiayweb/Hytale-F2P"
|
||||||
license=('custom')
|
license=('custom')
|
||||||
makedepends=('npm')
|
makedepends=('npm' 'git' 'rpm-tools' 'libxcrypt-compat')
|
||||||
source=("git+$url.git" "Hytale-F2P.desktop")
|
source=("git+$url.git" "Hytale-F2P.desktop")
|
||||||
sha256sums=('SKIP' '8c78a6931fade2b0501122980dc238e042b9f6f0292b5ca74c391d7b3c1543c0')
|
sha256sums=('SKIP' '8c78a6931fade2b0501122980dc238e042b9f6f0292b5ca74c391d7b3c1543c0')
|
||||||
|
|
||||||
pkgver() {
|
pkgver() {
|
||||||
cd "$_pkgname"
|
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() {
|
build() {
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -6,7 +6,7 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
**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)**
|
||||||
|
|
||||||
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
||||||
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
||||||
@@ -78,7 +78,21 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
|||||||
|
|
||||||
## 📋 Changelog
|
## 📋 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)
|
- 🎮 **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
|
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
|
||||||
- 🎨 **Chat Improvements** - Simplified chat color system
|
- 🎨 **Chat Improvements** - Simplified chat color system
|
||||||
|
|||||||
17
SERVER.md
17
SERVER.md
@@ -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
|
3. **Host via Online Play** - Use your Radmin VPN IP instead
|
||||||
4. **Friends connect** - They'll see you on the virtual LAN
|
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)
|
## Part 2: Dedicated Server (Advanced)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ function getAuthDomain() {
|
|||||||
}
|
}
|
||||||
// Then check config file
|
// Then check config file
|
||||||
const config = loadConfig();
|
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) {
|
if (config.authDomain) {
|
||||||
return config.authDomain;
|
return config.authDomain;
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,16 @@ function saveJavaPath(javaPath) {
|
|||||||
|
|
||||||
function loadJavaPath() {
|
function loadJavaPath() {
|
||||||
const config = loadConfig();
|
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 || '';
|
return config.javaPath || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,16 +147,36 @@ function loadDiscordRPC() {
|
|||||||
return config.discordRPC !== undefined ? config.discordRPC : true;
|
return config.discordRPC !== undefined ? config.discordRPC : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveLanguage(language) {
|
||||||
|
saveConfig({ language: language || 'en' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLanguage() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.language || 'en';
|
||||||
|
}
|
||||||
|
|
||||||
function saveModsToConfig(mods) {
|
function saveModsToConfig(mods) {
|
||||||
try {
|
try {
|
||||||
let config = loadConfig();
|
const config = loadConfig();
|
||||||
config.installedMods = mods;
|
|
||||||
|
// 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);
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
if (!fs.existsSync(configDir)) {
|
if (!fs.existsSync(configDir)) {
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||||
console.log('Mods saved to config.json');
|
console.log('Mods saved to config.json');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -153,6 +187,12 @@ function saveModsToConfig(mods) {
|
|||||||
function loadModsFromConfig() {
|
function loadModsFromConfig() {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
// Prefer Active Profile
|
||||||
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
||||||
|
return config.profiles[config.activeProfileId].mods || [];
|
||||||
|
}
|
||||||
|
|
||||||
return config.installedMods || [];
|
return config.installedMods || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading mods from config:', error);
|
console.error('Error loading mods from config:', error);
|
||||||
@@ -162,19 +202,19 @@ function loadModsFromConfig() {
|
|||||||
|
|
||||||
function isFirstLaunch() {
|
function isFirstLaunch() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
if ('hasLaunchedBefore' in config) {
|
if ('hasLaunchedBefore' in config) {
|
||||||
return !config.hasLaunchedBefore;
|
return !config.hasLaunchedBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUserData = config.installPath || config.username || config.javaPath ||
|
const hasUserData = config.installPath || config.username || config.javaPath ||
|
||||||
config.chatUsername || config.userUuids ||
|
config.chatUsername || config.userUuids ||
|
||||||
Object.keys(config).length > 0;
|
Object.keys(config).length > 0;
|
||||||
|
|
||||||
if (!hasUserData) {
|
if (!hasUserData) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,17 +235,17 @@ function getAllUuidMappings() {
|
|||||||
|
|
||||||
function setUuidForUser(username, uuid) {
|
function setUuidForUser(username, uuid) {
|
||||||
const { v4: uuidv4, validate: validateUuid } = require('uuid');
|
const { v4: uuidv4, validate: validateUuid } = require('uuid');
|
||||||
|
|
||||||
// Validate UUID format
|
// Validate UUID format
|
||||||
if (!validateUuid(uuid)) {
|
if (!validateUuid(uuid)) {
|
||||||
throw new Error('Invalid UUID format');
|
throw new Error('Invalid UUID format');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const userUuids = config.userUuids || {};
|
const userUuids = config.userUuids || {};
|
||||||
userUuids[username] = uuid;
|
userUuids[username] = uuid;
|
||||||
saveConfig({ userUuids });
|
saveConfig({ userUuids });
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,13 +257,13 @@ function generateNewUuid() {
|
|||||||
function deleteUuidForUser(username) {
|
function deleteUuidForUser(username) {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const userUuids = config.userUuids || {};
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
if (userUuids[username]) {
|
if (userUuids[username]) {
|
||||||
delete userUuids[username];
|
delete userUuids[username];
|
||||||
saveConfig({ userUuids });
|
saveConfig({ userUuids });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +271,7 @@ function resetCurrentUserUuid() {
|
|||||||
const username = loadUsername();
|
const username = loadUsername();
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const newUuid = uuidv4();
|
const newUuid = uuidv4();
|
||||||
|
|
||||||
return setUuidForUser(username, newUuid);
|
return setUuidForUser(username, newUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +286,15 @@ function loadChatColor() {
|
|||||||
return config.chatColor || '#3498db';
|
return config.chatColor || '#3498db';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveGpuPreference(gpuPreference) {
|
||||||
|
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadGpuPreference() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.gpuPreference || 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
@@ -262,6 +311,8 @@ module.exports = {
|
|||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
@@ -277,5 +328,8 @@ module.exports = {
|
|||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
resetCurrentUserUuid
|
resetCurrentUserUuid,
|
||||||
|
// GPU Preference exports
|
||||||
|
saveGpuPreference,
|
||||||
|
loadGpuPreference
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,32 +77,32 @@ function findClientPath(gameLatest) {
|
|||||||
|
|
||||||
function findUserDataPath(gameLatest) {
|
function findUserDataPath(gameLatest) {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
|
|
||||||
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
|
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
|
||||||
|
|
||||||
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData'));
|
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData'));
|
||||||
candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData'));
|
candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData'));
|
||||||
candidates.push(path.join(gameLatest, 'UserData'));
|
candidates.push(path.join(gameLatest, 'UserData'));
|
||||||
|
|
||||||
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
|
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (fs.existsSync(candidate)) {
|
if (fs.existsSync(candidate)) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaultPath;
|
let defaultPath;
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
defaultPath = path.join(gameLatest, 'Client', 'UserData');
|
defaultPath = path.join(gameLatest, 'Client', 'UserData');
|
||||||
} else {
|
} else {
|
||||||
defaultPath = path.join(gameLatest, 'Client', 'UserData');
|
defaultPath = path.join(gameLatest, 'Client', 'UserData');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(defaultPath)) {
|
if (!fs.existsSync(defaultPath)) {
|
||||||
fs.mkdirSync(defaultPath, { recursive: true });
|
fs.mkdirSync(defaultPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultPath;
|
return defaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,15 +110,15 @@ function findUserDataRecursive(gameLatest) {
|
|||||||
function searchDirectory(dir) {
|
function searchDirectory(dir) {
|
||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.isDirectory()) {
|
if (item.isDirectory()) {
|
||||||
const fullPath = path.join(dir, item.name);
|
const fullPath = path.join(dir, item.name);
|
||||||
|
|
||||||
if (item.name === 'UserData') {
|
if (item.name === 'UserData') {
|
||||||
return fullPath;
|
return fullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = searchDirectory(fullPath);
|
const found = searchDirectory(fullPath);
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
@@ -127,14 +127,14 @@ function findUserDataRecursive(gameLatest) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(gameLatest)) {
|
if (!fs.existsSync(gameLatest)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = searchDirectory(gameLatest);
|
const found = searchDirectory(gameLatest);
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
@@ -152,16 +152,14 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!installPath) {
|
if (!installPath) {
|
||||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
// Use the standard app directory logic which handles platforms correctly
|
||||||
installPath = path.join(localAppData, 'HytaleF2P');
|
installPath = getAppDir();
|
||||||
} else {
|
|
||||||
installPath = path.join(installPath, 'HytaleF2P');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
|
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
|
||||||
|
|
||||||
const userDataPath = findUserDataPath(gameLatest);
|
const userDataPath = findUserDataPath(gameLatest);
|
||||||
|
|
||||||
const modsPath = path.join(userDataPath, 'Mods');
|
const modsPath = path.join(userDataPath, 'Mods');
|
||||||
const disabledModsPath = path.join(userDataPath, 'DisabledMods');
|
const disabledModsPath = path.join(userDataPath, 'DisabledMods');
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,23 @@ const {
|
|||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
markAsLaunched,
|
markAsLaunched,
|
||||||
CONFIG_FILE,
|
|
||||||
// UUID Management
|
// UUID Management
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
resetCurrentUserUuid
|
resetCurrentUserUuid,
|
||||||
|
// GPU Preference
|
||||||
|
saveGpuPreference,
|
||||||
|
loadGpuPreference
|
||||||
} = require('./core/config');
|
} = require('./core/config');
|
||||||
|
|
||||||
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
||||||
@@ -38,7 +42,8 @@ const {
|
|||||||
installGame,
|
installGame,
|
||||||
uninstallGame,
|
uninstallGame,
|
||||||
updateGameFiles,
|
updateGameFiles,
|
||||||
checkExistingGameInstallation
|
checkExistingGameInstallation,
|
||||||
|
repairGame
|
||||||
} = require('./managers/gameManager');
|
} = require('./managers/gameManager');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -77,18 +82,22 @@ const {
|
|||||||
handleFirstLaunchCheck
|
handleFirstLaunchCheck
|
||||||
} = require('./services/firstLaunch');
|
} = require('./services/firstLaunch');
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
const { detectGpu } = require('./utils/platformUtils');
|
||||||
|
|
||||||
// Re-export all functions to maintain backward compatibility
|
// Re-export all functions to maintain backward compatibility
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Game launch functions
|
// Game launch functions
|
||||||
launchGame,
|
launchGame,
|
||||||
launchGameWithVersionCheck,
|
launchGameWithVersionCheck,
|
||||||
|
|
||||||
// Game installation functions
|
// Game installation functions
|
||||||
installGame,
|
installGame,
|
||||||
isGameInstalled,
|
isGameInstalled,
|
||||||
uninstallGame,
|
uninstallGame,
|
||||||
updateGameFiles,
|
updateGameFiles,
|
||||||
|
repairGame,
|
||||||
|
|
||||||
// User configuration functions
|
// User configuration functions
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
@@ -97,30 +106,39 @@ module.exports = {
|
|||||||
saveChatColor,
|
saveChatColor,
|
||||||
loadChatColor,
|
loadChatColor,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
|
|
||||||
// Java configuration functions
|
// Java configuration functions
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
loadJavaPath,
|
loadJavaPath,
|
||||||
getJavaDetection,
|
getJavaDetection,
|
||||||
|
|
||||||
// Installation path functions
|
// Installation path functions
|
||||||
saveInstallPath,
|
saveInstallPath,
|
||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
|
|
||||||
// Discord RPC functions
|
// Discord RPC functions
|
||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
|
||||||
|
// Language functions
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
|
|
||||||
|
// GPU Preference functions
|
||||||
|
saveGpuPreference,
|
||||||
|
loadGpuPreference,
|
||||||
|
detectGpu,
|
||||||
|
|
||||||
// Version functions
|
// Version functions
|
||||||
getInstalledClientVersion,
|
getInstalledClientVersion,
|
||||||
getLatestClientVersion,
|
getLatestClientVersion,
|
||||||
|
|
||||||
// News functions
|
// News functions
|
||||||
getHytaleNews,
|
getHytaleNews,
|
||||||
|
|
||||||
// Player ID functions
|
// Player ID functions
|
||||||
getOrCreatePlayerId,
|
getOrCreatePlayerId,
|
||||||
|
|
||||||
// UUID Management functions
|
// UUID Management functions
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
@@ -128,7 +146,7 @@ module.exports = {
|
|||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
resetCurrentUserUuid,
|
resetCurrentUserUuid,
|
||||||
|
|
||||||
// Mod management functions
|
// Mod management functions
|
||||||
getModsPath,
|
getModsPath,
|
||||||
loadInstalledMods,
|
loadInstalledMods,
|
||||||
@@ -137,20 +155,20 @@ module.exports = {
|
|||||||
toggleMod,
|
toggleMod,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
|
|
||||||
// UI file management functions
|
// UI file management functions
|
||||||
downloadAndReplaceHomePageUI,
|
downloadAndReplaceHomePageUI,
|
||||||
findHomePageUIPath,
|
findHomePageUIPath,
|
||||||
downloadAndReplaceLogo,
|
downloadAndReplaceLogo,
|
||||||
findLogoPath,
|
findLogoPath,
|
||||||
|
|
||||||
// First launch functions
|
// First launch functions
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
markAsLaunched,
|
markAsLaunched,
|
||||||
checkExistingGameInstallation,
|
checkExistingGameInstallation,
|
||||||
proposeGameUpdate,
|
proposeGameUpdate,
|
||||||
handleFirstLaunchCheck,
|
handleFirstLaunchCheck,
|
||||||
|
|
||||||
// Path functions
|
// Path functions
|
||||||
getResolvedAppDir
|
getResolvedAppDir
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ const { promisify } = require('util');
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
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 { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config');
|
||||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||||
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
|
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
|
||||||
const { updateGameFiles } = require('./gameManager');
|
const { updateGameFiles } = require('./gameManager');
|
||||||
|
const { syncModsForCurrentProfile } = require('./modManager');
|
||||||
|
|
||||||
// Client patcher for custom auth server (sanasol.ws)
|
// Client patcher for custom auth server (sanasol.ws)
|
||||||
let clientPatcher = null;
|
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 customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
|
||||||
@@ -130,7 +131,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
javaBin = getJavaExec(customJreDir);
|
javaBin = getJavaExec(customJreDir);
|
||||||
|
|
||||||
if (!getBundledJavaPath(customJreDir)) {
|
if (!getBundledJavaPath(customJreDir)) {
|
||||||
const fallback = await detectSystemJava();
|
const fallback = await detectSystemJava();
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
@@ -192,9 +193,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
const serverDir = path.join(gameLatest, 'Server');
|
const serverDir = path.join(gameLatest, 'Server');
|
||||||
|
|
||||||
const signPath = async (targetPath, deep = false) => {
|
const signPath = async (targetPath, deep = false) => {
|
||||||
await execAsync(`xattr -cr "${targetPath}"`).catch(() => {});
|
await execAsync(`xattr -cr "${targetPath}"`).catch(() => { });
|
||||||
const deepFlag = deep ? '--deep ' : '';
|
const deepFlag = deep ? '--deep ' : '';
|
||||||
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => {});
|
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => { });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fs.existsSync(appBundle)) {
|
if (fs.existsSync(appBundle)) {
|
||||||
@@ -216,8 +217,8 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(serverDir)) {
|
if (fs.existsSync(serverDir)) {
|
||||||
await execAsync(`xattr -cr "${serverDir}"`).catch(() => {});
|
await execAsync(`xattr -cr "${serverDir}"`).catch(() => { });
|
||||||
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {});
|
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => { });
|
||||||
console.log('Signed server binaries (after patching)');
|
console.log('Signed server binaries (after patching)');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,13 +261,28 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Starting game...', null, null, null, null);
|
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('Starting game...');
|
||||||
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
|
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
|
||||||
|
|
||||||
const env = { ...process.env };
|
const env = { ...process.env };
|
||||||
|
|
||||||
const waylandEnv = setupWaylandEnvironment();
|
const waylandEnv = setupWaylandEnvironment();
|
||||||
Object.assign(env, waylandEnv);
|
Object.assign(env, waylandEnv);
|
||||||
|
|
||||||
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
@@ -276,8 +292,8 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
spawnOptions.shell = false;
|
spawnOptions.shell = false;
|
||||||
spawnOptions.windowsHide = true;
|
spawnOptions.windowsHide = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = spawn(clientPath, args, spawnOptions);
|
const child = spawn(clientPath, args, spawnOptions);
|
||||||
@@ -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 {
|
try {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Checking for updates...', 0, null, null, null);
|
progressCallback('Checking for updates...', 0, null, null, null);
|
||||||
@@ -371,12 +387,12 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
try {
|
try {
|
||||||
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
|
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
|
||||||
console.log('Game updated successfully, waiting before launch...');
|
console.log('Game updated successfully, waiting before launch...');
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Preparing game launch...', 90, null, null, null);
|
progressCallback('Preparing game launch...', 90, null, null, null);
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
} catch (updateError) {
|
} catch (updateError) {
|
||||||
console.error('Update failed:', updateError);
|
console.error('Update failed:', updateError);
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -390,7 +406,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
progressCallback('Launching game...', 80, null, null, null);
|
progressCallback('Launching game...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride);
|
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in version check and launch:', error);
|
console.error('Error in version check and launch:', error);
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall
|
|||||||
const osName = getOS();
|
const osName = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`;
|
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`;
|
||||||
|
|
||||||
const dest = path.join(cacheDir, fileName);
|
const dest = path.join(cacheDir, fileName);
|
||||||
|
|
||||||
if (fs.existsSync(dest)) {
|
if (fs.existsSync(dest)) {
|
||||||
@@ -25,7 +25,7 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall
|
|||||||
console.log('Fetching PWR patch file:', url);
|
console.log('Fetching PWR patch file:', url);
|
||||||
await downloadFile(url, dest, progressCallback);
|
await downloadFile(url, dest, progressCallback);
|
||||||
console.log('PWR saved to:', dest);
|
console.log('PWR saved to:', dest);
|
||||||
|
|
||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,9 +33,9 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
const butlerPath = await installButler(toolsDir);
|
const butlerPath = await installButler(toolsDir);
|
||||||
const gameLatest = gameDir;
|
const gameLatest = gameDir;
|
||||||
const stagingDir = path.join(gameLatest, 'staging-temp');
|
const stagingDir = path.join(gameLatest, 'staging-temp');
|
||||||
|
|
||||||
const clientPath = findClientPath(gameLatest);
|
const clientPath = findClientPath(gameLatest);
|
||||||
|
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
console.log('Game files detected, skipping patch installation.');
|
console.log('Game files detected, skipping patch installation.');
|
||||||
return;
|
return;
|
||||||
@@ -53,11 +53,11 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Installing game patch...');
|
console.log('Installing game patch...');
|
||||||
|
|
||||||
if (!fs.existsSync(butlerPath)) {
|
if (!fs.existsSync(butlerPath)) {
|
||||||
throw new Error(`Butler tool not found at: ${butlerPath}`);
|
throw new Error(`Butler tool not found at: ${butlerPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(pwrFile)) {
|
if (!fs.existsSync(pwrFile)) {
|
||||||
throw new Error(`PWR file not found at: ${pwrFile}`);
|
throw new Error(`PWR file not found at: ${pwrFile}`);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
pwrFile,
|
pwrFile,
|
||||||
gameLatest
|
gameLatest
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const child = execFile(butlerPath, args, {
|
const child = execFile(butlerPath, args, {
|
||||||
@@ -108,7 +108,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
console.log(`Updating game files to version: ${newVersion}`);
|
console.log(`Updating game files to version: ${newVersion}`);
|
||||||
|
|
||||||
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||||
|
|
||||||
if (fs.existsSync(tempUpdateDir)) {
|
if (fs.existsSync(tempUpdateDir)) {
|
||||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -117,26 +117,26 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Downloading new game version...', 10, null, null, null);
|
progressCallback('Downloading new game version...', 10, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir);
|
const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Extracting new files...', 50, null, null, null);
|
progressCallback('Extracting new files...', 50, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
|
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Replacing game files...', 80, null, null, null);
|
progressCallback('Replacing game files...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let userDataBackup = null;
|
let userDataBackup = null;
|
||||||
const userDataPath = findUserDataRecursive(gameDir);
|
const userDataPath = findUserDataRecursive(gameDir);
|
||||||
|
|
||||||
if (userDataPath && fs.existsSync(userDataPath)) {
|
if (userDataPath && fs.existsSync(userDataPath)) {
|
||||||
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now());
|
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now());
|
||||||
console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`);
|
console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`);
|
||||||
|
|
||||||
function copyRecursive(src, dest) {
|
function copyRecursive(src, dest) {
|
||||||
const stat = fs.statSync(src);
|
const stat = fs.statSync(src);
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
@@ -151,35 +151,35 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
fs.copyFileSync(src, dest);
|
fs.copyFileSync(src, dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyRecursive(userDataPath, userDataBackup);
|
copyRecursive(userDataPath, userDataBackup);
|
||||||
} else {
|
} else {
|
||||||
console.log('No UserData folder found in game directory');
|
console.log('No UserData folder found in game directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(gameDir)) {
|
if (fs.existsSync(gameDir)) {
|
||||||
console.log('Removing old game files...');
|
console.log('Removing old game files...');
|
||||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(tempUpdateDir, gameDir);
|
fs.renameSync(tempUpdateDir, gameDir);
|
||||||
|
|
||||||
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
||||||
console.log('HomePage.ui update result after update:', homeUIResult);
|
console.log('HomePage.ui update result after update:', homeUIResult);
|
||||||
|
|
||||||
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after update:', logoResult);
|
console.log('Logo@2x.png update result after update:', logoResult);
|
||||||
|
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
||||||
const newUserDataPath = findUserDataPath(gameDir);
|
const newUserDataPath = findUserDataPath(gameDir);
|
||||||
const userDataParent = path.dirname(newUserDataPath);
|
const userDataParent = path.dirname(newUserDataPath);
|
||||||
|
|
||||||
if (!fs.existsSync(userDataParent)) {
|
if (!fs.existsSync(userDataParent)) {
|
||||||
fs.mkdirSync(userDataParent, { recursive: true });
|
fs.mkdirSync(userDataParent, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Restoring UserData to: ${newUserDataPath}`);
|
console.log(`Restoring UserData to: ${newUserDataPath}`);
|
||||||
|
|
||||||
function copyRecursive(src, dest) {
|
function copyRecursive(src, dest) {
|
||||||
const stat = fs.statSync(src);
|
const stat = fs.statSync(src);
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
@@ -194,12 +194,12 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
fs.copyFileSync(src, dest);
|
fs.copyFileSync(src, dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyRecursive(userDataBackup, newUserDataPath);
|
copyRecursive(userDataBackup, newUserDataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Game files updated successfully to version: ${newVersion}`);
|
console.log(`Game files updated successfully to version: ${newVersion}`);
|
||||||
|
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
||||||
@@ -208,18 +208,18 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Waiting for file system sync...');
|
console.log('Waiting for file system sync...');
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Game update completed', 100, null, null, null);
|
progressCallback('Game update completed', 100, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, updated: true, version: newVersion };
|
return { success: true, updated: true, version: newVersion };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating game files:', error);
|
console.error('Error updating game files:', error);
|
||||||
|
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
||||||
@@ -228,11 +228,11 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
||||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to update game files: ${error.message}`);
|
throw new Error(`Failed to update game files: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,31 +309,31 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
progressCallback('Fetching game files...', null, null, null, null);
|
progressCallback('Fetching game files...', null, null, null, null);
|
||||||
}
|
}
|
||||||
console.log('Installing game files...');
|
console.log('Installing game files...');
|
||||||
|
|
||||||
const latestVersion = await getLatestClientVersion();
|
const latestVersion = await getLatestClientVersion();
|
||||||
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
|
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
|
||||||
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
|
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
|
||||||
|
|
||||||
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
|
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
|
||||||
console.log('HomePage.ui update result after installation:', homeUIResult);
|
console.log('HomePage.ui update result after installation:', homeUIResult);
|
||||||
|
|
||||||
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after installation:', logoResult);
|
console.log('Logo@2x.png update result after installation:', logoResult);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Installation complete', 100, null, null, null);
|
progressCallback('Installation complete', 100, null, null, null);
|
||||||
}
|
}
|
||||||
console.log('Game installation completed successfully');
|
console.log('Game installation completed successfully');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
installed: true
|
installed: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uninstallGame() {
|
async function uninstallGame() {
|
||||||
const appDir = getResolvedAppDir();
|
const appDir = getResolvedAppDir();
|
||||||
|
|
||||||
if (!fs.existsSync(appDir)) {
|
if (!fs.existsSync(appDir)) {
|
||||||
throw new Error('Game is not installed');
|
throw new Error('Game is not installed');
|
||||||
}
|
}
|
||||||
@@ -341,7 +341,7 @@ async function uninstallGame() {
|
|||||||
try {
|
try {
|
||||||
fs.rmSync(appDir, { recursive: true, force: true });
|
fs.rmSync(appDir, { recursive: true, force: true });
|
||||||
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
|
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
|
||||||
|
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
delete config.installPath;
|
delete config.installPath;
|
||||||
@@ -355,25 +355,25 @@ async function uninstallGame() {
|
|||||||
function checkExistingGameInstallation() {
|
function checkExistingGameInstallation() {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
if (!config.installPath || !config.installPath.trim()) {
|
if (!config.installPath || !config.installPath.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const installPath = config.installPath.trim();
|
const installPath = config.installPath.trim();
|
||||||
const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest');
|
const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest');
|
||||||
|
|
||||||
if (!fs.existsSync(gameDir)) {
|
if (!fs.existsSync(gameDir)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientPath = findClientPath(gameDir);
|
const clientPath = findClientPath(gameDir);
|
||||||
if (!clientPath) {
|
if (!clientPath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDataPath = findUserDataRecursive(gameDir);
|
const userDataPath = findUserDataRecursive(gameDir);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gameDir: gameDir,
|
gameDir: gameDir,
|
||||||
clientPath: clientPath,
|
clientPath: clientPath,
|
||||||
@@ -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 = {
|
module.exports = {
|
||||||
downloadPWR,
|
downloadPWR,
|
||||||
applyPWR,
|
applyPWR,
|
||||||
@@ -394,5 +490,9 @@ module.exports = {
|
|||||||
isGameInstalled,
|
isGameInstalled,
|
||||||
installGame,
|
installGame,
|
||||||
uninstallGame,
|
uninstallGame,
|
||||||
checkExistingGameInstallation
|
isGameInstalled,
|
||||||
|
installGame,
|
||||||
|
uninstallGame,
|
||||||
|
checkExistingGameInstallation,
|
||||||
|
repairGame
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { getModsPath } = require('../core/paths');
|
const { getModsPath } = require('../core/paths');
|
||||||
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
||||||
|
const profileManager = require('./profileManager');
|
||||||
|
|
||||||
function generateModId(filename) {
|
function generateModId(filename) {
|
||||||
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
|
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
|
||||||
@@ -11,13 +12,13 @@ function generateModId(filename) {
|
|||||||
|
|
||||||
function extractModName(filename) {
|
function extractModName(filename) {
|
||||||
let name = path.parse(filename).name;
|
let name = path.parse(filename).name;
|
||||||
|
|
||||||
name = name.replace(/-v?\d+\.[\d\.]+.*$/i, '');
|
name = name.replace(/-v?\d+\.[\d\.]+.*$/i, '');
|
||||||
name = name.replace(/-\d+\.[\d\.]+.*$/i, '');
|
name = name.replace(/-\d+\.[\d\.]+.*$/i, '');
|
||||||
|
|
||||||
name = name.replace(/[-_]/g, ' ');
|
name = name.replace(/[-_]/g, ' ');
|
||||||
name = name.replace(/\b\w/g, l => l.toUpperCase());
|
name = name.replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
|
||||||
return name || 'Unknown Mod';
|
return name || 'Unknown Mod';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,77 +27,53 @@ function extractVersion(filename) {
|
|||||||
return versionMatch ? versionMatch[1] : null;
|
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) {
|
async function loadInstalledMods(modsPath) {
|
||||||
try {
|
try {
|
||||||
const configMods = loadModsFromConfig();
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
const modsMap = new Map();
|
if (!activeProfile) return [];
|
||||||
|
|
||||||
configMods.forEach(mod => {
|
const profileMods = activeProfile.mods || [];
|
||||||
modsMap.set(mod.fileName, mod);
|
const profileModFiles = new Set(profileMods.map(m => m.fileName));
|
||||||
});
|
|
||||||
|
// We only return mods that are explicitly in the profile
|
||||||
if (fs.existsSync(modsPath)) {
|
// Check which ones are physically present (either in mods/ or DisabledMods/)
|
||||||
const files = fs.readdirSync(modsPath);
|
|
||||||
|
const physicalModsPath = modsPath; // .../mods
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||||
if (fs.existsSync(disabledModsPath)) {
|
|
||||||
const files = fs.readdirSync(disabledModsPath);
|
const validMods = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const modConfig of profileMods) {
|
||||||
const filePath = path.join(disabledModsPath, file);
|
// Check if file exists in either location
|
||||||
const stats = fs.statSync(filePath);
|
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
|
||||||
|
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
|
||||||
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
|
|
||||||
const configMod = modsMap.get(file);
|
if (inEnabled || inDisabled) {
|
||||||
|
validMods.push({
|
||||||
const modInfo = {
|
...modConfig,
|
||||||
id: configMod?.id || generateModId(file),
|
// Set filePath based on physical location
|
||||||
name: configMod?.name || extractModName(file),
|
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
|
||||||
version: configMod?.version || extractVersion(file) || '1.0.0',
|
enabled: modConfig.enabled !== false // Default true
|
||||||
description: configMod?.description || 'Disabled mod',
|
});
|
||||||
author: configMod?.author || 'Unknown',
|
} else {
|
||||||
enabled: false,
|
console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
|
||||||
filePath: filePath,
|
// Include it so user can see it's missing or remove it
|
||||||
fileName: file,
|
validMods.push({
|
||||||
fileSize: configMod?.fileSize || stats.size,
|
...modConfig,
|
||||||
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
|
filePath: null,
|
||||||
curseForgeId: configMod?.curseForgeId,
|
missing: true,
|
||||||
curseForgeFileId: configMod?.curseForgeFileId
|
enabled: modConfig.enabled !== false
|
||||||
};
|
});
|
||||||
|
|
||||||
modsMap.set(file, modInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(modsMap.values());
|
return validMods;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading installed mods:', error);
|
console.error('Error loading installed mods:', error);
|
||||||
return [];
|
return [];
|
||||||
@@ -106,13 +83,13 @@ async function loadInstalledMods(modsPath) {
|
|||||||
async function downloadMod(modInfo) {
|
async function downloadMod(modInfo) {
|
||||||
try {
|
try {
|
||||||
const modsPath = await getModsPath();
|
const modsPath = await getModsPath();
|
||||||
|
|
||||||
if (!modInfo.downloadUrl && !modInfo.fileId) {
|
if (!modInfo.downloadUrl && !modInfo.fileId) {
|
||||||
throw new Error('No download URL or file ID provided');
|
throw new Error('No download URL or file ID provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadUrl = modInfo.downloadUrl;
|
let downloadUrl = modInfo.downloadUrl;
|
||||||
|
|
||||||
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
|
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
|
||||||
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
|
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -120,56 +97,61 @@ async function downloadMod(modInfo) {
|
|||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadUrl = response.data.data.downloadUrl;
|
downloadUrl = response.data.data.downloadUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!downloadUrl) {
|
if (!downloadUrl) {
|
||||||
throw new Error('Could not determine download URL');
|
throw new Error('Could not determine download URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`;
|
const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`;
|
||||||
const filePath = path.join(modsPath, fileName);
|
const filePath = path.join(modsPath, fileName);
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: downloadUrl,
|
url: downloadUrl,
|
||||||
responseType: 'stream'
|
responseType: 'stream'
|
||||||
});
|
});
|
||||||
|
|
||||||
const writer = fs.createWriteStream(filePath);
|
const writer = fs.createWriteStream(filePath);
|
||||||
response.data.pipe(writer);
|
response.data.pipe(writer);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
writer.on('finish', () => {
|
writer.on('finish', () => {
|
||||||
const configMods = loadModsFromConfig();
|
// NEW: Update Active Profile instead of global config
|
||||||
const newMod = {
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
id: modInfo.id || generateModId(fileName),
|
if (activeProfile) {
|
||||||
name: modInfo.name || extractModName(fileName),
|
const newMod = {
|
||||||
version: modInfo.version || '1.0.0',
|
id: modInfo.id || generateModId(fileName),
|
||||||
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
|
name: modInfo.name || extractModName(fileName),
|
||||||
author: modInfo.author || 'Unknown',
|
version: modInfo.version || '1.0.0',
|
||||||
enabled: true,
|
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
|
||||||
fileName: fileName,
|
author: modInfo.author || 'Unknown',
|
||||||
fileSize: fs.statSync(filePath).size,
|
enabled: true,
|
||||||
dateInstalled: new Date().toISOString(),
|
fileName: fileName,
|
||||||
curseForgeId: modInfo.modId,
|
fileSize: fs.statSync(filePath).size,
|
||||||
curseForgeFileId: modInfo.fileId
|
dateInstalled: new Date().toISOString(),
|
||||||
};
|
curseForgeId: modInfo.modId,
|
||||||
|
curseForgeFileId: modInfo.fileId
|
||||||
configMods.push(newMod);
|
};
|
||||||
saveModsToConfig(configMods);
|
|
||||||
|
const updatedMods = [...(activeProfile.mods || []), newMod];
|
||||||
resolve({
|
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||||
success: true,
|
|
||||||
filePath: filePath,
|
resolve({
|
||||||
fileName: fileName,
|
success: true,
|
||||||
modInfo: newMod
|
filePath: filePath,
|
||||||
});
|
fileName: fileName,
|
||||||
|
modInfo: newMod
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error('No active profile to save mod to'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
writer.on('error', reject);
|
writer.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error downloading mod:', error);
|
console.error('Error downloading mod:', error);
|
||||||
return {
|
return {
|
||||||
@@ -181,36 +163,39 @@ async function downloadMod(modInfo) {
|
|||||||
|
|
||||||
async function uninstallMod(modId, modsPath) {
|
async function uninstallMod(modId, modsPath) {
|
||||||
try {
|
try {
|
||||||
const configMods = loadModsFromConfig();
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
const mod = configMods.find(m => m.id === modId);
|
if (!activeProfile) throw new Error('No active profile');
|
||||||
|
|
||||||
|
const profileMods = activeProfile.mods || [];
|
||||||
|
const mod = profileMods.find(m => m.id === modId);
|
||||||
|
|
||||||
if (!mod) {
|
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');
|
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||||
const enabledPath = path.join(modsPath, mod.fileName);
|
const enabledPath = path.join(modsPath, mod.fileName);
|
||||||
const disabledPath = path.join(disabledModsPath, mod.fileName);
|
const disabledPath = path.join(disabledModsPath, mod.fileName);
|
||||||
|
|
||||||
let fileRemoved = false;
|
let fileRemoved = false;
|
||||||
|
// Try to remove file from both locations to be safe
|
||||||
if (fs.existsSync(enabledPath)) {
|
if (fs.existsSync(enabledPath)) {
|
||||||
fs.unlinkSync(enabledPath);
|
fs.unlinkSync(enabledPath);
|
||||||
fileRemoved = true;
|
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) {
|
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);
|
const updatedMods = profileMods.filter(m => m.id !== modId);
|
||||||
saveModsToConfig(updatedMods);
|
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||||
console.log('Mod removed from config.json');
|
|
||||||
|
console.log('Mod removed from profile');
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uninstalling mod:', error);
|
console.error('Error uninstalling mod:', error);
|
||||||
@@ -223,38 +208,53 @@ async function uninstallMod(modId, modsPath) {
|
|||||||
|
|
||||||
async function toggleMod(modId, modsPath) {
|
async function toggleMod(modId, modsPath) {
|
||||||
try {
|
try {
|
||||||
const mods = await loadInstalledMods(modsPath);
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
const mod = mods.find(m => m.id === modId);
|
if (!activeProfile) throw new Error('No active profile');
|
||||||
|
|
||||||
if (!mod) {
|
const profileMods = activeProfile.mods || [];
|
||||||
throw new Error('Mod not found');
|
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');
|
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||||
if (!fs.existsSync(disabledModsPath)) {
|
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPath = mod.filePath;
|
const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
|
||||||
let newPath, newEnabled;
|
|
||||||
|
|
||||||
if (mod.enabled) {
|
// Determine target paths
|
||||||
newPath = path.join(disabledModsPath, path.basename(currentPath));
|
|
||||||
newEnabled = false;
|
const targetDir = newEnabled ? modsPath : disabledModsPath;
|
||||||
|
const targetPath = path.join(targetDir, mod.fileName);
|
||||||
|
|
||||||
|
if (fs.existsSync(currentPath)) {
|
||||||
|
fs.renameSync(currentPath, targetPath);
|
||||||
} else {
|
} else {
|
||||||
newPath = path.join(modsPath, path.basename(currentPath));
|
// Fallback: check if it's already in target?
|
||||||
newEnabled = true;
|
|
||||||
|
|
||||||
|
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 };
|
return { success: true, enabled: newEnabled };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling mod:', error);
|
console.error('Error toggling mod:', error);
|
||||||
@@ -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 = {
|
module.exports = {
|
||||||
loadInstalledMods,
|
loadInstalledMods,
|
||||||
downloadMod,
|
downloadMod,
|
||||||
uninstallMod,
|
uninstallMod,
|
||||||
toggleMod,
|
toggleMod,
|
||||||
|
syncModsForCurrentProfile,
|
||||||
generateModId,
|
generateModId,
|
||||||
extractModName,
|
extractModName,
|
||||||
extractVersion
|
extractVersion
|
||||||
|
|||||||
206
backend/managers/profileManager.js
Normal file
206
backend/managers/profileManager.js
Normal 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();
|
||||||
@@ -143,6 +143,29 @@ class ClientPatcher {
|
|||||||
return { buffer: result, count };
|
return { buffer: result, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
||||||
|
*/
|
||||||
|
patchDiscordUrl(data) {
|
||||||
|
let count = 0;
|
||||||
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
|
const oldUrl = '.gg/hytale';
|
||||||
|
const newUrl = '.gg/MHkEjepMQ7';
|
||||||
|
|
||||||
|
const oldUtf16 = this.stringToUtf16LE(oldUrl);
|
||||||
|
const newUtf16 = this.stringToUtf16LE(newUrl);
|
||||||
|
|
||||||
|
const positions = this.findAllOccurrences(result, oldUtf16);
|
||||||
|
|
||||||
|
for (const pos of positions) {
|
||||||
|
newUtf16.copy(result, pos);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buffer: result, count };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the client binary has already been patched
|
* Check if the client binary has already been patched
|
||||||
*/
|
*/
|
||||||
@@ -256,9 +279,12 @@ class ClientPatcher {
|
|||||||
console.log('Patching domain references...');
|
console.log('Patching domain references...');
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
||||||
|
|
||||||
if (count === 0) {
|
console.log('Patching Discord URLs...');
|
||||||
console.log('No occurrences of hytale.com found - binary may already be modified or has different format');
|
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||||
return { success: true, patchCount: 0, warning: 'No domain occurrences found' };
|
|
||||||
|
if (count === 0 && discordCount === 0) {
|
||||||
|
console.log('No occurrences found - binary may already be modified or has different format');
|
||||||
|
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -266,7 +292,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Writing patched binary...');
|
console.log('Writing patched binary...');
|
||||||
fs.writeFileSync(clientPath, patchedData);
|
fs.writeFileSync(clientPath, finalData);
|
||||||
|
|
||||||
this.markAsPatched(clientPath);
|
this.markAsPatched(clientPath);
|
||||||
|
|
||||||
@@ -274,10 +300,10 @@ class ClientPatcher {
|
|||||||
progressCallback('Patching complete', 100);
|
progressCallback('Patching complete', 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully patched ${count} occurrences`);
|
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
||||||
console.log('=== Patching Complete ===');
|
console.log('=== Patching Complete ===');
|
||||||
|
|
||||||
return { success: true, patchCount: count };
|
return { success: true, patchCount: count + discordCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ const axios = require('axios');
|
|||||||
|
|
||||||
async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
|
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
|
||||||
|
|
||||||
if (attempt > 0 && progressCallback) {
|
if (attempt > 0 && progressCallback) {
|
||||||
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
|
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
|
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
|
||||||
@@ -53,19 +53,19 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
|||||||
response.data.on('data', (chunk) => {
|
response.data.on('data', (chunk) => {
|
||||||
downloaded += chunk.length;
|
downloaded += chunk.length;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Reset stalled timer on data received
|
// Reset stalled timer on data received
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new stalled timer (30 seconds without data = stalled)
|
// Set new stalled timer (30 seconds without data = stalled)
|
||||||
stalledTimeout = setTimeout(() => {
|
stalledTimeout = setTimeout(() => {
|
||||||
downloadStalled = true;
|
downloadStalled = true;
|
||||||
writer.destroy();
|
writer.destroy();
|
||||||
response.data.destroy();
|
response.data.destroy();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
|
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
|
||||||
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
|
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
|
||||||
const elapsed = (now - startTime) / 1000;
|
const elapsed = (now - startTime) / 1000;
|
||||||
@@ -97,14 +97,14 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
|||||||
reject(new Error('Download stalled'));
|
reject(new Error('Download stalled'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
writer.on('error', (error) => {
|
writer.on('error', (error) => {
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
response.data.on('error', (error) => {
|
response.data.on('error', (error) => {
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
@@ -119,7 +119,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
|
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
|
||||||
|
|
||||||
// Nettoyer le fichier partiel en cas d'erreur
|
// Nettoyer le fichier partiel en cas d'erreur
|
||||||
if (fs.existsSync(dest)) {
|
if (fs.existsSync(dest)) {
|
||||||
try {
|
try {
|
||||||
@@ -128,23 +128,24 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
|||||||
console.warn('Could not cleanup partial file:', cleanupError.message);
|
console.warn('Could not cleanup partial file:', cleanupError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si c'est une erreur réseau que l'on peut retry
|
// Vérifier si c'est une erreur réseau que l'on peut retry
|
||||||
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
|
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
|
||||||
const isRetryable = retryableErrors.includes(error.code) ||
|
const isRetryable = retryableErrors.includes(error.code) ||
|
||||||
error.message.includes('timeout') ||
|
error.message.includes('timeout') ||
|
||||||
error.message.includes('stalled') ||
|
error.message.includes('stalled') ||
|
||||||
(error.response && error.response.status >= 500);
|
(error.response && error.response.status >= 500);
|
||||||
|
|
||||||
if (!isRetryable || attempt === maxRetries - 1) {
|
if (!isRetryable || attempt === maxRetries - 1) {
|
||||||
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
|
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
|
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +153,7 @@ function findHomePageUIPath(gameLatest) {
|
|||||||
function searchDirectory(dir) {
|
function searchDirectory(dir) {
|
||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.isFile() && item.name === 'HomePage.ui') {
|
if (item.isFile() && item.name === 'HomePage.ui') {
|
||||||
return path.join(dir, item.name);
|
return path.join(dir, item.name);
|
||||||
@@ -165,14 +166,14 @@ function findHomePageUIPath(gameLatest) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(gameLatest)) {
|
if (!fs.existsSync(gameLatest)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchDirectory(gameLatest);
|
return searchDirectory(gameLatest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ function findLogoPath(gameLatest) {
|
|||||||
function searchDirectory(dir) {
|
function searchDirectory(dir) {
|
||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.isFile() && item.name === 'Logo@2x.png') {
|
if (item.isFile() && item.name === 'Logo@2x.png') {
|
||||||
return path.join(dir, item.name);
|
return path.join(dir, item.name);
|
||||||
@@ -193,14 +194,14 @@ function findLogoPath(gameLatest) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(gameLatest)) {
|
if (!fs.existsSync(gameLatest)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchDirectory(gameLatest);
|
return searchDirectory(gameLatest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,177 @@ function setupWaylandEnvironment() {
|
|||||||
return envVars;
|
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 = {
|
module.exports = {
|
||||||
getOS,
|
getOS,
|
||||||
getArch,
|
getArch,
|
||||||
isWaylandSession,
|
isWaylandSession,
|
||||||
setupWaylandEnvironment
|
setupWaylandEnvironment,
|
||||||
|
detectGpu,
|
||||||
|
setupGpuEnvironment
|
||||||
};
|
};
|
||||||
|
|||||||
242
main.js
242
main.js
@@ -1,9 +1,10 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, 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 UpdateManager = require('./backend/updateManager');
|
||||||
const logger = require('./backend/logger');
|
const logger = require('./backend/logger');
|
||||||
|
const profileManager = require('./backend/managers/profileManager');
|
||||||
|
|
||||||
logger.interceptConsole();
|
logger.interceptConsole();
|
||||||
|
|
||||||
@@ -25,16 +26,16 @@ function initDiscordRPC() {
|
|||||||
|
|
||||||
const { Client } = require('discord-rpc');
|
const { Client } = require('discord-rpc');
|
||||||
discordRPC = new Client({ transport: 'ipc' });
|
discordRPC = new Client({ transport: 'ipc' });
|
||||||
|
|
||||||
discordRPC.on('ready', () => {
|
discordRPC.on('ready', () => {
|
||||||
console.log('Discord RPC connected');
|
console.log('Discord RPC connected');
|
||||||
setDiscordActivity();
|
setDiscordActivity();
|
||||||
});
|
});
|
||||||
|
|
||||||
discordRPC.on('disconnected', () => {
|
discordRPC.on('disconnected', () => {
|
||||||
console.log('Discord RPC disconnected');
|
console.log('Discord RPC disconnected');
|
||||||
});
|
});
|
||||||
|
|
||||||
discordRPC.login({ clientId: DISCORD_CLIENT_ID }).catch(err => {
|
discordRPC.login({ clientId: DISCORD_CLIENT_ID }).catch(err => {
|
||||||
console.log('Failed to connect to Discord:', err.message);
|
console.log('Failed to connect to Discord:', err.message);
|
||||||
});
|
});
|
||||||
@@ -45,7 +46,7 @@ function initDiscordRPC() {
|
|||||||
|
|
||||||
function setDiscordActivity() {
|
function setDiscordActivity() {
|
||||||
if (!discordRPC) return;
|
if (!discordRPC) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
discordRPC.setActivity({
|
discordRPC.setActivity({
|
||||||
details: 'Using HytaleF2P',
|
details: 'Using HytaleF2P',
|
||||||
@@ -66,7 +67,7 @@ function setDiscordActivity() {
|
|||||||
|
|
||||||
function toggleDiscordRPC(enabled) {
|
function toggleDiscordRPC(enabled) {
|
||||||
console.log('Toggling Discord RPC:', enabled);
|
console.log('Toggling Discord RPC:', enabled);
|
||||||
|
|
||||||
if (enabled && !discordRPC) {
|
if (enabled && !discordRPC) {
|
||||||
console.log('Initializing Discord RPC...');
|
console.log('Initializing Discord RPC...');
|
||||||
initDiscordRPC();
|
initDiscordRPC();
|
||||||
@@ -119,11 +120,9 @@ function createWindow() {
|
|||||||
mainWindow.webContents.send('show-update-popup', updateInfo);
|
mainWindow.webContents.send('show-update-popup', updateInfo);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
//mainWindow.webContents.openDevTools();
|
|
||||||
|
|
||||||
|
mainWindow.webContents.on('devtools-opened', () => {
|
||||||
mainWindow.webContents.on('devtools-opened', () => {
|
mainWindow.webContents.closeDevTools();
|
||||||
mainWindow.webContents.closeDevTools();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
@@ -159,31 +158,49 @@ app.whenReady().then(async () => {
|
|||||||
console.log('Electron version:', process.versions.electron);
|
console.log('Electron version:', process.versions.electron);
|
||||||
console.log('Node.js version:', process.versions.node);
|
console.log('Node.js version:', process.versions.node);
|
||||||
console.log('Log directory:', logger.getLogDirectory());
|
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();
|
createWindow();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
let timeoutReached = false;
|
let timeoutReached = false;
|
||||||
|
|
||||||
const unlockPlayButton = () => {
|
const unlockPlayButton = () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('lock-play-button', false);
|
mainWindow.webContents.send('lock-play-button', false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
timeoutReached = true;
|
timeoutReached = true;
|
||||||
console.warn('First launch check timeout reached, unlocking play button');
|
console.warn('First launch check timeout reached, unlocking play button');
|
||||||
unlockPlayButton();
|
unlockPlayButton();
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Starting first launch check...');
|
console.log('Starting first launch check...');
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('lock-play-button', true);
|
mainWindow.webContents.send('lock-play-button', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
|
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
|
||||||
@@ -196,20 +213,20 @@ app.whenReady().then(async () => {
|
|||||||
setTimeout(() => reject(new Error('First launch check timeout')), 12000);
|
setTimeout(() => reject(new Error('First launch check timeout')), 12000);
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (timeoutReached) {
|
if (timeoutReached) {
|
||||||
console.log('Timeout already reached, skipping result processing');
|
console.log('Timeout already reached, skipping result processing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('First launch check result:', firstLaunchResult);
|
console.log('First launch check result:', firstLaunchResult);
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
if (firstLaunchResult.needsUpdate && firstLaunchResult.existingGame) {
|
if (firstLaunchResult.needsUpdate && firstLaunchResult.existingGame) {
|
||||||
console.log('Sending show-first-launch-update event...');
|
console.log('Sending show-first-launch-update event...');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('show-first-launch-update', {
|
mainWindow.webContents.send('show-first-launch-update', {
|
||||||
@@ -218,10 +235,10 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
} else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) {
|
} else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) {
|
||||||
console.log('Sending show-first-launch-welcome event...');
|
console.log('Sending show-first-launch-welcome event...');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('show-first-launch-welcome');
|
mainWindow.webContents.send('show-first-launch-welcome');
|
||||||
@@ -268,15 +285,15 @@ app.on('before-quit', () => {
|
|||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
console.log('=== LAUNCHER CLOSING ===');
|
console.log('=== LAUNCHER CLOSING ===');
|
||||||
|
|
||||||
cleanupDiscordRPC();
|
cleanupDiscordRPC();
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) => {
|
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
|
||||||
try {
|
try {
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
@@ -291,19 +308,19 @@ 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;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Launch error:', error);
|
console.error('Launch error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mainWindow.webContents.send('progress-complete');
|
mainWindow.webContents.send('progress-complete');
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
return { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -324,12 +341,12 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await installGame(playerName, progressCallback, javaPath, installPath);
|
const result = await installGame(playerName, progressCallback, javaPath, installPath);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Install error:', error);
|
console.error('Install error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
return { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -388,12 +405,21 @@ ipcMain.handle('load-discord-rpc', () => {
|
|||||||
return loadDiscordRPC();
|
return loadDiscordRPC();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-language', (event, language) => {
|
||||||
|
saveLanguage(language);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-language', () => {
|
||||||
|
return loadLanguage();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('select-install-path', async () => {
|
ipcMain.handle('select-install-path', async () => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ['openDirectory'],
|
properties: ['openDirectory'],
|
||||||
title: 'Select Installation Folder'
|
title: 'Select Installation Folder'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
}
|
}
|
||||||
@@ -416,7 +442,7 @@ ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await proposeGameUpdate(existingGame, progressCallback);
|
const result = await proposeGameUpdate(existingGame, progressCallback);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('First launch update error:', error);
|
console.error('First launch update error:', error);
|
||||||
@@ -450,13 +476,35 @@ ipcMain.handle('is-game-installed', async () => {
|
|||||||
ipcMain.handle('uninstall-game', async () => {
|
ipcMain.handle('uninstall-game', async () => {
|
||||||
try {
|
try {
|
||||||
await uninstallGame();
|
await uninstallGame();
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Uninstall error:', error);
|
console.error('Uninstall error:', error);
|
||||||
return { success: false, error: error.message };
|
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 () => {
|
ipcMain.handle('get-hytale-news', async () => {
|
||||||
try {
|
try {
|
||||||
const news = await getHytaleNews();
|
const news = await getHytaleNews();
|
||||||
@@ -481,7 +529,7 @@ ipcMain.handle('open-game-location', async () => {
|
|||||||
try {
|
try {
|
||||||
const { getResolvedAppDir } = require('./backend/launcher');
|
const { getResolvedAppDir } = require('./backend/launcher');
|
||||||
const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game');
|
const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game');
|
||||||
|
|
||||||
if (fs.existsSync(gameDir)) {
|
if (fs.existsSync(gameDir)) {
|
||||||
await shell.openPath(gameDir);
|
await shell.openPath(gameDir);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -497,9 +545,9 @@ ipcMain.handle('open-game-location', async () => {
|
|||||||
ipcMain.handle('browse-java-path', async () => {
|
ipcMain.handle('browse-java-path', async () => {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === 'darwin';
|
||||||
|
|
||||||
let dialogOptions;
|
let dialogOptions;
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
dialogOptions = {
|
dialogOptions = {
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
@@ -528,9 +576,9 @@ ipcMain.handle('browse-java-path', async () => {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, dialogOptions);
|
const result = await dialog.showOpenDialog(mainWindow, dialogOptions);
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
}
|
}
|
||||||
@@ -653,7 +701,7 @@ ipcMain.handle('select-mod-files', async () => {
|
|||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
return result.filePaths;
|
return result.filePaths;
|
||||||
}
|
}
|
||||||
@@ -664,9 +712,9 @@ ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => {
|
|||||||
try {
|
try {
|
||||||
const fileName = path.basename(sourcePath);
|
const fileName = path.basename(sourcePath);
|
||||||
const destPath = path.join(modsPath, fileName);
|
const destPath = path.join(modsPath, fileName);
|
||||||
|
|
||||||
fs.copyFileSync(sourcePath, destPath);
|
fs.copyFileSync(sourcePath, destPath);
|
||||||
|
|
||||||
return { success: true, fileName };
|
return { success: true, fileName };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying mod file:', error);
|
console.error('Error copying mod file:', error);
|
||||||
@@ -686,13 +734,13 @@ ipcMain.handle('check-for-updates', async () => {
|
|||||||
ipcMain.handle('open-download-page', async () => {
|
ipcMain.handle('open-download-page', async () => {
|
||||||
try {
|
try {
|
||||||
await shell.openExternal(updateManager.getDownloadUrl());
|
await shell.openExternal(updateManager.getDownloadUrl());
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.close();
|
mainWindow.close();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening download page:', error);
|
console.error('Error opening download page:', error);
|
||||||
@@ -704,6 +752,35 @@ ipcMain.handle('get-update-info', async () => {
|
|||||||
return updateManager.getUpdateInfo();
|
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', () => {
|
ipcMain.handle('window-close', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.close();
|
mainWindow.close();
|
||||||
@@ -786,7 +863,7 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
|||||||
try {
|
try {
|
||||||
const logDir = logger.getLogDirectory();
|
const logDir = logger.getLogDirectory();
|
||||||
if (!logDir) return null;
|
if (!logDir) return null;
|
||||||
|
|
||||||
const files = fs.readdirSync(logDir)
|
const files = fs.readdirSync(logDir)
|
||||||
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
|
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
|
||||||
.map(file => ({
|
.map(file => ({
|
||||||
@@ -795,17 +872,80 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
|||||||
mtime: fs.statSync(path.join(logDir, file)).mtime
|
mtime: fs.statSync(path.join(logDir, file)).mtime
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.mtime - a.mtime);
|
.sort((a, b) => b.mtime - a.mtime);
|
||||||
|
|
||||||
if (files.length === 0) return null;
|
if (files.length === 0) return null;
|
||||||
|
|
||||||
const latestLogFile = files[0].path;
|
const latestLogFile = files[0].path;
|
||||||
const content = fs.readFileSync(latestLogFile, 'utf8');
|
const content = fs.readFileSync(latestLogFile, 'utf8');
|
||||||
const lines = content.split('\n');
|
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) {
|
} catch (error) {
|
||||||
console.error('Error reading logs:', error);
|
console.error('Error reading logs:', error);
|
||||||
return null;
|
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
48
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcherv2",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2b",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcherv2",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2b",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
@@ -3459,15 +3459,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
@@ -4504,6 +4506,18 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/temp": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
||||||
@@ -4568,20 +4582,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"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": {
|
"node_modules/tiny-async-pool": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",
|
||||||
|
|||||||
70
package.json
70
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcherv2",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2b",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -23,6 +23,17 @@
|
|||||||
"auto-update",
|
"auto-update",
|
||||||
"mod-manager",
|
"mod-manager",
|
||||||
"chat"
|
"chat"
|
||||||
|
],
|
||||||
|
"maintainers": [
|
||||||
|
{
|
||||||
|
"name": "Terromur",
|
||||||
|
"url": "https://github.com/Terromur"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fari Gading",
|
||||||
|
"email": "fazrigading@gmail.com",
|
||||||
|
"url": "https://github.com/fazrigading"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"author": {
|
"author": {
|
||||||
"name": "AMIAY",
|
"name": "AMIAY",
|
||||||
@@ -45,7 +56,8 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.hytalef2p.launcher",
|
"appId": "com.hytalef2p.launcher",
|
||||||
"productName": "Hytale F2P",
|
"productName": "Hytale F2P Launcher",
|
||||||
|
"artifactName": "${name}_${version}_${arch}.${ext}",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
@@ -58,54 +70,25 @@
|
|||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{ "target": "nsis", "arch": ["x64", "arm64"] },
|
||||||
"target": "nsis",
|
{ "target": "portable", "arch": ["x64"] }
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "portable",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"icon": "icon.ico"
|
"icon": "icon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{ "target": "AppImage", "arch": ["x64", "arm64"] },
|
||||||
"target": "AppImage",
|
{ "target": "deb", "arch": ["x64", "arm64"] },
|
||||||
"arch": [
|
{ "target": "rpm", "arch": ["x64", "arm64"] },
|
||||||
"x64"
|
{ "target": "pacman", "arch": ["x64", "arm64"] }
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "deb",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"icon": "build/icon.png",
|
"icon": "build/icon.png",
|
||||||
"category": "Game",
|
"category": "Game"
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support"
|
|
||||||
},
|
},
|
||||||
"mac": {
|
"mac": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{ "target": "dmg", "arch": ["universal"] },
|
||||||
"target": "dmg",
|
{ "target": "zip", "arch": ["universal"] }
|
||||||
"arch": [
|
|
||||||
"universal"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "zip",
|
|
||||||
"arch": [
|
|
||||||
"universal"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"icon": "build/icon.icns",
|
"icon": "build/icon.icns",
|
||||||
"category": "public.app-category.games"
|
"category": "public.app-category.games"
|
||||||
@@ -117,7 +100,4 @@
|
|||||||
"createStartMenuShortcut": true
|
"createStartMenuShortcut": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
25
preload.js
25
preload.js
@@ -1,7 +1,7 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
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),
|
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
|
||||||
closeWindow: () => ipcRenderer.invoke('window-close'),
|
closeWindow: () => ipcRenderer.invoke('window-close'),
|
||||||
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
||||||
@@ -17,10 +17,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
|
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
|
||||||
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
|
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
|
||||||
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
|
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
|
||||||
|
saveLanguage: (language) => ipcRenderer.invoke('save-language', language),
|
||||||
|
loadLanguage: () => ipcRenderer.invoke('load-language'),
|
||||||
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
||||||
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
||||||
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
||||||
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
|
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
|
||||||
|
repairGame: () => ipcRenderer.invoke('repair-game'),
|
||||||
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
|
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
|
||||||
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
||||||
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
|
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
|
||||||
@@ -49,6 +52,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.on('show-update-popup', (event, data) => callback(data));
|
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),
|
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
|
||||||
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
|
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
|
||||||
onFirstLaunchUpdate: (callback) => {
|
onFirstLaunchUpdate: (callback) => {
|
||||||
@@ -65,13 +73,24 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
|
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
|
||||||
|
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
|
||||||
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
|
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
|
||||||
|
|
||||||
// UUID Management methods
|
// UUID Management methods
|
||||||
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
|
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
|
||||||
getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'),
|
getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'),
|
||||||
setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid),
|
setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid),
|
||||||
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
|
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
|
||||||
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
|
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)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user