Compare commits

..

36 commits
v3.0.0 ... main

Author SHA1 Message Date
gardenGnostic a42c1a9b0b
Merge pull request #254 from Ratintosh/assets-refactor
Move static assets into assets/ directory
2026-04-17 17:32:10 +02:00
Ratintosh cb4c62aff2 Moved images and audio into assets folder, updated paths accordingly 2026-03-27 01:02:44 -04:00
gardenGnostic e733a250ec
Update README.md 2026-03-16 14:41:09 +01:00
gardenGnostic aa7c257553
Merge pull request #209 from nothingman4ik/feat-fullscreen-new
feat: add fullscreen launch option to new version
2026-03-14 13:30:57 +01:00
nothingman4ik 3e094c6a1d feat: add fullscreen launch option to new version 2026-03-14 14:18:13 +02:00
gardenGnostic 90d40ba2af
Merge pull request #204 from rubiidev18alt/main
Add loading circle + better looking downloading/connecting text
2026-03-14 11:36:43 +01:00
rubiidev18alt c514c981c7
Merge pull request #7 from rubiidev18alt/codex/add-2d-mode-to-skin-renderer
Add 2D/3D toggle for main skin viewer and UI button
2026-03-13 16:50:12 -07:00
rubiidev18alt 4858f83ea1 Keep 2D skin mode fully static 2026-03-13 16:49:42 -07:00
rubiidev18alt 124fb9dbcb
Merge pull request #6 from rubiidev18alt/codex/add-loading-circle-next-to-downloading-text
Add animated loader/progress UI and smarter progress text parsing
2026-03-13 16:28:17 -07:00
rubiidev18alt 8c36195731 Left-align download status row and shrink spinner 2026-03-13 16:27:48 -07:00
gardenGnostic 1c1fbc3d46
Update package.json 2026-03-12 23:10:55 +01:00
gardenGnostic 1e27d63fae
Update release.yml 2026-03-12 23:10:34 +01:00
gardenGnostic 8e05e6b29d
Update release.yml 2026-03-12 23:05:44 +01:00
gardenGnostic 033315526c
v3.5.0 2026-03-12 22:58:49 +01:00
gardenGnostic 1377ece419
Delete JDSherbert - Ultimate UI SFX Pack - Select - 1.mp3 2026-03-12 22:57:23 +01:00
gardenGnostic 38092692a7
Delete JDSherbert - Ultimate UI SFX Pack - Popup Open - 1.mp3 2026-03-12 22:57:15 +01:00
gardenGnostic b54331475a
Delete JDSherbert - Ultimate UI SFX Pack - Popup Close - 1.mp3 2026-03-12 22:57:08 +01:00
gardenGnostic 6cda5ad9ac
Delete JDSherbert - Ultimate UI SFX Pack - Error - 1.mp3 2026-03-12 22:56:59 +01:00
gardenGnostic 9c189b56df
Delete JDSherbert - Ultimate UI SFX Pack - Cursor - 1.mp3 2026-03-12 22:56:51 +01:00
gardenGnostic 32b2909fee
Delete JDSherbert - Ultimate UI SFX Pack - Cancel - 1.mp3 2026-03-12 22:56:45 +01:00
gardenGnostic 4c42fd0e55
Update release.yml 2026-03-12 22:44:58 +01:00
gardenGnostic 3a104398f4
Merge pull request #171 from rubiidev18alt/main
Add Steam Deck UI mode, repo presets, and stronger hover glow (#2)
2026-03-12 14:28:09 +01:00
rubiidev18alt bb721c5ae0
Merge pull request #5 from rubiidev18alt/codex/integrate-controller-button-assets-into-ui
Add controller layout preset UI with interactions and sprite attribution
2026-03-12 00:02:01 -07:00
rubiidev18alt 08c332ecb7 Use exact spritesheet frame coords for controller layout icons 2026-03-11 23:56:53 -07:00
rubiidev18alt 0a06148338
Controller button sprites for ui 2026-03-11 23:01:30 -07:00
rubiidev18alt a8a972a0f6
Merge branch 'gradenGnostic:main' into main 2026-03-11 23:00:58 -07:00
rubiidev18alt 9d3e4fcc57
Add controller layout setting and guide-button quit (#3) 2026-03-11 20:09:16 -07:00
rubiidev18alt 25116b6eba Add controller layout setting and guide-button quit 2026-03-11 20:00:30 -07:00
rubiidev18alt e44695860c
Add Steam Deck UI mode, repo presets, and stronger hover glow (#2) 2026-03-11 19:32:58 -07:00
gardenGnostic 6c59ad20d6
Merge pull request #167 from rubiidev18alt/main
Better accuracy to steam big picture mode
2026-03-12 00:10:16 +01:00
rubiidev18alt 3355244dc4
Merge pull request #1 from rubiidev18alt/codex/integrate-ui-sounds-and-enhance-console-ui
Add UI sound effects, Steam Deck fullscreen support, Proton detection improvements, and UI polish
2026-03-11 15:38:37 -07:00
rubiidev18alt e1a550674a Refine Linux compat scan to better detect Proton-GE runtimes 2026-03-11 15:37:45 -07:00
rubiidev18alt 12b8a503d9
Placeholder sounds
Thanks JDSherbert on itch!
2026-03-11 14:54:20 -07:00
gardenGnostic 3a5b37a8ca
Bump version from 3.0.0 to 3.0.1 2026-03-10 15:56:56 +01:00
gardenGnostic 8f1aec38be
Merge pull request #121 from dtentiion/main
Fix UI overflow, add classic MC launcher UI, text truncation
2026-03-10 10:29:56 +01:00
dtentiion dceef71f87 Fix UI overflow, add classic MC launcher UI, text truncation 2026-03-09 19:46:24 +00:00
19 changed files with 1733 additions and 137 deletions

View file

@ -33,7 +33,6 @@ jobs:
name: linux-dist
path: dist/*.AppImage
if-no-files-found: error
build-windows:
name: Build Windows Installer
runs-on: windows-latest
@ -59,7 +58,6 @@ jobs:
dist/*.exe
dist/*.msi
if-no-files-found: error
build-mac:
name: Build macOS DMG (Apple Silicon)
runs-on: macos-latest
@ -83,10 +81,9 @@ jobs:
name: mac-dist
path: dist/*.dmg
if-no-files-found: error
build-mac-intel:
name: Build macOS DMG (Intel)
runs-on: macos-13
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@ -98,7 +95,7 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build macOS DMG (Intel)
run: npm run dist:mac
run: npm run dist:mac -- --x64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload macOS Intel artifact
@ -107,7 +104,6 @@ jobs:
name: mac-intel-dist
path: dist/*.dmg
if-no-files-found: error
release:
name: Create GitHub Release
needs: [build-linux, build-windows, build-mac, build-mac-intel]

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

View file

@ -4,6 +4,8 @@ A custom launcher for Minecraft Legacy Console Edition.
<img width="1277" height="717" alt="image" src="https://github.com/user-attachments/assets/eaa9bae6-3b3b-4e39-a3c1-156e34abf3cc" />
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/gradengnostic)
## Features
@ -77,6 +79,11 @@ The launcher supports several compatibility options for Linux:
- **extract-zip**: ZIP archive extraction
- **Tailwind CSS**: UI styling (via CDN)
## Assets
- Controller button sprites: [greatdocbrown](https://greatdocbrown.itch.io/gamepad-ui)
- UI Sounds: Using the free version of [JDSherbert's Ultimate UI SFX Pack on itch.io](https://jdsherbert.itch.io/ultimate-ui-sfx-pack)
## Development
The launcher is built with:

View file

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/Click_stereo.ogg.mp3 Normal file

Binary file not shown.

BIN
assets/gdb-keyboard-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/gdb-switch-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/gdb-xbox-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 987 KiB

After

Width:  |  Height:  |  Size: 987 KiB

View file

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LegacyLauncher</title>
<link rel="icon" type="image/png" href="512x512.png">
<link rel="icon" type="image/png" href="assets/512x512.png">
<link rel="stylesheet" href="style.css">
<script src="https://cdn.tailwindcss.com"></script>
</head>
@ -13,16 +13,16 @@
<div class="title-bar">
<div class="title-bar-text">LegacyLauncher</div>
<div class="window-controls">
<div class="win-btn" onclick="minimizeWindow()"></div>
<div class="win-btn" id="maximize-btn" onclick="toggleMaximize()"></div>
<div class="win-btn close" onclick="closeWindow()">×</div>
<div class="win-btn nav-item" onclick="minimizeWindow()" tabindex="0" title="Minimize"></div>
<div class="win-btn nav-item" id="maximize-btn" onclick="toggleMaximize()" tabindex="0" title="Maximize / Restore"></div>
<div class="win-btn close nav-item" onclick="closeWindow()" tabindex="0" title="Close">×</div>
</div>
</div>
<div id="loader">
<div class="loader-content">
<div class="loader-spinner"></div>
<div id="loader-text">CONNECTING...</div>
<div id="loader-text"><span id="loader-text-label">CONNECTING</span><span class="loading-dots animate" id="loader-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span></div>
</div>
</div>
@ -42,11 +42,19 @@
</div>
<div class="content-area" style="flex-direction: row; align-items: stretch; padding: 0;">
<!-- Classic Mode: Logo + Splash (only visible in classic theme) -->
<div class="classic-logo-area" id="classic-logo-area">
<div style="position: relative;">
<img src="assets/minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo" style="margin-bottom: 0; margin-top: 0;">
<div id="classic-splash-text" class="splash-text">Splash!</div>
</div>
</div>
<!-- Left Side: Main Menu -->
<div class="menu-column" style="flex: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; z-index: 5;">
<div class="relative">
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<img src="assets/minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="splash-text" class="splash-text">Splash!</div>
</div>
@ -62,7 +70,7 @@
</select>
</div>
<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">
<img src="assets/restart.png" alt="Update">
</div>
</div>
@ -78,13 +86,19 @@
<div class="skin-column" style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; min-width: 300px; padding-right: 0px;">
<div id="main-skin-viewer" style="width: 100%; height: 500px; cursor: grab; display: flex; align-items: center; justify-content: center;"></div>
<div class="btn-mc nav-item" onclick="openSkinManager()" style="width: 250px; height: 48px; font-size: 20px; margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;" tabindex="0">
CHANGE SKIN
<div class="skin-action-row" style="margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;">
<div class="btn-mc nav-item skin-action-btn skin-action-btn-main" onclick="openSkinManager()" tabindex="0">
CHANGE SKIN
</div>
<div id="btn-skin-render-mode" class="btn-mc nav-item skin-action-btn skin-action-btn-mode" onclick="toggleMainSkinRenderMode()" tabindex="0">3D</div>
</div>
</div>
<div class="progress-container" id="progress-container">
<div class="progress-text" id="progress-text">Downloading...</div>
<div class="progress-text-wrap" id="progress-text-wrap">
<span class="loader-spinner progress-spinner" id="progress-spinner" aria-hidden="true"></span>
<div class="progress-text" id="progress-text"><span id="progress-text-label">Downloading</span><span class="loading-dots" id="progress-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span><span id="progress-text-suffix"></span></div>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progress-bar-fill"></div>
</div>
@ -187,12 +201,21 @@
<input type="text" id="install-path-input" class="mc-input nav-item !mb-0" placeholder="Default: Documents/LegacyClient" tabindex="0">
<div class="btn-mc btn-mini nav-item !w-[60px] !h-[48px]" onclick="browseInstallDir()" tabindex="0">...</div>
</div>
<div class="btn-mc w-full !h-[40px] !text-lg !mb-0 nav-item" onclick="openGameDir()" tabindex="0">OPEN FOLDER</div>
<div class="flex gap-2">
<div class="btn-mc flex-grow !h-[40px] !text-lg !mb-0 nav-item" onclick="openGameDir()" tabindex="0">GAME FOLDER</div>
<div class="btn-mc flex-grow !h-[40px] !text-lg !mb-0 nav-item" onclick="openScreenshotsGallery()" tabindex="0">SCREENSHOTS</div>
</div>
</div>
<div class="mc-input-group">
<label class="mc-label">GitHub Repository Source:</label>
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
<label class="mc-label" style="margin-top: 10px;">Repository Preset:</label>
<select id="repo-preset-select" class="mc-input nav-item" onchange="applyRepoPreset()" tabindex="0" style="height: 48px; margin-top: 8px;">
<option value="custom">Custom</option>
<option value="smartcmd/MinecraftConsoles">smartcmd (default)</option>
<option value="cath0degaytube/MinecraftConsoles">cath0degaytube (no watermark)</option>
</select>
</div>
<div class="mc-input-group">
@ -218,7 +241,10 @@
<div class="mc-input-group">
<label class="mc-label">Launcher Music Volume:</label>
<input type="range" id="volume-slider" min="0" max="1" step="0.05" value="1" class="nav-item w-full" tabindex="0">
<div class="mc-slider-container">
<input type="range" id="volume-slider" min="0" max="1" step="0.01" value="1" class="mc-slider nav-item w-full" tabindex="0">
<div id="volume-percent" class="mc-slider-percent">100%</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 w-full mt-2">
@ -232,6 +258,13 @@
</div>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="fullscreen-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Launch in Fullscreen (-fullscreen)
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="server-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
@ -239,6 +272,39 @@
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="classic-theme-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Use Classic Minecraft Launcher UI
</label>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="steamdeck-mode-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
Use Steam Deck Optimized UI Mode
</label>
</div>
<div class="mc-input-group">
<label class="mc-label">Controller Confirm/Cancel Layout:</label>
<select id="controller-layout-select" class="mc-input nav-item" tabindex="0" style="height: 48px; margin-top: 8px;">
<option value="auto">Auto Detect (Recommended)</option>
<option value="xbox">Xbox Style (A = Confirm, B = Cancel)</option>
<option value="nintendo">Nintendo Style (B = Confirm, A = Cancel)</option>
</select>
<div id="controller-layout-presets" class="controller-layout-presets" role="radiogroup" aria-label="Controller layout presets">
<button type="button" class="controller-layout-preset nav-item" data-layout="xbox" tabindex="0" aria-pressed="false">
<span class="controller-layout-icon controller-layout-icon-xbox" aria-hidden="true"></span>
<span class="controller-layout-text">Xbox Style (A = Confirm, B = Cancel)</span>
</button>
<button type="button" class="controller-layout-preset nav-item" data-layout="nintendo" tabindex="0" aria-pressed="false">
<span class="controller-layout-icon controller-layout-icon-switch" aria-hidden="true"></span>
<span class="controller-layout-text">Nintendo Style (B = Confirm, A = Cancel)</span>
</button>
</div>
</div>
<div class="flex gap-4 w-full mt-4">
<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>
@ -246,6 +312,21 @@
</div>
</div>
<div class="modal-overlay" id="gallery-modal">
<div class="modal-box" style="max-width: 1000px; width: 85vw; height: 85vh;">
<div class="modal-title">SCREENSHOTS GALLERY</div>
<div id="gallery-container" class="w-full flex-grow overflow-y-auto mb-6 border-2 border-[#555] bg-black p-4 grid grid-cols-2 md:grid-cols-3 gap-4 auto-rows-max">
<!-- Screenshots will be dynamically loaded here -->
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" onclick="openScreenshotsDir()" tabindex="0">OPEN FOLDER</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleGallery(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="profile-modal">
<div class="modal-box">
<div class="modal-title">PLAYER PROFILE</div>
@ -330,6 +411,47 @@
</div>
</div>
<!-- Classic Launcher Bottom Bar (only shown when classic theme is active) -->
<div id="classic-bottom-bar" class="classic-bottom-bar">
<!-- Left: Profile Section -->
<div class="classic-profile-section">
<div class="classic-avatar" id="classic-avatar">S</div>
<div class="classic-profile-info">
<div class="classic-username-label">Logged in as</div>
<div class="classic-username-display" id="classic-username-display">Player</div>
</div>
<div class="classic-mini-btn" onclick="toggleProfile(true)" title="Edit Profile">Edit Profile</div>
<div class="classic-mini-btn" onclick="openSkinManager()" title="Change Skin">Skin</div>
</div>
<!-- Center: Play Button -->
<div class="classic-center-section">
<div class="btn-mc btn-play classic-play-btn nav-item" id="classic-btn-play" onclick="launchGame()" tabindex="0">PLAY</div>
</div>
<!-- Right: Version + Nav -->
<div class="classic-right-section">
<div class="classic-version-row">
<span class="version-label" style="font-size: 16px; white-space: nowrap;">Version:</span>
<div id="classic-version-select-box" class="version-select-box nav-item" tabindex="0" style="height: 40px; font-size: 16px; flex-grow: 1; padding-right: 51px;">
<span id="classic-version-display">Loading...</span>
<div class="select-arrow" style="width: 36px;"></div>
<select id="classic-version-select" class="hidden-select" onchange="syncVersionFromClassic()">
<option>Loading...</option>
</select>
</div>
<div class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0" style="width: 40px !important; height: 40px; max-width: 40px !important;">
<img src="restart.png" alt="Update">
</div>
</div>
<div class="classic-nav-links">
<span class="classic-link nav-item" onclick="toggleInstances(true)" tabindex="0">Instances</span>
<span class="classic-link nav-item" onclick="toggleServers(true)" tabindex="0">Servers</span>
<span class="classic-link nav-item" onclick="toggleOptions(true)" tabindex="0">Options</span>
</div>
</div>
</div>
<div id="toast">NOTIFICATION</div>
<div id="music-toggle" class="music-btn nav-item" onclick="toggleMusic()" title="Toggle Music" tabindex="0">

123
main.js
View file

@ -1,4 +1,4 @@
const { app, BrowserWindow, shell, ipcMain, dialog } = require('electron');
const { app, BrowserWindow, shell, ipcMain, dialog, globalShortcut, desktopCapturer } = require('electron');
const path = require('path');
const Store = require('electron-store');
const fs = require('fs');
@ -7,9 +7,11 @@ const extractZip = require('extract-zip');
const { exec } = require('child_process');
const store = new Store();
let mainWindow = null;
let isGameRunning = false;
function createWindow() {
const win = new BrowserWindow({
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
minWidth: 1024,
@ -27,17 +29,116 @@ function createWindow() {
}
});
win.loadFile('index.html');
mainWindow.loadFile('index.html');
ipcMain.on('window-minimize', () => win.minimize());
ipcMain.on('window-minimize', () => mainWindow.minimize());
ipcMain.on('window-maximize', () => {
if (win.isMaximized()) {
win.unmaximize();
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
win.maximize();
mainWindow.maximize();
}
});
ipcMain.on('window-close', () => win.close());
ipcMain.on('window-close', () => mainWindow.close());
ipcMain.on('window-fullscreen', () => {
mainWindow.setFullScreen(!mainWindow.isFullScreen());
});
ipcMain.on('window-set-fullscreen', (event, enabled) => {
mainWindow.setFullScreen(Boolean(enabled));
});
ipcMain.handle('take-screenshot', async (event) => {
try {
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `screenshot-${timestamp}.png`;
const filePath = path.join(screenshotsDir, fileName);
if (isGameRunning) {
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 3840, height: 2160 } // High res for screenshot
});
// Find Minecraft window or fallback to primary screen
const source = sources.find(s => s.name.toLowerCase().includes('minecraft')) ||
sources.find(s => s.id.startsWith('screen'));
if (source) {
fs.writeFileSync(filePath, source.thumbnail.toPNG());
return filePath;
}
}
// Fallback to launcher capture if game isn't running or not found
const win = BrowserWindow.fromWebContents(event.sender) || mainWindow;
if (!win) throw new Error("Window not found");
const image = await win.capturePage();
fs.writeFileSync(filePath, image.toPNG());
return filePath;
} catch (err) {
console.error("Screenshot capture error:", err);
throw err;
}
});
ipcMain.on('game-running-state', (event, running) => {
isGameRunning = running;
if (running) {
globalShortcut.register('F2', () => {
if (mainWindow) {
mainWindow.webContents.send('trigger-screenshot');
}
});
} else {
globalShortcut.unregister('F2');
}
});
ipcMain.handle('list-screenshots', async () => {
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
if (!fs.existsSync(screenshotsDir)) {
return [];
}
const files = fs.readdirSync(screenshotsDir);
return files
.filter(f => f.toLowerCase().endsWith('.png'))
.sort((a, b) => {
try {
// Extract timestamp from 'screenshot-YYYY-MM-DDTHH-mm-ss-SSSZ.png'
const timeA = a.replace('screenshot-', '').replace('.png', '').replace(/-/g, ':');
const timeB = b.replace('screenshot-', '').replace('.png', '').replace(/-/g, ':');
return new Date(timeB) - new Date(timeA);
} catch (e) {
return 0;
}
})
.map(f => ({
name: f,
path: path.join(screenshotsDir, f),
url: `file://${path.join(screenshotsDir, f)}`
}));
});
ipcMain.handle('delete-screenshot', async (event, fileName) => {
const filePath = path.join(app.getPath('userData'), 'Screenshots', fileName);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
});
ipcMain.handle('open-screenshots-dir', async () => {
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
shell.openPath(screenshotsDir);
});
ipcMain.handle('store-get', (event, key) => store.get(key));
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
@ -49,10 +150,10 @@ function createWindow() {
return result.filePaths[0];
});
win.on('maximize', () => win.webContents.send('window-is-maximized', true));
win.on('unmaximize', () => win.webContents.send('window-is-maximized', false));
mainWindow.on('maximize', () => mainWindow.webContents.send('window-is-maximized', true));
mainWindow.on('unmaximize', () => mainWindow.webContents.send('window-is-maximized', false));
win.webContents.setWindowOpenHandler(({ url }) => {
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});

10
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "legacylauncher",
"version": "2.2.0",
"version": "3.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "legacylauncher",
"version": "2.2.0",
"version": "3.5.0",
"license": "ISC",
"dependencies": {
"electron-store": "^6.0.1",
@ -4690,9 +4690,9 @@
}
},
"node_modules/tar": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {

View file

@ -1,7 +1,7 @@
{
"name": "legacylauncher",
"version": "3.0.0",
"description": "",
"version": "3.5.0",
"description": "A Minecraft: Legacy Console Edition launcher",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -30,7 +30,17 @@
"flatpak"
],
"category": "Game",
"icon": "512x512.png"
"icon": "512x512.png",
"desktop": {
"entry": {
"Name": "Minecraft LCE Launcher",
"GenericName": "Minecraft Launcher",
"Comment": "A Minecraft: Legacy Console Edition launcher",
"Categories": "Game;Emulation;",
"StartupNotify": "true"
}
},
"executableName": "legacylauncher"
},
"mac": {
"target": [

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
let isMainSkinDragging = false;
let mainMenuSkinRenderMode = '3d';
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
let isSkinDragging = false;
@ -19,6 +20,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
if (dropZone) {
dropZone.addEventListener('click', () => skinInput?.click());
dropZone.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
skinInput?.click();
}
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-green-500');
@ -68,12 +76,13 @@ function initMainMenuSkinViewer() {
// Interaction for Main Menu Viewer
let prevX = 0;
container.addEventListener('mousedown', (e) => {
if (mainMenuSkinRenderMode !== '3d') return;
isMainSkinDragging = true;
prevX = e.clientX;
});
window.addEventListener('mouseup', () => isMainSkinDragging = false);
window.addEventListener('mousemove', (e) => {
if (isMainSkinDragging && mainMenuPlayerGroup) {
if (mainMenuSkinRenderMode === '3d' && isMainSkinDragging && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
prevX = e.clientX;
}
@ -82,7 +91,9 @@ function initMainMenuSkinViewer() {
// Auto-rotate slowly
function animateMain() {
requestAnimationFrame(animateMain);
if (!isMainSkinDragging && mainMenuPlayerGroup) mainMenuPlayerGroup.rotation.y += 0.005;
if (mainMenuSkinRenderMode === '3d' && !isMainSkinDragging && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y += 0.005;
}
if (mainMenuRenderer && mainMenuScene && mainMenuCamera) mainMenuRenderer.render(mainMenuScene, mainMenuCamera);
}
animateMain();
@ -116,7 +127,7 @@ async function loadMainMenuSkin() {
const img = new Image();
img.onload = () => {
const isLegacy = img.height === 32;
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup);
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup, mainMenuSkinRenderMode);
};
img.src = url;
} else {
@ -127,7 +138,25 @@ async function loadMainMenuSkin() {
}
}
function updateSkinModel(dataUrl, isLegacy, targetGroup) {
function toggleMainSkinRenderMode() {
mainMenuSkinRenderMode = mainMenuSkinRenderMode === '3d' ? '2d' : '3d';
const modeButton = document.getElementById('btn-skin-render-mode');
if (modeButton) modeButton.textContent = mainMenuSkinRenderMode.toUpperCase();
const container = document.getElementById('main-skin-viewer');
if (container) {
container.style.cursor = mainMenuSkinRenderMode === '3d' ? 'grab' : 'default';
}
isMainSkinDragging = false;
if (mainMenuSkinRenderMode === '2d' && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y = 0;
}
loadMainMenuSkin();
}
function updateSkinModel(dataUrl, isLegacy, targetGroup, renderMode = '3d') {
if (!targetGroup) return;
new THREE.TextureLoader().load(dataUrl, (texture) => {
@ -171,6 +200,50 @@ function updateSkinModel(dataUrl, isLegacy, targetGroup) {
left: [x+8, y+4, 4, 12], back: [x+12, y+4, 4, 12]
});
const create2DPart = (tex, uvFront, width, height, x, y, scale = 1) => {
const texWidth = tex.image.width;
const texHeight = tex.image.height;
const partTex = tex.clone();
partTex.magFilter = THREE.NearestFilter;
partTex.minFilter = THREE.NearestFilter;
partTex.repeat.set(uvFront[2] / texWidth, uvFront[3] / texHeight);
partTex.offset.set(uvFront[0] / texWidth, 1 - (uvFront[1] + uvFront[3]) / texHeight);
partTex.needsUpdate = true;
const geometry = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({ map: partTex, transparent: true, alphaTest: 0.5, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0);
mesh.scale.set(scale, scale, 1);
return mesh;
};
if (renderMode === '2d') {
const frontParts = [
create2DPart(texture, [8, 8, 8, 8], 8, 8, 0, 10),
create2DPart(texture, [40, 8, 8, 8], 8, 8, 0, 10, 1.12),
create2DPart(texture, [20, 20, 8, 12], 8, 12, 0, 0),
create2DPart(texture, [44, 20, 4, 12], 4, 12, -6, 0),
create2DPart(texture, isLegacy ? [44, 20, 4, 12] : [36, 52, 4, 12], 4, 12, 6, 0),
create2DPart(texture, [4, 20, 4, 12], 4, 12, -2, -12),
create2DPart(texture, isLegacy ? [4, 20, 4, 12] : [20, 52, 4, 12], 4, 12, 2, -12)
];
if (!isLegacy) {
frontParts.push(
create2DPart(texture, [20, 36, 8, 12], 8, 12, 0, 0, 1.05),
create2DPart(texture, [44, 36, 4, 12], 4, 12, -6, 0, 1.05),
create2DPart(texture, [52, 52, 4, 12], 4, 12, 6, 0, 1.05),
create2DPart(texture, [4, 36, 4, 12], 4, 12, -2, -12, 1.05),
create2DPart(texture, [4, 52, 4, 12], 4, 12, 2, -12, 1.05)
);
}
frontParts.forEach((part) => targetGroup.add(part));
targetGroup.rotation.y = 0;
return;
}
// Head
const headUvs = { top: [8, 0, 8, 8], bottom: [16, 0, 8, 8], right: [0, 8, 8, 8], left: [16, 8, 8, 8], front: [8, 8, 8, 8], back: [24, 8, 8, 8] };
const head = createBodyPart(8, 8, 8, texture, headUvs);
@ -211,6 +284,8 @@ function updateSkinModel(dataUrl, isLegacy, targetGroup) {
targetGroup.add(layer);
}
});
targetGroup.rotation.y = 0;
});
}
@ -311,7 +386,7 @@ function processSkinImage(img, srcUrl, isInitialLoad = false) {
}
if (!skinScene) initPreviewEngine();
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup);
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup, '3d');
}
function initPreviewEngine() {
@ -391,3 +466,4 @@ async function saveSkinToDisk() {
window.openSkinManager = openSkinManager;
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
window.loadMainMenuSkin = loadMainMenuSkin;
window.toggleMainSkinRenderMode = toggleMainSkinRenderMode;

744
style.css
View file

@ -1,6 +1,6 @@
@font-face {
font-family: 'Minecraft';
src: url('Minecraft.ttf') format('truetype');
src: url('assets/Minecraft.ttf') format('truetype');
}
@ -94,6 +94,15 @@ body {
background: #c42b1c;
}
.win-btn.nav-item:focus {
transform: scale(1.08);
z-index: 1001;
box-shadow: 0 0 0 1px rgba(255,255,255,0.95), 0 0 8px rgba(255,255,255,0.55) !important;
background: #444;
color: #fff;
}
.main-wrapper {
display: flex;
flex-grow: 1;
@ -104,15 +113,16 @@ body {
.sidebar {
width: 380px;
background: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.75);
border-right: 4px solid #000;
display: flex;
flex-direction: column;
padding: 30px 20px;
overflow-y: auto;
z-index: 10;
backdrop-filter: blur(8px);
backdrop-filter: blur(12px);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), padding 0.3s ease;
box-shadow: 10px 0 20px rgba(0,0,0,0.5);
}
.sidebar.collapsed {
@ -233,6 +243,9 @@ body {
line-height: 1.6;
white-space: pre-wrap;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow-wrap: break-word;
word-break: break-word;
overflow: hidden;
}
.pn-body li {
@ -283,7 +296,7 @@ body {
.content-area {
flex-grow: 1;
background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('minecraft.jpg');
background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('assets/minecraft.jpg');
background-size: cover;
background-position: center;
display: flex;
@ -333,7 +346,7 @@ body {
width: 500px;
max-width: 90%;
height: 60px;
background-color: var(--mc-button-bg);
background: linear-gradient(to bottom, #7e7e7e, #6e6e6e);
border: 2px solid #000;
color: #e0e0e0;
font-size: 24px;
@ -342,33 +355,43 @@ body {
justify-content: center;
padding-bottom: 6px;
cursor: pointer;
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa;
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa, 0 4px 6px rgba(0,0,0,0.3);
margin-bottom: 16px;
position: relative;
transition: transform(0.05s);
transition: transform 0.1s ease, box-shadow 0.1s ease, filter 0.1s ease;
text-shadow: 2px 2px 0 #333;
}
.nav-item {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
outline: none !important;
}
.nav-item:focus {
transform: scale(1.05);
transform: scale(1.02);
z-index: 1000;
box-shadow: 0 0 0 3px #fff, 0 0 20px rgba(255, 255, 255, 0.4) !important;
box-shadow: 0 0 0 2px #fff, 0 0 10px rgba(255, 255, 255, 0.62), 0 0 20px rgba(90, 170, 255, 0.28) !important;
filter: brightness(1.1);
}
.nav-item:hover {
outline: 2px solid rgba(255,255,255,0.92) !important;
box-shadow: 0 0 0 1px rgba(255,255,255,0.85), 0 0 12px rgba(255, 255, 255, 0.52), 0 0 24px rgba(90, 170, 255, 0.22) !important;
}
.nav-item.active-bump {
transform: scale(0.95) !important;
transition: transform 0.1s !important;
transform: scale(0.96) !important;
transition: transform 0.05s !important;
box-shadow: inset 3px 3px 0px #333, inset -3px -3px 0px #aaa !important;
background: linear-gradient(to bottom, #555, #666) !important;
}
.btn-mc:hover:not(.disabled) {
background-color: var(--mc-button-hover);
background: linear-gradient(to bottom, #8e8e8e, #7e7e7e);
color: #fff;
outline: 2px solid #fff !important;
outline: 2px solid rgba(255,255,255,0.95) !important;
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa, 0 0 12px rgba(255,255,255,0.25);
}
.btn-mc.controller-active {
@ -397,6 +420,58 @@ body {
text-shadow: 2px 2px 0 #000;
}
/* Steam Deck optimized mode */
body.steamdeck-mode .title-bar {
height: 40px;
}
body.steamdeck-mode .title-bar-text {
font-size: 18px;
}
body.steamdeck-mode .btn-mc {
min-height: 68px;
font-size: 28px;
}
body.steamdeck-mode .btn-play {
min-height: 110px;
font-size: 52px;
}
body.steamdeck-mode .version-select-box {
height: 64px;
font-size: 26px;
}
body.steamdeck-mode .mc-input,
body.steamdeck-mode #repo-preset-select,
body.steamdeck-mode .mc-label,
body.steamdeck-mode .classic-link {
font-size: 20px !important;
}
body.steamdeck-mode .nav-item:focus {
transform: scale(1.06);
box-shadow: 0 0 0 2px rgba(255,255,255,0.95), 0 0 22px rgba(255,255,255,0.7), 0 0 34px rgba(90,170,255,0.36) !important;
}
body.steamdeck-mode .controller-layout-preset {
min-height: 62px;
font-size: 22px;
}
body.steamdeck-mode .controller-layout-icon {
transform: scale(1.3);
transform-origin: left center;
margin-right: 4px;
}
body.steamdeck-mode .sidebar {
width: 420px;
}
.version-row {
display: flex;
width: 500px;
@ -420,21 +495,30 @@ body {
border: 2px solid #555;
display: flex;
align-items: center;
padding: 0 15px 6px 15px;
padding: 0 63px 6px 15px;
color: white;
font-size: 24px;
cursor: pointer;
position: relative;
transition: border-color 0.2s;
overflow: hidden;
}
.version-select-box:hover {
border-color: #fff;
}
#current-version-display,
#classic-version-display {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.select-arrow {
width: 48px;
height: 52px;
background: #6e6e6e;
border-left: 2px solid #555;
display: flex;
@ -443,6 +527,8 @@ body {
color: white;
position: absolute;
right: 0;
top: 0;
bottom: 0;
}
.progress-container {
@ -482,12 +568,71 @@ body {
transition: width 0.2s ease-out;
}
.progress-text-wrap {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
margin-bottom: 6px;
}
.progress-spinner {
width: 12px;
height: 12px;
border-width: 2px;
border-color: #333;
border-top-color: var(--mc-progress-fill);
margin: 0;
flex-shrink: 0;
animation-duration: 1s;
}
.progress-text {
color: #fff;
font-size: 20px;
text-shadow: 2px 2px 0 #000;
margin-bottom: 6px;
text-align: center;
text-align: left;
display: inline-flex;
align-items: center;
}
.loading-dots {
display: inline-flex;
width: 1.8em;
justify-content: flex-start;
}
.loading-dots span {
visibility: hidden;
}
.loading-dots.animate span:nth-child(1) {
animation: dotCycle1 1.1s steps(1, end) infinite;
}
.loading-dots.animate span:nth-child(2) {
animation: dotCycle2 1.1s steps(1, end) infinite;
}
.loading-dots.animate span:nth-child(3) {
animation: dotCycle3 1.1s steps(1, end) infinite;
}
@keyframes dotCycle1 {
0%, 49% { visibility: hidden; }
50%, 100% { visibility: visible; }
}
@keyframes dotCycle2 {
0%, 32% { visibility: hidden; }
33%, 66% { visibility: visible; }
67%, 100% { visibility: hidden; }
}
@keyframes dotCycle3 {
0%, 16% { visibility: hidden; }
17%, 82% { visibility: visible; }
83%, 100% { visibility: hidden; }
}
.modal-overlay {
@ -506,14 +651,14 @@ body {
max-width: 850px;
min-width: 600px;
max-height: 85vh;
background: var(--mc-gui-bg);
background: #c6c6c6;
border: 4px solid #000;
padding: clamp(20px, 4vw, 50px);
display: flex;
flex-direction: column;
align-items: center;
box-shadow: inset 4px 4px 0 #fff;
animation: modalPop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: inset 4px 4px 0 #fff, inset -4px -4px 0 #555, 0 20px 50px rgba(0,0,0,0.8);
animation: modalPop 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow-y: auto;
}
@ -594,11 +739,100 @@ body {
padding: 12px;
font-size: 22px;
outline: none;
transition: border-color 0.2s;
transition: all 0.2s;
box-shadow: inset 2px 2px 5px rgba(0,0,0,0.5);
}
.mc-input:focus, .mc-input.focused {
border-color: #fff !important;
background: #111;
box-shadow: 0 0 10px rgba(255,255,255,0.2), inset 2px 2px 5px rgba(0,0,0,0.5);
}
.controller-layout-presets {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
margin-top: 12px;
}
.controller-layout-preset {
width: 100%;
min-height: 52px;
border: 2px solid #555;
background: #0a0a0a;
color: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
padding: 8px 12px;
cursor: pointer;
font-size: 19px;
text-align: left;
}
.controller-layout-preset:hover,
.controller-layout-preset:focus {
border-color: #fff;
}
.controller-layout-preset.active {
border-color: #55ff55;
box-shadow: inset 0 0 0 1px #55ff55;
background: #111;
}
.controller-layout-icon {
--sprite-x: -51px;
--sprite-y: -531px;
--sprite-w: 26px;
--sprite-h: 26px;
--sprite-pressed-x: -51px;
--sprite-pressed-y: -563px;
width: var(--sprite-w);
height: var(--sprite-h);
flex: 0 0 var(--sprite-w);
display: inline-block;
overflow: hidden;
image-rendering: pixelated;
background-repeat: no-repeat;
background-size: 560px 640px;
background-position: var(--sprite-x) var(--sprite-y);
}
.controller-layout-preset:hover .controller-layout-icon,
.controller-layout-preset:focus .controller-layout-icon,
.controller-layout-preset.active .controller-layout-icon,
.controller-layout-preset.is-pressed .controller-layout-icon {
background-position: var(--sprite-pressed-x) var(--sprite-pressed-y);
}
.controller-layout-icon-xbox {
background-image: url('assets/gdb-xbox-2.png');
/* action_button_189 (idle) + action_button_228 (pressed) from spritesheet JSON */
--sprite-x: -51px;
--sprite-y: -531px;
--sprite-w: 26px;
--sprite-h: 26px;
--sprite-pressed-x: -51px;
--sprite-pressed-y: -563px;
}
.controller-layout-icon-switch {
background-image: url('assets/gdb-switch-2.png');
/* Switch sheet uses the same atlas layout coordinates */
--sprite-x: -51px;
--sprite-y: -531px;
--sprite-w: 26px;
--sprite-h: 26px;
--sprite-pressed-x: -51px;
--sprite-pressed-y: -563px;
}
.controller-layout-text {
line-height: 1.2;
}
#server-checkbox.focused {
@ -616,7 +850,7 @@ body {
color: white;
font-size: 36px;
z-index: 2000;
background-image: url('minecraft.jpg');
background-image: url('assets/minecraft.jpg');
background-size: cover;
background-position: center;
}
@ -628,12 +862,18 @@ body {
background: rgba(0,0,0,0.8);
}
.loader-content {
position: relative;
z-index: 1;
text-align: center;
}
#loader-text {
display: inline-flex;
align-items: center;
}
.loader-spinner {
width: 80px;
height: 80px;
@ -704,6 +944,25 @@ select.hidden-select {
opacity: 0;
cursor: pointer;
width: 100%;
/* Fix for Linux contrast issues in native dropdowns */
background-color: #1a1a1a !important;
color: #ffffff !important;
}
select.hidden-select option {
background-color: #1a1a1a !important;
color: #ffffff !important;
}
/* Ensure all select elements have good contrast on Linux */
select.mc-input {
background-color: #000000 !important;
color: #ffffff !important;
}
select.mc-input option {
background-color: #1a1a1a !important;
color: #ffffff !important;
}
::-webkit-scrollbar {
@ -724,3 +983,442 @@ select.hidden-select {
::-webkit-scrollbar-thumb:hover {
background: #7e7e7e;
}
/* --- Minecraft Style Slider --- */
.mc-slider-container {
position: relative;
width: 100%;
height: 44px;
background: #000;
border: 2px solid #555;
display: flex;
align-items: center;
justify-content: center;
}
.mc-slider {
-webkit-appearance: none;
width: 100%;
height: 100%;
background: transparent;
outline: none;
position: relative;
z-index: 2;
cursor: pointer;
margin: 0;
}
.mc-slider::-webkit-slider-runnable-track {
width: 100%;
height: 100%;
background: transparent;
}
.mc-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 100%;
background: #6e6e6e;
border: 2px solid #fff;
box-shadow: inset -2px -2px 0 #333, inset 2px 2px 0 #aaa;
}
.mc-slider:focus::-webkit-slider-thumb {
background: #7e7e7e;
box-shadow: inset -2px -2px 0 #333, inset 2px 2px 0 #aaa, 0 0 8px #fff;
}
.mc-slider-percent {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 20px;
text-shadow: 2px 2px 0 #000;
pointer-events: none;
z-index: 1;
}
/* --- Screenshots Gallery --- */
.gallery-item {
position: relative;
aspect-ratio: 16 / 9;
background: #111;
border: 2px solid #444;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
}
.gallery-item:hover, .gallery-item:focus {
border-color: #fff;
transform: scale(1.02);
z-index: 10;
box-shadow: 0 0 15px rgba(255,255,255,0.2);
}
.gallery-thumb {
width: 100%;
height: 100%;
object-fit: cover;
image-rendering: auto;
}
.gallery-item-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.8);
display: flex;
justify-content: space-around;
padding: 4px;
transform: translateY(100%);
transition: transform 0.2s;
}
.gallery-item:hover .gallery-item-actions,
.gallery-item:focus .gallery-item-actions {
transform: translateY(0);
}
.gallery-action-btn {
color: #fff;
font-size: 14px;
padding: 2px 8px;
cursor: pointer;
text-transform: uppercase;
}
.gallery-action-btn:hover {
color: #55ff55;
text-shadow: 0 0 5px rgba(85,255,85,0.5);
}
.gallery-action-btn.delete:hover {
color: #ff5555;
text-shadow: 0 0 5px rgba(255,85,85,0.5);
}
.gallery-empty {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: #888;
font-size: 24px;
}
/* ============================================================
CLASSIC MINECRAFT LAUNCHER THEME
Applied when <body class="classic-theme"> is set
============================================================ */
/* --- Classic Bottom Bar (hidden by default) --- */
.classic-bottom-bar {
display: none;
height: 82px;
min-height: 82px;
background: linear-gradient(to bottom, #1a1a1a 0%, #0d0d0d 100%);
border-top: 3px solid #000;
box-shadow: inset 0 2px 0 rgba(255,255,255,0.07);
align-items: center;
padding: 0 16px;
gap: 16px;
z-index: 200;
flex-shrink: 0;
}
/* Classic Logo Area (hidden by default, shown in classic mode) */
.classic-logo-area {
display: none;
align-items: center;
justify-content: center;
padding-top: 30px;
pointer-events: none;
}
/* ---- Show/hide elements in classic mode ---- */
body.classic-theme .classic-bottom-bar {
display: flex;
}
body.classic-theme .classic-logo-area {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
pointer-events: none;
z-index: 3;
justify-content: center;
padding-top: 18px;
}
body.classic-theme .classic-logo-area .mc-logo-img {
width: 480px;
margin-top: 0;
margin-bottom: 0;
filter: drop-shadow(4px 4px 12px rgba(0,0,0,0.9));
}
body.classic-theme .classic-logo-area .splash-text {
top: 60%;
left: 88%;
font-size: 17px;
}
/* ---- Classic mode layout restructure ---- */
body.classic-theme {
flex-direction: column;
}
body.classic-theme .main-wrapper {
flex: 1;
min-height: 0;
}
/* Force sidebar to always be expanded in classic mode */
body.classic-theme .sidebar {
width: 320px !important;
padding: 20px 18px !important;
background: rgba(0, 0, 0, 0.82) !important;
border-right: 3px solid #000 !important;
backdrop-filter: blur(10px) !important;
}
body.classic-theme .sidebar.collapsed {
width: 320px !important;
padding: 20px 18px !important;
}
body.classic-theme .sidebar.collapsed #updates-list {
display: flex !important;
}
body.classic-theme .sidebar.collapsed .sidebar-title {
border-bottom: 2px solid #555 !important;
margin-bottom: 20px !important;
justify-content: space-between !important;
padding-bottom: 12px !important;
}
body.classic-theme .sidebar.collapsed #sidebar-title-text {
display: block !important;
}
body.classic-theme .sidebar.collapsed #sidebar-toggle-icon {
font-size: 24px !important;
}
body.classic-theme .sidebar-title {
font-size: 24px;
color: #aaa;
text-shadow: 1px 1px 0 #000;
letter-spacing: 2px;
}
body.classic-theme #sidebar-title-text::before {
content: "NEWS — ";
color: #55ff55;
font-size: 14px;
letter-spacing: 3px;
}
/* Hide center menu column in classic mode */
body.classic-theme .menu-column {
display: none !important;
}
/* Hide skin column in classic mode */
body.classic-theme .skin-column {
display: none !important;
}
/* Content area in classic: full panorama, no center column */
body.classic-theme .content-area {
position: relative;
align-items: center;
justify-content: center;
}
/* Keep progress bar visible in classic mode */
body.classic-theme .progress-container {
z-index: 100;
}
/* ---- Classic Bottom Bar Layout ---- */
.classic-profile-section {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 0 0 auto;
max-width: 380px;
}
.classic-avatar {
width: 48px;
height: 48px;
background: #4CAF50;
border: 3px solid #000;
box-shadow: inset -2px -2px 0 #2a6b2a, inset 2px 2px 0 #7ecf7e;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: #fff;
font-family: 'Minecraft', monospace;
text-shadow: 2px 2px 0 #000;
flex-shrink: 0;
image-rendering: pixelated;
}
.classic-profile-info {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
flex: 1 1 0;
min-width: 0;
}
.classic-username-label {
font-size: 11px;
color: #666;
letter-spacing: 1px;
white-space: nowrap;
text-transform: uppercase;
}
.classic-username-display {
font-size: 18px;
color: #eee;
text-shadow: 1px 1px 0 #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.classic-mini-btn {
background: #3a3a3a;
border: 2px solid #000;
color: #ccc;
font-size: 13px;
padding: 4px 10px 6px;
cursor: pointer;
font-family: 'Minecraft', monospace;
box-shadow: inset -2px -2px 0 #222, inset 2px 2px 0 #555;
white-space: nowrap;
flex-shrink: 0;
}
.classic-mini-btn:hover {
background: #4a4a4a;
color: #fff;
outline: 1px solid #fff;
}
/* Center play section */
.classic-center-section {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.classic-play-btn {
width: 260px !important;
height: 58px !important;
font-size: 30px !important;
margin-bottom: 0 !important;
}
/* Right section: version + nav links */
.classic-right-section {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
gap: 6px;
flex: 0 0 auto;
max-width: 320px;
}
.classic-version-row {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
}
.classic-nav-links {
display: flex;
gap: 14px;
align-items: center;
justify-content: flex-end;
}
.classic-link {
color: #888;
font-size: 14px;
cursor: pointer;
letter-spacing: 1px;
text-transform: uppercase;
transition: color 0.15s;
padding: 2px 4px;
}
.classic-link:hover {
color: #fff;
text-shadow: 0 0 6px rgba(255,255,255,0.4);
}
.classic-link:focus {
color: #55ff55;
outline: none;
}
/* hide the toggle arrow and disable the click in classic mode */
body.classic-theme #sidebar-toggle-icon {
display: none;
}
body.classic-theme .sidebar-title {
cursor: default;
pointer-events: none;
}
/* ---- Classic mode music button position ---- */
body.classic-theme .music-btn {
bottom: 92px;
}
.skin-action-row {
display: flex;
align-items: stretch;
justify-content: center;
}
.skin-action-btn {
height: 48px !important;
margin-bottom: 0 !important;
}
.skin-action-btn-main {
width: 250px;
font-size: 20px;
border-right-width: 1px;
}
.skin-action-btn-mode {
width: 64px;
font-size: 20px;
border-left-width: 1px;
margin-left: -2px;
}