mirror of
https://github.com/gradenGnostic/LegacyLauncher.git
synced 2026-04-23 07:27:28 +00:00
v3.0.0
This commit is contained in:
parent
1937c941ea
commit
7b4942ee7e
32
index.html
32
index.html
|
|
@ -26,6 +26,10 @@
|
|||
</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="sidebar collapsed" id="main-sidebar">
|
||||
<div class="sidebar-title" onclick="toggleSidebar()">
|
||||
|
|
@ -78,7 +82,7 @@
|
|||
CHANGE SKIN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="progress-container" id="progress-container">
|
||||
<div class="progress-text" id="progress-text">Downloading...</div>
|
||||
<div class="progress-bar-bg">
|
||||
|
|
@ -157,6 +161,22 @@
|
|||
</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-box">
|
||||
<div class="modal-title">LAUNCHER OPTIONS</div>
|
||||
|
|
@ -191,6 +211,16 @@
|
|||
</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="mc-input-group">
|
||||
<label class="mc-label">Connect/Bind IP:</label>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "legacylauncher",
|
||||
"version": "2.9.1",
|
||||
"version": "3.0.0",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
424
renderer.js
424
renderer.js
|
|
@ -20,6 +20,8 @@ let currentReleaseIndex = 0;
|
|||
let isProcessing = false;
|
||||
let isGameRunning = false;
|
||||
|
||||
let snapshotInstanceId = null;
|
||||
|
||||
const Store = {
|
||||
async get(key, defaultValue) {
|
||||
const val = await ipcRenderer.invoke('store-get', key);
|
||||
|
|
@ -33,7 +35,6 @@ const Store = {
|
|||
}
|
||||
};
|
||||
|
||||
// Gamepad Controller Support
|
||||
const GamepadManager = {
|
||||
active: false,
|
||||
lastInputTime: 0,
|
||||
|
|
@ -138,7 +139,7 @@ const GamepadManager = {
|
|||
},
|
||||
|
||||
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;
|
||||
for (const id of modals) {
|
||||
const m = document.getElementById(id);
|
||||
|
|
@ -237,11 +238,12 @@ const GamepadManager = {
|
|||
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 === 'skin-modal') closeSkinManager();
|
||||
else if (activeModal.id === 'snapshots-modal') toggleSnapshots(false);
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
const m = document.getElementById(id);
|
||||
if (m && m.style.display === 'flex') return m;
|
||||
|
|
@ -284,10 +286,13 @@ const GamepadManager = {
|
|||
scrollActive(val) {
|
||||
const serverList = document.getElementById('servers-list-container');
|
||||
const instanceList = document.getElementById('instances-list-container');
|
||||
const snapshotList = document.getElementById('snapshots-list-container');
|
||||
if (this.getActiveModal()?.id === 'servers-modal' && serverList) {
|
||||
serverList.scrollTop += val;
|
||||
} else if (this.getActiveModal()?.id === 'instances-modal' && instanceList) {
|
||||
instanceList.scrollTop += val;
|
||||
} else if (this.getActiveModal()?.id === 'snapshots-modal' && snapshotList) {
|
||||
snapshotList.scrollTop += val;
|
||||
} else if (!this.getActiveModal()) {
|
||||
const sidebar = document.getElementById('updates-list')?.parentElement;
|
||||
if (sidebar) sidebar.scrollTop += val;
|
||||
|
|
@ -295,7 +300,6 @@ const GamepadManager = {
|
|||
}
|
||||
};
|
||||
|
||||
// Music Player logic
|
||||
const MusicManager = {
|
||||
audio: new Audio(),
|
||||
playlist: [],
|
||||
|
|
@ -304,12 +308,21 @@ const MusicManager = {
|
|||
|
||||
async init() {
|
||||
this.enabled = await Store.get('legacy_music_enabled', true);
|
||||
this.audio.volume = await Store.get('legacy_music_volume', 0.5);
|
||||
this.updateIcon();
|
||||
this.audio.volume = 1.0;
|
||||
this.audio.onended = () => this.playNext();
|
||||
if (this.enabled) {
|
||||
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() {
|
||||
|
|
@ -430,40 +443,75 @@ async function migrateLegacyConfig() {
|
|||
}
|
||||
|
||||
window.onload = async () => {
|
||||
await migrateLegacyConfig();
|
||||
|
||||
document.getElementById('repo-input').value = currentInstance.repo;
|
||||
document.getElementById('exec-input').value = currentInstance.execPath;
|
||||
document.getElementById('username-input').value = await Store.get('legacy_username', "");
|
||||
document.getElementById('ip-input').value = currentInstance.ip;
|
||||
document.getElementById('port-input').value = currentInstance.port;
|
||||
document.getElementById('server-checkbox').checked = currentInstance.isServer;
|
||||
document.getElementById('install-path-input').value = currentInstance.installPath;
|
||||
|
||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||
document.getElementById('compat-option-container').style.display = 'block';
|
||||
scanCompatibilityLayers();
|
||||
} else {
|
||||
currentInstance.compatLayer = 'direct';
|
||||
await saveInstancesToStore();
|
||||
}
|
||||
try {
|
||||
await migrateLegacyConfig();
|
||||
|
||||
const repoInput = document.getElementById('repo-input');
|
||||
const execInput = document.getElementById('exec-input');
|
||||
const usernameInput = document.getElementById('username-input');
|
||||
const ipInput = document.getElementById('ip-input');
|
||||
const portInput = document.getElementById('port-input');
|
||||
const serverCheck = document.getElementById('server-checkbox');
|
||||
const installInput = document.getElementById('install-path-input');
|
||||
|
||||
ipcRenderer.on('window-is-maximized', (event, isMaximized) => {
|
||||
const btn = document.getElementById('maximize-btn');
|
||||
if (btn) btn.textContent = isMaximized ? '❐' : '▢';
|
||||
});
|
||||
|
||||
fetchGitHubData();
|
||||
checkForLauncherUpdates();
|
||||
loadSplashText();
|
||||
MusicManager.init();
|
||||
GamepadManager.init();
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'F9') {
|
||||
checkForLauncherUpdates(true);
|
||||
if (repoInput) repoInput.value = currentInstance.repo;
|
||||
if (execInput) execInput.value = currentInstance.execPath;
|
||||
if (usernameInput) usernameInput.value = await Store.get('legacy_username', "");
|
||||
if (ipInput) ipInput.value = currentInstance.ip;
|
||||
if (portInput) portInput.value = currentInstance.port;
|
||||
if (serverCheck) serverCheck.checked = currentInstance.isServer;
|
||||
if (installInput) installInput.value = currentInstance.installPath;
|
||||
|
||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||
const compatContainer = document.getElementById('compat-option-container');
|
||||
if (compatContainer) {
|
||||
compatContainer.style.display = 'block';
|
||||
scanCompatibilityLayers();
|
||||
}
|
||||
} 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() {
|
||||
|
|
@ -509,6 +557,7 @@ async function renderInstancesList() {
|
|||
<span class="text-gray-500 text-xs">${inst.installPath}</span>
|
||||
</div>
|
||||
<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>` : ''}
|
||||
<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>
|
||||
|
|
@ -645,6 +694,19 @@ async function updatePlayButtonText() {
|
|||
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];
|
||||
if (!release) {
|
||||
btn.textContent = "PLAY";
|
||||
|
|
@ -704,9 +766,26 @@ async function fetchGitHubData() {
|
|||
const repo = currentInstance.repo;
|
||||
const loader = document.getElementById('loader');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
const offlineInd = document.getElementById('offline-indicator');
|
||||
|
||||
if (loader) loader.style.display = 'flex';
|
||||
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 {
|
||||
const [relRes, commRes] = await Promise.all([
|
||||
fetch(`https://api.github.com/repos/${repo}/releases`),
|
||||
|
|
@ -721,24 +800,27 @@ async function fetchGitHubData() {
|
|||
populateVersions();
|
||||
populateUpdatesSidebar();
|
||||
|
||||
setTimeout(() => {
|
||||
if (loader) {
|
||||
loader.style.opacity = '0';
|
||||
setTimeout(() => loader.style.display = 'none', 300);
|
||||
}
|
||||
}, 500);
|
||||
setTimeout(hideLoader, 500);
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
if (loaderText) loaderText.textContent = "REPO NOT FOUND OR API ERROR";
|
||||
showToast("Check repository name in Options.");
|
||||
setTimeout(() => {
|
||||
if (loader) {
|
||||
loader.style.opacity = '0';
|
||||
setTimeout(() => loader.style.display = 'none', 300);
|
||||
}
|
||||
}, 2500);
|
||||
|
||||
// Even if we fail due to some API error, we should still allow offline play if installed
|
||||
handleOfflineData();
|
||||
|
||||
showToast("Entering Offline Mode.");
|
||||
if (offlineInd) offlineInd.style.display = 'block';
|
||||
setTimeout(hideLoader, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOfflineData() {
|
||||
releasesData = [];
|
||||
commitsData = [];
|
||||
populateVersions();
|
||||
populateUpdatesSidebar();
|
||||
}
|
||||
|
||||
function populateVersions() {
|
||||
const select = document.getElementById('version-select');
|
||||
const display = document.getElementById('current-version-display');
|
||||
|
|
@ -746,7 +828,17 @@ function populateVersions() {
|
|||
select.innerHTML = '';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -798,6 +890,20 @@ function updateSelectedRelease() {
|
|||
|
||||
async function launchGame() {
|
||||
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];
|
||||
if (!release) return;
|
||||
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); }
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const compat = currentInstance.compatLayer;
|
||||
const username = await Store.get('legacy_username', "");
|
||||
const ip = currentInstance.ip;
|
||||
const port = currentInstance.port;
|
||||
|
|
@ -906,8 +1011,13 @@ async function launchLocalClient() {
|
|||
const argString = args.map(a => `"${a}"`).join(" ");
|
||||
let cmd = `"${fullPath}" ${argString}`;
|
||||
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}`;
|
||||
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');
|
||||
if (!fs.existsSync(prefix)) fs.mkdirSync(prefix, { recursive: true });
|
||||
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 zipPath = path.join(parentDir, TARGET_FILE);
|
||||
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 + "...");
|
||||
await downloadFile(url, zipPath);
|
||||
updateProgress(75, "Extracting Archive...");
|
||||
|
|
@ -1108,6 +1225,7 @@ async function saveOptions() {
|
|||
const ip = document.getElementById('ip-input').value.trim();
|
||||
const port = document.getElementById('port-input').value.trim();
|
||||
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 oldInstallPath = currentInstance.installPath;
|
||||
if (newInstallPath && newInstallPath !== oldInstallPath) {
|
||||
|
|
@ -1124,7 +1242,10 @@ async function saveOptions() {
|
|||
if (newRepo) currentInstance.repo = newRepo;
|
||||
if (newExec) currentInstance.execPath = newExec;
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -1149,6 +1270,10 @@ function scanCompatibilityLayers() {
|
|||
const select = document.getElementById('compat-select'); if (!select) return;
|
||||
const savedValue = currentInstance.compatLayer;
|
||||
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 = [];
|
||||
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')];
|
||||
|
|
@ -1158,11 +1283,18 @@ function scanCompatibilityLayers() {
|
|||
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; });
|
||||
updateCompatDisplay();
|
||||
|
||||
const customPathInput = document.getElementById('custom-proton-path');
|
||||
if (customPathInput) customPathInput.value = currentInstance.customCompatPath || "";
|
||||
}
|
||||
|
||||
function updateCompatDisplay() {
|
||||
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() {
|
||||
|
|
@ -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
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
window.minimizeWindow = minimizeWindow;
|
||||
|
|
@ -1280,3 +1560,37 @@ window.saveNewInstance = saveNewInstance;
|
|||
window.switchInstance = switchInstance;
|
||||
window.deleteInstance = deleteInstance;
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -102,7 +102,10 @@ function initMainMenuSkinViewer() {
|
|||
|
||||
async function loadMainMenuSkin() {
|
||||
try {
|
||||
// Ensure install dir is available (currentInstance might not be ready)
|
||||
const installDir = await window.getInstallDir();
|
||||
if (!installDir) return;
|
||||
|
||||
const skinPath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
|
||||
|
||||
if (fs.existsSync(skinPath)) {
|
||||
|
|
@ -120,7 +123,7 @@ async function loadMainMenuSkin() {
|
|||
console.log("No skin found at " + skinPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load main menu skin:", e);
|
||||
console.warn("Could not load main menu skin (startup race condition?):", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue