mirror of
https://github.com/gradenGnostic/LegacyLauncher.git
synced 2026-05-01 03:21:15 +00:00
v2.0.0
This commit is contained in:
parent
2081965f7a
commit
708916d956
69
index.html
69
index.html
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "legacylauncher",
|
||||
"version": "1.1.1",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
583
renderer.js
583
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 = '<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();
|
||||
|
|
|
|||
33
style.css
33
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue