From 708916d95699f4ffe1964f0489163cc4fbd82a28 Mon Sep 17 00:00:00 2001 From: gardenGnostic Date: Sat, 7 Mar 2026 01:44:51 +0100 Subject: [PATCH] v2.0.0 --- index.html | 69 ++++-- package-lock.json | 4 +- package.json | 2 +- renderer.js | 583 ++++++++++++++++++++++++++++++++-------------- style.css | 33 ++- 5 files changed, 478 insertions(+), 213 deletions(-) diff --git a/index.html b/index.html index 54b5914..e829270 100644 --- a/index.html +++ b/index.html @@ -37,25 +37,26 @@
Minecraft Logo -
PLAY
+
Version: -
+ -
+
-
PROFILE
-
OPTIONS
+ + +
@@ -67,23 +68,55 @@
+ +
@@ -146,8 +179,8 @@ A new version is available. Would you like to update now?
-
UPDATE NOW
-
LATER
+ +
diff --git a/package-lock.json b/package-lock.json index 414d5d5..960cd1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "legacylauncher", - "version": "1.0.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "legacylauncher", - "version": "1.0.0", + "version": "1.1.1", "license": "ISC", "dependencies": { "electron-store": "^6.0.1", diff --git a/package.json b/package.json index c0a5c07..1f3edb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "legacylauncher", - "version": "1.1.1", + "version": "2.0.0", "description": "", "main": "main.js", "scripts": { diff --git a/renderer.js b/renderer.js index c4fa115..1dbb296 100644 --- a/renderer.js +++ b/renderer.js @@ -27,6 +27,272 @@ const Store = { } }; +// Gamepad Controller Support +const GamepadManager = { + active: false, + lastInputTime: 0, + COOLDOWN: 180, + loopStarted: false, + lastAPressed: false, + + init() { + window.addEventListener("gamepadconnected", () => { + if (!this.active) { + this.startLoop(); + } + }); + this.startLoop(); + }, + + startLoop() { + if (this.loopStarted) return; + this.loopStarted = true; + const loop = () => { + try { + this.poll(); + } catch (e) { + console.error("Gamepad poll error:", e); + } + requestAnimationFrame(loop); + }; + loop(); + }, + + poll() { + const gamepads = navigator.getGamepads(); + let gp = null; + for (let i = 0; i < gamepads.length; i++) { + if (gamepads[i] && gamepads[i].connected && gamepads[i].buttons.length > 0) { + gp = gamepads[i]; + break; + } + } + + if (!gp) { + if (this.active) { + this.active = false; + showToast("Controller Disconnected"); + } + return; + } + + if (!this.active) { + this.active = true; + showToast("Controller Connected"); + if (!document.activeElement || !document.activeElement.classList.contains('nav-item')) { + this.focusFirstVisible(); + } + } + + const now = Date.now(); + const buttons = gp.buttons; + const axes = gp.axes; + + // Helper to safely get button state + const isPressed = (idx) => buttons[idx] ? buttons[idx].pressed : false; + const getAxis = (idx) => axes[idx] !== undefined ? axes[idx] : 0; + + if (now - this.lastInputTime > this.COOLDOWN) { + const threshold = 0.5; + const axisX = getAxis(0); + const axisY = getAxis(1); + + // D-pad indices are usually 12, 13, 14, 15 + const up = isPressed(12) || axisY < -threshold; + const down = isPressed(13) || axisY > threshold; + 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; } + + // Shoulder buttons (L1/R1 are 4/5) + else if (isPressed(4)) { this.cycleActiveSelection(-1); this.lastInputTime = now; } + else if (isPressed(5)) { this.cycleActiveSelection(1); this.lastInputTime = now; } + + // B Button (Cancel) + else if (isPressed(1)) { this.cancelCurrent(); this.lastInputTime = now; } + + // X Button (Refresh) + else if (isPressed(2)) { checkForUpdatesManual(); this.lastInputTime = now; } + } + + // A Button (Confirm) - Immediate responsive check + const aPressed = isPressed(0); + if (aPressed && !this.lastAPressed) { + this.clickActive(); + } + this.lastAPressed = aPressed; + + // Right stick for scrolling (Axis 2/3 or 5) + const rStickY = getAxis(3) || getAxis(2) || getAxis(5); + if (Math.abs(rStickY) > 0.1) { + this.scrollActive(rStickY * 15); + } + }, + + focusFirstVisible() { + const visibleItems = this.getVisibleNavItems(); + if (visibleItems.length > 0) visibleItems[0].focus(); + }, + + getVisibleNavItems() { + // Find if any modal is open + const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal']; + let activeModal = null; + for (const id of modals) { + const m = document.getElementById(id); + if (m && m.style.display === 'flex') { + activeModal = m; + break; + } + } + + const allItems = Array.from(document.querySelectorAll('.nav-item')); + return allItems.filter(item => { + if (activeModal) { + return activeModal.contains(item) && item.offsetParent !== null; + } + // Ensure item is not inside any hidden modal + let parent = item.parentElement; + while (parent) { + if (parent.classList?.contains('modal-overlay') && parent.style.display !== 'flex') return false; + parent = parent.parentElement; + } + return item.offsetParent !== null; + }); + }, + + navigate(direction) { + const current = document.activeElement; + const items = this.getVisibleNavItems(); + + if (!items.includes(current)) { + items[0]?.focus(); + return; + } + + const currentRect = current.getBoundingClientRect(); + const cx = currentRect.left + currentRect.width / 2; + const cy = currentRect.top + currentRect.height / 2; + + let bestMatch = null; + let minScore = Infinity; + + items.forEach(item => { + if (item === current) return; + const rect = item.getBoundingClientRect(); + const ix = rect.left + rect.width / 2; + const iy = rect.top + rect.height / 2; + + const dx = ix - cx; + const dy = iy - cy; + const angle = Math.atan2(dy, dx) * 180 / Math.PI; + + let inDirection = false; + if (direction === 'right' && angle >= -45 && angle <= 45) inDirection = true; + if (direction === 'left' && (angle >= 135 || angle <= -135)) inDirection = true; + if (direction === 'down' && angle > 45 && angle < 135) inDirection = true; + if (direction === 'up' && angle < -45 && angle > -135) inDirection = true; + + if (inDirection) { + const distance = Math.sqrt(dx * dx + dy * dy); + const penalty = (direction === 'left' || direction === 'right') ? Math.abs(dy) * 2.5 : Math.abs(dx) * 2.5; + const score = distance + penalty; + + if (score < minScore) { + minScore = score; + bestMatch = item; + } + } + }); + + if (bestMatch) { + bestMatch.focus(); + bestMatch.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }, + + clickActive() { + const active = document.activeElement; + if (active && active.classList.contains('nav-item')) { + active.classList.add('active-bump'); + setTimeout(() => active.classList.remove('active-bump'), 100); + + if (active.tagName === 'INPUT' && active.type === 'checkbox') { + active.checked = !active.checked; + active.dispatchEvent(new Event('change')); + } else { + active.click(); + } + } + }, + + cancelCurrent() { + const activeModal = this.getActiveModal(); + if (activeModal) { + if (activeModal.id === 'options-modal') toggleOptions(false); + else if (activeModal.id === 'profile-modal') toggleProfile(false); + else if (activeModal.id === 'servers-modal') toggleServers(false); + else if (activeModal.id === 'update-modal') document.getElementById('btn-skip-update')?.click(); + } + }, + + getActiveModal() { + const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal']; + for (const id of modals) { + const m = document.getElementById(id); + if (m && m.style.display === 'flex') return m; + } + return null; + }, + + cycleActiveSelection(dir) { + const active = document.activeElement; + if (active && active.id === 'version-select-box') { + const select = document.getElementById('version-select'); + if (select) { + let newIdx = select.selectedIndex + dir; + if (newIdx < 0) newIdx = select.options.length - 1; + if (newIdx >= select.options.length) newIdx = 0; + select.selectedIndex = newIdx; + updateSelectedRelease(); + } + } else if (active && active.id === 'compat-select-box') { + const select = document.getElementById('compat-select'); + if (select) { + let newIdx = select.selectedIndex + dir; + if (newIdx < 0) newIdx = select.options.length - 1; + if (newIdx >= select.options.length) newIdx = 0; + select.selectedIndex = newIdx; + updateCompatDisplay(); + } + } else if (!this.getActiveModal()) { + // Shortcut for version cycle on main menu + const select = document.getElementById('version-select'); + if (select) { + let newIdx = select.selectedIndex + dir; + if (newIdx < 0) newIdx = select.options.length - 1; + if (newIdx >= select.options.length) newIdx = 0; + select.selectedIndex = newIdx; + updateSelectedRelease(); + } + } + }, + + scrollActive(val) { + const serverList = document.getElementById('servers-list-container'); + if (this.getActiveModal()?.id === 'servers-modal' && serverList) { + serverList.scrollTop += val; + } else if (!this.getActiveModal()) { + const sidebar = document.getElementById('updates-list')?.parentElement; + if (sidebar) sidebar.scrollTop += val; + } + } +}; + window.onload = async () => { document.getElementById('repo-input').value = await Store.get('legacy_repo', DEFAULT_REPO); document.getElementById('exec-input').value = await Store.get('legacy_exec_path', DEFAULT_EXEC); @@ -586,6 +852,7 @@ async function handleElectronFlow(url) { 'servers.txt', 'username.txt', 'settings.dat', + 'UID.dat', path.join('Windows64', 'GameHDD') ]; @@ -722,6 +989,138 @@ async function toggleProfile(show) { } } +async function toggleServers(show) { + if (isProcessing) return; + const modal = document.getElementById('servers-modal'); + if (show) { + await loadServers(); + modal.style.display = 'flex'; + modal.style.opacity = '1'; + } else { + modal.style.opacity = '0'; + setTimeout(() => modal.style.display = 'none', 300); + } +} + +async function getServersFilePath() { + const fullPath = await getInstalledPath(); + return path.join(path.dirname(fullPath), 'servers.txt'); +} + +async function loadServers() { + const filePath = await getServersFilePath(); + const container = document.getElementById('servers-list-container'); + container.innerHTML = ''; + + if (!fs.existsSync(filePath)) { + container.innerHTML = '
No servers added yet.
'; + return; + } + + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n').map(l => l.trim()).filter(l => l !== ''); + const servers = []; + + for (let i = 0; i < lines.length; i += 3) { + if (lines[i] && lines[i+1] && lines[i+2]) { + servers.push({ + ip: lines[i], + port: lines[i+1], + name: lines[i+2] + }); + } + } + + if (servers.length === 0) { + container.innerHTML = '
No servers added yet.
'; + return; + } + + servers.forEach((s, index) => { + const item = document.createElement('div'); + item.className = 'flex justify-between items-center p-3 border-b border-[#333] hover:bg-[#111]'; + item.innerHTML = ` +
+ ${s.name} + ${s.ip}:${s.port} +
+
DELETE
+ `; + container.appendChild(item); + }); + } catch (e) { + console.error("Failed to load servers:", e); + container.innerHTML = '
Error loading servers.
'; + } +} + +async function addServer() { + const nameInput = document.getElementById('server-name-input'); + const ipInput = document.getElementById('server-ip-input'); + const portInput = document.getElementById('server-port-input'); + + const name = nameInput.value.trim(); + const ip = ipInput.value.trim(); + const port = portInput.value.trim() || "25565"; + + if (!name || !ip) { + showToast("Name and IP are required!"); + return; + } + + const filePath = await getServersFilePath(); + const serverEntry = `${ip}\n${port}\n${name}\n`; + + try { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + fs.appendFileSync(filePath, serverEntry); + + nameInput.value = ''; + ipInput.value = ''; + portInput.value = ''; + + showToast("Server Added!"); + loadServers(); + } catch (e) { + showToast("Failed to save server: " + e.message); + } +} + +async function removeServer(index) { + const filePath = await getServersFilePath(); + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n').map(l => l.trim()).filter(l => l !== ''); + const servers = []; + + for (let i = 0; i < lines.length; i += 3) { + if (lines[i] && lines[i+1] && lines[i+2]) { + servers.push({ + ip: lines[i], + port: lines[i+1], + name: lines[i+2] + }); + } + } + + servers.splice(index, 1); + + let newContent = ""; + servers.forEach(s => { + newContent += `${s.ip}\n${s.port}\n${s.name}\n`; + }); + + fs.writeFileSync(filePath, newContent); + loadServers(); + showToast("Server Removed"); + } catch (e) { + showToast("Failed to remove server: " + e.message); + } +} + async function updatePlaytimeDisplay() { const el = document.getElementById('playtime-display'); const playtime = await Store.get('legacy_playtime', 0); @@ -785,189 +1184,11 @@ window.closeWindow = closeWindow; window.launchGame = launchGame; window.updateSelectedRelease = updateSelectedRelease; window.toggleProfile = toggleProfile; +window.toggleServers = toggleServers; +window.addServer = addServer; +window.removeServer = removeServer; window.toggleOptions = toggleOptions; window.saveOptions = saveOptions; window.saveProfile = saveProfile; window.updateCompatDisplay = updateCompatDisplay; window.checkForUpdatesManual = checkForUpdatesManual; - -// Gamepad Controller Support -const GamepadManager = { - active: false, - focusedIndex: 0, - currentGroup: 'main', - lastA: false, - lastUp: false, - lastDown: false, - lastLeft: false, - lastRight: false, - groups: { - main: ['btn-play-main', 'version-select-box', 'btn-profile', 'btn-options'], - options: ['repo-input', 'exec-input', 'compat-select-box', 'ip-input', 'port-input', 'server-checkbox', 'btn-options-done', 'btn-options-cancel'], - profile: ['username-input', 'btn-profile-save', 'btn-profile-cancel'], - update: ['btn-confirm-update', 'btn-skip-update'] - }, - - init() { - window.addEventListener("gamepadconnected", (e) => { - console.log("Gamepad connected:", e.gamepad.id); - if (!this.active) { - this.active = true; - this.startLoop(); - showToast("Controller Connected"); - } - }); - - // Check for already connected gamepad - const gamepads = navigator.getGamepads(); - if (gamepads[0]) { - this.active = true; - this.startLoop(); - } - }, - - startLoop() { - const loop = () => { - this.poll(); - requestAnimationFrame(loop); - }; - loop(); - }, - - poll() { - const gamepads = navigator.getGamepads(); - const gp = gamepads[0]; // Use first controller - if (!gp) return; - - // Determine current group based on visible modals - if (document.getElementById('update-modal').style.display === 'flex') { - if (this.currentGroup !== 'update') { this.currentGroup = 'update'; this.focusedIndex = 0; } - } else if (document.getElementById('options-modal').style.display === 'flex') { - if (this.currentGroup !== 'options') { this.currentGroup = 'options'; this.focusedIndex = 0; } - } else if (document.getElementById('profile-modal').style.display === 'flex') { - if (this.currentGroup !== 'profile') { this.currentGroup = 'profile'; this.focusedIndex = 0; } - } else { - if (this.currentGroup !== 'main') { this.currentGroup = 'main'; this.focusedIndex = 0; } - } - - const buttons = gp.buttons; - const axes = gp.axes; - - // A Button (Button 0) - const aPressed = buttons[0].pressed; - if (aPressed && !this.lastA) { - this.clickFocused(); - } - this.lastA = aPressed; - - // Navigation (D-Pad or Left Stick) - const up = buttons[12].pressed || axes[1] < -0.5; - const down = buttons[13].pressed || axes[1] > 0.5; - const left = buttons[14].pressed || axes[0] < -0.5; - const right = buttons[15].pressed || axes[0] > 0.5; - - if (up && !this.lastUp) this.moveFocus(-1); - if (down && !this.lastDown) this.moveFocus(1); - - // Horizontal navigation for side-by-side buttons - if (this.currentGroup === 'main' && this.focusedIndex >= 2) { - if (left && !this.lastLeft) this.moveFocus(-1); - if (right && !this.lastRight) this.moveFocus(1); - } else if (this.currentGroup === 'options' && this.focusedIndex >= 6) { - if (left && !this.lastLeft) this.moveFocus(-1); - if (right && !this.lastRight) this.moveFocus(1); - } else if (this.currentGroup === 'profile' && this.focusedIndex >= 1) { - if (left && !this.lastLeft) this.moveFocus(-1); - if (right && !this.lastRight) this.moveFocus(1); - } - - // Special case: Version selection cycling with Left/Right - if (this.currentGroup === 'main' && this.focusedIndex === 1) { - if (left && !this.lastLeft) this.cycleVersion(-1); - if (right && !this.lastRight) this.cycleVersion(1); - } - - // Special case: Compatibility selection cycling with Left/Right - if (this.currentGroup === 'options' && this.focusedIndex === 2) { - if (left && !this.lastLeft) this.cycleCompat(-1); - if (right && !this.lastRight) this.cycleCompat(1); - } - - this.lastUp = up; - this.lastDown = down; - this.lastLeft = left; - this.lastRight = right; - - this.updateVisualFocus(); - }, - - moveFocus(dir) { - const group = this.groups[this.currentGroup]; - // Skip hidden elements (like compat-select-box on Windows) - let nextIndex = (this.focusedIndex + dir + group.length) % group.length; - let el = document.getElementById(group[nextIndex]); - - // Safety to prevent infinite loop if everything is hidden (unlikely) - let attempts = 0; - while ((!el || el.offsetParent === null) && attempts < group.length) { - nextIndex = (nextIndex + dir + group.length) % group.length; - el = document.getElementById(group[nextIndex]); - attempts++; - } - - this.focusedIndex = nextIndex; - }, - - cycleVersion(dir) { - const select = document.getElementById('version-select'); - if (select && select.options.length > 0) { - let newIndex = select.selectedIndex + dir; - if (newIndex < 0) newIndex = select.options.length - 1; - if (newIndex >= select.options.length) newIndex = 0; - select.selectedIndex = newIndex; - updateSelectedRelease(); - } - }, - - cycleCompat(dir) { - const select = document.getElementById('compat-select'); - if (select && select.options.length > 0) { - let newIndex = select.selectedIndex + dir; - if (newIndex < 0) newIndex = select.options.length - 1; - if (newIndex >= select.options.length) newIndex = 0; - select.selectedIndex = newIndex; - updateCompatDisplay(); - } - }, - - updateVisualFocus() { - const group = this.groups[this.currentGroup]; - group.forEach((id, idx) => { - const el = document.getElementById(id); - if (el) { - if (idx === this.focusedIndex) { - el.classList.add('focused'); - if (el.tagName === 'INPUT') el.focus(); - } else { - el.classList.remove('focused'); - if (el.tagName === 'INPUT') el.blur(); - } - } - }); - }, - - clickFocused() { - const group = this.groups[this.currentGroup]; - const id = group[this.focusedIndex]; - const el = document.getElementById(id); - if (el) { - if (el.tagName === 'INPUT' && el.type === 'checkbox') { - el.checked = !el.checked; - } else { - el.click(); - } - } - } -}; - -GamepadManager.init(); diff --git a/style.css b/style.css index 6430f5a..daddbff 100644 --- a/style.css +++ b/style.css @@ -286,20 +286,31 @@ body { transition: transform(0.05s); } -.btn-mc:hover:not(.disabled), .btn-mc.focused:not(.disabled) { +.nav-item { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + outline: none !important; +} + +.nav-item:focus { + transform: scale(1.05); + z-index: 1000; + box-shadow: 0 0 0 3px #fff, 0 0 20px rgba(255, 255, 255, 0.4) !important; +} + +.nav-item.active-bump { + transform: scale(0.95) !important; + transition: transform 0.1s !important; +} + +.btn-mc:hover:not(.disabled) { background-color: var(--mc-button-hover); color: #fff; - outline: 2px solid #fff; - z-index: 5; + outline: 2px solid #fff !important; } -.version-select-box:hover, .version-select-box.focused { - border-color: #fff; -} - -.btn-mc:active:not(.disabled) { - box-shadow: inset 3px 3px 0px #333, inset -3px -3px 0px #aaa; - transform: translateY(2px); +.btn-mc.controller-active { + transform: translateY(2px) scale(0.98); } .btn-mc.disabled, .btn-mc.running { @@ -509,7 +520,7 @@ body { } .mc-input:focus, .mc-input.focused { - border-color: #fff; + border-color: #fff !important; } #server-checkbox.focused {