feat: add 'Close launcher on game start' option and improve app termination behavior (#93)

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

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

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

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

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

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

* fix release build naming

* Prepare release v2.0.2b

* feat: add 'Close launcher on game start' option and improve app termination behavior

- Added 'Close launcher on game start' setting in GUI and backend.
- Implemented automatic app quit after game launch if setting is enabled.
- Added Cmd+Q (Mac) and Ctrl+Q/Alt+F4 (Win/Linux) shortcuts to quit the app.
- Updated 'window-close' handler to fully quit the app instead of just closing the window.
- Added i18n support for the new setting in English, Spanish, and Portuguese.

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
Co-authored-by: Arnav Singh <hi.arnavsingh3@gmail.com>
This commit is contained in:
Arnav Singh
2026-01-22 15:41:16 +05:30
committed by GitHub
parent a8da559e93
commit 68d697576a
11 changed files with 184 additions and 67 deletions

3
.gitignore vendored
View File

@@ -8,6 +8,7 @@ pkg/
# Package files # Package files
*.tar.zst *.tar.zst
*.zst.DS_Store
*.zst *.zst
bun.lockb bun.lockb
.env .env

View File

@@ -431,6 +431,27 @@
</div> </div>
</div> </div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-window-close"></i>
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="closeLauncherCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher on game start</div>
<div class="checkbox-description" data-i18n="settings.closeOnStartDescription">
Automatically close the launcher after Hytale has launched
</div>
</div>
</label>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-coffee"></i> <i class="fas fa-coffee"></i>

View File

@@ -3,9 +3,11 @@ let customJavaCheck;
let customJavaOptions; let customJavaOptions;
let customJavaPath; let customJavaPath;
let browseJavaBtn; let browseJavaBtn;
let settingsPlayerName; let settingsPlayerName;
let discordRPCCheck; let discordRPCCheck;
let gpuPreferenceRadios; let closeLauncherCheck;
let gpuPreferenceRadios;
// UUID Management elements // UUID Management elements
let currentUuidDisplay; let currentUuidDisplay;
@@ -159,9 +161,11 @@ function setupSettingsElements() {
customJavaOptions = document.getElementById('customJavaOptions'); customJavaOptions = document.getElementById('customJavaOptions');
customJavaPath = document.getElementById('customJavaPath'); customJavaPath = document.getElementById('customJavaPath');
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"]'); closeLauncherCheck = document.getElementById('closeLauncherCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
// UUID Management elements // UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid'); currentUuidDisplay = document.getElementById('currentUuid');
@@ -190,9 +194,14 @@ function setupSettingsElements() {
settingsPlayerName.addEventListener('change', savePlayerName); settingsPlayerName.addEventListener('change', savePlayerName);
} }
if (discordRPCCheck) { if (discordRPCCheck) {
discordRPCCheck.addEventListener('change', saveDiscordRPC); discordRPCCheck.addEventListener('change', saveDiscordRPC);
} }
if (closeLauncherCheck) {
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
}
// UUID event listeners // UUID event listeners
if (copyUuidBtn) { if (copyUuidBtn) {
@@ -335,18 +344,43 @@ async function saveDiscordRPC() {
} }
} }
async function loadDiscordRPC() { async function loadDiscordRPC() {
try { try {
if (window.electronAPI && window.electronAPI.loadDiscordRPC) { if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
const enabled = await window.electronAPI.loadDiscordRPC(); const enabled = await window.electronAPI.loadDiscordRPC();
if (discordRPCCheck) { if (discordRPCCheck) {
discordRPCCheck.checked = enabled; discordRPCCheck.checked = enabled;
} }
} }
} catch (error) { } catch (error) {
console.error('Error loading Discord RPC setting:', error); console.error('Error loading Discord RPC setting:', error);
} }
} }
async function saveCloseLauncher() {
try {
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
const enabled = closeLauncherCheck.checked;
await window.electronAPI.saveCloseLauncher(enabled);
}
} catch (error) {
console.error('Error saving close launcher setting:', error);
}
}
async function loadCloseLauncher() {
try {
if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
const enabled = await window.electronAPI.loadCloseLauncher();
if (closeLauncherCheck) {
closeLauncherCheck.checked = enabled;
}
}
} catch (error) {
console.error('Error loading close launcher setting:', error);
}
}
async function savePlayerName() { async function savePlayerName() {
try { try {
@@ -457,13 +491,15 @@ async function loadGpuPreference() {
} }
} }
async function loadAllSettings() { async function loadAllSettings() {
await loadCustomJavaPath(); await loadCustomJavaPath();
await loadPlayerName(); await loadPlayerName();
await loadCurrentUuid(); await loadCurrentUuid();
await loadDiscordRPC(); await loadDiscordRPC();
await loadGpuPreference(); await loadCloseLauncher();
} await loadGpuPreference();
}
async function openGameLocation() { async function openGameLocation() {
try { try {

View File

@@ -122,7 +122,10 @@
"logsCopy": "Copy", "logsCopy": "Copy",
"logsRefresh": "Refresh", "logsRefresh": "Refresh",
"logsFolder": "Open Folder", "logsFolder": "Open Folder",
"logsLoading": "Loading logs..." "logsLoading": "Loading logs...",
"closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start",
"closeOnStartDescription": "Automatically close the launcher after Hytale has launched"
}, },
"uuid": { "uuid": {
"modalTitle": "UUID Management", "modalTitle": "UUID Management",

View File

@@ -122,7 +122,10 @@
"logsCopy": "Copiar", "logsCopy": "Copiar",
"logsRefresh": "Actualizar", "logsRefresh": "Actualizar",
"logsFolder": "Abrir Carpeta", "logsFolder": "Abrir Carpeta",
"logsLoading": "Cargando registros..." "logsLoading": "Cargando registros...",
"closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego",
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado"
}, },
"uuid": { "uuid": {
"modalTitle": "Gestión de UUID", "modalTitle": "Gestión de UUID",

View File

@@ -122,7 +122,10 @@
"logsCopy": "Copiar", "logsCopy": "Copiar",
"logsRefresh": "Atualizar", "logsRefresh": "Atualizar",
"logsFolder": "Abrir Pasta", "logsFolder": "Abrir Pasta",
"logsLoading": "Carregando registros..." "logsLoading": "Carregando registros...",
"closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado"
}, },
"uuid": { "uuid": {
"modalTitle": "Gerenciamento de UUID", "modalTitle": "Gerenciamento de UUID",

View File

@@ -156,6 +156,15 @@ function loadLanguage() {
return config.language || 'en'; return config.language || 'en';
} }
function saveCloseLauncherOnStart(enabled) {
saveConfig({ closeLauncherOnStart: !!enabled });
}
function loadCloseLauncherOnStart() {
const config = loadConfig();
return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false;
}
function saveModsToConfig(mods) { function saveModsToConfig(mods) {
try { try {
const config = loadConfig(); const config = loadConfig();
@@ -331,5 +340,8 @@ module.exports = {
resetCurrentUserUuid, resetCurrentUserUuid,
// GPU Preference exports // GPU Preference exports
saveGpuPreference, saveGpuPreference,
loadGpuPreference loadGpuPreference,
// Close Launcher export
saveCloseLauncherOnStart,
loadCloseLauncherOnStart
}; };

View File

@@ -17,6 +17,8 @@ const {
loadDiscordRPC, loadDiscordRPC,
saveLanguage, saveLanguage,
loadLanguage, loadLanguage,
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
getUuidForUser, getUuidForUser,
@@ -124,6 +126,10 @@ module.exports = {
saveLanguage, saveLanguage,
loadLanguage, loadLanguage,
// Close Launcher functions
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
// GPU Preference functions // GPU Preference functions
saveGpuPreference, saveGpuPreference,
loadGpuPreference, loadGpuPreference,

94
main.js
View File

@@ -2,7 +2,8 @@ const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') }); require('dotenv').config({ path: path.join(__dirname, '.env') });
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const fs = require('fs'); const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const UpdateManager = require('./backend/updateManager'); const UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger'); const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager'); const profileManager = require('./backend/managers/profileManager');
@@ -186,10 +187,21 @@ function createWindow() {
if (input.key === 'F12') { if (input.key === 'F12') {
event.preventDefault(); event.preventDefault();
} }
if (input.key === 'F5') { if (input.key === 'F5') {
event.preventDefault(); event.preventDefault();
} }
});
// Close application shortcuts
const isMac = process.platform === 'darwin';
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
(!isMac && input.alt && input.key === 'F4');
if (quitShortcut) {
app.quit();
}
});
mainWindow.webContents.on('context-menu', (e) => { mainWindow.webContents.on('context-menu', (e) => {
@@ -333,15 +345,14 @@ app.on('before-quit', () => {
cleanupDiscordRPC(); cleanupDiscordRPC();
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ==='); console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC(); cleanupDiscordRPC();
if (process.platform !== 'darwin') { app.quit();
app.quit(); });
}
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => { ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try { try {
@@ -358,9 +369,20 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
} }
}; };
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
return result; if (result.success && result.launched) {
const closeOnStart = loadCloseLauncherOnStart();
if (closeOnStart) {
console.log('Close Launcher on start enabled, quitting application...');
setTimeout(() => {
app.quit();
}, 1000);
}
}
return result;
} 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();
@@ -475,11 +497,21 @@ ipcMain.handle('save-language', (event, language) => {
return { success: true }; return { success: true };
}); });
ipcMain.handle('load-language', () => { ipcMain.handle('load-language', () => {
return loadLanguage(); return loadLanguage();
}); });
ipcMain.handle('select-install-path', async () => { ipcMain.handle('save-close-launcher', (event, enabled) => {
saveCloseLauncherOnStart(enabled);
return { success: true };
});
ipcMain.handle('load-close-launcher', () => {
return loadCloseLauncherOnStart();
});
ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], properties: ['openDirectory'],
title: 'Select Installation Folder' title: 'Select Installation Folder'
@@ -804,11 +836,10 @@ ipcMain.handle('open-download-page', async () => {
try { try {
await shell.openExternal(updateManager.getDownloadUrl()); await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => { setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) { app.quit();
mainWindow.close(); }, 1000);
}
}, 1000);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -850,11 +881,10 @@ ipcMain.handle('get-detected-gpu', () => {
return global.detectedGpu; return global.detectedGpu;
}); });
ipcMain.handle('window-close', () => { ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) { app.quit();
mainWindow.close(); });
}
});
ipcMain.handle('window-minimize', () => { ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "hytale-f2p-launcherv2", "name": "hytale-f2p-launcher",
"version": "2.0.2b", "version": "2.0.2b",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hytale-f2p-launcherv2", "name": "hytale-f2p-launcher",
"version": "2.0.2b", "version": "2.0.2b",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -21,6 +21,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'), loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
saveLanguage: (language) => ipcRenderer.invoke('save-language', language), saveLanguage: (language) => ipcRenderer.invoke('save-language', language),
loadLanguage: () => ipcRenderer.invoke('load-language'), loadLanguage: () => ipcRenderer.invoke('load-language'),
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'), selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),