This commit is contained in:
gardenGnostic 2026-03-09 16:34:54 +01:00 committed by GitHub
parent 1937c941ea
commit 7b4942ee7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 405 additions and 58 deletions

View file

@ -26,6 +26,10 @@
</div> </div>
</div> </div>
<div id="offline-indicator" style="display: none; position: fixed; top: 40px; left: 50%; transform: translateX(-50%); background: #c42b1c; color: #fff; padding: 5px 20px; border: 2px solid #000; z-index: 100; font-weight: bold; font-size: 14px; box-shadow: 0 4px 10px rgba(0,0,0,0.5);">
OFFLINE MODE — UPDATES DISABLED
</div>
<div class="main-wrapper"> <div class="main-wrapper">
<div class="sidebar collapsed" id="main-sidebar"> <div class="sidebar collapsed" id="main-sidebar">
<div class="sidebar-title" onclick="toggleSidebar()"> <div class="sidebar-title" onclick="toggleSidebar()">
@ -78,7 +82,7 @@
CHANGE SKIN CHANGE SKIN
</div> </div>
</div> </div>
<div class="progress-container" id="progress-container"> <div class="progress-container" id="progress-container">
<div class="progress-text" id="progress-text">Downloading...</div> <div class="progress-text" id="progress-text">Downloading...</div>
<div class="progress-bar-bg"> <div class="progress-bar-bg">
@ -157,6 +161,22 @@
</div> </div>
</div> </div>
<div class="modal-overlay" id="snapshots-modal">
<div class="modal-box" style="max-width: 800px;">
<div class="modal-title">SNAPSHOTS / ROLLBACK</div>
<div id="snapshot-instance-name" style="color: #aaa; text-align: center; margin-top: -15px; margin-bottom: 20px; text-transform: uppercase;">Instance Name</div>
<div id="snapshots-list-container" class="w-full max-h-[350px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
<div class="text-center text-gray-400 py-4">No snapshots found.</div>
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" onclick="createSnapshotManual()" tabindex="0">NEW SNAPSHOT</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleSnapshots(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="options-modal"> <div class="modal-overlay" id="options-modal">
<div class="modal-box"> <div class="modal-box">
<div class="modal-title">LAUNCHER OPTIONS</div> <div class="modal-title">LAUNCHER OPTIONS</div>
@ -191,6 +211,16 @@
</div> </div>
</div> </div>
<div class="mc-input-group" id="custom-proton-group" style="display: none;">
<label class="mc-label">Custom Proton Path:</label>
<input type="text" id="custom-proton-path" class="mc-input nav-item" placeholder="/path/to/proton" tabindex="0">
</div>
<div class="mc-input-group">
<label class="mc-label">Launcher Music Volume:</label>
<input type="range" id="volume-slider" min="0" max="1" step="0.05" value="1" class="nav-item w-full" tabindex="0">
</div>
<div class="grid grid-cols-2 gap-4 w-full mt-2"> <div class="grid grid-cols-2 gap-4 w-full mt-2">
<div class="mc-input-group"> <div class="mc-input-group">
<label class="mc-label">Connect/Bind IP:</label> <label class="mc-label">Connect/Bind IP:</label>

View file

