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 @@
DONE
CANCEL
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;
}