+
- PATCH NOTES
+
+
Loading updates...
-
-
-
- PROFILE
- OPTIONS
-
-
-
Downloading...
-
-
+
-
+
+
+
+
+ PLAY
+
+
+ Version:
+
+
+
+ Loading...
+
+ ▼
+
+
+
+
+
+
+
+
+ PROFILE
+ OPTIONS
+
+
Downloading...
+
+
+
LAUNCHER OPTIONS
@@ -73,7 +83,7 @@
-
-
+
Default (Direct)
▼
-
DONE
- CANCEL
+ DONE
+ CANCEL
PLAYER PROFILE
@@ -123,13 +132,12 @@
-
SAVE
- CANCEL
+ SAVE
+ CANCEL
UPDATE AVAILABLE
diff --git a/main.js b/main.js
index 114d773..26893af 100644
--- a/main.js
+++ b/main.js
@@ -1,6 +1,5 @@
const { app, BrowserWindow, shell, ipcMain } = require('electron');
const path = require('path');
-const DiscordRPC = require('discord-rpc');
const Store = require('electron-store');
const fs = require('fs');
const https = require('https');
@@ -8,59 +7,6 @@ const extractZip = require('extract-zip');
const { exec } = require('child_process');
const store = new Store();
-const clientId = '1346541144141103114';
-let rpc;
-
-function initRPC() {
- rpc = new DiscordRPC.Client({ transport: 'ipc' });
-
- rpc.on('ready', () => {
- console.log('Discord RPC ready');
- setActivity();
- });
-
- rpc.on('error', (err) => {
- console.error('Discord RPC Error:', err);
- });
-
- rpc.on('disconnected', () => {
- console.log('Discord RPC disconnected, retrying...');
- setTimeout(connectRPC, 15000);
- });
-
- connectRPC();
-}
-
-function connectRPC() {
- rpc.login({ clientId }).catch(err => {
-
- console.log('Discord RPC connection failed, retrying in 20s...');
- setTimeout(connectRPC, 20000);
- });
-}
-
-
-initRPC();
-
-function setActivity(details = 'In Menus', state = 'Ready to Play', startTime = null) {
- if (!rpc || !rpc.user) return;
-
- const activity = {
- details: details,
- state: state,
- largeImageKey: 'logo',
- largeImageText: 'LegacyLauncher',
- instance: false,
- };
-
- if (startTime) {
- activity.startTimestamp = startTime;
- }
-
- rpc.setActivity(activity).catch(() => {
-
- });
-}
function createWindow() {
const win = new BrowserWindow({
@@ -75,7 +21,7 @@ function createWindow() {
transparent: true,
autoHideMenuBar: true,
webPreferences: {
- nodeIntegration: true, // Keeping for now to minimize breakage during refactor, but moving store to main
+ nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
}
@@ -83,7 +29,6 @@ function createWindow() {
win.loadFile('index.html');
- // Handle window controls
ipcMain.on('window-minimize', () => win.minimize());
ipcMain.on('window-maximize', () => {
if (win.isMaximized()) {
@@ -94,18 +39,12 @@ function createWindow() {
});
ipcMain.on('window-close', () => win.close());
- // Store IPC handlers
ipcMain.handle('store-get', (event, key) => store.get(key));
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
- ipcMain.on('update-rpc', (event, data) => {
- setActivity(data.details, data.state, data.startTime);
- });
-
win.on('maximize', () => win.webContents.send('window-is-maximized', true));
win.on('unmaximize', () => win.webContents.send('window-is-maximized', false));
-
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
diff --git a/package-lock.json b/package-lock.json
index 4144eb8..414d5d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,6 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "discord-rpc": "^4.0.1",
"electron-store": "^6.0.1",
"extract-zip": "^2.0.1"
},
@@ -1238,16 +1237,6 @@
],
"license": "MIT"
},
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "file-uri-to-path": "1.0.0"
- }
- },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -2024,19 +2013,6 @@
"node": "*"
}
},
- "node_modules/discord-rpc": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-4.0.1.tgz",
- "integrity": "sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA==",
- "license": "MIT",
- "dependencies": {
- "node-fetch": "^2.6.1",
- "ws": "^7.3.1"
- },
- "optionalDependencies": {
- "register-scheme": "github:devsnek/node-register-scheme"
- }
- },
"node_modules/dmg-builder": {
"version": "26.8.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz",
@@ -2423,6 +2399,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -2612,13 +2589,6 @@
}
}
},
- "node_modules/file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "license": "MIT",
- "optional": true
- },
"node_modules/filelist": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
@@ -3127,7 +3097,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -3780,6 +3750,7 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
+ "dev": true,
"license": "MIT",
"optional": true
},
@@ -3806,26 +3777,6 @@
"node": ">=10"
}
},
- "node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
"node_modules/node-gyp": {
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz",
@@ -4291,17 +4242,6 @@
"node": ">= 6"
}
},
- "node_modules/register-scheme": {
- "version": "0.0.2",
- "resolved": "git+ssh://git@github.com/devsnek/node-register-scheme.git#e7cc9a63a1f512565da44cb57316d9fb10750e17",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "bindings": "^1.3.0",
- "node-addon-api": "^1.3.0"
- }
- },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4433,7 +4373,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/sanitize-filename": {
@@ -4897,12 +4837,6 @@
"tmp": "^0.2.0"
}
},
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
- },
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -5019,22 +4953,6 @@
"defaults": "^1.0.3"
}
},
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "license": "MIT",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/which": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
@@ -5094,27 +5012,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
- "node_modules/ws": {
- "version": "7.5.10",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
- "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8.3.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
diff --git a/package.json b/package.json
index 31a8edf..64273b0 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,14 @@
{
"name": "legacylauncher",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .",
"dist": "electron-builder --linux AppImage",
- "dist:win": "electron-builder --win nsis"
+ "dist:win": "electron-builder --win nsis",
+ "dist:mac": "electron-builder --mac dmg"
},
"build": {
"appId": "com.legacylauncher.app",
@@ -29,6 +30,13 @@
"category": "Game",
"icon": "256x256.png"
},
+ "mac": {
+ "target": [
+ "dmg"
+ ],
+ "icon": "256x256.png",
+ "category": "public.app-category.games"
+ },
"files": [
"**/*",
"!dist/*"
@@ -38,7 +46,6 @@
"author": "",
"license": "ISC",
"dependencies": {
- "discord-rpc": "^4.0.1",
"electron-store": "^6.0.1",
"extract-zip": "^2.0.1"
},
@@ -46,4 +53,4 @@
"electron": "^40.7.0",
"electron-builder": "^26.8.1"
}
-}
+}
\ No newline at end of file
diff --git a/renderer.js b/renderer.js
index 9189bde..054621c 100644
--- a/renderer.js
+++ b/renderer.js
@@ -10,6 +10,7 @@ const DEFAULT_EXEC = "Minecraft.Client.exe";
const TARGET_FILE = "LCEWindows64.zip";
let releasesData = [];
+let commitsData = [];
let currentReleaseIndex = 0;
let isProcessing = false;
let isGameRunning = false;
@@ -33,9 +34,12 @@ window.onload = async () => {
document.getElementById('port-input').value = await Store.get('legacy_port', "");
document.getElementById('server-checkbox').checked = await Store.get('legacy_is_server', false);
- if (process.platform === 'linux') {
+ if (process.platform === 'linux' || process.platform === 'darwin') {
document.getElementById('compat-option-container').style.display = 'block';
scanCompatibilityLayers();
+ } else {
+ // Force Windows to direct mode if somehow changed
+ await Store.set('legacy_compat_layer', 'direct');
}
ipcRenderer.on('window-is-maximized', (event, isMaximized) => {
@@ -43,6 +47,7 @@ window.onload = async () => {
});
fetchGitHubData();
+ GamepadManager.init();
};
async function scanCompatibilityLayers() {
@@ -56,17 +61,26 @@ async function scanCompatibilityLayers() {
];
const homeDir = require('os').homedir();
- const steamPaths = [
- path.join(homeDir, '.steam', 'steam', 'steamapps', 'common'),
- path.join(homeDir, '.local', 'share', 'Steam', 'steamapps', 'common'),
- path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam', 'steamapps', 'common')
- ];
+ let steamPaths = [];
+
+ if (process.platform === 'linux') {
+ steamPaths = [
+ path.join(homeDir, '.steam', 'steam', 'steamapps', 'common'),
+ path.join(homeDir, '.local', 'share', 'Steam', 'steamapps', 'common'),
+ path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam', 'steamapps', 'common')
+ ];
+ } else if (process.platform === 'darwin') {
+ steamPaths = [
+ path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common')
+ ];
+ }
for (const steamPath of steamPaths) {
if (fs.existsSync(steamPath)) {
try {
const dirs = fs.readdirSync(steamPath);
- dirs.filter(d => d.startsWith('Proton')).forEach(d => {
+ dirs.filter(d => d.startsWith('Proton') || d.includes('Wine') || d.includes('CrossOver')).forEach(d => {
+ // Check for common Proton structure
const protonPath = path.join(steamPath, d, 'proton');
if (fs.existsSync(protonPath)) {
layers.push({ name: d, cmd: protonPath });
@@ -99,7 +113,7 @@ function updateCompatDisplay() {
async function getInstalledPath() {
const homeDir = require('os').homedir();
const execPath = await Store.get('legacy_exec_path', DEFAULT_EXEC);
- return path.join(homeDir, 'Downloads', 'LegacyClient', execPath);
+ return path.join(homeDir, 'Documents', 'LegacyClient', execPath);
}
async function checkIsInstalled(tag) {
@@ -138,17 +152,9 @@ async function updatePlayButtonText() {
}
}
-function updateRPC(details, state, startTime = null) {
- ipcRenderer.send('update-rpc', { details, state, startTime });
-}
-
function setGameRunning(running) {
isGameRunning = running;
updatePlayButtonText();
-
- if (!running) {
- updateRPC('In Menus', 'Ready to Play');
- }
}
async function monitorProcess(proc) {
@@ -156,11 +162,6 @@ async function monitorProcess(proc) {
const sessionStart = Date.now();
setGameRunning(true);
- const release = releasesData[currentReleaseIndex];
- const version = release ? release.tag_name : 'Unknown';
- const isServer = await Store.get('legacy_is_server', false);
- updateRPC(`Playing Legacy (${version})`, isServer ? 'Running Headless Server' : 'In Game', sessionStart);
-
proc.on('exit', async () => {
const sessionDuration = Math.floor((Date.now() - sessionStart) / 1000);
const playtime = await Store.get('legacy_playtime', 0);
@@ -193,11 +194,18 @@ async function fetchGitHubData() {
loaderText.textContent = "SYNCING: " + repo;
try {
- const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
- if (!response.ok) throw new Error("Rate Limited");
+ const [relRes, commRes] = await Promise.all([
+ fetch(`https://api.github.com/repos/${repo}/releases`),
+ fetch(`https://api.github.com/repos/${repo}/commits`)
+ ]);
+
+ if (!relRes.ok || !commRes.ok) throw new Error("Rate Limited or API Error");
+
+ releasesData = await relRes.json();
+ commitsData = await commRes.json();
- releasesData = await response.json();
populateVersions();
+ populateUpdatesSidebar();
setTimeout(() => {
loader.style.opacity = '0';
@@ -234,6 +242,35 @@ function populateVersions() {
updatePlayButtonText();
}
+function populateUpdatesSidebar() {
+ const list = document.getElementById('updates-list');
+ list.innerHTML = '';
+
+ if (commitsData.length === 0) {
+ list.innerHTML = 'No recent activity found.
';
+ return;
+ }
+
+ // Show the last 20 commits
+ commitsData.slice(0, 20).forEach((c) => {
+ const item = document.createElement('div');
+ item.className = 'update-item patch-note-card commit-card';
+
+ const date = new Date(c.commit.author.date).toLocaleString();
+ const shortSha = c.sha.substring(0, 7);
+ const message = c.commit.message;
+
+ item.innerHTML = `
+
+ ${date}
+ #${shortSha}
+
+ ${message}
+ `;
+ list.appendChild(item);
+ });
+}
+
function updateSelectedRelease() {
const select = document.getElementById('version-select');
currentReleaseIndex = select.value;
@@ -311,6 +348,48 @@ async function promptUpdate(newTag) {
});
}
+// Manual trigger for checking updates via UI button
+async function checkForUpdatesManual() {
+ // If we have releases data loaded, allow reinstall/update flow regardless of current tag
+ const rel = releasesData[currentReleaseIndex];
+ if (!rel) {
+ showToast("No releases loaded yet");
+ return;
+ }
+
+ const asset = rel.assets.find(a => a.name === TARGET_FILE);
+ if (!asset) {
+ showToast("ZIP Asset missing in this version!");
+ return;
+ }
+
+ const installedTag = await Store.get('installed_version_tag', 'Unknown');
+ // Prompt user to update/install; Update path will reinstall (delete existing LegacyClient)
+ const choice = await promptUpdate(rel.tag_name);
+ if (choice === 'update') {
+ // Delete existing LegacyClient folder if present to ensure clean install
+ try {
+ const extractDir = require('path').join(require('os').homedir(), 'Documents', 'LegacyClient');
+ if (require('fs').existsSync(extractDir)) {
+ require('fs').rmSync(extractDir, { recursive: true, force: true });
+ }
+ } catch (e) {
+ // ignore cleanup errors
+ }
+ // Re-download and install (and launch, as install flow does)
+ setProcessingState(true);
+ await handleElectronFlow(asset.browser_download_url);
+ setProcessingState(false);
+ } else {
+ // User chose to launch existing/older version
+ setProcessingState(true);
+ updateProgress(100, "Launching Existing...");
+ await launchLocalClient();
+ setProcessingState(false);
+ }
+ updatePlayButtonText();
+}
+
async function launchLocalClient() {
const fullPath = await getInstalledPath();
@@ -343,7 +422,7 @@ async function launchLocalClient() {
const argString = args.map(a => `"${a}"`).join(" ");
let cmd = `"${fullPath}" ${argString}`;
- if (process.platform === 'linux') {
+ if (process.platform === 'linux' || process.platform === 'darwin') {
if (compat === 'wine64' || compat === 'wine') {
cmd = `${compat} "${fullPath}" ${argString}`;
} else if (compat.includes('Proton')) {
@@ -396,9 +475,9 @@ function updateProgress(percent, text) {
async function handleElectronFlow(url) {
try {
const homeDir = require('os').homedir();
- const downloadDir = path.join(homeDir, 'Downloads');
- const zipPath = path.join(downloadDir, TARGET_FILE);
- const extractDir = path.join(downloadDir, 'LegacyClient');
+ const docDir = path.join(homeDir, 'Documents');
+ const zipPath = path.join(docDir, TARGET_FILE);
+ const extractDir = path.join(docDir, 'LegacyClient');
updateProgress(5, "Downloading " + TARGET_FILE + "...");
await downloadFile(url, zipPath);
@@ -436,6 +515,11 @@ function downloadFile(url, destPath) {
fs.mkdirSync(dir, { recursive: true });
}
+ // Always re-download by removing any existing file first
+ if (fs.existsSync(destPath)) {
+ try { fs.unlinkSync(destPath); } catch (e) { /* ignore */ }
+ }
+
const file = fs.createWriteStream(destPath);
let totalSize = 0;
let downloadedSize = 0;
@@ -563,3 +647,185 @@ 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/restart.png b/restart.png
new file mode 100644
index 0000000..e3336e1
Binary files /dev/null and b/restart.png differ
diff --git a/style.css b/style.css
index f2dd1d0..11216e7 100644
--- a/style.css
+++ b/style.css
@@ -3,6 +3,20 @@
src: url('Minecraft.ttf') format('truetype');
}
+
+.btn-mini {
+ width: 56px !important;
+ height: 56px;
+ max-width: 56px !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ margin-left: 8px !important;
+}
+.btn-mini img {
+ height: 24px;
+ width: auto;
+}
+
:root {
--mc-gui-bg: #c6c6c6;
--mc-progress-bg: #313131;
@@ -34,7 +48,6 @@ body {
border: 2px solid #000;
}
-/* Title Bar */
.title-bar {
height: 32px;
background: #222;
@@ -81,6 +94,153 @@ body {
background: #c42b1c;
}
+.main-wrapper {
+ display: flex;
+ flex-grow: 1;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+}
+
+.sidebar {
+ width: 380px;
+ background: rgba(0, 0, 0, 0.7);
+ border-right: 4px solid #000;
+ display: flex;
+ flex-direction: column;
+ padding: 30px 20px;
+ overflow-y: auto;
+ z-index: 10;
+ backdrop-filter: blur(8px);
+}
+
+.sidebar-title {
+ font-size: 32px;
+ color: #fff;
+ text-shadow: 2px 2px 0 #000;
+ margin-bottom: 25px;
+ text-align: center;
+ border-bottom: 2px solid #555;
+ padding-bottom: 15px;
+}
+
+.commit-author {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.author-img {
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ border: 1px solid #555;
+}
+
+.commit-sha {
+ font-size: 16px !important;
+ color: #55ff55 !important;
+ font-family: monospace;
+ margin-bottom: 8px !important;
+}
+
+.commit-msg {
+ font-size: 15px !important;
+ color: #eee !important;
+ line-height: 1.4 !important;
+}
+
+#updates-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.update-item.patch-note-card {
+ background-color: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: none;
+ padding: 20px;
+ cursor: default;
+ transition: none;
+ margin-bottom: 20px;
+ border-radius: 4px;
+}
+
+.update-item.patch-note-card:hover {
+ outline: none;
+ background-color: rgba(255, 255, 255, 0.08);
+}
+
+.pn-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.pn-title {
+ color: #fff;
+ font-size: 22px;
+ font-weight: bold;
+ margin-bottom: 12px;
+ text-shadow: 1px 1px 0 #000;
+}
+
+.pn-body {
+ color: #ccc;
+ font-size: 16px;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+.pn-body li {
+ margin-left: 15px;
+ list-style-type: disc;
+}
+
+.pn-h1 { font-size: 20px; color: #fff; margin-top: 10px; }
+.pn-h2 { font-size: 18px; color: #fff; margin-top: 8px; }
+.pn-h3 { font-size: 16px; color: #fff; margin-top: 6px; }
+
+.update-tag {
+ color: #55ff55;
+ font-size: 20px;
+ text-shadow: 1px 1px 0 #000;
+ display: block;
+ margin-bottom: 4px;
+}
+
+.update-name {
+ color: #fff;
+ font-size: 18px;
+ display: block;
+ margin-bottom: 4px;
+}
+
+.update-date {
+ color: #aaa;
+ font-size: 14px;
+}
+
+.sidebar::-webkit-scrollbar {
+ width: 10px;
+}
+
+.sidebar::-webkit-scrollbar-track {
+ background: rgba(0,0,0,0.3);
+}
+
+.sidebar::-webkit-scrollbar-thumb {
+ background: #555;
+ border: 2px solid #000;
+}
+
+.sidebar::-webkit-scrollbar-thumb:hover {
+ background: #666;
+}
+
.content-area {
flex-grow: 1;
background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('minecraft.jpg');
@@ -92,6 +252,7 @@ body {
justify-content: center;
position: relative;
overflow: hidden;
+ height: 100%;
}
.mc-logo-img {
@@ -122,16 +283,20 @@ body {
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa;
margin-bottom: 16px;
position: relative;
- transition: transform 0.05s;
+ transition: transform(0.05s);
}
-.btn-mc:hover:not(.disabled) {
+.btn-mc:hover:not(.disabled), .btn-mc.focused:not(.disabled) {
background-color: var(--mc-button-hover);
color: #fff;
outline: 2px solid #fff;
z-index: 5;
}
+.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);
@@ -207,7 +372,6 @@ body {
right: 0;
}
-/* Progress Bar Styling */
.progress-container {
position: absolute;
bottom: 0;
@@ -253,7 +417,6 @@ body {
text-align: center;
}
-/* Settings Modal */
.modal-overlay {
position: absolute;
inset: 0;
@@ -314,10 +477,14 @@ body {
transition: border-color 0.2s;
}
-.mc-input:focus {
+.mc-input:focus, .mc-input.focused {
border-color: #fff;
}
+#server-checkbox.focused {
+ outline: 2px solid #fff;
+}
+
#loader {
position: absolute;
inset: 0;
@@ -386,7 +553,6 @@ select.hidden-select {
width: 100%;
}
-/* Custom Scrollbar */
::-webkit-scrollbar {
width: 12px;
}