Merge pull request #171 from rubiidev18alt/main

Add Steam Deck UI mode, repo presets, and stronger hover glow (#2)
This commit is contained in:
gardenGnostic 2026-03-12 14:28:09 +01:00 committed by GitHub
commit 3a104398f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 391 additions and 16 deletions

View file

@ -76,7 +76,11 @@ 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)
## Assets
- Controller button sprites: [greatdocbrown](https://greatdocbrown.itch.io/gamepad-ui)
- 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

BIN
gdb-keyboard-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
gdb-switch-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
gdb-xbox-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -13,9 +13,9 @@
<div class="title-bar">
<div class="title-bar-text">LegacyLauncher</div>
<div class="window-controls">
<div class="win-btn" onclick="minimizeWindow()"></div>
<div class="win-btn" id="maximize-btn" onclick="toggleMaximize()"></div>
<div class="win-btn close" onclick="closeWindow()">×</div>
<div class="win-btn nav-item" onclick="minimizeWindow()" tabindex="0" title="Minimize"></div>
<div class="win-btn nav-item" id="maximize-btn" onclick="toggleMaximize()" tabindex="0" title="Maximize / Restore"></div>
<div class="win-btn close nav-item" onclick="closeWindow()" tabindex="0" title="Close">×</div>
</div>
</div>
@ -201,6 +201,12 @@
<div class="mc-input-group">
<label class="mc-label">GitHub Repository Source:</label>
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
<label class="mc-label" style="margin-top: 10px;">Repository Preset:</label>
<select id="repo-preset-select" class="mc-input nav-item" onchange="applyRepoPreset()" tabindex="0" style="height: 48px; margin-top: 8px;">
<option value="custom">Custom</option>
<option value="smartcmd/MinecraftConsoles">smartcmd (default)</option>
<option value="cath0degaytube/MinecraftConsoles">cath0degaytube (no watermark)</option>
</select>
</div>
<div class="mc-input-group">
@ -254,6 +260,32 @@
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="steamdeck-mode-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Use Steam Deck Optimized UI Mode
</label>
</div>
<div class="mc-input-group">
<label class="mc-label">Controller Confirm/Cancel Layout:</label>
<select id="controller-layout-select" class="mc-input nav-item" tabindex="0" style="height: 48px; margin-top: 8px;">
<option value="auto">Auto Detect (Recommended)</option>
<option value="xbox">Xbox Style (A = Confirm, B = Cancel)</option>
<option value="nintendo">Nintendo Style (B = Confirm, A = Cancel)</option>
</select>
<div id="controller-layout-presets" class="controller-layout-presets" role="radiogroup" aria-label="Controller layout presets">
<button type="button" class="controller-layout-preset nav-item" data-layout="xbox" tabindex="0" aria-pressed="false">
<span class="controller-layout-icon controller-layout-icon-xbox" aria-hidden="true"></span>
<span class="controller-layout-text">Xbox Style (A = Confirm, B = Cancel)</span>
</button>
<button type="button" class="controller-layout-preset nav-item" data-layout="nintendo" tabindex="0" aria-pressed="false">
<span class="controller-layout-icon controller-layout-icon-switch" aria-hidden="true"></span>
<span class="controller-layout-text">Nintendo Style (B = Confirm, A = Cancel)</span>
</button>
</div>
</div>
<div class="flex gap-4 w-full mt-4">
<div id="btn-options-done" class="btn-mc flex-grow nav-item" onclick="saveOptions()" tabindex="0">DONE</div>
<div id="btn-options-cancel" class="btn-mc flex-grow nav-item" onclick="toggleOptions(false)" tabindex="0">CANCEL</div>

View file

@ -9,6 +9,10 @@ const DEFAULT_REPO = "smartcmd/MinecraftConsoles";
const DEFAULT_EXEC = "Minecraft.Client.exe";
const TARGET_FILE = "LCEWindows64.zip";
const LAUNCHER_REPO = "gradenGnostic/LegacyLauncher";
const REPO_PRESETS = {
default: 'smartcmd/MinecraftConsoles',
noWatermark: 'cath0degaytube/MinecraftConsoles'
};
let instances = [];
let currentInstanceId = null;
@ -41,6 +45,21 @@ const GamepadManager = {
COOLDOWN: 180,
loopStarted: false,
lastAPressed: false,
lastGuidePressed: false,
controlLayoutMode: 'auto',
setControlLayoutMode(mode = 'auto') {
const validModes = ['auto', 'xbox', 'nintendo'];
this.controlLayoutMode = validModes.includes(mode) ? mode : 'auto';
},
shouldSwapABButtons(gamepadId = '') {
if (this.controlLayoutMode === 'nintendo') return true;
if (this.controlLayoutMode === 'xbox') return false;
const id = gamepadId.toLowerCase();
return id.includes('nintendo switch pro controller') || id.includes(' switch pro controller') || id.includes('nintendo co., ltd');
},
init() {
window.addEventListener("gamepadconnected", () => {
@ -76,6 +95,7 @@ const GamepadManager = {
}
if (!gp) {
this.lastGuidePressed = false;
if (this.active) {
this.active = false;
showToast("Controller Disconnected");
@ -98,6 +118,18 @@ const GamepadManager = {
const isPressed = (idx) => buttons[idx] ? buttons[idx].pressed : false;
const getAxis = (idx) => axes[idx] !== undefined ? axes[idx] : 0;
const shouldSwapAB = this.shouldSwapABButtons(gp.id);
const confirmButton = shouldSwapAB ? 1 : 0;
const cancelButton = shouldSwapAB ? 0 : 1;
const guidePressed = isPressed(16) || isPressed(17);
if (guidePressed && !this.lastGuidePressed) {
showToast("Closing Launcher...");
ipcRenderer.send('window-close');
return;
}
this.lastGuidePressed = guidePressed;
if (now - this.lastInputTime > this.COOLDOWN) {
const threshold = 0.5;
const axisX = getAxis(0);
@ -108,21 +140,22 @@ const GamepadManager = {
const left = isPressed(14) || axisX < -threshold;
const right = isPressed(15) || axisX > threshold;
if (up) { this.navigate('up'); this.lastInputTime = now; }
else if (down) { this.navigate('down'); this.lastInputTime = now; }
else if (left) { this.navigate('left'); this.lastInputTime = now; }
else if (right) { this.navigate('right'); this.lastInputTime = now; }
if (up) { UiSoundManager.setInputSource('controller'); this.navigate('up'); this.lastInputTime = now; }
else if (down) { UiSoundManager.setInputSource('controller'); this.navigate('down'); this.lastInputTime = now; }
else if (left) { UiSoundManager.setInputSource('controller'); this.navigate('left'); this.lastInputTime = now; }
else if (right) { UiSoundManager.setInputSource('controller'); this.navigate('right'); this.lastInputTime = now; }
else if (isPressed(4)) { this.cycleActiveSelection(-1); this.lastInputTime = now; }
else if (isPressed(5)) { this.cycleActiveSelection(1); this.lastInputTime = now; }
else if (isPressed(4)) { UiSoundManager.setInputSource('controller'); this.cycleActiveSelection(-1); this.lastInputTime = now; }
else if (isPressed(5)) { UiSoundManager.setInputSource('controller'); this.cycleActiveSelection(1); this.lastInputTime = now; }
else if (isPressed(1)) { this.cancelCurrent(); this.lastInputTime = now; }
else if (isPressed(cancelButton)) { UiSoundManager.setInputSource('controller'); this.cancelCurrent(); this.lastInputTime = now; }
else if (isPressed(2)) { checkForUpdatesManual(); this.lastInputTime = now; }
else if (isPressed(2)) { UiSoundManager.setInputSource('controller'); checkForUpdatesManual(); this.lastInputTime = now; }
}
const aPressed = isPressed(0);
const aPressed = isPressed(confirmButton);
if (aPressed && !this.lastAPressed) {
UiSoundManager.setInputSource('controller');
this.clickActive();
}
this.lastAPressed = aPressed;
@ -218,7 +251,24 @@ const GamepadManager = {
if (active && active.classList.contains('nav-item')) {
active.classList.add('active-bump');
setTimeout(() => active.classList.remove('active-bump'), 100);
if (active.id === 'version-select-box') {
this.cycleActiveSelection(1);
return;
}
if (active.id === 'classic-version-select-box') {
const classicSelect = document.getElementById('classic-version-select');
if (classicSelect) {
classicSelect.selectedIndex = (classicSelect.selectedIndex + 1) % classicSelect.options.length;
syncVersionFromClassic();
}
return;
}
if (active.id === 'compat-select-box') {
this.cycleActiveSelection(1);
return;
}
if (active.tagName === 'INPUT' && active.type === 'checkbox') {
active.checked = !active.checked;
active.dispatchEvent(new Event('change'));
@ -313,6 +363,15 @@ const UiSoundManager = {
lastPlayedAt: {},
cooldownMs: 70,
lastHoverItem: null,
inputSource: 'mouse',
setInputSource(source) {
this.inputSource = source;
},
shouldPlay() {
return this.inputSource === 'controller';
},
init() {
Object.entries(this.files).forEach(([key, file]) => {
@ -321,6 +380,11 @@ const UiSoundManager = {
this.cache[key].volume = key === 'cursor' ? 0.45 : 0.6;
});
const markMouseInput = () => this.setInputSource('mouse');
['mousemove', 'mousedown', 'touchstart', 'wheel', 'keydown'].forEach((ev) => {
document.addEventListener(ev, markMouseInput, { passive: true });
});
document.addEventListener('focusin', (e) => {
if (e.target?.classList?.contains('nav-item')) this.play('cursor');
});
@ -349,6 +413,7 @@ const UiSoundManager = {
},
play(name) {
if (!this.shouldPlay()) return;
const now = Date.now();
if (this.lastPlayedAt[name] && now - this.lastPlayedAt[name] < this.cooldownMs) return;
this.lastPlayedAt[name] = now;
@ -534,6 +599,70 @@ function focusPrimaryPlayButton() {
setTimeout(() => target.classList.remove('controller-active'), 180);
}
function syncRepoPresetFromInput() {
const presetSelect = document.getElementById('repo-preset-select');
const repoInput = document.getElementById('repo-input');
if (!presetSelect || !repoInput) return;
if (repoInput.value.trim() === REPO_PRESETS.default) presetSelect.value = REPO_PRESETS.default;
else if (repoInput.value.trim() === REPO_PRESETS.noWatermark) presetSelect.value = REPO_PRESETS.noWatermark;
else presetSelect.value = 'custom';
}
function applyRepoPreset() {
const presetSelect = document.getElementById('repo-preset-select');
const repoInput = document.getElementById('repo-input');
if (!presetSelect || !repoInput) return;
if (presetSelect.value === 'custom') return;
repoInput.value = presetSelect.value;
}
function applyControllerLayoutPresetState(layoutMode = 'auto') {
const presets = document.querySelectorAll('.controller-layout-preset');
const activeLayout = layoutMode === 'nintendo' ? 'nintendo' : 'xbox';
presets.forEach((preset) => {
const isActive = preset.dataset.layout === activeLayout;
preset.classList.toggle('active', isActive);
preset.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
}
function initControllerLayoutPresets() {
const layoutSelect = document.getElementById('controller-layout-select');
if (!layoutSelect) return;
const presets = document.querySelectorAll('.controller-layout-preset');
presets.forEach((preset) => {
const pressOn = () => preset.classList.add('is-pressed');
const pressOff = () => preset.classList.remove('is-pressed');
preset.addEventListener('pointerdown', pressOn);
preset.addEventListener('pointerup', pressOff);
preset.addEventListener('pointerleave', pressOff);
preset.addEventListener('blur', pressOff);
preset.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') pressOn();
});
preset.addEventListener('keyup', (event) => {
if (event.key === 'Enter' || event.key === ' ') pressOff();
});
preset.addEventListener('click', () => {
const selectedLayout = preset.dataset.layout || 'xbox';
layoutSelect.value = selectedLayout;
applyControllerLayoutPresetState(selectedLayout);
});
});
layoutSelect.addEventListener('change', () => {
applyControllerLayoutPresetState(layoutSelect.value || 'auto');
});
applyControllerLayoutPresetState(layoutSelect.value || 'auto');
}
window.onload = async () => {
try {
await migrateLegacyConfig();
@ -545,14 +674,21 @@ window.onload = async () => {
const portInput = document.getElementById('port-input');
const serverCheck = document.getElementById('server-checkbox');
const installInput = document.getElementById('install-path-input');
const controllerLayoutSelect = document.getElementById('controller-layout-select');
if (repoInput) repoInput.value = currentInstance.repo;
if (repoInput) {
repoInput.value = currentInstance.repo;
repoInput.addEventListener('input', syncRepoPresetFromInput);
}
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 (controllerLayoutSelect) controllerLayoutSelect.value = await Store.get('legacy_controller_layout_mode', 'auto');
initControllerLayoutPresets();
syncRepoPresetFromInput();
if (process.platform === 'linux' || process.platform === 'darwin') {
const compatContainer = document.getElementById('compat-option-container');
@ -572,6 +708,8 @@ window.onload = async () => {
// Initialize features
await loadTheme();
await loadSteamDeckMode();
await loadControllerLayoutMode();
fetchGitHubData();
checkForLauncherUpdates();
loadSplashText();
@ -731,6 +869,7 @@ async function switchInstance(id) {
await saveInstancesToStore();
document.getElementById('repo-input').value = currentInstance.repo;
syncRepoPresetFromInput();
document.getElementById('exec-input').value = currentInstance.execPath;
document.getElementById('ip-input').value = currentInstance.ip;
document.getElementById('port-input').value = currentInstance.port;
@ -1243,13 +1382,23 @@ function downloadFile(url, destPath) {
});
}
function toggleOptions(show) {
async function toggleOptions(show) {
if (isProcessing) return;
const modal = document.getElementById('options-modal');
if (show) {
// Sync classic theme checkbox to current state
const cb = document.getElementById('classic-theme-checkbox');
if (cb) cb.checked = document.body.classList.contains('classic-theme');
const steamDeckCb = document.getElementById('steamdeck-mode-checkbox');
if (steamDeckCb) steamDeckCb.checked = document.body.classList.contains('steamdeck-mode');
const layoutSelect = document.getElementById('controller-layout-select');
if (layoutSelect) {
const savedLayoutMode = await Store.get('legacy_controller_layout_mode', 'auto');
layoutSelect.value = savedLayoutMode;
GamepadManager.setControlLayoutMode(savedLayoutMode);
applyControllerLayoutPresetState(savedLayoutMode);
}
syncRepoPresetFromInput();
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
UiSoundManager.play('popupOpen');
}
@ -1364,8 +1513,15 @@ async function saveOptions() {
currentInstance.customCompatPath = customProtonPath;
}
const isClassic = document.getElementById('classic-theme-checkbox')?.checked || false;
const isSteamDeckMode = document.getElementById('steamdeck-mode-checkbox')?.checked || false;
const controllerLayoutMode = document.getElementById('controller-layout-select')?.value || 'auto';
await Store.set('legacy_classic_theme', isClassic);
await Store.set('legacy_steamdeck_mode', isSteamDeckMode);
await Store.set('legacy_controller_layout_mode', controllerLayoutMode);
GamepadManager.setControlLayoutMode(controllerLayoutMode);
applyControllerLayoutPresetState(controllerLayoutMode);
applyTheme(isClassic);
applySteamDeckMode(isSteamDeckMode);
await saveInstancesToStore(); toggleOptions(false); fetchGitHubData(); updatePlayButtonText(); showToast("Settings Saved");
}
@ -1596,6 +1752,29 @@ async function loadTheme() {
applyTheme(isClassic);
}
async function loadSteamDeckMode() {
const autoSteamDeck = isSteamDeckEnvironment();
const saved = await Store.get('legacy_steamdeck_mode', null);
const enabled = saved === null ? autoSteamDeck : saved;
const cb = document.getElementById('steamdeck-mode-checkbox');
if (cb) cb.checked = enabled;
applySteamDeckMode(enabled);
}
async function loadControllerLayoutMode() {
const mode = await Store.get('legacy_controller_layout_mode', 'auto');
GamepadManager.setControlLayoutMode(mode);
const select = document.getElementById('controller-layout-select');
if (select) {
select.value = mode;
applyControllerLayoutPresetState(mode);
}
}
function applySteamDeckMode(enabled) {
document.body.classList.toggle('steamdeck-mode', !!enabled);
}
function applyTheme(isClassic) {
document.body.classList.toggle('classic-theme', isClassic);
if (isClassic) {
@ -1806,6 +1985,7 @@ window.checkForUpdatesManual = checkForUpdatesManual;
window.browseInstallDir = browseInstallDir;
window.openGameDir = openGameDir;
window.toggleMusic = toggleMusic;
window.applyRepoPreset = applyRepoPreset;
window.getInstallDir = getInstallDir;
window.showToast = showToast;
window.toggleInstances = toggleInstances;

View file

@ -19,6 +19,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
if (dropZone) {
dropZone.addEventListener('click', () => skinInput?.click());
dropZone.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
skinInput?.click();
}
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-green-500');

152
style.css
View file

@ -94,6 +94,15 @@ body {
background: #c42b1c;
}
.win-btn.nav-item:focus {
transform: scale(1.08);
z-index: 1001;
box-shadow: 0 0 0 1px rgba(255,255,255,0.95), 0 0 8px rgba(255,255,255,0.55) !important;
background: #444;
color: #fff;
}
.main-wrapper {
display: flex;
flex-grow: 1;
@ -364,6 +373,11 @@ body {
filter: brightness(1.08);
}
.nav-item:hover {
outline: 1px solid rgba(255,255,255,0.92) !important;
box-shadow: 0 0 0 1px rgba(255,255,255,0.85), 0 0 12px rgba(255, 255, 255, 0.52), 0 0 24px rgba(90, 170, 255, 0.22) !important;
}
.nav-item.active-bump {
transform: scale(0.95) !important;
transition: transform 0.1s !important;
@ -402,6 +416,58 @@ body {
text-shadow: 2px 2px 0 #000;
}
/* Steam Deck optimized mode */
body.steamdeck-mode .title-bar {
height: 40px;
}
body.steamdeck-mode .title-bar-text {
font-size: 18px;
}
body.steamdeck-mode .btn-mc {
min-height: 68px;
font-size: 28px;
}
body.steamdeck-mode .btn-play {
min-height: 110px;
font-size: 52px;
}
body.steamdeck-mode .version-select-box {
height: 64px;
font-size: 26px;
}
body.steamdeck-mode .mc-input,
body.steamdeck-mode #repo-preset-select,
body.steamdeck-mode .mc-label,
body.steamdeck-mode .classic-link {
font-size: 20px !important;
}
body.steamdeck-mode .nav-item:focus {
transform: scale(1.06);
box-shadow: 0 0 0 2px rgba(255,255,255,0.95), 0 0 22px rgba(255,255,255,0.7), 0 0 34px rgba(90,170,255,0.36) !important;
}
body.steamdeck-mode .controller-layout-preset {
min-height: 62px;
font-size: 22px;
}
body.steamdeck-mode .controller-layout-icon {
transform: scale(1.3);
transform-origin: left center;
margin-right: 4px;
}
body.steamdeck-mode .sidebar {
width: 420px;
}
.version-row {
display: flex;
width: 500px;
@ -617,6 +683,92 @@ body {
border-color: #fff !important;
}
.controller-layout-presets {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
margin-top: 12px;
}
.controller-layout-preset {
width: 100%;
min-height: 52px;
border: 2px solid #555;
background: #0a0a0a;
color: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
padding: 8px 12px;
cursor: pointer;
font-size: 19px;
text-align: left;
}
.controller-layout-preset:hover,
.controller-layout-preset:focus {
border-color: #fff;
}
.controller-layout-preset.active {
border-color: #55ff55;
box-shadow: inset 0 0 0 1px #55ff55;
background: #111;
}
.controller-layout-icon {
--sprite-x: -51px;
--sprite-y: -531px;
--sprite-w: 26px;
--sprite-h: 26px;
--sprite-pressed-x: -51px;
--sprite-pressed-y: -563px;
width: var(--sprite-w);
height: var(--sprite-h);
flex: 0 0 var(--sprite-w);
display: inline-block;
overflow: hidden;
image-rendering: pixelated;
background-repeat: no-repeat;
background-size: 560px 640px;
background-position: var(--sprite-x) var(--sprite-y);
}
.controller-layout-preset:hover .controller-layout-icon,
.controller-layout-preset:focus .controller-layout-icon,
.controller-layout-preset.active .controller-layout-icon,
.controller-layout-preset.is-pressed .controller-layout-icon {
background-position: var(--sprite-pressed-x) var(--sprite-pressed-y);
}
.controller-layout-icon-xbox {
background-image: url('gdb-xbox-2.png');
/* action_button_189 (idle) + action_button_228 (pressed) from spritesheet JSON */
--sprite-x: -51px;
--sprite-y: -531px;
--sprite-w: 26px;
--sprite-h: 26px;
--sprite-pressed-x: -51px;
--sprite-pressed-y: -563px;
}
.controller-layout-icon-switch {
background-image: url('gdb-switch-2.png');
/* Switch sheet uses the same atlas layout coordinates */
--sprite-x: -51px;
--sprite-y: -531px;
--sprite-w: 26px;
--sprite-h: 26px;
--sprite-pressed-x: -51px;
--sprite-pressed-y: -563px;
}
.controller-layout-text {
line-height: 1.2;
}
#server-checkbox.focused {
outline: 2px solid #fff;
}