mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 14:51:48 -03:00
Merge branch 'main' of https://github.com/amiayweb/Hytale-F2P
This commit is contained in:
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -17,7 +17,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 --linux --publish never
|
- run: npx electron-builder --linux --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@@ -35,7 +34,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
|
||||||
@@ -52,7 +50,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,7 +63,7 @@ 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.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -83,10 +80,14 @@ jobs:
|
|||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('manual-{0}', github.sha) }}
|
||||||
|
name: ${{ github.ref_type == 'tag' && github.ref_name || format('Manual build {0}', github.sha) }}
|
||||||
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: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
dist/*
|
dist/*
|
||||||
node_modules/*
|
node_modules/*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
bun.lock
|
||||||
|
|||||||
164
GUI/index.html
164
GUI/index.html
@@ -1,25 +1,32 @@
|
|||||||
<!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>
|
||||||
|
|
||||||
@@ -61,6 +68,24 @@
|
|||||||
<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>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>
|
||||||
@@ -93,7 +118,8 @@
|
|||||||
<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">Player Name</label>
|
||||||
<input type="text" id="installPlayerName" placeholder="Enter your name" class="form-input" value="Player" />
|
<input type="text" id="installPlayerName" placeholder="Enter your name"
|
||||||
|
class="form-input" value="Player" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -106,7 +132,8 @@
|
|||||||
<div class="form-subgroup">
|
<div class="form-subgroup">
|
||||||
<label class="form-label">Installation Folder</label>
|
<label class="form-label">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" placeholder="Default location"
|
||||||
|
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>
|
||||||
@@ -224,13 +251,9 @@
|
|||||||
|
|
||||||
<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"
|
placeholder="Type your message... (Links are automatically censored)" 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>
|
||||||
@@ -268,7 +291,8 @@
|
|||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
<div class="checkbox-content">
|
<div class="checkbox-content">
|
||||||
<div class="checkbox-title">Use Custom Java Path</div>
|
<div class="checkbox-title">Use Custom Java Path</div>
|
||||||
<div class="checkbox-description">Override the bundled Java runtime with your own installation</div>
|
<div class="checkbox-description">Override the bundled Java runtime with
|
||||||
|
your own installation</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,13 +301,8 @@
|
|||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Java Executable Path</label>
|
<label class="settings-input-label">Java Executable Path</label>
|
||||||
<div class="settings-input-with-button">
|
<div class="settings-input-with-button">
|
||||||
<input
|
<input type="text" id="customJavaPath" class="settings-input"
|
||||||
type="text"
|
placeholder="Select Java path..." readonly />
|
||||||
id="customJavaPath"
|
|
||||||
class="settings-input"
|
|
||||||
placeholder="Select Java path..."
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
<button id="browseJavaBtn" class="settings-browse-btn">
|
<button id="browseJavaBtn" class="settings-browse-btn">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
Browse
|
Browse
|
||||||
@@ -309,7 +328,8 @@
|
|||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
<div class="checkbox-content">
|
<div class="checkbox-content">
|
||||||
<div class="checkbox-title">Enable Discord Rich Presence</div>
|
<div class="checkbox-title">Enable Discord Rich Presence</div>
|
||||||
<div class="checkbox-description">Show your launcher activity on Discord</div>
|
<div class="checkbox-description">Show your launcher activity on Discord
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,13 +344,8 @@
|
|||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Player Name</label>
|
<label class="settings-input-label">Player Name</label>
|
||||||
<input
|
<input type="text" id="settingsPlayerName" class="settings-input"
|
||||||
type="text"
|
placeholder="Enter your player name" maxlength="16" />
|
||||||
id="settingsPlayerName"
|
|
||||||
class="settings-input"
|
|
||||||
placeholder="Enter your player name"
|
|
||||||
maxlength="16"
|
|
||||||
/>
|
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
This name will be used in-game (1-16 characters)
|
This name will be used in-game (1-16 characters)
|
||||||
@@ -340,7 +355,8 @@
|
|||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
<div class="settings-button-group">
|
<div class="settings-button-group">
|
||||||
<button id="openGameLocationBtn" class="settings-action-btn" onclick="openGameLocation()">
|
<button id="openGameLocationBtn" class="settings-action-btn"
|
||||||
|
onclick="openGameLocation()">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<div class="btn-content">
|
<div class="btn-content">
|
||||||
<div class="btn-title">Open Game Location</div>
|
<div class="btn-title">Open Game Location</div>
|
||||||
@@ -361,17 +377,13 @@
|
|||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label">Current UUID</label>
|
<label class="settings-input-label">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 placeholder="Loading UUID..." />
|
||||||
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>
|
||||||
@@ -458,14 +470,8 @@
|
|||||||
</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">Username</label>
|
||||||
<input
|
<input type="text" id="chatUsernameInput" class="chat-username-input"
|
||||||
type="text"
|
placeholder="Enter your username..." maxlength="20" autocomplete="off" />
|
||||||
id="chatUsernameInput"
|
|
||||||
class="chat-username-input"
|
|
||||||
placeholder="Enter your username..."
|
|
||||||
maxlength="20"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span>
|
<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>
|
||||||
@@ -504,7 +510,8 @@
|
|||||||
<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>
|
||||||
@@ -529,13 +536,9 @@
|
|||||||
<div class="uuid-custom-section">
|
<div class="uuid-custom-section">
|
||||||
<h3 class="uuid-section-title">Set Custom UUID</h3>
|
<h3 class="uuid-section-title">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"
|
|
||||||
id="customUuidInput"
|
|
||||||
class="uuid-input"
|
|
||||||
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
|
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
|
||||||
maxlength="36"
|
maxlength="36" />
|
||||||
/>
|
|
||||||
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
Set UUID
|
Set UUID
|
||||||
@@ -550,27 +553,64 @@
|
|||||||
</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>
|
||||||
|
Manage Profiles
|
||||||
|
</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" placeholder="New Profile Name" class="profile-input"
|
||||||
|
maxlength="20">
|
||||||
|
<button class="profile-create-btn" onclick="createNewProfile()">
|
||||||
|
<i class="fas fa-plus"></i> Create Profile
|
||||||
|
</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">Join our Discord community!</span>
|
||||||
<button class="notification-action" onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
<button class="notification-action"
|
||||||
|
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
||||||
Join Discord
|
Join Discord
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,8 +31,172 @@ export function setupLauncher() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
|
||||||
|
window.electronAPI.onProgressUpdate((data) => {
|
||||||
|
if (!isDownloading) return;
|
||||||
|
if (window.LauncherUI) {
|
||||||
|
window.LauncherUI.updateProgress(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Switching Profile...' });
|
||||||
|
|
||||||
|
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) {
|
||||||
|
window.LauncherUI.updateProgress({ message: 'Profile Switched!' });
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -282,18 +282,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\">
|
||||||
@@ -320,15 +327,15 @@ function createBrowseModCard(mod) {
|
|||||||
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
|
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
|
INSTALLED
|
||||||
</button>`
|
</button>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -717,7 +724,9 @@ window.modsManager = {
|
|||||||
deleteMod,
|
deleteMod,
|
||||||
openMyModsModal,
|
openMyModsModal,
|
||||||
closeMyModsModal,
|
closeMyModsModal,
|
||||||
viewModPage
|
viewModPage,
|
||||||
|
loadInstalledMods,
|
||||||
|
loadBrowseMods
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initModsManager);
|
document.addEventListener('DOMContentLoaded', initModsManager);
|
||||||
|
|||||||
10
GUI/js/ui.js
10
GUI/js/ui.js
@@ -49,6 +49,13 @@ function setupWindowControls() {
|
|||||||
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';
|
||||||
@@ -59,6 +66,9 @@ function setupWindowControls() {
|
|||||||
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) {
|
||||||
|
|||||||
386
GUI/style.css
386
GUI/style.css
@@ -263,6 +263,7 @@ body {
|
|||||||
|
|
||||||
.window-controls {
|
.window-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -293,6 +294,7 @@ body {
|
|||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -590,6 +592,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-pages {
|
.content-pages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1430,6 +1433,7 @@ body {
|
|||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
border-color: rgba(147, 51, 234, 0.3);
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px rgba(0, 0, 0, 0.6),
|
0 8px 32px rgba(0, 0, 0, 0.6),
|
||||||
@@ -1463,8 +1467,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes textPulse {
|
@keyframes textPulse {
|
||||||
0%, 100% { opacity: 0.8; }
|
|
||||||
50% { opacity: 1; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#progressPercent {
|
#progressPercent {
|
||||||
@@ -1476,10 +1487,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes percentGlow {
|
@keyframes percentGlow {
|
||||||
0%, 100% {
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
text-shadow: 0 0 20px rgba(147, 51, 234, 0.8);
|
text-shadow: 0 0 20px rgba(147, 51, 234, 0.8);
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
text-shadow: 0 0 30px rgba(147, 51, 234, 1);
|
text-shadow: 0 0 30px rgba(147, 51, 234, 1);
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
@@ -1506,27 +1520,30 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(90deg,
|
||||||
transparent,
|
transparent,
|
||||||
rgba(255, 255, 255, 0.2),
|
rgba(255, 255, 255, 0.2),
|
||||||
transparent
|
transparent);
|
||||||
);
|
|
||||||
animation: shimmer 2s infinite;
|
animation: shimmer 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { left: -100%; }
|
0% {
|
||||||
100% { left: 100%; }
|
left: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-fill {
|
.progress-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(90deg,
|
||||||
#9333ea 0%,
|
#9333ea 0%,
|
||||||
#a855f7 25%,
|
#a855f7 25%,
|
||||||
#3b82f6 50%,
|
#3b82f6 50%,
|
||||||
#06b6d4 75%,
|
#06b6d4 75%,
|
||||||
#10b981 100%
|
#10b981 100%);
|
||||||
);
|
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
@@ -1540,8 +1557,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progressFlow {
|
@keyframes progressFlow {
|
||||||
0% { background-position: 200% 0; }
|
0% {
|
||||||
100% { background-position: -200% 0; }
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-fill::after {
|
.progress-bar-fill::after {
|
||||||
@@ -1552,16 +1574,22 @@ body {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(90deg,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
rgba(255, 255, 255, 0.4) 50%,
|
rgba(255, 255, 255, 0.4) 50%,
|
||||||
transparent 100%
|
transparent 100%);
|
||||||
);
|
|
||||||
animation: progressPulse 1.5s ease-in-out infinite;
|
animation: progressPulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progressPulse {
|
@keyframes progressPulse {
|
||||||
0%, 100% { opacity: 0; }
|
|
||||||
50% { opacity: 1; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-details {
|
.progress-details {
|
||||||
@@ -1798,8 +1826,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { transform: translateX(-100%); }
|
0% {
|
||||||
100% { transform: translateX(200%); }
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(200%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-image img {
|
.mod-image img {
|
||||||
@@ -2028,8 +2061,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-mods span {
|
.loading-mods span {
|
||||||
@@ -2593,7 +2631,7 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 1%;
|
margin-top: 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-header {
|
.mod-header {
|
||||||
@@ -2879,6 +2917,7 @@ body {
|
|||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -2902,6 +2941,7 @@ body {
|
|||||||
transform: translateY(50px);
|
transform: translateY(50px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -3319,8 +3359,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-dot {
|
@keyframes pulse-dot {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3373,6 +3420,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -3550,6 +3598,7 @@ body {
|
|||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -3571,6 +3620,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -3798,13 +3848,13 @@ body {
|
|||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-checkbox input[type="checkbox"]:checked + .checkmark {
|
.settings-checkbox input[type="checkbox"]:checked+.checkmark {
|
||||||
background: linear-gradient(135deg, #9333ea, #3b82f6);
|
background: linear-gradient(135deg, #9333ea, #3b82f6);
|
||||||
border-color: #9333ea;
|
border-color: #9333ea;
|
||||||
box-shadow: 0 0 20px rgba(147, 51, 234, 0.4);
|
box-shadow: 0 0 20px rgba(147, 51, 234, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-checkbox input[type="checkbox"]:checked + .checkmark::after {
|
.settings-checkbox input[type="checkbox"]:checked+.checkmark::after {
|
||||||
content: '✓';
|
content: '✓';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -4068,6 +4118,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.8) translateY(-20px);
|
transform: scale(0.8) translateY(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1) translateY(0);
|
transform: scale(1) translateY(0);
|
||||||
@@ -4208,8 +4259,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes updatePulse {
|
@keyframes updatePulse {
|
||||||
0%, 100% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.02); }
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -4330,6 +4388,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-20px) scale(0.95);
|
transform: translateY(-20px) scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
@@ -4773,16 +4832,27 @@ body {
|
|||||||
|
|
||||||
/* Chat Badges et Animations */
|
/* Chat Badges et Animations */
|
||||||
@keyframes rainbow {
|
@keyframes rainbow {
|
||||||
0% { background-position: 0% 50%; }
|
0% {
|
||||||
50% { background-position: 100% 50%; }
|
background-position: 0% 50%;
|
||||||
100% { background-position: 0% 50%; }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes contributorGlow {
|
@keyframes contributorGlow {
|
||||||
0%, 100% {
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 50%;
|
background-position: 100% 50%;
|
||||||
filter: brightness(1.2);
|
filter: brightness(1.2);
|
||||||
@@ -4929,12 +4999,12 @@ body {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-type-option input[type="radio"]:checked + .radio-custom {
|
.color-type-option input[type="radio"]:checked+.radio-custom {
|
||||||
border-color: #9333ea;
|
border-color: #9333ea;
|
||||||
background: rgba(147, 51, 234, 0.2);
|
background: rgba(147, 51, 234, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-type-option input[type="radio"]:checked + .radio-custom::after {
|
.color-type-option input[type="radio"]:checked+.radio-custom::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -5090,6 +5160,7 @@ body {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 16px rgba(147, 51, 234, 0.4);
|
box-shadow: 0 8px 16px rgba(147, 51, 234, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-container {
|
.color-picker-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -5114,3 +5185,238 @@ body {
|
|||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Profile Selector Styles */
|
||||||
|
.profile-selector {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: 1rem;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
z-index: 100000 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(147, 51, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-btn i {
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 200px;
|
||||||
|
background: rgba(20, 20, 20, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: none;
|
||||||
|
/* Toggled by JS */
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 2000;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: fadeIn 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-dropdown.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-item {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-item.active {
|
||||||
|
background: rgba(147, 51, 234, 0.2);
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-item.active::before {
|
||||||
|
content: '•';
|
||||||
|
color: #9333ea;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-action {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #9333ea;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-action:hover {
|
||||||
|
background: rgba(147, 51, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Modal Styles */
|
||||||
|
.profile-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-modal-content {
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 90%;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-modal-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-manager-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-manager-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-manager-item:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-manager-item.active {
|
||||||
|
border-color: #9333ea;
|
||||||
|
background: rgba(147, 51, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-delete-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.4rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-delete-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-create-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-create-btn {
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
background: #9333ea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-create-btn:hover {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,14 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
|||||||
|
|
||||||
## 📋 Changelog
|
## 📋 Changelog
|
||||||
|
|
||||||
### 🆕 v2.0.2 *(Latest)*
|
### 🆕 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
|
||||||
|
|||||||
@@ -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 || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +149,19 @@ function loadDiscordRPC() {
|
|||||||
|
|
||||||
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)) {
|
||||||
@@ -153,6 +178,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);
|
||||||
@@ -168,8 +199,8 @@ function isFirstLaunch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -152,10 +152,8 @@ 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');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServ
|
|||||||
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;
|
||||||
@@ -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,6 +261,18 @@ 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(' ')}`);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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));
|
||||||
});
|
|
||||||
|
|
||||||
if (fs.existsSync(modsPath)) {
|
// We only return mods that are explicitly in the profile
|
||||||
const files = fs.readdirSync(modsPath);
|
// Check which ones are physically present (either in mods/ or DisabledMods/)
|
||||||
|
|
||||||
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 physicalModsPath = modsPath; // .../mods
|
||||||
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);
|
|
||||||
|
|
||||||
for (const file of files) {
|
const validMods = [];
|
||||||
const filePath = path.join(disabledModsPath, file);
|
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
|
|
||||||
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
|
for (const modConfig of profileMods) {
|
||||||
const configMod = modsMap.get(file);
|
// Check if file exists in either location
|
||||||
|
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
|
||||||
|
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
|
||||||
|
|
||||||
const modInfo = {
|
if (inEnabled || inDisabled) {
|
||||||
id: configMod?.id || generateModId(file),
|
validMods.push({
|
||||||
name: configMod?.name || extractModName(file),
|
...modConfig,
|
||||||
version: configMod?.version || extractVersion(file) || '1.0.0',
|
// Set filePath based on physical location
|
||||||
description: configMod?.description || 'Disabled mod',
|
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
|
||||||
author: configMod?.author || 'Unknown',
|
enabled: modConfig.enabled !== false // Default true
|
||||||
enabled: false,
|
});
|
||||||
filePath: filePath,
|
} else {
|
||||||
fileName: file,
|
console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
|
||||||
fileSize: configMod?.fileSize || stats.size,
|
// Include it so user can see it's missing or remove it
|
||||||
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
|
validMods.push({
|
||||||
curseForgeId: configMod?.curseForgeId,
|
...modConfig,
|
||||||
curseForgeFileId: configMod?.curseForgeFileId
|
filePath: null,
|
||||||
};
|
missing: true,
|
||||||
|
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 [];
|
||||||
@@ -142,30 +119,35 @@ async function downloadMod(modInfo) {
|
|||||||
|
|
||||||
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);
|
const updatedMods = [...(activeProfile.mods || []), newMod];
|
||||||
saveModsToConfig(configMods);
|
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
success: true,
|
success: true,
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
modInfo: newMod
|
modInfo: newMod
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error('No active profile to save mod to'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
writer.on('error', reject);
|
writer.on('error', reject);
|
||||||
});
|
});
|
||||||
@@ -181,11 +163,14 @@ 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');
|
||||||
@@ -193,23 +178,23 @@ async function uninstallMod(modId, modsPath) {
|
|||||||
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)) {
|
if (fs.existsSync(disabledPath)) {
|
||||||
fs.unlinkSync(disabledPath);
|
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { }
|
||||||
fileRemoved = true;
|
|
||||||
console.log('Removed mod from DisabledMods folder:', disabledPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -223,36 +208,51 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.renameSync(currentPath, newPath);
|
|
||||||
|
|
||||||
const configMods = loadModsFromConfig();
|
if (fs.existsSync(targetPath)) {
|
||||||
const configModIndex = configMods.findIndex(m => m.id === modId);
|
// It's already there, maybe just state was wrong.
|
||||||
if (configModIndex !== -1) {
|
|
||||||
configMods[configModIndex].enabled = newEnabled;
|
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
|
||||||
saveModsToConfig(configMods);
|
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, enabled: newEnabled };
|
return { success: true, enabled: newEnabled };
|
||||||
@@ -265,11 +265,69 @@ async function toggleMod(modId, modsPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncModsForCurrentProfile() {
|
||||||
|
try {
|
||||||
|
const activeProfile = profileManager.getActiveProfile();
|
||||||
|
if (!activeProfile) {
|
||||||
|
console.warn('No active profile found during mod sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
|
||||||
|
|
||||||
|
const modsPath = await getModsPath();
|
||||||
|
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||||
|
|
||||||
|
if (!fs.existsSync(disabledModsPath)) {
|
||||||
|
fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all physical files from both folders
|
||||||
|
const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||||
|
const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||||
|
|
||||||
|
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
||||||
|
|
||||||
|
// Profile.mods contains the list of ALL mods for that profile, with their enabled state.
|
||||||
|
|
||||||
|
const profileMods = activeProfile.mods || [];
|
||||||
|
|
||||||
|
for (const fileName of allFiles) {
|
||||||
|
const modConfig = profileMods.find(m => m.fileName === fileName);
|
||||||
|
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
|
||||||
|
|
||||||
|
// Logic:
|
||||||
|
// If it should be enabled -> Move to mods/
|
||||||
|
// If it should be disabled -> Move to DisabledMods/
|
||||||
|
|
||||||
|
const currentPath = enabledFiles.includes(fileName) ? path.join(modsPath, fileName) : path.join(disabledModsPath, fileName);
|
||||||
|
const targetDir = shouldBeEnabled ? modsPath : disabledModsPath;
|
||||||
|
const targetPath = path.join(targetDir, fileName);
|
||||||
|
|
||||||
|
if (path.dirname(currentPath) !== targetDir) {
|
||||||
|
console.log(`[Mod Sync] Moving ${fileName} to ${shouldBeEnabled ? 'Enabled' : 'Disabled'}`);
|
||||||
|
try {
|
||||||
|
fs.renameSync(currentPath, targetPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to move ${fileName}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ModManager] Error syncing mods:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
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();
|
||||||
53
main.js
53
main.js
@@ -4,6 +4,7 @@ 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, isGameInstalled, uninstallGame, 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();
|
||||||
|
|
||||||
@@ -119,11 +120,11 @@ function createWindow() {
|
|||||||
mainWindow.webContents.send('show-update-popup', updateInfo);
|
mainWindow.webContents.send('show-update-popup', updateInfo);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
//mainWindow.webContents.openDevTools();
|
//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) => {
|
||||||
@@ -160,6 +161,11 @@ app.whenReady().then(async () => {
|
|||||||
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());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize Profile Manager (runs migration if needed)
|
||||||
|
profileManager.init();
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -809,3 +815,44 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
12
preload.js
12
preload.js
@@ -73,5 +73,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
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