feat: Add Repair Game button, UserData backup and cache clearing (#79)

* feat: Add Repair Game functionality including UserData backup and cache clearing

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

* feat: Add Open Logs feature

* disable dev tools

* Fix Settings UI

* fix reorder settings section in index.html

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

---------

Co-authored-by: Fazri Gading <super.fai700@gmail.com>
This commit is contained in:
Rahul Sahani
2026-01-21 03:57:33 +05:30
committed by GitHub
parent 30265549cf
commit b05aeef66d
10 changed files with 616 additions and 203 deletions

View File

@@ -55,6 +55,10 @@
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
<span class="nav-tooltip">Skins</span> <span class="nav-tooltip">Skins</span>
</div> </div>
<div class="nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
<i class="fas fa-terminal"></i>
<span class="nav-tooltip">Logs</span>
</div>
</div> </div>
@@ -279,61 +283,6 @@
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
Java Runtime
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div>
<div class="checkbox-description">Override the bundled Java runtime with
your own installation</div>
</div>
</label>
</div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label>
<div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input"
placeholder="Select Java path..." readonly />
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
Browse
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select the Java installation folder (supports Windows, Mac, Linux)
</p>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
@@ -366,39 +315,39 @@
</div> </div>
</div> </div>
<div class="settings-input-group">
<label class="settings-input-label">GPU Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
<label for="gpu-auto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference" value="integrated">
<label for="gpu-integrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference" value="dedicated">
<label for="gpu-dedicated">Dedicated</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select your preferred GPU (Linux: affects DRI_PRIME)
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</h3>
<div class="settings-option"> <div class="settings-option">
<label class="settings-checkbox"> <div class="settings-button-group">
<input type="checkbox" id="discordRPCCheck" checked /> <button id="repairGameBtn" class="settings-action-btn"
<span class="checkmark"></span> onclick="repairGame()">
<div class="checkbox-content"> <i class="fas fa-tools"></i>
<div class="checkbox-title">Enable Discord Rich Presence</div> <div class="btn-content">
<div class="checkbox-description">Show your launcher activity on Discord</div> <div class="btn-title">Repair Game</div>
<div class="btn-description">Reinstall game files (preserves data)
</div>
</div>
</button>
</div>
<div class="settings-input-group">
<label class="settings-input-label">GPU Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto"
checked>
<label for="gpu-auto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference"
value="integrated">
<label for="gpu-integrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference"
value="dedicated">
<label for="gpu-dedicated">Dedicated</label>
</div> </div>
</label> <p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select your preferred GPU (Linux: affects DRI_PRIME)
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div> </div>
</div> </div>
@@ -435,41 +384,57 @@
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
<div class="btn-content"> <div class="btn-content">
<div class="btn-title">Manage All UUIDs</div> <div class="btn-title">Manage All UUIDs</div>
<div class="btn-description">View and manage all player UUIDs</div> <div class="btn-description">View and manage all player UUIDs
</div>
</div> </div>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-coffee"></i> <i class="fas fa-coffee"></i>
Java Runtime Java Runtime
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
<label class="settings-checkbox"> <label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" /> <input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span> <span class="checkmark"></span>
<div class="checkbox-content"> <div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div> <div class="checkbox-title">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>
<div id="customJavaOptions" class="custom-java-options" style="display: none;"> <div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label> <label class="settings-input-label">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
@@ -482,7 +447,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -494,8 +459,32 @@
<p>Skin customization coming soon...</p> <p>Skin customization coming soon...</p>
</div> </div>
</div> </div>
<div id="logs-page" class="page">
<div class="logs-container">
<div class="logs-header">
<h2 class="logs-title">
<i class="fas fa-terminal"></i>
SYSTEM LOGS
</h2>
<div class="logs-actions">
<button class="logs-action-btn" onclick="copyLogs()">
<i class="fas fa-copy"></i> Copy
</button>
<button class="logs-action-btn" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button class="logs-action-btn" onclick="openLogsFolder()">
<i class="fas fa-folder-open"></i> Open Folder
</button>
</div>
</div>
<div id="logsTerminal" class="logs-terminal">
<div class="text-gray-500 text-center mt-10">Loading logs...</div>
</div>
</div>
</div>
</div> </div>
</div>
</main> </main>
</div> </div>
@@ -742,4 +731,5 @@
<script type="module" src="js/update.js"></script> <script type="module" src="js/update.js"></script>
</body> </body>
</html>
</html>

View File

@@ -23,14 +23,6 @@ export function setupLauncher() {
javaPathInput.addEventListener('change', saveJavaPath); javaPathInput.addEventListener('change', saveJavaPath);
} }
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) {
window.LauncherUI.updateProgress(data);
}
});
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) { if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => { window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return; if (!isDownloading) return;
@@ -445,6 +437,49 @@ async function performUninstall() {
} }
} }
export async function repairGame() {
showCustomConfirm(
'Are you sure you want to repair Hytale? This will reinstall the game files but keep your data (saves, screenshots, etc.).',
'Repair Game',
async () => {
await performRepair();
},
null,
'Repair',
'Cancel'
);
}
async function performRepair() {
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Repairing game...' });
isDownloading = true;
try {
if (window.electronAPI && window.electronAPI.repairGame) {
const result = await window.electronAPI.repairGame();
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game repaired successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
}, 2000);
}
} else {
throw new Error(result.error || 'Repair failed');
}
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Repair failed: ${error.message}` });
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
}
} finally {
isDownloading = false;
}
}
function resetPlayButton() { function resetPlayButton() {
isDownloading = false; isDownloading = false;
if (playBtn) { if (playBtn) {
@@ -537,5 +572,59 @@ async function loadCustomJavaPath() {
window.launch = launch; window.launch = launch;
window.uninstallGame = uninstallGame; window.uninstallGame = uninstallGame;
window.repairGame = repairGame;
window.openLogs = async () => {
if (window.LauncherUI) {
window.LauncherUI.showPage('logs-page');
window.LauncherUI.setActiveNav('logs');
}
await refreshLogs();
};
window.openLogsFolder = async () => {
try {
if (window.electronAPI && window.electronAPI.openLogsFolder) {
await window.electronAPI.openLogsFolder();
}
} catch (error) {
console.error('Failed to open logs folder:', error);
}
};
window.refreshLogs = async () => {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
try {
if (window.electronAPI && window.electronAPI.getRecentLogs) {
// Fetch up to MAX_LOG_LINES lines
const logs = await window.electronAPI.getRecentLogs(MAX_LOG_LINES);
if (logs) {
// Formatting for colors could be done here if needed
terminal.textContent = logs;
terminal.scrollTop = terminal.scrollHeight;
} else {
terminal.textContent = 'No logs available.';
}
}
} catch (error) {
terminal.textContent = 'Error loading logs: ' + error.message;
}
};
window.copyLogs = () => {
const terminal = document.getElementById('logsTerminal');
if (terminal) {
navigator.clipboard.writeText(terminal.textContent)
.then(() => alert('Logs copied to clipboard!'))
.catch(err => console.error('Failed to copy logs:', err));
}
};
window.repairGame = repairGame;
// Constants
const MAX_LOG_LINES = 500;
document.addEventListener('DOMContentLoaded', setupLauncher); document.addEventListener('DOMContentLoaded', setupLauncher);

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

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

View File

@@ -6,8 +6,9 @@ import './mods.js';
import './players.js'; import './players.js';
import './chat.js'; import './chat.js';
import './settings.js'; import './settings.js';
import './logs.js';
window.closeDiscordNotification = function() { window.closeDiscordNotification = function () {
const notification = document.getElementById('discordNotification'); const notification = document.getElementById('discordNotification');
if (notification) { if (notification) {
notification.classList.add('hidden'); notification.classList.add('hidden');
@@ -24,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!dismissed) { if (!dismissed) {
setTimeout(() => { setTimeout(() => {
notification.style.display = 'flex'; notification.style.display = 'flex';
}, 3000); }, 3000);
} else { } else {
notification.style.display = 'none'; notification.style.display = 'none';
} }
@@ -32,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const originalClose = window.closeDiscordNotification; const originalClose = window.closeDiscordNotification;
window.closeDiscordNotification = function() { window.closeDiscordNotification = function () {
localStorage.setItem('discordNotificationDismissed', 'true'); localStorage.setItem('discordNotificationDismissed', 'true');
originalClose(); originalClose();
}; };

View File

@@ -795,6 +795,96 @@ body {
} }
.custom-java-hint i {
color: #3b82f6;
}
/* Logs Page Styles */
.logs-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem 2rem 2rem;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.logs-title {
font-size: 2rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
.logs-actions {
display: flex;
gap: 0.75rem;
}
.logs-action-btn {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #d1d5db;
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logs-action-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
border-color: rgba(255, 255, 255, 0.2);
}
.logs-terminal {
flex: 1;
background: #0d1117;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
color: #e5e7eb;
overflow-y: auto;
white-space: pre-wrap;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
/* Custom scrollbar */
scrollbar-width: thin;
scrollbar-color: rgba(147, 51, 234, 0.3) transparent;
}
.log-line {
margin-bottom: 0.25rem;
}
.log-line.error {
color: #ef4444;
}
.log-line.warn {
color: #f59e0b;
}
.log-line.info {
color: #3b82f6;
}
.news-section { .news-section {
padding: 1rem 2rem; padding: 1rem 2rem;
flex: 1; flex: 1;

View File

@@ -40,7 +40,8 @@ const {
installGame, installGame,
uninstallGame, uninstallGame,
updateGameFiles, updateGameFiles,
checkExistingGameInstallation checkExistingGameInstallation,
repairGame
} = require('./managers/gameManager'); } = require('./managers/gameManager');
const { const {
@@ -87,13 +88,14 @@ module.exports = {
// Game launch functions // Game launch functions
launchGame, launchGame,
launchGameWithVersionCheck, launchGameWithVersionCheck,
// Game installation functions // Game installation functions
installGame, installGame,
isGameInstalled, isGameInstalled,
uninstallGame, uninstallGame,
updateGameFiles, updateGameFiles,
repairGame,
// User configuration functions // User configuration functions
saveUsername, saveUsername,
loadUsername, loadUsername,
@@ -102,16 +104,16 @@ module.exports = {
saveChatColor, saveChatColor,
loadChatColor, loadChatColor,
getUuidForUser, getUuidForUser,
// Java configuration functions // Java configuration functions
saveJavaPath, saveJavaPath,
loadJavaPath, loadJavaPath,
getJavaDetection, getJavaDetection,
// Installation path functions // Installation path functions
saveInstallPath, saveInstallPath,
loadInstallPath, loadInstallPath,
// Discord RPC functions // Discord RPC functions
saveDiscordRPC, saveDiscordRPC,
loadDiscordRPC, loadDiscordRPC,
@@ -124,13 +126,13 @@ module.exports = {
// Version functions // Version functions
getInstalledClientVersion, getInstalledClientVersion,
getLatestClientVersion, getLatestClientVersion,
// News functions // News functions
getHytaleNews, getHytaleNews,
// Player ID functions // Player ID functions
getOrCreatePlayerId, getOrCreatePlayerId,
// UUID Management functions // UUID Management functions
getCurrentUuid, getCurrentUuid,
getAllUuidMappings, getAllUuidMappings,
@@ -138,7 +140,7 @@ module.exports = {
generateNewUuid, generateNewUuid,
deleteUuidForUser, deleteUuidForUser,
resetCurrentUserUuid, resetCurrentUserUuid,
// Mod management functions // Mod management functions
getModsPath, getModsPath,
loadInstalledMods, loadInstalledMods,
@@ -147,13 +149,13 @@ module.exports = {
toggleMod, toggleMod,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
// UI file management functions // UI file management functions
downloadAndReplaceHomePageUI, downloadAndReplaceHomePageUI,
findHomePageUIPath, findHomePageUIPath,
downloadAndReplaceLogo, downloadAndReplaceLogo,
findLogoPath, findLogoPath,
// First launch functions // First launch functions
isFirstLaunch, isFirstLaunch,
markAsLaunched, markAsLaunched,

View File

@@ -14,7 +14,7 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall
const osName = getOS(); const osName = getOS();
const arch = getArch(); const arch = getArch();
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`; const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`;
const dest = path.join(cacheDir, fileName); const dest = path.join(cacheDir, fileName);
if (fs.existsSync(dest)) { if (fs.existsSync(dest)) {
@@ -25,7 +25,7 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall
console.log('Fetching PWR patch file:', url); console.log('Fetching PWR patch file:', url);
await downloadFile(url, dest, progressCallback); await downloadFile(url, dest, progressCallback);
console.log('PWR saved to:', dest); console.log('PWR saved to:', dest);
return dest; return dest;
} }
@@ -33,9 +33,9 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
const butlerPath = await installButler(toolsDir); const butlerPath = await installButler(toolsDir);
const gameLatest = gameDir; const gameLatest = gameDir;
const stagingDir = path.join(gameLatest, 'staging-temp'); const stagingDir = path.join(gameLatest, 'staging-temp');
const clientPath = findClientPath(gameLatest); const clientPath = findClientPath(gameLatest);
if (clientPath) { if (clientPath) {
console.log('Game files detected, skipping patch installation.'); console.log('Game files detected, skipping patch installation.');
return; return;
@@ -53,11 +53,11 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
} }
console.log('Installing game patch...'); console.log('Installing game patch...');
if (!fs.existsSync(butlerPath)) { if (!fs.existsSync(butlerPath)) {
throw new Error(`Butler tool not found at: ${butlerPath}`); throw new Error(`Butler tool not found at: ${butlerPath}`);
} }
if (!fs.existsSync(pwrFile)) { if (!fs.existsSync(pwrFile)) {
throw new Error(`PWR file not found at: ${pwrFile}`); throw new Error(`PWR file not found at: ${pwrFile}`);
} }
@@ -69,7 +69,7 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
pwrFile, pwrFile,
gameLatest gameLatest
]; ];
try { try {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const child = execFile(butlerPath, args, { const child = execFile(butlerPath, args, {
@@ -108,7 +108,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
console.log(`Updating game files to version: ${newVersion}`); console.log(`Updating game files to version: ${newVersion}`);
tempUpdateDir = path.join(gameDir, '..', 'temp_update'); tempUpdateDir = path.join(gameDir, '..', 'temp_update');
if (fs.existsSync(tempUpdateDir)) { if (fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true }); fs.rmSync(tempUpdateDir, { recursive: true, force: true });
} }
@@ -117,26 +117,26 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
if (progressCallback) { if (progressCallback) {
progressCallback('Downloading new game version...', 10, null, null, null); progressCallback('Downloading new game version...', 10, null, null, null);
} }
const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir); const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir);
if (progressCallback) { if (progressCallback) {
progressCallback('Extracting new files...', 50, null, null, null); progressCallback('Extracting new files...', 50, null, null, null);
} }
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir); await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
if (progressCallback) { if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null); progressCallback('Replacing game files...', 80, null, null, null);
} }
let userDataBackup = null; let userDataBackup = null;
const userDataPath = findUserDataRecursive(gameDir); const userDataPath = findUserDataRecursive(gameDir);
if (userDataPath && fs.existsSync(userDataPath)) { if (userDataPath && fs.existsSync(userDataPath)) {
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now()); userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now());
console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`); console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`);
function copyRecursive(src, dest) { function copyRecursive(src, dest) {
const stat = fs.statSync(src); const stat = fs.statSync(src);
if (stat.isDirectory()) { if (stat.isDirectory()) {
@@ -151,35 +151,35 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
fs.copyFileSync(src, dest); fs.copyFileSync(src, dest);
} }
} }
copyRecursive(userDataPath, userDataBackup); copyRecursive(userDataPath, userDataBackup);
} else { } else {
console.log('No UserData folder found in game directory'); console.log('No UserData folder found in game directory');
} }
if (fs.existsSync(gameDir)) { if (fs.existsSync(gameDir)) {
console.log('Removing old game files...'); console.log('Removing old game files...');
fs.rmSync(gameDir, { recursive: true, force: true }); fs.rmSync(gameDir, { recursive: true, force: true });
} }
fs.renameSync(tempUpdateDir, gameDir); fs.renameSync(tempUpdateDir, gameDir);
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback); const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult); console.log('HomePage.ui update result after update:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback); const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult); console.log('Logo@2x.png update result after update:', logoResult);
if (userDataBackup && fs.existsSync(userDataBackup)) { if (userDataBackup && fs.existsSync(userDataBackup)) {
const newUserDataPath = findUserDataPath(gameDir); const newUserDataPath = findUserDataPath(gameDir);
const userDataParent = path.dirname(newUserDataPath); const userDataParent = path.dirname(newUserDataPath);
if (!fs.existsSync(userDataParent)) { if (!fs.existsSync(userDataParent)) {
fs.mkdirSync(userDataParent, { recursive: true }); fs.mkdirSync(userDataParent, { recursive: true });
} }
console.log(`Restoring UserData to: ${newUserDataPath}`); console.log(`Restoring UserData to: ${newUserDataPath}`);
function copyRecursive(src, dest) { function copyRecursive(src, dest) {
const stat = fs.statSync(src); const stat = fs.statSync(src);
if (stat.isDirectory()) { if (stat.isDirectory()) {
@@ -194,12 +194,12 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
fs.copyFileSync(src, dest); fs.copyFileSync(src, dest);
} }
} }
copyRecursive(userDataBackup, newUserDataPath); copyRecursive(userDataBackup, newUserDataPath);
} }
console.log(`Game files updated successfully to version: ${newVersion}`); console.log(`Game files updated successfully to version: ${newVersion}`);
if (userDataBackup && fs.existsSync(userDataBackup)) { if (userDataBackup && fs.existsSync(userDataBackup)) {
try { try {
fs.rmSync(userDataBackup, { recursive: true, force: true }); fs.rmSync(userDataBackup, { recursive: true, force: true });
@@ -208,18 +208,18 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
console.warn('Could not clean up UserData backup:', cleanupError.message); console.warn('Could not clean up UserData backup:', cleanupError.message);
} }
} }
console.log('Waiting for file system sync...'); console.log('Waiting for file system sync...');
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
if (progressCallback) { if (progressCallback) {
progressCallback('Game update completed', 100, null, null, null); progressCallback('Game update completed', 100, null, null, null);
} }
return { success: true, updated: true, version: newVersion }; return { success: true, updated: true, version: newVersion };
} catch (error) { } catch (error) {
console.error('Error updating game files:', error); console.error('Error updating game files:', error);
if (userDataBackup && fs.existsSync(userDataBackup)) { if (userDataBackup && fs.existsSync(userDataBackup)) {
try { try {
fs.rmSync(userDataBackup, { recursive: true, force: true }); fs.rmSync(userDataBackup, { recursive: true, force: true });
@@ -228,11 +228,11 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
console.warn('Could not clean up UserData backup:', cleanupError.message); console.warn('Could not clean up UserData backup:', cleanupError.message);
} }
} }
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) { if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true }); fs.rmSync(tempUpdateDir, { recursive: true, force: true });
} }
throw new Error(`Failed to update game files: ${error.message}`); throw new Error(`Failed to update game files: ${error.message}`);
} }
} }
@@ -309,31 +309,31 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
progressCallback('Fetching game files...', null, null, null, null); progressCallback('Fetching game files...', null, null, null, null);
} }
console.log('Installing game files...'); console.log('Installing game files...');
const latestVersion = await getLatestClientVersion(); const latestVersion = await getLatestClientVersion();
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback); const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult); console.log('HomePage.ui update result after installation:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback); const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult); console.log('Logo@2x.png update result after installation:', logoResult);
if (progressCallback) { if (progressCallback) {
progressCallback('Installation complete', 100, null, null, null); progressCallback('Installation complete', 100, null, null, null);
} }
console.log('Game installation completed successfully'); console.log('Game installation completed successfully');
return { return {
success: true, success: true,
installed: true installed: true
}; };
} }
async function uninstallGame() { async function uninstallGame() {
const appDir = getResolvedAppDir(); const appDir = getResolvedAppDir();
if (!fs.existsSync(appDir)) { if (!fs.existsSync(appDir)) {
throw new Error('Game is not installed'); throw new Error('Game is not installed');
} }
@@ -341,7 +341,7 @@ async function uninstallGame() {
try { try {
fs.rmSync(appDir, { recursive: true, force: true }); fs.rmSync(appDir, { recursive: true, force: true });
console.log('Game uninstalled successfully - removed entire HytaleF2P folder'); console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
if (fs.existsSync(CONFIG_FILE)) { if (fs.existsSync(CONFIG_FILE)) {
const config = loadConfig(); const config = loadConfig();
delete config.installPath; delete config.installPath;
@@ -355,25 +355,25 @@ async function uninstallGame() {
function checkExistingGameInstallation() { function checkExistingGameInstallation() {
try { try {
const config = loadConfig(); const config = loadConfig();
if (!config.installPath || !config.installPath.trim()) { if (!config.installPath || !config.installPath.trim()) {
return null; return null;
} }
const installPath = config.installPath.trim(); const installPath = config.installPath.trim();
const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest'); const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest');
if (!fs.existsSync(gameDir)) { if (!fs.existsSync(gameDir)) {
return null; return null;
} }
const clientPath = findClientPath(gameDir); const clientPath = findClientPath(gameDir);
if (!clientPath) { if (!clientPath) {
return null; return null;
} }
const userDataPath = findUserDataRecursive(gameDir); const userDataPath = findUserDataRecursive(gameDir);
return { return {
gameDir: gameDir, gameDir: gameDir,
clientPath: clientPath, clientPath: clientPath,
@@ -387,6 +387,102 @@ function checkExistingGameInstallation() {
} }
} }
async function repairGame(progressCallback) {
const appDir = getResolvedAppDir();
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
// Check if game exists
if (!fs.existsSync(gameDir)) {
throw new Error('Game directory not found. Cannot repair.');
}
// Locate UserData
const userDataPath = findUserDataRecursive(gameDir);
let userDataBackup = null;
if (progressCallback) {
progressCallback('Backing up user data...', 10, null, null, null);
}
// Backup UserData
if (userDataPath && fs.existsSync(userDataPath)) {
userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now());
console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`);
// Copy function
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataPath, userDataBackup);
}
if (progressCallback) {
progressCallback('Removing old game files...', 30, null, null, null);
}
// Delete Game and Cache Directory
console.log('Removing corrupted game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
const cacheDir = path.join(appDir, 'cache');
if (fs.existsSync(cacheDir)) {
console.log('Clearing cache directory...');
fs.rmSync(cacheDir, { recursive: true, force: true });
}
console.log('Reinstalling game files...');
// Passing null/undefined for overrides to use defaults/saved configs
// installGame calls progressCallback internally
await installGame('Player', progressCallback);
// Restore UserData
if (userDataBackup && fs.existsSync(userDataBackup)) {
if (progressCallback) {
progressCallback('Restoring user data...', 90, null, null, null);
}
// installGame creates: path.join(customGameDir, 'Client', 'UserData')
const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
const newUserDataPath = path.join(newGameDir, 'Client', 'UserData');
if (!fs.existsSync(newUserDataPath)) {
fs.mkdirSync(newUserDataPath, { recursive: true });
}
console.log(`Restoring UserData to ${newUserDataPath}`);
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataBackup, newUserDataPath);
// Cleanup Backup
console.log('Cleaning up repair backup...');
fs.rmSync(userDataBackup, { recursive: true, force: true });
}
if (progressCallback) {
progressCallback('Repair completed successfully!', 100, null, null, null);
}
return { success: true, repaired: true };
}
module.exports = { module.exports = {
downloadPWR, downloadPWR,
applyPWR, applyPWR,
@@ -394,5 +490,9 @@ module.exports = {
isGameInstalled, isGameInstalled,
installGame, installGame,
uninstallGame, uninstallGame,
checkExistingGameInstallation isGameInstalled,
installGame,
uninstallGame,
checkExistingGameInstallation,
repairGame
}; };

View File

@@ -4,11 +4,11 @@ const axios = require('axios');
async function downloadFile(url, dest, progressCallback, maxRetries = 3) { async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
let lastError = null; let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) { for (let attempt = 0; attempt < maxRetries; attempt++) {
try { try {
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`); console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
if (attempt > 0 && progressCallback) { if (attempt > 0 && progressCallback) {
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null); progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
@@ -53,19 +53,19 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
response.data.on('data', (chunk) => { response.data.on('data', (chunk) => {
downloaded += chunk.length; downloaded += chunk.length;
const now = Date.now(); const now = Date.now();
// Reset stalled timer on data received // Reset stalled timer on data received
if (stalledTimeout) { if (stalledTimeout) {
clearTimeout(stalledTimeout); clearTimeout(stalledTimeout);
} }
// Set new stalled timer (30 seconds without data = stalled) // Set new stalled timer (30 seconds without data = stalled)
stalledTimeout = setTimeout(() => { stalledTimeout = setTimeout(() => {
downloadStalled = true; downloadStalled = true;
writer.destroy(); writer.destroy();
response.data.destroy(); response.data.destroy();
}, 30000); }, 30000);
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100)); const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
const elapsed = (now - startTime) / 1000; const elapsed = (now - startTime) / 1000;
@@ -97,14 +97,14 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
reject(new Error('Download stalled')); reject(new Error('Download stalled'));
} }
}); });
writer.on('error', (error) => { writer.on('error', (error) => {
if (stalledTimeout) { if (stalledTimeout) {
clearTimeout(stalledTimeout); clearTimeout(stalledTimeout);
} }
reject(error); reject(error);
}); });
response.data.on('error', (error) => { response.data.on('error', (error) => {
if (stalledTimeout) { if (stalledTimeout) {
clearTimeout(stalledTimeout); clearTimeout(stalledTimeout);
@@ -119,7 +119,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
} catch (error) { } catch (error) {
lastError = error; lastError = error;
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message); console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
// Nettoyer le fichier partiel en cas d'erreur // Nettoyer le fichier partiel en cas d'erreur
if (fs.existsSync(dest)) { if (fs.existsSync(dest)) {
try { try {
@@ -128,23 +128,24 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
console.warn('Could not cleanup partial file:', cleanupError.message); console.warn('Could not cleanup partial file:', cleanupError.message);
} }
} }
// Vérifier si c'est une erreur réseau que l'on peut retry // Vérifier si c'est une erreur réseau que l'on peut retry
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO']; const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
const isRetryable = retryableErrors.includes(error.code) || const isRetryable = retryableErrors.includes(error.code) ||
error.message.includes('timeout') || error.message.includes('timeout') ||
error.message.includes('stalled') || error.message.includes('stalled') ||
(error.response && error.response.status >= 500); (error.response && error.response.status >= 500);
if (!isRetryable || attempt === maxRetries - 1) { if (!isRetryable || attempt === maxRetries - 1) {
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`); console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
break; break;
} }
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`); console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
} }
} }
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`); throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
} }
@@ -152,7 +153,7 @@ function findHomePageUIPath(gameLatest) {
function searchDirectory(dir) { function searchDirectory(dir) {
try { try {
const items = fs.readdirSync(dir, { withFileTypes: true }); const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) { for (const item of items) {
if (item.isFile() && item.name === 'HomePage.ui') { if (item.isFile() && item.name === 'HomePage.ui') {
return path.join(dir, item.name); return path.join(dir, item.name);
@@ -165,14 +166,14 @@ function findHomePageUIPath(gameLatest) {
} }
} catch (error) { } catch (error) {
} }
return null; return null;
} }
if (!fs.existsSync(gameLatest)) { if (!fs.existsSync(gameLatest)) {
return null; return null;
} }
return searchDirectory(gameLatest); return searchDirectory(gameLatest);
} }
@@ -180,7 +181,7 @@ function findLogoPath(gameLatest) {
function searchDirectory(dir) { function searchDirectory(dir) {
try { try {
const items = fs.readdirSync(dir, { withFileTypes: true }); const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) { for (const item of items) {
if (item.isFile() && item.name === 'Logo@2x.png') { if (item.isFile() && item.name === 'Logo@2x.png') {
return path.join(dir, item.name); return path.join(dir, item.name);
@@ -193,14 +194,14 @@ function findLogoPath(gameLatest) {
} }
} catch (error) { } catch (error) {
} }
return null; return null;
} }
if (!fs.existsSync(gameLatest)) { if (!fs.existsSync(gameLatest)) {
return null; return null;
} }
return searchDirectory(gameLatest); return searchDirectory(gameLatest);
} }

54
main.js
View File

@@ -1,7 +1,7 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, 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');
@@ -120,8 +120,6 @@ function createWindow() {
mainWindow.webContents.send('show-update-popup', updateInfo); mainWindow.webContents.send('show-update-popup', updateInfo);
} }
}, 3000); }, 3000);
//mainWindow.webContents.openDevTools();
mainWindow.webContents.on('devtools-opened', () => { mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools(); mainWindow.webContents.closeDevTools();
@@ -311,7 +309,7 @@ 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; return result;
} catch (error) { } catch (error) {
console.error('Launch error:', error); console.error('Launch error:', error);
@@ -469,13 +467,35 @@ ipcMain.handle('is-game-installed', async () => {
ipcMain.handle('uninstall-game', async () => { ipcMain.handle('uninstall-game', async () => {
try { try {
await uninstallGame(); await uninstallGame();
return { success: true };
} catch (error) { } catch (error) {
console.error('Uninstall error:', error); console.error('Uninstall error:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('repair-game', async () => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await repairGame(progressCallback);
return result;
} catch (error) {
console.error('Repair error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-hytale-news', async () => { ipcMain.handle('get-hytale-news', async () => {
try { try {
const news = await getHytaleNews(); const news = await getHytaleNews();
@@ -850,7 +870,14 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
const content = fs.readFileSync(latestLogFile, 'utf8'); const content = fs.readFileSync(latestLogFile, 'utf8');
const lines = content.split('\n'); const lines = content.split('\n');
return lines.slice(-maxLines).join('\n'); let result = lines.slice(-maxLines).join('\n');
if (lines.length > maxLines) {
const truncatedMsg = `\n--- ⚠️ LOG TRUNCATED: Showing last ${maxLines} lines of ${lines.length}. Open Logs Folder for full history ---\n\n`;
return result + truncatedMsg;
}
return result;
} catch (error) { } catch (error) {
console.error('Error reading logs:', error); console.error('Error reading logs:', error);
return null; return null;
@@ -858,6 +885,21 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
}); });
ipcMain.handle('open-logs-folder', async () => {
try {
const logDir = logger.getLogDirectory();
if (logDir && fs.existsSync(logDir)) {
await shell.openPath(logDir);
return { success: true };
}
return { success: false, error: 'Logs directory not found' };
} catch (error) {
console.error('Error opening logs folder:', error);
return { success: false, error: error.message };
}
});
// Profile Management IPC // Profile Management IPC
ipcMain.handle('profile-create', async (event, name) => { ipcMain.handle('profile-create', async (event, name) => {
try { try {

View File

@@ -21,6 +21,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
uninstallGame: () => ipcRenderer.invoke('uninstall-game'), uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
repairGame: () => ipcRenderer.invoke('repair-game'),
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'), getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
openExternal: (url) => ipcRenderer.invoke('open-external', url), openExternal: (url) => ipcRenderer.invoke('open-external', url),
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url), openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
@@ -70,6 +71,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
}, },
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'), getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines), getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
// UUID Management methods // UUID Management methods