This commit is contained in:
gardenGnostic 2026-03-07 01:44:51 +01:00 committed by GitHub
parent 2081965f7a
commit 708916d956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 478 additions and 213 deletions

View file

@ -37,25 +37,26 @@
<div class="content-area">
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="btn-play-main" class="btn-mc btn-play" onclick="launchGame()">PLAY</div>
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
<div class="version-row">
<span class="version-label">Version:</span>
<div class="version-select-box">
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
<span id="current-version-display">Loading...</span>
<div class="select-arrow"></div>
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
<option>Loading...</option>
</select>
</div>
<div id="btn-check-update" class="btn-mc btn-mini" onclick="checkForUpdatesManual()" title="Check for Updates">
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
<img src="restart.png" alt="Update">
</div>
</div>
<div class="flex gap-4 w-[500px] max-w-[90%]">
<div class="btn-mc flex-grow" id="btn-profile" onclick="toggleProfile(true)">PROFILE</div>
<div class="btn-mc flex-grow" id="btn-options" onclick="toggleOptions(true)">OPTIONS</div>
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0">PROFILE</div>
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0">SERVERS</div>
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0">OPTIONS</div>
</div>
<div class="progress-container" id="progress-container">
@ -67,23 +68,55 @@
</div>
</div>
<div class="modal-overlay" id="servers-modal">
<div class="modal-box" style="width: 850px;">
<div class="modal-title">CUSTOM SERVERS</div>
<div id="servers-list-container" class="w-full max-h-[300px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
<!-- Servers will be listed here -->
<div class="text-center text-gray-400 py-4">No servers added yet.</div>
</div>
<div class="grid grid-cols-3 gap-4 w-full mb-4">
<div class="mc-input-group !mb-0">
<label class="mc-label">Name:</label>
<input type="text" id="server-name-input" class="mc-input nav-item" placeholder="My Server" tabindex="0">
</div>
<div class="mc-input-group !mb-0">
<label class="mc-label">IP/Host:</label>
<input type="text" id="server-ip-input" class="mc-input nav-item" placeholder="127.0.0.1" tabindex="0">
</div>
<div class="mc-input-group !mb-0">
<label class="mc-label">Port:</label>
<input type="text" id="server-port-input" class="mc-input nav-item" placeholder="25565" tabindex="0">
</div>
</div>
<div class="btn-mc w-full !mb-6 nav-item" id="btn-add-server" onclick="addServer()" tabindex="0">ADD SERVER</div>
<div class="flex justify-center w-full">
<div id="btn-servers-done" class="btn-mc nav-item" onclick="toggleServers(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="options-modal">
<div class="modal-box">
<div class="modal-title">LAUNCHER OPTIONS</div>
<div class="mc-input-group">
<label class="mc-label">GitHub Repository Source:</label>
<input type="text" id="repo-input" class="mc-input" placeholder="user/repo">
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
</div>
<div class="mc-input-group">
<label class="mc-label">Client Executable Name:</label>
<input type="text" id="exec-input" class="mc-input" placeholder="Minecraft.Client.exe">
<input type="text" id="exec-input" class="mc-input nav-item" placeholder="Minecraft.Client.exe" tabindex="0">
</div>
<div id="compat-option-container" class="mc-input-group" style="display: none;">
<label class="mc-label">Compatibility Layer (Linux):</label>
<div id="compat-select-box" class="version-select-box" style="width: 100%; height: 48px; margin-top: 8px;">
<div id="compat-select-box" class="version-select-box nav-item" style="width: 100%; height: 48px; margin-top: 8px;" tabindex="0">
<span id="current-compat-display">Default (Direct)</span>
<div class="select-arrow" style="height: 44px;"></div>
<select id="compat-select" class="hidden-select" onchange="updateCompatDisplay()">
@ -95,24 +128,24 @@
<div class="grid grid-cols-2 gap-4 w-full mt-2">
<div class="mc-input-group">
<label class="mc-label">Connect/Bind IP:</label>
<input type="text" id="ip-input" class="mc-input" placeholder="Optional">
<input type="text" id="ip-input" class="mc-input nav-item" placeholder="Optional" tabindex="0">
</div>
<div class="mc-input-group">
<label class="mc-label">Port:</label>
<input type="text" id="port-input" class="mc-input" placeholder="Optional">
<input type="text" id="port-input" class="mc-input nav-item" placeholder="Optional" tabindex="0">
</div>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="server-checkbox" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;">
<input type="checkbox" id="server-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Launch as Headless Server (-server)
</label>
</div>
<div class="flex gap-4 w-full mt-4">
<div id="btn-options-done" class="btn-mc flex-grow" onclick="saveOptions()">DONE</div>
<div id="btn-options-cancel" class="btn-mc flex-grow" onclick="toggleOptions(false)">CANCEL</div>
<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>
</div>
</div>
</div>
@ -123,7 +156,7 @@
<div class="mc-input-group">
<label class="mc-label">In-Game Username:</label>
<input type="text" id="username-input" class="mc-input" placeholder="Steve">
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0">
</div>
<div class="mc-input-group mt-4">
@ -132,8 +165,8 @@
</div>
<div class="flex gap-4 w-full mt-4">
<div id="btn-profile-save" class="btn-mc flex-grow" onclick="saveProfile()">SAVE</div>
<div id="btn-profile-cancel" class="btn-mc flex-grow" onclick="toggleProfile(false)">CANCEL</div>
<div id="btn-profile-save" class="btn-mc flex-grow nav-item" onclick="saveProfile()" tabindex="0">SAVE</div>
<div id="btn-profile-cancel" class="btn-mc flex-grow nav-item" onclick="toggleProfile(false)" tabindex="0">CANCEL</div>
</div>
</div>
</div>
@ -146,8 +179,8 @@
A new version is available. Would you like to update now?
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow" id="btn-confirm-update">UPDATE NOW</div>
<div class="btn-mc flex-grow" id="btn-skip-update">LATER</div>
<div class="btn-mc flex-grow nav-item" id="btn-confirm-update">UPDATE NOW</div>
<div class="btn-mc flex-grow nav-item" id="btn-skip-update">LATER</div>
</div>
</div>
</div>

4
package-lock.json generated
View file

@ -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",

View file

@ -1,6 +1,6 @@
{
"name": "legacylauncher",
"version": "1.1.1",
"version": "2.0.0",
"description": "",
"main": "main.js",
"scripts": {

View file

@ -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 = '<div class="text-center text-gray-400 py-4">No servers added yet.</div>';
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 = '<div class="text-center text-gray-400 py-4">No servers added yet.</div>';
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 = `
<div class="flex flex-col">
<span class="text-white text-xl">${s.name}</span>
<span class="text-gray-400 text-sm">${s.ip}:${s.port}</span>
</div>
<div class="btn-mc !w-[100px] !h-[40px] !text-lg !mb-0" onclick="removeServer(${index})">DELETE</div>
`;
container.appendChild(item);
});
} catch (e) {
console.error("Failed to load servers:", e);
container.innerHTML = '<div class="text-center text-red-400 py-4">Error loading servers.</div>';
}
}
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();

View file

@ -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 {