mirror of
https://github.com/gradenGnostic/LegacyLauncher.git
synced 2026-05-08 06:37:19 +00:00
Refine Linux compat scan to better detect Proton-GE runtimes
This commit is contained in:
parent
12b8a503d9
commit
e1a550674a
|
|
@ -76,6 +76,7 @@ The launcher supports several compatibility options for Linux:
|
|||
- **discord-rpc**: Discord Rich Presence integration
|
||||
- **extract-zip**: ZIP archive extraction
|
||||
- **Tailwind CSS**: UI styling (via CDN)
|
||||
- **UI Sounds**: Using the free version of [JDSherbert's Ultimate UI SFX Pack on itch.io](https://jdsherbert.itch.io/ultimate-ui-sfx-pack)
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
|||
6
main.js
6
main.js
|
|
@ -38,6 +38,12 @@ function createWindow() {
|
|||
}
|
||||
});
|
||||
ipcMain.on('window-close', () => win.close());
|
||||
ipcMain.on('window-fullscreen', () => {
|
||||
win.setFullScreen(!win.isFullScreen());
|
||||
});
|
||||
ipcMain.on('window-set-fullscreen', (event, enabled) => {
|
||||
win.setFullScreen(Boolean(enabled));
|
||||
});
|
||||
|
||||
ipcMain.handle('store-get', (event, key) => store.get(key));
|
||||
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
|
||||
|
|
|
|||
208
renderer.js
208
renderer.js
|
|
@ -300,6 +300,73 @@ const GamepadManager = {
|
|||
}
|
||||
};
|
||||
|
||||
const UiSoundManager = {
|
||||
files: {
|
||||
cursor: 'JDSherbert - Ultimate UI SFX Pack - Cursor - 1.mp3',
|
||||
select: 'JDSherbert - Ultimate UI SFX Pack - Select - 1.mp3',
|
||||
cancel: 'JDSherbert - Ultimate UI SFX Pack - Cancel - 1.mp3',
|
||||
popupOpen: 'JDSherbert - Ultimate UI SFX Pack - Popup Open - 1.mp3',
|
||||
popupClose: 'JDSherbert - Ultimate UI SFX Pack - Popup Close - 1.mp3',
|
||||
error: 'JDSherbert - Ultimate UI SFX Pack - Error - 1.mp3'
|
||||
},
|
||||
cache: {},
|
||||
lastPlayedAt: {},
|
||||
cooldownMs: 70,
|
||||
lastHoverItem: null,
|
||||
|
||||
init() {
|
||||
Object.entries(this.files).forEach(([key, file]) => {
|
||||
this.cache[key] = new Audio(file);
|
||||
this.cache[key].preload = 'auto';
|
||||
this.cache[key].volume = key === 'cursor' ? 0.45 : 0.6;
|
||||
});
|
||||
|
||||
document.addEventListener('focusin', (e) => {
|
||||
if (e.target?.classList?.contains('nav-item')) this.play('cursor');
|
||||
});
|
||||
|
||||
document.addEventListener('pointerover', (e) => {
|
||||
const navItem = e.target?.closest?.('.nav-item');
|
||||
if (!navItem || navItem === this.lastHoverItem) return;
|
||||
this.lastHoverItem = navItem;
|
||||
this.play('cursor');
|
||||
});
|
||||
|
||||
document.addEventListener('pointerleave', () => {
|
||||
this.lastHoverItem = null;
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const navItem = e.target?.closest?.('.nav-item');
|
||||
if (!navItem) return;
|
||||
const label = (navItem.textContent || '').trim().toLowerCase();
|
||||
if (label.includes('cancel') || label.includes('close') || label.includes('back') || label.includes('later')) {
|
||||
this.play('cancel');
|
||||
return;
|
||||
}
|
||||
this.play('select');
|
||||
});
|
||||
},
|
||||
|
||||
play(name) {
|
||||
const now = Date.now();
|
||||
if (this.lastPlayedAt[name] && now - this.lastPlayedAt[name] < this.cooldownMs) return;
|
||||
this.lastPlayedAt[name] = now;
|
||||
|
||||
const audio = this.cache[name];
|
||||
if (!audio) return;
|
||||
audio.currentTime = 0;
|
||||
audio.play().catch(() => {});
|
||||
},
|
||||
|
||||
playToast(message) {
|
||||
const normalized = String(message || '').toLowerCase();
|
||||
if (normalized.includes('error') || normalized.includes('failed') || normalized.includes('missing') || normalized.includes('required')) {
|
||||
this.play('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MusicManager = {
|
||||
audio: new Audio(),
|
||||
playlist: [],
|
||||
|
|
@ -442,6 +509,31 @@ async function migrateLegacyConfig() {
|
|||
currentInstance = instances.find(i => i.id === currentInstanceId) || instances[0];
|
||||
}
|
||||
|
||||
function isSteamDeckEnvironment() {
|
||||
if (process.platform !== 'linux') return false;
|
||||
|
||||
const env = process.env || {};
|
||||
if (env.STEAMDECK === '1' || env.SteamDeck === '1') return true;
|
||||
|
||||
try {
|
||||
const osRelease = fs.readFileSync('/etc/os-release', 'utf8').toLowerCase();
|
||||
if (osRelease.includes('steamos') || osRelease.includes('steam deck')) return true;
|
||||
} catch (_) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function focusPrimaryPlayButton() {
|
||||
const classicPlayBtn = document.getElementById('classic-btn-play');
|
||||
const mainPlayBtn = document.getElementById('btn-play-main');
|
||||
const target = (classicPlayBtn && classicPlayBtn.offsetParent !== null) ? classicPlayBtn : mainPlayBtn;
|
||||
if (!target) return;
|
||||
|
||||
target.focus();
|
||||
target.classList.add('controller-active');
|
||||
setTimeout(() => target.classList.remove('controller-active'), 180);
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
try {
|
||||
await migrateLegacyConfig();
|
||||
|
|
@ -485,11 +577,21 @@ window.onload = async () => {
|
|||
loadSplashText();
|
||||
MusicManager.init();
|
||||
GamepadManager.init();
|
||||
UiSoundManager.init();
|
||||
|
||||
if (isSteamDeckEnvironment()) {
|
||||
ipcRenderer.send('window-set-fullscreen', true);
|
||||
setTimeout(() => focusPrimaryPlayButton(), 150);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'F9') {
|
||||
checkForLauncherUpdates(true);
|
||||
}
|
||||
if (e.key === 'F11') {
|
||||
e.preventDefault();
|
||||
ipcRenderer.send('window-fullscreen');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
|
|
@ -528,8 +630,10 @@ async function toggleInstances(show) {
|
|||
document.activeElement?.blur();
|
||||
modal.style.display = 'flex';
|
||||
modal.style.opacity = '1';
|
||||
UiSoundManager.play('popupOpen');
|
||||
} else {
|
||||
modal.style.opacity = '0';
|
||||
UiSoundManager.play('popupClose');
|
||||
setTimeout(() => modal.style.display = 'none', 300);
|
||||
}
|
||||
}
|
||||
|
|
@ -574,8 +678,10 @@ function toggleAddInstance(show) {
|
|||
document.getElementById('new-instance-repo').value = DEFAULT_REPO;
|
||||
modal.style.display = 'flex';
|
||||
modal.style.opacity = '1';
|
||||
UiSoundManager.play('popupOpen');
|
||||
} else {
|
||||
modal.style.opacity = '0';
|
||||
UiSoundManager.play('popupClose');
|
||||
setTimeout(() => modal.style.display = 'none', 300);
|
||||
}
|
||||
}
|
||||
|
|
@ -950,8 +1056,10 @@ async function promptUpdate(newTag) {
|
|||
document.activeElement?.blur();
|
||||
modal.style.display = 'flex';
|
||||
modal.style.opacity = '1';
|
||||
UiSoundManager.play('popupOpen');
|
||||
const cleanup = (result) => {
|
||||
modal.style.opacity = '0';
|
||||
UiSoundManager.play('popupClose');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
if (modalText) modalText.style.display = 'none';
|
||||
|
|
@ -1143,22 +1251,23 @@ function toggleOptions(show) {
|
|||
const cb = document.getElementById('classic-theme-checkbox');
|
||||
if (cb) cb.checked = document.body.classList.contains('classic-theme');
|
||||
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
|
||||
UiSoundManager.play('popupOpen');
|
||||
}
|
||||
else { modal.style.opacity = '0'; setTimeout(() => modal.style.display = 'none', 300); }
|
||||
else { modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => modal.style.display = 'none', 300); }
|
||||
}
|
||||
|
||||
async function toggleProfile(show) {
|
||||
if (isProcessing) return;
|
||||
const modal = document.getElementById('profile-modal');
|
||||
if (show) { await updatePlaytimeDisplay(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; }
|
||||
else { modal.style.opacity = '0'; setTimeout(() => modal.style.display = 'none', 300); }
|
||||
if (show) { await updatePlaytimeDisplay(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; UiSoundManager.play('popupOpen'); }
|
||||
else { modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => modal.style.display = 'none', 300); }
|
||||
}
|
||||
|
||||
async function toggleServers(show) {
|
||||
if (isProcessing) return;
|
||||
const modal = document.getElementById('servers-modal');
|
||||
if (show) { await loadServers(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; }
|
||||
else { modal.style.opacity = '0'; setTimeout(() => modal.style.display = 'none', 300); }
|
||||
if (show) { await loadServers(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; UiSoundManager.play('popupOpen'); }
|
||||
else { modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => modal.style.display = 'none', 300); }
|
||||
}
|
||||
|
||||
async function getServersFilePath() { return path.join(currentInstance.installPath, 'servers.txt'); }
|
||||
|
|
@ -1272,6 +1381,7 @@ async function saveProfile() {
|
|||
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast'); if (!t) return;
|
||||
UiSoundManager.playToast(msg);
|
||||
t.textContent = msg; t.style.display = 'block'; t.style.animation = 'none'; t.offsetHeight; t.style.animation = 'slideUp 0.3s ease-out';
|
||||
setTimeout(() => { t.style.display = 'none'; }, 3000);
|
||||
}
|
||||
|
|
@ -1282,20 +1392,81 @@ 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')];
|
||||
for (const steamPath of steamPaths) {
|
||||
if (fs.existsSync(steamPath)) { try { const dirs = fs.readdirSync(steamPath); dirs.filter(d => d.startsWith('Proton') || d.includes('Wine') || d.includes('CrossOver')).forEach(d => { const protonPath = path.join(steamPath, d, 'proton'); if (fs.existsSync(protonPath)) layers.push({ name: d, cmd: protonPath }); }); } catch (e) {} }
|
||||
|
||||
const seen = new Set(layers.map(l => l.cmd));
|
||||
const foundProtonLayers = [];
|
||||
const addLayer = (name, cmd) => {
|
||||
if (!name || !cmd || seen.has(cmd)) return;
|
||||
seen.add(cmd);
|
||||
foundProtonLayers.push({ name, cmd });
|
||||
};
|
||||
|
||||
const homeDir = require('os').homedir();
|
||||
const protonCandidates = [];
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
const steamRoots = [
|
||||
path.join(homeDir, '.steam', 'steam'),
|
||||
path.join(homeDir, '.local', 'share', 'Steam'),
|
||||
path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam')
|
||||
];
|
||||
|
||||
steamRoots.forEach((root) => {
|
||||
protonCandidates.push(path.join(root, 'steamapps', 'common'));
|
||||
protonCandidates.push(path.join(root, 'compatibilitytools.d'));
|
||||
});
|
||||
} else if (process.platform === 'darwin') {
|
||||
protonCandidates.push(path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common'));
|
||||
protonCandidates.push(path.join(homeDir, 'Library', 'Application Support', 'Steam', 'compatibilitytools.d'));
|
||||
}
|
||||
|
||||
const toolNameMatches = (dir) => {
|
||||
const n = dir.toLowerCase();
|
||||
return n.startsWith('proton') || n.startsWith('ge-proton') || n.includes('proton-ge') || n.includes('wine') || n.includes('crossover') || n.includes('umu-proton');
|
||||
};
|
||||
|
||||
for (const basePath of protonCandidates) {
|
||||
if (!fs.existsSync(basePath)) continue;
|
||||
try {
|
||||
const dirs = fs.readdirSync(basePath);
|
||||
dirs.forEach((dirName) => {
|
||||
if (!toolNameMatches(dirName)) return;
|
||||
const protonPath = path.join(basePath, dirName, 'proton');
|
||||
if (fs.existsSync(protonPath)) addLayer(dirName, protonPath);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Compatibility scan error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
foundProtonLayers.sort((a, b) => {
|
||||
const aGe = /(^|\b)(ge-proton|proton-ge|umu-proton)/i.test(a.name) ? 1 : 0;
|
||||
const bGe = /(^|\b)(ge-proton|proton-ge|umu-proton)/i.test(b.name) ? 1 : 0;
|
||||
if (aGe !== bGe) return bGe - aGe;
|
||||
return b.name.localeCompare(a.name, undefined, { numeric: true, sensitivity: 'base' });
|
||||
});
|
||||
|
||||
layers.push(...foundProtonLayers);
|
||||
|
||||
// Add custom option at end so discovered runtimes are easier to browse first.
|
||||
layers.push({ name: 'Custom (Linux)', cmd: 'custom' });
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
// If no saved compat layer or still on direct, prefer the newest detected GE/Proton on Linux.
|
||||
if (process.platform === 'linux' && (savedValue === 'direct' || !savedValue) && foundProtonLayers.length > 0) {
|
||||
select.value = foundProtonLayers[0].cmd;
|
||||
}
|
||||
|
||||
updateCompatDisplay();
|
||||
|
||||
|
||||
const customPathInput = document.getElementById('custom-proton-path');
|
||||
if (customPathInput) customPathInput.value = currentInstance.customCompatPath || "";
|
||||
}
|
||||
|
|
@ -1363,8 +1534,9 @@ async function promptLauncherUpdate(version, changelog) {
|
|||
modalText.style.display = 'block';
|
||||
}
|
||||
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
|
||||
UiSoundManager.play('popupOpen');
|
||||
const cleanup = (result) => {
|
||||
modal.style.opacity = '0'; setTimeout(() => { modal.style.display = 'none'; if (modalText) modalText.style.display = 'none'; }, 300);
|
||||
modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => { modal.style.display = 'none'; if (modalText) modalText.style.display = 'none'; }, 300);
|
||||
confirmBtn.onclick = null; skipBtn.onclick = null; closeBtn.onclick = null; resolve(result);
|
||||
};
|
||||
confirmBtn.onclick = () => cleanup(true); skipBtn.onclick = () => cleanup(false); closeBtn.onclick = () => cleanup(false);
|
||||
|
|
@ -1475,8 +1647,10 @@ async function toggleSnapshots(show, id = null) {
|
|||
document.activeElement?.blur();
|
||||
modal.style.display = 'flex';
|
||||
modal.style.opacity = '1';
|
||||
UiSoundManager.play('popupOpen');
|
||||
} else {
|
||||
modal.style.opacity = '0';
|
||||
UiSoundManager.play('popupClose');
|
||||
setTimeout(() => modal.style.display = 'none', 300);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
style.css
10
style.css
|
|
@ -348,7 +348,7 @@ body {
|
|||
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
transition: transform(0.05s);
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
|
|
@ -358,9 +358,10 @@ body {
|
|||
}
|
||||
|
||||
.nav-item:focus {
|
||||
transform: scale(1.05);
|
||||
transform: scale(1.04);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 0 0 3px #fff, 0 0 20px rgba(255, 255, 255, 0.4) !important;
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,0.92), 0 0 10px rgba(255, 255, 255, 0.62), 0 0 20px rgba(90, 170, 255, 0.28) !important;
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.nav-item.active-bump {
|
||||
|
|
@ -371,7 +372,8 @@ body {
|
|||
.btn-mc:hover:not(.disabled) {
|
||||
background-color: var(--mc-button-hover);
|
||||
color: #fff;
|
||||
outline: 2px solid #fff !important;
|
||||
outline: 1px solid rgba(255,255,255,0.95) !important;
|
||||
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa, 0 0 12px rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.btn-mc.controller-active {
|
||||
|
|
|
|||
Loading…
Reference in a new issue