@ -1,6 +1,6 @@
{ {
"name": "legacylauncher", "name": "legacylauncher",
"version": "2.9.1", "version": "3.0.0",
"description": "", "description": "",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -20,6 +20,8 @@ let currentReleaseIndex = 0;
let isProcessing = false; let isProcessing = false;
let isGameRunning = false; let isGameRunning = false;
let snapshotInstanceId = null;
const Store = { const Store = {
async get(key, defaultValue) { async get(key, defaultValue) {
const val = await ipcRenderer.invoke('store-get', key); const val = await ipcRenderer.invoke('store-get', key);
@ -33,7 +35,6 @@ const Store = {
} }
}; };
// Gamepad Controller Support
const GamepadManager = { const GamepadManager = {
active: false, active: false,
lastInputTime: 0, lastInputTime: 0,
@ -138,7 +139,7 @@ const GamepadManager = {
}, },
getVisibleNavItems() { getVisibleNavItems() {
const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal', 'instances-modal', 'add-instance-modal', 'skin-modal']; const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal', 'instances-modal', 'add-instance-modal', 'skin-modal', 'snapshots-modal'];
let activeModal = null; let activeModal = null;
for (const id of modals) { for (const id of modals) {
const m = document.getElementById(id); const m = document.getElementById(id);
@ -237,11 +238,12 @@ const GamepadManager = {
else if (activeModal.id === 'add-instance-modal') toggleAddInstance(false); else if (activeModal.id === 'add-instance-modal') toggleAddInstance(false);
else if (activeModal.id === 'update-modal') document.getElementById('btn-skip-update')?.click(); else if (activeModal.id === 'update-modal') document.getElementById('btn-skip-update')?.click();
else if (activeModal.id === 'skin-modal') closeSkinManager(); else if (activeModal.id === 'skin-modal') closeSkinManager();
else if (activeModal.id === 'snapshots-modal') toggleSnapshots(false);
} }
}, },
getActiveModal() { getActiveModal() {
const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal', 'instances-modal', 'add-instance-modal', 'skin-modal']; const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal', 'instances-modal', 'add-instance-modal', 'skin-modal', 'snapshots-modal'];
for (const id of modals) { for (const id of modals) {
const m = document.getElementById(id); const m = document.getElementById(id);
if (m && m.style.display === 'flex') return m; if (m && m.style.display === 'flex') return m;
@ -284,10 +286,13 @@ const GamepadManager = {
scrollActive(val) { scrollActive(val) {
const serverList = document.getElementById('servers-list-container'); const serverList = document.getElementById('servers-list-container');
const instanceList = document.getElementById('instances-list-container'); const instanceList = document.getElementById('instances-list-container');
const snapshotList = document.getElementById('snapshots-list-container');
if (this.getActiveModal()?.id === 'servers-modal' && serverList) { if (this.getActiveModal()?.id === 'servers-modal' && serverList) {
serverList.scrollTop += val; serverList.scrollTop += val;
} else if (this.getActiveModal()?.id === 'instances-modal' && instanceList) { } else if (this.getActiveModal()?.id === 'instances-modal' && instanceList) {
instanceList.scrollTop += val; instanceList.scrollTop += val;
} else if (this.getActiveModal()?.id === 'snapshots-modal' && snapshotList) {
snapshotList.scrollTop += val;
} else if (!this.getActiveModal()) { } else if (!this.getActiveModal()) {
const sidebar = document.getElementById('updates-list')?.parentElement; const sidebar = document.getElementById('updates-list')?.parentElement;
if (sidebar) sidebar.scrollTop += val; if (sidebar) sidebar.scrollTop += val;
@ -295,7 +300,6 @@ const GamepadManager = {
} }
}; };
// Music Player logic
const MusicManager = { const MusicManager = {
audio: new Audio(), audio: new Audio(),
playlist: [], playlist: [],
@ -304,12 +308,21 @@ const MusicManager = {
async init() { async init() {
this.enabled = await Store.get('legacy_music_enabled', true); this.enabled = await Store.get('legacy_music_enabled', true);
this.audio.volume = await Store.get('legacy_music_volume', 0.5);
this.updateIcon(); this.updateIcon();
this.audio.volume = 1.0;
this.audio.onended = () => this.playNext(); this.audio.onended = () => this.playNext();
if (this.enabled) { if (this.enabled) {
this.start(); this.start();
} }
const slider = document.getElementById('volume-slider');
if (slider) {
slider.value = this.audio.volume;
slider.oninput = async () => {
this.audio.volume = slider.value;
await Store.set('legacy_music_volume', this.audio.volume);
};
}
}, },
async scan() { async scan() {
@ -430,40 +443,75 @@ async function migrateLegacyConfig() {
} }
window.onload = async () => { window.onload = async () => {
await migrateLegacyConfig(); try {
await migrateLegacyConfig();
document.getElementById('repo-input').value = currentInstance.repo;
document.getElementById('exec-input').value = currentInstance.execPath; const repoInput = document.getElementById('repo-input');
document.getElementById('username-input').value = await Store.get('legacy_username', ""); const execInput = document.getElementById('exec-input');
document.getElementById('ip-input').value = currentInstance.ip; const usernameInput = document.getElementById('username-input');
document.getElementById('port-input').value = currentInstance.port; const ipInput = document.getElementById('ip-input');
document.getElementById('server-checkbox').checked = currentInstance.isServer; const portInput = document.getElementById('port-input');
document.getElementById('install-path-input').value = currentInstance.installPath; const serverCheck = document.getElementById('server-checkbox');
const installInput = document.getElementById('install-path-input');
if (process.platform === 'linux' || process.platform === 'darwin') {
document.getElementById('compat-option-container').style.display = 'block';
scanCompatibilityLayers();
} else {
currentInstance.compatLayer = 'direct';
await saveInstancesToStore();
}
ipcRenderer.on('window-is-maximized', (event, isMaximized) => { if (repoInput) repoInput.value = currentInstance.repo;
const btn = document.getElementById('maximize-btn'); if (execInput) execInput.value = currentInstance.execPath;
if (btn) btn.textContent = isMaximized ? '❐' : '▢'; if (usernameInput) usernameInput.value = await Store.get('legacy_username', "");
}); if (ipInput) ipInput.value = currentInstance.ip;
if (portInput) portInput.value = currentInstance.port;
fetchGitHubData(); if (serverCheck) serverCheck.checked = currentInstance.isServer;
checkForLauncherUpdates(); if (installInput) installInput.value = currentInstance.installPath;
loadSplashText();
MusicManager.init(); if (process.platform === 'linux' || process.platform === 'darwin') {
GamepadManager.init(); const compatContainer = document.getElementById('compat-option-container');
if (compatContainer) {
window.addEventListener('keydown', (e) => { compatContainer.style.display = 'block';
if (e.key === 'F9') { scanCompatibilityLayers();
checkForLauncherUpdates(true); }
} else {
currentInstance.compatLayer = 'direct';
await saveInstancesToStore();
} }
});
ipcRenderer.on('window-is-maximized', (event, isMaximized) => {
const btn = document.getElementById('maximize-btn');
if (btn) btn.textContent = isMaximized ? '❐' : '▢';
});
// Initialize features
fetchGitHubData();
checkForLauncherUpdates();
loadSplashText();
MusicManager.init();
GamepadManager.init();
window.addEventListener('keydown', (e) => {
if (e.key === 'F9') {
checkForLauncherUpdates(true);
}
});
window.addEventListener('online', () => {
document.getElementById('offline-indicator').style.display = 'none';
showToast("Back Online! Refreshing...");
fetchGitHubData();
});
window.addEventListener('offline', () => {
document.getElementById('offline-indicator').style.display = 'block';
showToast("Connection Lost. Entering Offline Mode.");
});
if (!navigator.onLine) {
document.getElementById('offline-indicator').style.display = 'block';
}
} catch (e) {
console.error("Startup error:", e);
// Hide loader anyway so user isn't stuck
const loader = document.getElementById('loader');
if (loader) loader.style.display = 'none';
showToast("Error during startup: " + e.message);
}
}; };
async function saveInstancesToStore() { async function saveInstancesToStore() {
@ -509,6 +557,7 @@ async function renderInstancesList() {
<span class="text-gray-500 text-xs">${inst.installPath}</span> <span class="text-gray-500 text-xs">${inst.installPath}</span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="openSnapshotsManager('${inst.id}')">BACKUPS</div>
${!isActive ? `<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="switchInstance('${inst.id}')">SWITCH</div>` : ''} ${!isActive ? `<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="switchInstance('${inst.id}')">SWITCH</div>` : ''}
<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="deleteInstance('${inst.id}')" style="${isActive ? 'opacity: 0.5; pointer-events: none;' : ''}">DELETE</div> <div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="deleteInstance('${inst.id}')" style="${isActive ? 'opacity: 0.5; pointer-events: none;' : ''}">DELETE</div>
</div> </div>
@ -645,6 +694,19 @@ async function updatePlayButtonText() {
btn.classList.remove('running'); btn.classList.remove('running');
} }
// Offline / No Data Case
if (releasesData.length === 0) {
const fullPath = await getInstalledPath();
if (currentInstance.installedTag && fs.existsSync(fullPath)) {
btn.textContent = "PLAY";
btn.classList.remove('disabled');
} else {
btn.textContent = "OFFLINE";
btn.classList.add('disabled');
}
return;
}
const release = releasesData[currentReleaseIndex]; const release = releasesData[currentReleaseIndex];
if (!release) { if (!release) {
btn.textContent = "PLAY"; btn.textContent = "PLAY";
@ -704,9 +766,26 @@ async function fetchGitHubData() {
const repo = currentInstance.repo; const repo = currentInstance.repo;
const loader = document.getElementById('loader'); const loader = document.getElementById('loader');
const loaderText = document.getElementById('loader-text'); const loaderText = document.getElementById('loader-text');
const offlineInd = document.getElementById('offline-indicator');
if (loader) loader.style.display = 'flex'; if (loader) loader.style.display = 'flex';
if (loaderText) loaderText.textContent = "SYNCING: " + repo; if (loaderText) loaderText.textContent = "SYNCING: " + repo;
const hideLoader = () => {
if (loader) {
loader.style.opacity = '0';
setTimeout(() => { loader.style.display = 'none'; }, 300);
}
};
if (!navigator.onLine) {
console.log("Offline detected, skipping GitHub sync.");
if (offlineInd) offlineInd.style.display = 'block';
handleOfflineData();
setTimeout(hideLoader, 500);
return;
}
try { try {
const [relRes, commRes] = await Promise.all([ const [relRes, commRes] = await Promise.all([
fetch(`https://api.github.com/repos/${repo}/releases`), fetch(`https://api.github.com/repos/${repo}/releases`),
@ -721,24 +800,27 @@ async function fetchGitHubData() {
populateVersions(); populateVersions();
populateUpdatesSidebar(); populateUpdatesSidebar();
setTimeout(() => { setTimeout(hideLoader, 500);
if (loader) {
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 300);
}
}, 500);
} catch (err) { } catch (err) {
console.error("Fetch error:", err);
if (loaderText) loaderText.textContent = "REPO NOT FOUND OR API ERROR"; if (loaderText) loaderText.textContent = "REPO NOT FOUND OR API ERROR";
showToast("Check repository name in Options.");
setTimeout(() => { // Even if we fail due to some API error, we should still allow offline play if installed
if (loader) { handleOfflineData();
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 300); showToast("Entering Offline Mode.");
} if (offlineInd) offlineInd.style.display = 'block';
}, 2500); setTimeout(hideLoader, 2500);
} }
} }
function handleOfflineData() {
releasesData = [];
commitsData = [];
populateVersions();
populateUpdatesSidebar();
}
function populateVersions() { function populateVersions() {
const select = document.getElementById('version-select'); const select = document.getElementById('version-select');
const display = document.getElementById('current-version-display'); const display = document.getElementById('current-version-display');
@ -746,7 +828,17 @@ function populateVersions() {
select.innerHTML = ''; select.innerHTML = '';
if(releasesData.length === 0) { if(releasesData.length === 0) {
if (display) display.textContent = "No Releases Found"; // Check if we have a local version installed
if (currentInstance.installedTag) {
const opt = document.createElement('option');
opt.value = 0;
opt.textContent = `Installed (${currentInstance.installedTag})`;
select.appendChild(opt);
if (display) display.textContent = opt.textContent;
} else {
if (display) display.textContent = "No Connection / No Install";
}
updatePlayButtonText();
return; return;
} }
@ -798,6 +890,20 @@ function updateSelectedRelease() {
async function launchGame() { async function launchGame() {
if (isProcessing || isGameRunning) return; if (isProcessing || isGameRunning) return;
if (!navigator.onLine || releasesData.length === 0) {
const fullPath = await getInstalledPath();
if (currentInstance.installedTag && fs.existsSync(fullPath)) {
setProcessingState(true);
updateProgress(100, "Offline Launch...");
await launchLocalClient();
setProcessingState(false);
} else {
showToast("You need an internet connection to install the game!");
}
return;
}
const release = releasesData[currentReleaseIndex]; const release = releasesData[currentReleaseIndex];
if (!release) return; if (!release) return;
const asset = release.assets.find(a => a.name === TARGET_FILE); const asset = release.assets.find(a => a.name === TARGET_FILE);
@ -893,7 +999,6 @@ async function launchLocalClient() {
try { fs.chmodSync(fullPath, 0o755); } catch (e) { console.warn("Failed to set executable permissions:", e); } try { fs.chmodSync(fullPath, 0o755); } catch (e) { console.warn("Failed to set executable permissions:", e); }
} }
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const compat = currentInstance.compatLayer;
const username = await Store.get('legacy_username', ""); const username = await Store.get('legacy_username', "");
const ip = currentInstance.ip; const ip = currentInstance.ip;
const port = currentInstance.port; const port = currentInstance.port;
@ -906,8 +1011,13 @@ async function launchLocalClient() {
const argString = args.map(a => `"${a}"`).join(" "); const argString = args.map(a => `"${a}"`).join(" ");
let cmd = `"${fullPath}" ${argString}`; let cmd = `"${fullPath}" ${argString}`;
if (process.platform === 'linux' || process.platform === 'darwin') { if (process.platform === 'linux' || process.platform === 'darwin') {
let compat = currentInstance.compatLayer;
if (compat === 'custom' && currentInstance.customCompatPath) {
compat = currentInstance.customCompatPath;
}
if (compat === 'wine64' || compat === 'wine') cmd = `${compat} "${fullPath}" ${argString}`; if (compat === 'wine64' || compat === 'wine') cmd = `${compat} "${fullPath}" ${argString}`;
else if (compat.includes('Proton')) { else if (compat.includes('Proton') || compat.includes('/proton') || (currentInstance.compatLayer === 'custom' && currentInstance.customCompatPath)) {
const prefix = path.join(path.dirname(fullPath), 'pfx'); const prefix = path.join(path.dirname(fullPath), 'pfx');
if (!fs.existsSync(prefix)) fs.mkdirSync(prefix, { recursive: true }); if (!fs.existsSync(prefix)) fs.mkdirSync(prefix, { recursive: true });
cmd = `STEAM_COMPAT_CLIENT_INSTALL_PATH="" STEAM_COMPAT_DATA_PATH="${prefix}" "${compat}" run "${fullPath}" ${argString}`; cmd = `STEAM_COMPAT_CLIENT_INSTALL_PATH="" STEAM_COMPAT_DATA_PATH="${prefix}" "${compat}" run "${fullPath}" ${argString}`;
@ -953,6 +1063,13 @@ async function handleElectronFlow(url) {
const parentDir = path.dirname(extractDir); const parentDir = path.dirname(extractDir);
const zipPath = path.join(parentDir, TARGET_FILE); const zipPath = path.join(parentDir, TARGET_FILE);
const backupDir = path.join(parentDir, 'LegacyClient_Backup'); const backupDir = path.join(parentDir, 'LegacyClient_Backup');
// Snapshot before update
if (fs.existsSync(extractDir)) {
updateProgress(0, "Snapshotting Instance...");
await createSnapshot(currentInstance);
}
updateProgress(5, "Downloading " + TARGET_FILE + "..."); updateProgress(5, "Downloading " + TARGET_FILE + "...");
await downloadFile(url, zipPath); await downloadFile(url, zipPath);
updateProgress(75, "Extracting Archive..."); updateProgress(75, "Extracting Archive...");
@ -1108,6 +1225,7 @@ async function saveOptions() {
const ip = document.getElementById('ip-input').value.trim(); const ip = document.getElementById('ip-input').value.trim();
const port = document.getElementById('port-input').value.trim(); const port = document.getElementById('port-input').value.trim();
const isServer = document.getElementById('server-checkbox').checked; const isServer = document.getElementById('server-checkbox').checked;
const customProtonPath = document.getElementById('custom-proton-path').value.trim();
const newInstallPath = document.getElementById('install-path-input').value.trim(); const newInstallPath = document.getElementById('install-path-input').value.trim();
const oldInstallPath = currentInstance.installPath; const oldInstallPath = currentInstance.installPath;
if (newInstallPath && newInstallPath !== oldInstallPath) { if (newInstallPath && newInstallPath !== oldInstallPath) {
@ -1124,7 +1242,10 @@ async function saveOptions() {
if (newRepo) currentInstance.repo = newRepo; if (newRepo) currentInstance.repo = newRepo;
if (newExec) currentInstance.execPath = newExec; if (newExec) currentInstance.execPath = newExec;
currentInstance.ip = ip; currentInstance.port = port; currentInstance.isServer = isServer; currentInstance.ip = ip; currentInstance.port = port; currentInstance.isServer = isServer;
if (compatSelect) currentInstance.compatLayer = compatSelect.value; if (compatSelect) {
currentInstance.compatLayer = compatSelect.value;
currentInstance.customCompatPath = customProtonPath;
}
await saveInstancesToStore(); toggleOptions(false); fetchGitHubData(); updatePlayButtonText(); showToast("Settings Saved"); await saveInstancesToStore(); toggleOptions(false); fetchGitHubData(); updatePlayButtonText(); showToast("Settings Saved");
} }
@ -1149,6 +1270,10 @@ function scanCompatibilityLayers() {
const select = document.getElementById('compat-select'); if (!select) return; const select = document.getElementById('compat-select'); if (!select) return;
const savedValue = currentInstance.compatLayer; const savedValue = currentInstance.compatLayer;
const layers = [{ name: 'Default (Direct)', cmd: 'direct' }, { name: 'Wine64', cmd: 'wine64' }, { name: 'Wine', cmd: 'wine' }]; const layers = [{ name: 'Default (Direct)', cmd: 'direct' }, { name: 'Wine64', cmd: 'wine64' }, { name: 'Wine', cmd: 'wine' }];
// Add custom option
layers.push({ name: 'Custom (Linux)', cmd: 'custom' });
const homeDir = require('os').homedir(); let steamPaths = []; const homeDir = require('os').homedir(); let steamPaths = [];
if (process.platform === 'linux') steamPaths = [path.join(homeDir, '.steam', 'steam', 'steamapps', 'common'), path.join(homeDir, '.local', 'share', 'Steam', 'steamapps', 'common'), path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam', 'steamapps', 'common')]; if (process.platform === 'linux') steamPaths = [path.join(homeDir, '.steam', 'steam', 'steamapps', 'common'), path.join(homeDir, '.local', 'share', 'Steam', 'steamapps', 'common'), path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam', 'steamapps', 'common')];
else if (process.platform === 'darwin') steamPaths = [path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common')]; else if (process.platform === 'darwin') steamPaths = [path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common')];
@ -1158,11 +1283,18 @@ function scanCompatibilityLayers() {
select.innerHTML = ''; select.innerHTML = '';
layers.forEach(l => { const opt = document.createElement('option'); opt.value = l.cmd; opt.textContent = l.name; select.appendChild(opt); if (l.cmd === savedValue) opt.selected = true; }); layers.forEach(l => { const opt = document.createElement('option'); opt.value = l.cmd; opt.textContent = l.name; select.appendChild(opt); if (l.cmd === savedValue) opt.selected = true; });
updateCompatDisplay(); updateCompatDisplay();
const customPathInput = document.getElementById('custom-proton-path');
if (customPathInput) customPathInput.value = currentInstance.customCompatPath || "";
} }
function updateCompatDisplay() { function updateCompatDisplay() {
const select = document.getElementById('compat-select'); const display = document.getElementById('current-compat-display'); const select = document.getElementById('compat-select'); const display = document.getElementById('current-compat-display');
if (select && display && select.selectedIndex !== -1) display.textContent = select.options[select.selectedIndex].text; const customGroup = document.getElementById('custom-proton-group');
if (select && display && select.selectedIndex !== -1) {
display.textContent = select.options[select.selectedIndex].text;
if (customGroup) customGroup.style.display = select.value === 'custom' ? 'block' : 'none';
}
} }
function toggleSidebar() { function toggleSidebar() {
@ -1253,6 +1385,154 @@ async function loadSplashText() {
} }
} }
async function toggleSnapshots(show, id = null) {
const modal = document.getElementById('snapshots-modal');
if (show) {
snapshotInstanceId = id || currentInstanceId;
const inst = instances.find(i => i.id === snapshotInstanceId);
document.getElementById('snapshot-instance-name').textContent = inst ? inst.name : "";
await renderSnapshotsList();
document.activeElement?.blur();
modal.style.display = 'flex';
modal.style.opacity = '1';
} else {
modal.style.opacity = '0';
setTimeout(() => modal.style.display = 'none', 300);
}
}
async function renderSnapshotsList() {
const container = document.getElementById('snapshots-list-container');
container.innerHTML = '';
const inst = instances.find(i => i.id === snapshotInstanceId);
if (!inst || !inst.snapshots || inst.snapshots.length === 0) {
container.innerHTML = '<div class="text-center text-gray-400 py-4">No snapshots found.</div>';
return;
}
inst.snapshots.sort((a,b) => b.timestamp - a.timestamp).forEach((snap) => {
const item = document.createElement('div');
item.className = 'flex justify-between items-center p-3 border-b border-[#333] hover:bg-[#111]';
const date = new Date(snap.timestamp).toLocaleString();
item.innerHTML = `
<div class="flex flex-col">
<span class="text-white text-lg font-bold">${snap.tag || 'Unknown Version'}</span>
<span class="text-gray-400 text-sm">${date}</span>
</div>
<div class="flex gap-2">
<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="rollbackToSnapshot('${snap.id}')">ROLLBACK</div>
<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="deleteSnapshot('${snap.id}')">DELETE</div>
</div>
`;
container.appendChild(item);
});
}
function openSnapshotsManager(id) {
toggleSnapshots(true, id);
}
async function createSnapshotManual() {
const inst = instances.find(i => i.id === snapshotInstanceId);
if (!inst) return;
setProcessingState(true);
updateProgress(0, "Creating Snapshot...");
try {
await createSnapshot(inst);
showToast("Snapshot Created!");
await renderSnapshotsList();
} catch (e) {
showToast("Failed to create snapshot: " + e.message);
}
setProcessingState(false);
}
async function createSnapshot(inst) {
if (!fs.existsSync(inst.installPath)) return;
const snapshotId = 'snap-' + Date.now();
const snapshotsDir = path.join(path.dirname(inst.installPath), 'Snapshots', inst.id);
if (!fs.existsSync(snapshotsDir)) fs.mkdirSync(snapshotsDir, { recursive: true });
const dest = path.join(snapshotsDir, snapshotId);
// Copy entire folder. fs.cpSync is available in modern Node/Electron
fs.cpSync(inst.installPath, dest, { recursive: true });
if (!inst.snapshots) inst.snapshots = [];
inst.snapshots.push({
id: snapshotId,
timestamp: Date.now(),
tag: inst.installedTag || 'Manual Snapshot',
path: dest
});
await saveInstancesToStore();
}
async function rollbackToSnapshot(snapId) {
const inst = instances.find(i => i.id === snapshotInstanceId);
if (!inst) return;
const snap = inst.snapshots.find(s => s.id === snapId);
if (!snap) return;
if (!confirm(`Are you sure you want to ROLLBACK ${inst.name} to the snapshot from ${new Date(snap.timestamp).toLocaleString()}? This will overwrite your current files.`)) return;
setProcessingState(true);
updateProgress(10, "Preparing Rollback...");
try {
if (fs.existsSync(inst.installPath)) {
// Move current to temp just in case
const temp = inst.installPath + "_rollback_temp";
if (fs.existsSync(temp)) fs.rmSync(temp, { recursive: true, force: true });
fs.renameSync(inst.installPath, temp);
}
updateProgress(50, "Restoring Files...");
fs.cpSync(snap.path, inst.installPath, { recursive: true });
inst.installedTag = snap.tag;
await saveInstancesToStore();
// Cleanup temp
const temp = inst.installPath + "_rollback_temp";
if (fs.existsSync(temp)) fs.rmSync(temp, { recursive: true, force: true });
showToast("Rollback Successful!");
if (snapshotInstanceId === currentInstanceId) {
updatePlayButtonText();
if (window.loadMainMenuSkin) window.loadMainMenuSkin();
}
} catch (e) {
showToast("Rollback Failed: " + e.message);
console.error(e);
}
setProcessingState(false);
}
async function deleteSnapshot(snapId) {
const inst = instances.find(i => i.id === snapshotInstanceId);
if (!inst) return;
const snapIndex = inst.snapshots.findIndex(s => s.id === snapId);
if (snapIndex === -1) return;
if (!confirm("Delete this snapshot? (This will free up disk space)")) return;
try {
const snap = inst.snapshots[snapIndex];
if (fs.existsSync(snap.path)) {
fs.rmSync(snap.path, { recursive: true, force: true });
}
inst.snapshots.splice(snapIndex, 1);
await saveInstancesToStore();
renderSnapshotsList();
showToast("Snapshot Deleted");
} catch (e) {
showToast("Error deleting snapshot: " + e.message);
}
}
// Global functions for HTML onclick // Global functions for HTML onclick
window.toggleSidebar = toggleSidebar; window.toggleSidebar = toggleSidebar;
window.minimizeWindow = minimizeWindow; window.minimizeWindow = minimizeWindow;
@ -1280,3 +1560,37 @@ window.saveNewInstance = saveNewInstance;
window.switchInstance = switchInstance; window.switchInstance = switchInstance;
window.deleteInstance = deleteInstance; window.deleteInstance = deleteInstance;
window.toggleAddInstance = toggleAddInstance; window.toggleAddInstance = toggleAddInstance;
window.openSnapshotsManager = openSnapshotsManager;
window.rollbackToSnapshot = rollbackToSnapshot;
window.deleteSnapshot = deleteSnapshot;
window.createSnapshotManual = createSnapshotManual;
window.toggleSnapshots = toggleSnapshots;
// Desktop shortcut for Linux AppImage
function ensureDesktopShortcut() {
if (typeof process === 'undefined' || process.platform !== 'linux') return;
try {
const os = require('os');
const fs = require('fs');
const path = require('path');
const home = os.homedir();
const desktopDir = path.join(home, '.local', 'share', 'applications');
const desktopPath = path.join(desktopDir, 'LegacyLauncher.desktop');
if (fs.existsSync(desktopPath)) return;
const appPath = process.env.APPIMAGE || process.argv[0];
if (!appPath) return;
const content = `[Desktop Entry]
Type=Application
Name=LegacyLauncher
Comment=LegacyLauncher AppImage
Exec="${appPath}" %U
Icon=LegacyLauncher
Terminal=false
Categories=Game;Emulation;`;
fs.mkdirSync(desktopDir, { recursive: true });
fs.writeFileSync(desktopPath, content);
} catch (e) {
console.error('Failed to create desktop shortcut:', e);
}
}
// Ensure shortcut exists on startup
ensureDesktopShortcut();

View file

@ -102,7 +102,10 @@ function initMainMenuSkinViewer() {
async function loadMainMenuSkin() { async function loadMainMenuSkin() {
try { try {
// Ensure install dir is available (currentInstance might not be ready)
const installDir = await window.getInstallDir(); const installDir = await window.getInstallDir();
if (!installDir) return;
const skinPath = path.join(installDir, 'Common', 'res', 'mob', 'char.png'); const skinPath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
if (fs.existsSync(skinPath)) { if (fs.existsSync(skinPath)) {
@ -120,7 +123,7 @@ async function loadMainMenuSkin() {
console.log("No skin found at " + skinPath); console.log("No skin found at " + skinPath);
} }
} catch (e) { } catch (e) {
console.error("Failed to load main menu skin:", e); console.warn("Could not load main menu skin (startup race condition?):", e);
} }
} }