mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 10:31:47 -03:00
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:
324
GUI/index.html
324
GUI/index.html
@@ -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,6 +283,134 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fas fa-gamepad"></i>
|
||||||
|
Game Options
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<div class="settings-input-group">
|
||||||
|
<label class="settings-input-label">Player Name</label>
|
||||||
|
<input type="text" id="settingsPlayerName" class="settings-input"
|
||||||
|
placeholder="Enter your player name" maxlength="16" />
|
||||||
|
<p class="settings-hint">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
This name will be used in-game (1-16 characters)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<div class="settings-button-group">
|
||||||
|
<button id="openGameLocationBtn" class="settings-action-btn"
|
||||||
|
onclick="openGameLocation()">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
<div class="btn-content">
|
||||||
|
<div class="btn-title">Open Game Location</div>
|
||||||
|
<div class="btn-description">Open the game installation folder</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<div class="settings-button-group">
|
||||||
|
<button id="repairGameBtn" class="settings-action-btn"
|
||||||
|
onclick="repairGame()">
|
||||||
|
<i class="fas fa-tools"></i>
|
||||||
|
<div class="btn-content">
|
||||||
|
<div class="btn-title">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>
|
||||||
|
<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 class="settings-section">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fas fa-fingerprint"></i>
|
||||||
|
Player UUID Management
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<div class="settings-input-group">
|
||||||
|
<label class="settings-input-label">Current UUID</label>
|
||||||
|
<div class="uuid-display-container">
|
||||||
|
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
||||||
|
readonly placeholder="Loading UUID..." />
|
||||||
|
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
|
||||||
|
title="Generate New UUID">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="settings-hint">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Your unique player identifier for this username
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<div class="settings-button-group">
|
||||||
|
<button id="manageUuidsBtn" class="settings-action-btn">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
<div class="btn-content">
|
||||||
|
<div class="btn-title">Manage All UUIDs</div>
|
||||||
|
<div class="btn-description">View and manage all player UUIDs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
@@ -316,173 +448,6 @@
|
|||||||
</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">
|
|
||||||
<h3 class="settings-section-title">
|
|
||||||
<i class="fas fa-gamepad"></i>
|
|
||||||
Game Options
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-input-group">
|
|
||||||
<label class="settings-input-label">Player Name</label>
|
|
||||||
<input type="text" id="settingsPlayerName" class="settings-input"
|
|
||||||
placeholder="Enter your player name" maxlength="16" />
|
|
||||||
<p class="settings-hint">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
This name will be used in-game (1-16 characters)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-button-group">
|
|
||||||
<button id="openGameLocationBtn" class="settings-action-btn"
|
|
||||||
onclick="openGameLocation()">
|
|
||||||
<i class="fas fa-folder-open"></i>
|
|
||||||
<div class="btn-content">
|
|
||||||
<div class="btn-title">Open Game Location</div>
|
|
||||||
<div class="btn-description">Open the game installation folder</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div 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">
|
|
||||||
<label class="settings-checkbox">
|
|
||||||
<input type="checkbox" id="discordRPCCheck" checked />
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
<div class="checkbox-content">
|
|
||||||
<div class="checkbox-title">Enable Discord Rich Presence</div>
|
|
||||||
<div class="checkbox-description">Show your launcher activity on Discord</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-section">
|
|
||||||
<h3 class="settings-section-title">
|
|
||||||
<i class="fas fa-fingerprint"></i>
|
|
||||||
Player UUID Management
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-input-group">
|
|
||||||
<label class="settings-input-label">Current UUID</label>
|
|
||||||
<div class="uuid-display-container">
|
|
||||||
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
|
||||||
readonly placeholder="Loading UUID..." />
|
|
||||||
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
|
|
||||||
title="Generate New UUID">
|
|
||||||
<i class="fas fa-sync-alt"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="settings-hint">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
Your unique player identifier for this username
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-button-group">
|
|
||||||
<button id="manageUuidsBtn" class="settings-action-btn">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
<div class="btn-content">
|
|
||||||
<div class="btn-title">Manage All UUIDs</div>
|
|
||||||
<div class="btn-description">View and manage all player UUIDs</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -494,6 +459,30 @@
|
|||||||
<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>
|
||||||
@@ -742,4 +731,5 @@
|
|||||||
<script type="module" src="js/update.js"></script>
|
<script type="module" src="js/update.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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
96
GUI/js/logs.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
|
||||||
|
// Logs Page Logic
|
||||||
|
|
||||||
|
async function loadLogs() {
|
||||||
|
const terminal = document.getElementById('logsTerminal');
|
||||||
|
if (!terminal) return;
|
||||||
|
|
||||||
|
terminal.innerHTML = '<div class="text-gray-500 text-center mt-10">Loading logs...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logs = await window.electronAPI.getRecentLogs(500); // Fetch last 500 lines
|
||||||
|
|
||||||
|
if (logs) {
|
||||||
|
// Escape HTML to prevent XSS and preserve format
|
||||||
|
const safeLogs = logs.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
|
terminal.innerHTML = `<pre class="logs-content">${safeLogs}</pre>`;
|
||||||
|
|
||||||
|
// Auto scroll to bottom
|
||||||
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
|
} else {
|
||||||
|
terminal.innerHTML = '<div class="text-gray-500 text-center mt-10">No logs found.</div>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs:', error);
|
||||||
|
terminal.innerHTML = `<div class="text-red-500 text-center mt-10">Error loading logs: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLogs() {
|
||||||
|
const btn = document.querySelector('button[onclick="refreshLogs()"] i');
|
||||||
|
if (btn) btn.classList.add('fa-spin');
|
||||||
|
|
||||||
|
await loadLogs();
|
||||||
|
|
||||||
|
if (btn) setTimeout(() => btn.classList.remove('fa-spin'), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLogs() {
|
||||||
|
const terminal = document.getElementById('logsTerminal');
|
||||||
|
if (!terminal) return;
|
||||||
|
|
||||||
|
const content = terminal.innerText;
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
|
||||||
|
const btn = document.querySelector('button[onclick="copyLogs()"]');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
|
||||||
|
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy logs:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openLogsFolder() {
|
||||||
|
await window.electronAPI.openLogsFolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLogs() {
|
||||||
|
// Navigation is handled by sidebar logic, but we can trigger a refresh
|
||||||
|
window.LauncherUI.showPage('logs-page');
|
||||||
|
window.LauncherUI.setActiveNav('logs');
|
||||||
|
refreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose functions globally
|
||||||
|
window.refreshLogs = refreshLogs;
|
||||||
|
window.copyLogs = copyLogs;
|
||||||
|
window.openLogsFolder = openLogsFolder;
|
||||||
|
window.openLogs = openLogs;
|
||||||
|
|
||||||
|
// Auto-load logs when the page becomes active
|
||||||
|
const logsObserver = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.target.classList.contains('active') && mutation.target.id === 'logs-page') {
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const logsPage = document.getElementById('logs-page');
|
||||||
|
if (logsPage) {
|
||||||
|
logsObserver.observe(logsPage, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
@@ -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();
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const {
|
|||||||
installGame,
|
installGame,
|
||||||
uninstallGame,
|
uninstallGame,
|
||||||
updateGameFiles,
|
updateGameFiles,
|
||||||
checkExistingGameInstallation
|
checkExistingGameInstallation,
|
||||||
|
repairGame
|
||||||
} = require('./managers/gameManager');
|
} = require('./managers/gameManager');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -93,6 +94,7 @@ module.exports = {
|
|||||||
isGameInstalled,
|
isGameInstalled,
|
||||||
uninstallGame,
|
uninstallGame,
|
||||||
updateGameFiles,
|
updateGameFiles,
|
||||||
|
repairGame,
|
||||||
|
|
||||||
// User configuration functions
|
// User configuration functions
|
||||||
saveUsername,
|
saveUsername,
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
52
main.js
52
main.js
@@ -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();
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user