diff --git a/README.md b/README.md index ec59e86..9079e8f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/gdb-keyboard-2.png b/gdb-keyboard-2.png new file mode 100644 index 0000000..da9dd18 Binary files /dev/null and b/gdb-keyboard-2.png differ diff --git a/gdb-switch-2.png b/gdb-switch-2.png new file mode 100644 index 0000000..de6fbcc Binary files /dev/null and b/gdb-switch-2.png differ diff --git a/gdb-xbox-2.png b/gdb-xbox-2.png new file mode 100644 index 0000000..49dcd12 Binary files /dev/null and b/gdb-xbox-2.png differ diff --git a/index.html b/index.html index 2b4f8b5..5740caf 100644 --- a/index.html +++ b/index.html @@ -13,9 +13,9 @@
LegacyLauncher
-
-
-
×
+ + +
@@ -201,6 +201,12 @@
+ +
@@ -254,6 +260,32 @@
+
+ +
+ +
+ + +
+ + +
+
+
diff --git a/renderer.js b/renderer.js index 6c98d06..c52a9c3 100644 --- a/renderer.js +++ b/renderer.js @@ -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; diff --git a/skin_manager.js b/skin_manager.js index 97b1ddf..031a496 100644 --- a/skin_manager.js +++ b/skin_manager.js @@ -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'); diff --git a/style.css b/style.css index 4f073df..8a14551 100644 --- a/style.css +++ b/style.css @@ -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; }