Compare commits
33 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42c1a9b0b | ||
|
|
cb4c62aff2 | ||
|
|
e733a250ec | ||
|
|
aa7c257553 | ||
|
|
3e094c6a1d | ||
|
|
90d40ba2af | ||
|
|
c514c981c7 | ||
|
|
4858f83ea1 | ||
|
|
124fb9dbcb | ||
|
|
8c36195731 | ||
|
|
1c1fbc3d46 | ||
|
|
1e27d63fae | ||
|
|
8e05e6b29d | ||
|
|
033315526c | ||
|
|
1377ece419 | ||
|
|
38092692a7 | ||
|
|
b54331475a | ||
|
|
6cda5ad9ac | ||
|
|
9c189b56df | ||
|
|
32b2909fee | ||
|
|
4c42fd0e55 | ||
|
|
3a104398f4 | ||
|
|
bb721c5ae0 | ||
|
|
08c332ecb7 | ||
|
|
0a06148338 | ||
|
|
a8a972a0f6 | ||
|
|
9d3e4fcc57 | ||
|
|
25116b6eba | ||
|
|
e44695860c | ||
|
|
6c59ad20d6 | ||
|
|
3355244dc4 | ||
|
|
e1a550674a | ||
|
|
12b8a503d9 |
8
.github/workflows/release.yml
vendored
|
|
@ -33,7 +33,6 @@ jobs:
|
||||||
name: linux-dist
|
name: linux-dist
|
||||||
path: dist/*.AppImage
|
path: dist/*.AppImage
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
name: Build Windows Installer
|
name: Build Windows Installer
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
@ -59,7 +58,6 @@ jobs:
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
dist/*.msi
|
dist/*.msi
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-mac:
|
build-mac:
|
||||||
name: Build macOS DMG (Apple Silicon)
|
name: Build macOS DMG (Apple Silicon)
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
@ -83,10 +81,9 @@ jobs:
|
||||||
name: mac-dist
|
name: mac-dist
|
||||||
path: dist/*.dmg
|
path: dist/*.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-mac-intel:
|
build-mac-intel:
|
||||||
name: Build macOS DMG (Intel)
|
name: Build macOS DMG (Intel)
|
||||||
runs-on: macos-13
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -98,7 +95,7 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build macOS DMG (Intel)
|
- name: Build macOS DMG (Intel)
|
||||||
run: npm run dist:mac
|
run: npm run dist:mac -- --x64
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Upload macOS Intel artifact
|
- name: Upload macOS Intel artifact
|
||||||
|
|
@ -107,7 +104,6 @@ jobs:
|
||||||
name: mac-intel-dist
|
name: mac-intel-dist
|
||||||
path: dist/*.dmg
|
path: dist/*.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
needs: [build-linux, build-windows, build-mac, build-mac-intel]
|
needs: [build-linux, build-windows, build-mac, build-mac-intel]
|
||||||
|
|
|
||||||
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
|
|
@ -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" />
|
<img width="1277" height="717" alt="image" src="https://github.com/user-attachments/assets/eaa9bae6-3b3b-4e39-a3c1-156e34abf3cc" />
|
||||||
|
|
||||||
|
[](https://ko-fi.com/gradengnostic)
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -77,6 +79,11 @@ The launcher supports several compatibility options for Linux:
|
||||||
- **extract-zip**: ZIP archive extraction
|
- **extract-zip**: ZIP archive extraction
|
||||||
- **Tailwind CSS**: UI styling (via CDN)
|
- **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
|
## Development
|
||||||
|
|
||||||
The launcher is built with:
|
The launcher is built with:
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
BIN
assets/Click_stereo.ogg.mp3
Normal file
BIN
assets/gdb-keyboard-2.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/gdb-switch-2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/gdb-xbox-2.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 987 KiB After Width: | Height: | Size: 987 KiB |
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
92
index.html
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LegacyLauncher</title>
|
<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">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -13,16 +13,16 @@
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="title-bar-text">LegacyLauncher</div>
|
<div class="title-bar-text">LegacyLauncher</div>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<div class="win-btn" onclick="minimizeWindow()">−</div>
|
<div class="win-btn nav-item" onclick="minimizeWindow()" tabindex="0" title="Minimize">−</div>
|
||||||
<div class="win-btn" id="maximize-btn" onclick="toggleMaximize()">▢</div>
|
<div class="win-btn nav-item" id="maximize-btn" onclick="toggleMaximize()" tabindex="0" title="Maximize / Restore">▢</div>
|
||||||
<div class="win-btn close" onclick="closeWindow()">×</div>
|
<div class="win-btn close nav-item" onclick="closeWindow()" tabindex="0" title="Close">×</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loader">
|
<div id="loader">
|
||||||
<div class="loader-content">
|
<div class="loader-content">
|
||||||
<div class="loader-spinner"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<!-- Classic Mode: Logo + Splash (only visible in classic theme) -->
|
<!-- Classic Mode: Logo + Splash (only visible in classic theme) -->
|
||||||
<div class="classic-logo-area" id="classic-logo-area">
|
<div class="classic-logo-area" id="classic-logo-area">
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo" style="margin-bottom: 0; margin-top: 0;">
|
<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 id="classic-splash-text" class="splash-text">Splash!</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
<!-- Left Side: Main Menu -->
|
<!-- 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="menu-column" style="flex: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; z-index: 5;">
|
||||||
<div class="relative">
|
<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 id="splash-text" class="splash-text">Splash!</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -86,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 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 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">
|
<div class="skin-action-row" style="margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;">
|
||||||
CHANGE SKIN
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="progress-container" id="progress-container">
|
<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-bg">
|
||||||
<div class="progress-bar-fill" id="progress-bar-fill"></div>
|
<div class="progress-bar-fill" id="progress-bar-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,12 +201,21 @@
|
||||||
<input type="text" id="install-path-input" class="mc-input nav-item !mb-0" placeholder="Default: Documents/LegacyClient" tabindex="0">
|
<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 class="btn-mc btn-mini nav-item !w-[60px] !h-[48px]" onclick="browseInstallDir()" tabindex="0">...</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label">GitHub Repository Source:</label>
|
<label class="mc-label">GitHub Repository Source:</label>
|
||||||
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
|
<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>
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
|
|
@ -226,7 +241,10 @@
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label">Launcher Music Volume:</label>
|
<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>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 w-full mt-2">
|
<div class="grid grid-cols-2 gap-4 w-full mt-2">
|
||||||
|
|
@ -240,6 +258,13 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
|
<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">
|
<input type="checkbox" id="server-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
|
||||||
|
|
@ -254,6 +279,32 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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 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-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 id="btn-options-cancel" class="btn-mc flex-grow nav-item" onclick="toggleOptions(false)" tabindex="0">CANCEL</div>
|
||||||
|
|
@ -261,6 +312,21 @@
|
||||||
</div>
|
</div>
|
||||||
</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-overlay" id="profile-modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<div class="modal-title">PLAYER PROFILE</div>
|
<div class="modal-title">PLAYER PROFILE</div>
|
||||||
|
|
|
||||||
123
main.js
|
|
@ -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 path = require('path');
|
||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
@ -7,9 +7,11 @@ const extractZip = require('extract-zip');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
let mainWindow = null;
|
||||||
|
let isGameRunning = false;
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const win = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
minWidth: 1024,
|
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', () => {
|
ipcMain.on('window-maximize', () => {
|
||||||
if (win.isMaximized()) {
|
if (mainWindow.isMaximized()) {
|
||||||
win.unmaximize();
|
mainWindow.unmaximize();
|
||||||
} else {
|
} 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-get', (event, key) => store.get(key));
|
||||||
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
|
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
|
||||||
|
|
@ -49,10 +150,10 @@ function createWindow() {
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
win.on('maximize', () => win.webContents.send('window-is-maximized', true));
|
mainWindow.on('maximize', () => mainWindow.webContents.send('window-is-maximized', true));
|
||||||
win.on('unmaximize', () => win.webContents.send('window-is-maximized', false));
|
mainWindow.on('unmaximize', () => mainWindow.webContents.send('window-is-maximized', false));
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
10
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "legacylauncher",
|
"name": "legacylauncher",
|
||||||
"version": "3.0.0",
|
"version": "3.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "legacylauncher",
|
"name": "legacylauncher",
|
||||||
"version": "3.0.0",
|
"version": "3.5.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-store": "^6.0.1",
|
"electron-store": "^6.0.1",
|
||||||
|
|
@ -4690,9 +4690,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.10",
|
"version": "7.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||||
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
|
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
16
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "legacylauncher",
|
"name": "legacylauncher",
|
||||||
"version": "3.0.1",
|
"version": "3.5.0",
|
||||||
"description": "",
|
"description": "A Minecraft: Legacy Console Edition launcher",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
|
@ -30,7 +30,17 @@
|
||||||
"flatpak"
|
"flatpak"
|
||||||
],
|
],
|
||||||
"category": "Game",
|
"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": {
|
"mac": {
|
||||||
"target": [
|
"target": [
|
||||||
|
|
|
||||||
574
renderer.js
|
|
@ -9,6 +9,10 @@ const DEFAULT_REPO = "smartcmd/MinecraftConsoles";
|
||||||
const DEFAULT_EXEC = "Minecraft.Client.exe";
|
const DEFAULT_EXEC = "Minecraft.Client.exe";
|
||||||
const TARGET_FILE = "LCEWindows64.zip";
|
const TARGET_FILE = "LCEWindows64.zip";
|
||||||
const LAUNCHER_REPO = "gradenGnostic/LegacyLauncher";
|
const LAUNCHER_REPO = "gradenGnostic/LegacyLauncher";
|
||||||
|
const REPO_PRESETS = {
|
||||||
|
default: 'smartcmd/MinecraftConsoles',
|
||||||
|
noWatermark: 'cath0degaytube/MinecraftConsoles'
|
||||||
|
};
|
||||||
|
|
||||||
let instances = [];
|
let instances = [];
|
||||||
let currentInstanceId = null;
|
let currentInstanceId = null;
|
||||||
|
|
@ -41,6 +45,21 @@ const GamepadManager = {
|
||||||
COOLDOWN: 180,
|
COOLDOWN: 180,
|
||||||
loopStarted: false,
|
loopStarted: false,
|
||||||
lastAPressed: false,
|
lastAPressed: false,
|
||||||
|
lastGuidePressed: false,
|
||||||
|
controlLayoutMode: 'auto',
|
||||||
|
|
||||||
|
setControlLayoutMode(mode = 'auto') {
|
||||||
|
const validModes = ['auto', 'xbox', 'nintendo'];
|
||||||
|
this.controlLayoutMode = validModes.includes(mode) ? mode : 'auto';
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldSwapABButtons(gamepadId = '') {
|
||||||
|
if (this.controlLayoutMode === 'nintendo') return true;
|
||||||
|
if (this.controlLayoutMode === 'xbox') return false;
|
||||||
|
|
||||||
|
const id = gamepadId.toLowerCase();
|
||||||
|
return id.includes('nintendo switch pro controller') || id.includes(' switch pro controller') || id.includes('nintendo co., ltd');
|
||||||
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
window.addEventListener("gamepadconnected", () => {
|
window.addEventListener("gamepadconnected", () => {
|
||||||
|
|
@ -76,6 +95,7 @@ const GamepadManager = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gp) {
|
if (!gp) {
|
||||||
|
this.lastGuidePressed = false;
|
||||||
if (this.active) {
|
if (this.active) {
|
||||||
this.active = false;
|
this.active = false;
|
||||||
showToast("Controller Disconnected");
|
showToast("Controller Disconnected");
|
||||||
|
|
@ -98,6 +118,18 @@ const GamepadManager = {
|
||||||
const isPressed = (idx) => buttons[idx] ? buttons[idx].pressed : false;
|
const isPressed = (idx) => buttons[idx] ? buttons[idx].pressed : false;
|
||||||
const getAxis = (idx) => axes[idx] !== undefined ? axes[idx] : 0;
|
const getAxis = (idx) => axes[idx] !== undefined ? axes[idx] : 0;
|
||||||
|
|
||||||
|
const shouldSwapAB = this.shouldSwapABButtons(gp.id);
|
||||||
|
const confirmButton = shouldSwapAB ? 1 : 0;
|
||||||
|
const cancelButton = shouldSwapAB ? 0 : 1;
|
||||||
|
|
||||||
|
const guidePressed = isPressed(16) || isPressed(17);
|
||||||
|
if (guidePressed && !this.lastGuidePressed) {
|
||||||
|
showToast("Closing Launcher...");
|
||||||
|
ipcRenderer.send('window-close');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastGuidePressed = guidePressed;
|
||||||
|
|
||||||
if (now - this.lastInputTime > this.COOLDOWN) {
|
if (now - this.lastInputTime > this.COOLDOWN) {
|
||||||
const threshold = 0.5;
|
const threshold = 0.5;
|
||||||
const axisX = getAxis(0);
|
const axisX = getAxis(0);
|
||||||
|
|
@ -108,21 +140,22 @@ const GamepadManager = {
|
||||||
const left = isPressed(14) || axisX < -threshold;
|
const left = isPressed(14) || axisX < -threshold;
|
||||||
const right = isPressed(15) || axisX > threshold;
|
const right = isPressed(15) || axisX > threshold;
|
||||||
|
|
||||||
if (up) { this.navigate('up'); this.lastInputTime = now; }
|
if (up) { UiSoundManager.setInputSource('controller'); this.navigate('up'); this.lastInputTime = now; }
|
||||||
else if (down) { this.navigate('down'); this.lastInputTime = now; }
|
else if (down) { UiSoundManager.setInputSource('controller'); this.navigate('down'); this.lastInputTime = now; }
|
||||||
else if (left) { this.navigate('left'); this.lastInputTime = now; }
|
else if (left) { UiSoundManager.setInputSource('controller'); this.navigate('left'); this.lastInputTime = now; }
|
||||||
else if (right) { this.navigate('right'); this.lastInputTime = now; }
|
else if (right) { UiSoundManager.setInputSource('controller'); this.navigate('right'); this.lastInputTime = now; }
|
||||||
|
|
||||||
else if (isPressed(4)) { this.cycleActiveSelection(-1); this.lastInputTime = now; }
|
else if (isPressed(4)) { UiSoundManager.setInputSource('controller'); this.cycleActiveSelection(-1); this.lastInputTime = now; }
|
||||||
else if (isPressed(5)) { this.cycleActiveSelection(1); this.lastInputTime = now; }
|
else if (isPressed(5)) { UiSoundManager.setInputSource('controller'); this.cycleActiveSelection(1); this.lastInputTime = now; }
|
||||||
|
|
||||||
else if (isPressed(1)) { this.cancelCurrent(); this.lastInputTime = now; }
|
else if (isPressed(cancelButton)) { UiSoundManager.setInputSource('controller'); this.cancelCurrent(); this.lastInputTime = now; }
|
||||||
|
|
||||||
else if (isPressed(2)) { checkForUpdatesManual(); this.lastInputTime = now; }
|
else if (isPressed(2)) { UiSoundManager.setInputSource('controller'); checkForUpdatesManual(); this.lastInputTime = now; }
|
||||||
}
|
}
|
||||||
|
|
||||||
const aPressed = isPressed(0);
|
const aPressed = isPressed(confirmButton);
|
||||||
if (aPressed && !this.lastAPressed) {
|
if (aPressed && !this.lastAPressed) {
|
||||||
|
UiSoundManager.setInputSource('controller');
|
||||||
this.clickActive();
|
this.clickActive();
|
||||||
}
|
}
|
||||||
this.lastAPressed = aPressed;
|
this.lastAPressed = aPressed;
|
||||||
|
|
@ -218,7 +251,24 @@ const GamepadManager = {
|
||||||
if (active && active.classList.contains('nav-item')) {
|
if (active && active.classList.contains('nav-item')) {
|
||||||
active.classList.add('active-bump');
|
active.classList.add('active-bump');
|
||||||
setTimeout(() => active.classList.remove('active-bump'), 100);
|
setTimeout(() => active.classList.remove('active-bump'), 100);
|
||||||
|
|
||||||
|
if (active.id === 'version-select-box') {
|
||||||
|
this.cycleActiveSelection(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active.id === 'classic-version-select-box') {
|
||||||
|
const classicSelect = document.getElementById('classic-version-select');
|
||||||
|
if (classicSelect) {
|
||||||
|
classicSelect.selectedIndex = (classicSelect.selectedIndex + 1) % classicSelect.options.length;
|
||||||
|
syncVersionFromClassic();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active.id === 'compat-select-box') {
|
||||||
|
this.cycleActiveSelection(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (active.tagName === 'INPUT' && active.type === 'checkbox') {
|
if (active.tagName === 'INPUT' && active.type === 'checkbox') {
|
||||||
active.checked = !active.checked;
|
active.checked = !active.checked;
|
||||||
active.dispatchEvent(new Event('change'));
|
active.dispatchEvent(new Event('change'));
|
||||||
|
|
@ -300,6 +350,88 @@ const GamepadManager = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UiSoundManager = {
|
||||||
|
files: {
|
||||||
|
cursor: 'JDSherbert - Ultimate UI SFX Pack - Cursor - 1.mp3', //doesnt exist?
|
||||||
|
select: 'assets/Click_stereo.ogg.mp3',
|
||||||
|
cancel: 'JDSherbert - Ultimate UI SFX Pack - Cancel - 1.mp3',
|
||||||
|
popupOpen: 'JDSherbert - Ultimate UI SFX Pack - Popup Open - 1.mp3',
|
||||||
|
popupClose: 'JDSherbert - Ultimate UI SFX Pack - Popup Close - 1.mp3',
|
||||||
|
error: 'JDSherbert - Ultimate UI SFX Pack - Error - 1.mp3'
|
||||||
|
},
|
||||||
|
cache: {},
|
||||||
|
lastPlayedAt: {},
|
||||||
|
cooldownMs: 70,
|
||||||
|
lastHoverItem: null,
|
||||||
|
inputSource: 'mouse',
|
||||||
|
|
||||||
|
setInputSource(source) {
|
||||||
|
this.inputSource = source;
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldPlay() {
|
||||||
|
return true; // Play sounds for both controller and mouse/keyboard
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Object.entries(this.files).forEach(([key, file]) => {
|
||||||
|
this.cache[key] = new Audio(file);
|
||||||
|
this.cache[key].preload = 'auto';
|
||||||
|
this.cache[key].volume = key === 'cursor' ? 0.45 : 0.6;
|
||||||
|
});
|
||||||
|
|
||||||
|
const markMouseInput = () => this.setInputSource('mouse');
|
||||||
|
['mousemove', 'mousedown', 'touchstart', 'wheel', 'keydown'].forEach((ev) => {
|
||||||
|
document.addEventListener(ev, markMouseInput, { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
if (e.target?.classList?.contains('nav-item')) this.play('cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('pointerover', (e) => {
|
||||||
|
const navItem = e.target?.closest?.('.nav-item');
|
||||||
|
if (!navItem || navItem === this.lastHoverItem) return;
|
||||||
|
this.lastHoverItem = navItem;
|
||||||
|
this.play('cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('pointerleave', () => {
|
||||||
|
this.lastHoverItem = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const navItem = e.target?.closest?.('.nav-item');
|
||||||
|
if (!navItem) return;
|
||||||
|
const label = (navItem.textContent || '').trim().toLowerCase();
|
||||||
|
if (label.includes('cancel') || label.includes('close') || label.includes('back') || label.includes('later')) {
|
||||||
|
this.play('cancel');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.play('select');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
play(name) {
|
||||||
|
if (!this.shouldPlay()) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (this.lastPlayedAt[name] && now - this.lastPlayedAt[name] < this.cooldownMs) return;
|
||||||
|
this.lastPlayedAt[name] = now;
|
||||||
|
|
||||||
|
const audio = this.cache[name];
|
||||||
|
if (!audio) return;
|
||||||
|
audio.currentTime = 0;
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
playToast(message) {
|
||||||
|
const normalized = String(message || '').toLowerCase();
|
||||||
|
if (normalized.includes('error') || normalized.includes('failed') || normalized.includes('missing') || normalized.includes('required')) {
|
||||||
|
this.play('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MusicManager = {
|
const MusicManager = {
|
||||||
audio: new Audio(),
|
audio: new Audio(),
|
||||||
playlist: [],
|
playlist: [],
|
||||||
|
|
@ -316,11 +448,20 @@ const MusicManager = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const slider = document.getElementById('volume-slider');
|
const slider = document.getElementById('volume-slider');
|
||||||
|
const percentText = document.getElementById('volume-percent');
|
||||||
|
const updatePercent = () => {
|
||||||
|
if (percentText) {
|
||||||
|
percentText.textContent = Math.round(this.audio.volume * 100) + "%";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (slider) {
|
if (slider) {
|
||||||
slider.value = this.audio.volume;
|
slider.value = this.audio.volume;
|
||||||
|
updatePercent();
|
||||||
slider.oninput = async () => {
|
slider.oninput = async () => {
|
||||||
this.audio.volume = slider.value;
|
this.audio.volume = slider.value;
|
||||||
await Store.set('legacy_music_volume', this.audio.volume);
|
updatePercent();
|
||||||
|
await Store.set('legacy_music_volume', parseFloat(slider.value));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -425,6 +566,7 @@ async function migrateLegacyConfig() {
|
||||||
ip: ip,
|
ip: ip,
|
||||||
port: port,
|
port: port,
|
||||||
isServer: isServer,
|
isServer: isServer,
|
||||||
|
fullscreen: false,
|
||||||
compatLayer: compat,
|
compatLayer: compat,
|
||||||
installPath: installDir,
|
installPath: installDir,
|
||||||
installedTag: installedTag
|
installedTag: installedTag
|
||||||
|
|
@ -442,6 +584,95 @@ async function migrateLegacyConfig() {
|
||||||
currentInstance = instances.find(i => i.id === currentInstanceId) || instances[0];
|
currentInstance = instances.find(i => i.id === currentInstanceId) || instances[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSteamDeckEnvironment() {
|
||||||
|
if (process.platform !== 'linux') return false;
|
||||||
|
|
||||||
|
const env = process.env || {};
|
||||||
|
if (env.STEAMDECK === '1' || env.SteamDeck === '1') return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const osRelease = fs.readFileSync('/etc/os-release', 'utf8').toLowerCase();
|
||||||
|
if (osRelease.includes('steamos') || osRelease.includes('steam deck')) return true;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusPrimaryPlayButton() {
|
||||||
|
const classicPlayBtn = document.getElementById('classic-btn-play');
|
||||||
|
const mainPlayBtn = document.getElementById('btn-play-main');
|
||||||
|
const target = (classicPlayBtn && classicPlayBtn.offsetParent !== null) ? classicPlayBtn : mainPlayBtn;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
target.focus();
|
||||||
|
target.classList.add('controller-active');
|
||||||
|
setTimeout(() => target.classList.remove('controller-active'), 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRepoPresetFromInput() {
|
||||||
|
const presetSelect = document.getElementById('repo-preset-select');
|
||||||
|
const repoInput = document.getElementById('repo-input');
|
||||||
|
if (!presetSelect || !repoInput) return;
|
||||||
|
|
||||||
|
if (repoInput.value.trim() === REPO_PRESETS.default) presetSelect.value = REPO_PRESETS.default;
|
||||||
|
else if (repoInput.value.trim() === REPO_PRESETS.noWatermark) presetSelect.value = REPO_PRESETS.noWatermark;
|
||||||
|
else presetSelect.value = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRepoPreset() {
|
||||||
|
const presetSelect = document.getElementById('repo-preset-select');
|
||||||
|
const repoInput = document.getElementById('repo-input');
|
||||||
|
if (!presetSelect || !repoInput) return;
|
||||||
|
if (presetSelect.value === 'custom') return;
|
||||||
|
repoInput.value = presetSelect.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyControllerLayoutPresetState(layoutMode = 'auto') {
|
||||||
|
const presets = document.querySelectorAll('.controller-layout-preset');
|
||||||
|
const activeLayout = layoutMode === 'nintendo' ? 'nintendo' : 'xbox';
|
||||||
|
presets.forEach((preset) => {
|
||||||
|
const isActive = preset.dataset.layout === activeLayout;
|
||||||
|
preset.classList.toggle('active', isActive);
|
||||||
|
preset.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initControllerLayoutPresets() {
|
||||||
|
const layoutSelect = document.getElementById('controller-layout-select');
|
||||||
|
if (!layoutSelect) return;
|
||||||
|
|
||||||
|
const presets = document.querySelectorAll('.controller-layout-preset');
|
||||||
|
presets.forEach((preset) => {
|
||||||
|
const pressOn = () => preset.classList.add('is-pressed');
|
||||||
|
const pressOff = () => preset.classList.remove('is-pressed');
|
||||||
|
|
||||||
|
preset.addEventListener('pointerdown', pressOn);
|
||||||
|
preset.addEventListener('pointerup', pressOff);
|
||||||
|
preset.addEventListener('pointerleave', pressOff);
|
||||||
|
preset.addEventListener('blur', pressOff);
|
||||||
|
|
||||||
|
preset.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') pressOn();
|
||||||
|
});
|
||||||
|
preset.addEventListener('keyup', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') pressOff();
|
||||||
|
});
|
||||||
|
|
||||||
|
preset.addEventListener('click', () => {
|
||||||
|
const selectedLayout = preset.dataset.layout || 'xbox';
|
||||||
|
layoutSelect.value = selectedLayout;
|
||||||
|
applyControllerLayoutPresetState(selectedLayout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutSelect.addEventListener('change', () => {
|
||||||
|
applyControllerLayoutPresetState(layoutSelect.value || 'auto');
|
||||||
|
});
|
||||||
|
|
||||||
|
applyControllerLayoutPresetState(layoutSelect.value || 'auto');
|
||||||
|
}
|
||||||
|
|
||||||
window.onload = async () => {
|
window.onload = async () => {
|
||||||
try {
|
try {
|
||||||
await migrateLegacyConfig();
|
await migrateLegacyConfig();
|
||||||
|
|
@ -453,14 +684,23 @@ window.onload = async () => {
|
||||||
const portInput = document.getElementById('port-input');
|
const portInput = document.getElementById('port-input');
|
||||||
const serverCheck = document.getElementById('server-checkbox');
|
const serverCheck = document.getElementById('server-checkbox');
|
||||||
const installInput = document.getElementById('install-path-input');
|
const installInput = document.getElementById('install-path-input');
|
||||||
|
const controllerLayoutSelect = document.getElementById('controller-layout-select');
|
||||||
|
|
||||||
if (repoInput) repoInput.value = currentInstance.repo;
|
if (repoInput) {
|
||||||
|
repoInput.value = currentInstance.repo;
|
||||||
|
repoInput.addEventListener('input', syncRepoPresetFromInput);
|
||||||
|
}
|
||||||
if (execInput) execInput.value = currentInstance.execPath;
|
if (execInput) execInput.value = currentInstance.execPath;
|
||||||
if (usernameInput) usernameInput.value = await Store.get('legacy_username', "");
|
if (usernameInput) usernameInput.value = await Store.get('legacy_username', "");
|
||||||
if (ipInput) ipInput.value = currentInstance.ip;
|
if (ipInput) ipInput.value = currentInstance.ip;
|
||||||
if (portInput) portInput.value = currentInstance.port;
|
if (portInput) portInput.value = currentInstance.port;
|
||||||
if (serverCheck) serverCheck.checked = currentInstance.isServer;
|
if (serverCheck) serverCheck.checked = currentInstance.isServer;
|
||||||
|
const fullscreenCheck = document.getElementById('fullscreen-checkbox');
|
||||||
|
if (fullscreenCheck) fullscreenCheck.checked = currentInstance.fullscreen || false;
|
||||||
if (installInput) installInput.value = currentInstance.installPath;
|
if (installInput) installInput.value = currentInstance.installPath;
|
||||||
|
if (controllerLayoutSelect) controllerLayoutSelect.value = await Store.get('legacy_controller_layout_mode', 'auto');
|
||||||
|
initControllerLayoutPresets();
|
||||||
|
syncRepoPresetFromInput();
|
||||||
|
|
||||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||||
const compatContainer = document.getElementById('compat-option-container');
|
const compatContainer = document.getElementById('compat-option-container');
|
||||||
|
|
@ -480,16 +720,52 @@ window.onload = async () => {
|
||||||
|
|
||||||
// Initialize features
|
// Initialize features
|
||||||
await loadTheme();
|
await loadTheme();
|
||||||
|
await loadSteamDeckMode();
|
||||||
|
await loadControllerLayoutMode();
|
||||||
fetchGitHubData();
|
fetchGitHubData();
|
||||||
checkForLauncherUpdates();
|
checkForLauncherUpdates();
|
||||||
loadSplashText();
|
loadSplashText();
|
||||||
MusicManager.init();
|
MusicManager.init();
|
||||||
GamepadManager.init();
|
GamepadManager.init();
|
||||||
|
UiSoundManager.init();
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
if (isSteamDeckEnvironment()) {
|
||||||
|
ipcRenderer.send('window-set-fullscreen', true);
|
||||||
|
setTimeout(() => focusPrimaryPlayButton(), 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takeScreenshot() {
|
||||||
|
try {
|
||||||
|
const filePath = await ipcRenderer.invoke('take-screenshot');
|
||||||
|
showToast(`Screenshot saved to: ${path.basename(filePath)}`);
|
||||||
|
UiSoundManager.play('select');
|
||||||
|
|
||||||
|
// Refresh gallery if it is visible
|
||||||
|
const galleryModal = document.getElementById('gallery-modal');
|
||||||
|
if (galleryModal && galleryModal.style.display === 'flex') {
|
||||||
|
renderGallery();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Screenshot error:", err);
|
||||||
|
showToast("Failed to take screenshot.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.on('trigger-screenshot', () => {
|
||||||
|
takeScreenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keydown', async (e) => {
|
||||||
|
if (e.key === 'F2') {
|
||||||
|
takeScreenshot();
|
||||||
|
}
|
||||||
if (e.key === 'F9') {
|
if (e.key === 'F9') {
|
||||||
checkForLauncherUpdates(true);
|
checkForLauncherUpdates(true);
|
||||||
}
|
}
|
||||||
|
if (e.key === 'F11') {
|
||||||
|
e.preventDefault();
|
||||||
|
ipcRenderer.send('window-fullscreen');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
|
|
@ -528,8 +804,10 @@ async function toggleInstances(show) {
|
||||||
document.activeElement?.blur();
|
document.activeElement?.blur();
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
modal.style.opacity = '1';
|
modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
} else {
|
} else {
|
||||||
modal.style.opacity = '0';
|
modal.style.opacity = '0';
|
||||||
|
UiSoundManager.play('popupClose');
|
||||||
setTimeout(() => modal.style.display = 'none', 300);
|
setTimeout(() => modal.style.display = 'none', 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -574,8 +852,10 @@ function toggleAddInstance(show) {
|
||||||
document.getElementById('new-instance-repo').value = DEFAULT_REPO;
|
document.getElementById('new-instance-repo').value = DEFAULT_REPO;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
modal.style.opacity = '1';
|
modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
} else {
|
} else {
|
||||||
modal.style.opacity = '0';
|
modal.style.opacity = '0';
|
||||||
|
UiSoundManager.play('popupClose');
|
||||||
setTimeout(() => modal.style.display = 'none', 300);
|
setTimeout(() => modal.style.display = 'none', 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -605,6 +885,7 @@ async function saveNewInstance() {
|
||||||
ip: "",
|
ip: "",
|
||||||
port: "",
|
port: "",
|
||||||
isServer: false,
|
isServer: false,
|
||||||
|
fullscreen: false,
|
||||||
compatLayer: 'direct',
|
compatLayer: 'direct',
|
||||||
installPath: installPath,
|
installPath: installPath,
|
||||||
installedTag: null
|
installedTag: null
|
||||||
|
|
@ -625,10 +906,13 @@ async function switchInstance(id) {
|
||||||
await saveInstancesToStore();
|
await saveInstancesToStore();
|
||||||
|
|
||||||
document.getElementById('repo-input').value = currentInstance.repo;
|
document.getElementById('repo-input').value = currentInstance.repo;
|
||||||
|
syncRepoPresetFromInput();
|
||||||
document.getElementById('exec-input').value = currentInstance.execPath;
|
document.getElementById('exec-input').value = currentInstance.execPath;
|
||||||
document.getElementById('ip-input').value = currentInstance.ip;
|
document.getElementById('ip-input').value = currentInstance.ip;
|
||||||
document.getElementById('port-input').value = currentInstance.port;
|
document.getElementById('port-input').value = currentInstance.port;
|
||||||
document.getElementById('server-checkbox').checked = currentInstance.isServer;
|
document.getElementById('server-checkbox').checked = currentInstance.isServer;
|
||||||
|
const fullscreenCheck = document.getElementById('fullscreen-checkbox');
|
||||||
|
if (fullscreenCheck) fullscreenCheck.checked = currentInstance.fullscreen || false;
|
||||||
document.getElementById('install-path-input').value = currentInstance.installPath;
|
document.getElementById('install-path-input').value = currentInstance.installPath;
|
||||||
|
|
||||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||||
|
|
@ -665,6 +949,85 @@ async function browseInstallDir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openScreenshotsGallery() {
|
||||||
|
await toggleGallery(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleGallery(show) {
|
||||||
|
const modal = document.getElementById('gallery-modal');
|
||||||
|
if (show) {
|
||||||
|
await renderGallery();
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
|
} else {
|
||||||
|
modal.style.opacity = '0';
|
||||||
|
UiSoundManager.play('popupClose');
|
||||||
|
setTimeout(() => modal.style.display = 'none', 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderGallery() {
|
||||||
|
const container = document.getElementById('gallery-container');
|
||||||
|
container.innerHTML = '<div class="gallery-empty">LOADING...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const screenshots = await ipcRenderer.invoke('list-screenshots');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (screenshots.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="gallery-empty">
|
||||||
|
<p>NO SCREENSHOTS FOUND</p>
|
||||||
|
<p style="font-size: 14px; margin-top: 10px;">PRESS F2 TO TAKE ONE!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshots.forEach(ss => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'gallery-item nav-item';
|
||||||
|
item.tabIndex = 0;
|
||||||
|
item.innerHTML = `
|
||||||
|
<img src="${ss.path}?t=${Date.now()}" class="gallery-thumb" alt="${ss.name}" onerror="this.src='assets/minecraft.jpg'">
|
||||||
|
<div class="gallery-item-actions">
|
||||||
|
<div class="gallery-action-btn" onclick="viewScreenshot('${ss.path.replace(/\\/g, '/')}')">VIEW</div>
|
||||||
|
<div class="gallery-action-btn delete" onclick="deleteScreenshot('${ss.name}')">DELETE</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
item.onkeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') viewScreenshot(ss.path);
|
||||||
|
if (e.key === 'Delete') deleteScreenshot(ss.name);
|
||||||
|
};
|
||||||
|
container.appendChild(item);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gallery render error:", err);
|
||||||
|
container.innerHTML = '<div class="gallery-empty" style="color: #ff5555;">ERROR LOADING GALLERY</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteScreenshot(fileName) {
|
||||||
|
if (confirm(`Are you sure you want to delete this screenshot?`)) {
|
||||||
|
const success = await ipcRenderer.invoke('delete-screenshot', fileName);
|
||||||
|
if (success) {
|
||||||
|
showToast("Screenshot deleted.");
|
||||||
|
renderGallery();
|
||||||
|
} else {
|
||||||
|
showToast("Failed to delete screenshot.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewScreenshot(path) {
|
||||||
|
shell.openPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openScreenshotsDir() {
|
||||||
|
ipcRenderer.invoke('open-screenshots-dir');
|
||||||
|
}
|
||||||
|
|
||||||
async function openGameDir() {
|
async function openGameDir() {
|
||||||
const dir = await getInstallDir();
|
const dir = await getInstallDir();
|
||||||
if (fs.existsSync(dir)) {
|
if (fs.existsSync(dir)) {
|
||||||
|
|
@ -726,6 +1089,7 @@ async function updatePlayButtonText() {
|
||||||
function setGameRunning(running) {
|
function setGameRunning(running) {
|
||||||
isGameRunning = running;
|
isGameRunning = running;
|
||||||
updatePlayButtonText();
|
updatePlayButtonText();
|
||||||
|
ipcRenderer.send('game-running-state', running);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function monitorProcess(proc) {
|
async function monitorProcess(proc) {
|
||||||
|
|
@ -767,7 +1131,11 @@ async function fetchGitHubData() {
|
||||||
const offlineInd = document.getElementById('offline-indicator');
|
const offlineInd = document.getElementById('offline-indicator');
|
||||||
|
|
||||||
if (loader) loader.style.display = 'flex';
|
if (loader) loader.style.display = 'flex';
|
||||||
if (loaderText) loaderText.textContent = "SYNCING: " + repo;
|
if (loaderText) {
|
||||||
|
const loaderLabel = document.getElementById('loader-text-label');
|
||||||
|
if (loaderLabel) loaderLabel.textContent = "SYNCING: " + repo;
|
||||||
|
else loaderText.textContent = "SYNCING: " + repo;
|
||||||
|
}
|
||||||
|
|
||||||
const hideLoader = () => {
|
const hideLoader = () => {
|
||||||
if (loader) {
|
if (loader) {
|
||||||
|
|
@ -950,8 +1318,10 @@ async function promptUpdate(newTag) {
|
||||||
document.activeElement?.blur();
|
document.activeElement?.blur();
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
modal.style.opacity = '1';
|
modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
const cleanup = (result) => {
|
const cleanup = (result) => {
|
||||||
modal.style.opacity = '0';
|
modal.style.opacity = '0';
|
||||||
|
UiSoundManager.play('popupClose');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
if (modalText) modalText.style.display = 'none';
|
if (modalText) modalText.style.display = 'none';
|
||||||
|
|
@ -1003,9 +1373,11 @@ async function launchLocalClient() {
|
||||||
const ip = currentInstance.ip;
|
const ip = currentInstance.ip;
|
||||||
const port = currentInstance.port;
|
const port = currentInstance.port;
|
||||||
const isServer = currentInstance.isServer;
|
const isServer = currentInstance.isServer;
|
||||||
|
const fullscreen = currentInstance.fullscreen;
|
||||||
let args = [];
|
let args = [];
|
||||||
if (username) args.push("-name", username);
|
if (username) args.push("-name", username);
|
||||||
if (isServer) args.push("-server");
|
if (isServer) args.push("-server");
|
||||||
|
if (fullscreen) args.push("-fullscreen");
|
||||||
if (ip) args.push("-ip", ip);
|
if (ip) args.push("-ip", ip);
|
||||||
if (port) args.push("-port", port);
|
if (port) args.push("-port", port);
|
||||||
const argString = args.map(a => `"${a}"`).join(" ");
|
const argString = args.map(a => `"${a}"`).join(" ");
|
||||||
|
|
@ -1056,8 +1428,29 @@ function setProcessingState(active) {
|
||||||
function updateProgress(percent, text) {
|
function updateProgress(percent, text) {
|
||||||
const bar = document.getElementById('progress-bar-fill');
|
const bar = document.getElementById('progress-bar-fill');
|
||||||
if (bar) bar.style.width = percent + "%";
|
if (bar) bar.style.width = percent + "%";
|
||||||
|
|
||||||
|
const label = document.getElementById('progress-text-label');
|
||||||
|
const dots = document.getElementById('progress-dots');
|
||||||
|
const suffix = document.getElementById('progress-text-suffix');
|
||||||
const txt = document.getElementById('progress-text');
|
const txt = document.getElementById('progress-text');
|
||||||
if (text && txt) txt.textContent = text;
|
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
if (label && dots && suffix) {
|
||||||
|
const match = text.match(/^(Downloading)(?:\.{0,3})?(.*)$/i);
|
||||||
|
if (match) {
|
||||||
|
label.textContent = match[1];
|
||||||
|
suffix.textContent = match[2] || '';
|
||||||
|
dots.classList.add('animate');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
label.textContent = text;
|
||||||
|
suffix.textContent = '';
|
||||||
|
dots.classList.remove('animate');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txt) txt.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleElectronFlow(url) {
|
async function handleElectronFlow(url) {
|
||||||
|
|
@ -1135,30 +1528,41 @@ function downloadFile(url, destPath) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleOptions(show) {
|
async function toggleOptions(show) {
|
||||||
if (isProcessing) return;
|
if (isProcessing) return;
|
||||||
const modal = document.getElementById('options-modal');
|
const modal = document.getElementById('options-modal');
|
||||||
if (show) {
|
if (show) {
|
||||||
// Sync classic theme checkbox to current state
|
// Sync classic theme checkbox to current state
|
||||||
const cb = document.getElementById('classic-theme-checkbox');
|
const cb = document.getElementById('classic-theme-checkbox');
|
||||||
if (cb) cb.checked = document.body.classList.contains('classic-theme');
|
if (cb) cb.checked = document.body.classList.contains('classic-theme');
|
||||||
|
const steamDeckCb = document.getElementById('steamdeck-mode-checkbox');
|
||||||
|
if (steamDeckCb) steamDeckCb.checked = document.body.classList.contains('steamdeck-mode');
|
||||||
|
const layoutSelect = document.getElementById('controller-layout-select');
|
||||||
|
if (layoutSelect) {
|
||||||
|
const savedLayoutMode = await Store.get('legacy_controller_layout_mode', 'auto');
|
||||||
|
layoutSelect.value = savedLayoutMode;
|
||||||
|
GamepadManager.setControlLayoutMode(savedLayoutMode);
|
||||||
|
applyControllerLayoutPresetState(savedLayoutMode);
|
||||||
|
}
|
||||||
|
syncRepoPresetFromInput();
|
||||||
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
|
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
}
|
}
|
||||||
else { modal.style.opacity = '0'; setTimeout(() => modal.style.display = 'none', 300); }
|
else { modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => modal.style.display = 'none', 300); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleProfile(show) {
|
async function toggleProfile(show) {
|
||||||
if (isProcessing) return;
|
if (isProcessing) return;
|
||||||
const modal = document.getElementById('profile-modal');
|
const modal = document.getElementById('profile-modal');
|
||||||
if (show) { await updatePlaytimeDisplay(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; }
|
if (show) { await updatePlaytimeDisplay(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; UiSoundManager.play('popupOpen'); }
|
||||||
else { modal.style.opacity = '0'; setTimeout(() => modal.style.display = 'none', 300); }
|
else { modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => modal.style.display = 'none', 300); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleServers(show) {
|
async function toggleServers(show) {
|
||||||
if (isProcessing) return;
|
if (isProcessing) return;
|
||||||
const modal = document.getElementById('servers-modal');
|
const modal = document.getElementById('servers-modal');
|
||||||
if (show) { await loadServers(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; }
|
if (show) { await loadServers(); document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1'; UiSoundManager.play('popupOpen'); }
|
||||||
else { modal.style.opacity = '0'; setTimeout(() => modal.style.display = 'none', 300); }
|
else { modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => modal.style.display = 'none', 300); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getServersFilePath() { return path.join(currentInstance.installPath, 'servers.txt'); }
|
async function getServersFilePath() { return path.join(currentInstance.installPath, 'servers.txt'); }
|
||||||
|
|
@ -1233,6 +1637,7 @@ async function saveOptions() {
|
||||||
const ip = document.getElementById('ip-input').value.trim();
|
const ip = document.getElementById('ip-input').value.trim();
|
||||||
const port = document.getElementById('port-input').value.trim();
|
const port = document.getElementById('port-input').value.trim();
|
||||||
const isServer = document.getElementById('server-checkbox').checked;
|
const isServer = document.getElementById('server-checkbox').checked;
|
||||||
|
const fullscreen = document.getElementById('fullscreen-checkbox')?.checked || false;
|
||||||
const customProtonPath = document.getElementById('custom-proton-path').value.trim();
|
const customProtonPath = document.getElementById('custom-proton-path').value.trim();
|
||||||
const newInstallPath = document.getElementById('install-path-input').value.trim();
|
const newInstallPath = document.getElementById('install-path-input').value.trim();
|
||||||
const oldInstallPath = currentInstance.installPath;
|
const oldInstallPath = currentInstance.installPath;
|
||||||
|
|
@ -1249,14 +1654,23 @@ async function saveOptions() {
|
||||||
}
|
}
|
||||||
if (newRepo) currentInstance.repo = newRepo;
|
if (newRepo) currentInstance.repo = newRepo;
|
||||||
if (newExec) currentInstance.execPath = newExec;
|
if (newExec) currentInstance.execPath = newExec;
|
||||||
currentInstance.ip = ip; currentInstance.port = port; currentInstance.isServer = isServer;
|
currentInstance.ip = ip; currentInstance.port = port;
|
||||||
|
currentInstance.isServer = isServer;
|
||||||
|
currentInstance.fullscreen = fullscreen;
|
||||||
if (compatSelect) {
|
if (compatSelect) {
|
||||||
currentInstance.compatLayer = compatSelect.value;
|
currentInstance.compatLayer = compatSelect.value;
|
||||||
currentInstance.customCompatPath = customProtonPath;
|
currentInstance.customCompatPath = customProtonPath;
|
||||||
}
|
}
|
||||||
const isClassic = document.getElementById('classic-theme-checkbox')?.checked || false;
|
const isClassic = document.getElementById('classic-theme-checkbox')?.checked || false;
|
||||||
|
const isSteamDeckMode = document.getElementById('steamdeck-mode-checkbox')?.checked || false;
|
||||||
|
const controllerLayoutMode = document.getElementById('controller-layout-select')?.value || 'auto';
|
||||||
await Store.set('legacy_classic_theme', isClassic);
|
await Store.set('legacy_classic_theme', isClassic);
|
||||||
|
await Store.set('legacy_steamdeck_mode', isSteamDeckMode);
|
||||||
|
await Store.set('legacy_controller_layout_mode', controllerLayoutMode);
|
||||||
|
GamepadManager.setControlLayoutMode(controllerLayoutMode);
|
||||||
|
applyControllerLayoutPresetState(controllerLayoutMode);
|
||||||
applyTheme(isClassic);
|
applyTheme(isClassic);
|
||||||
|
applySteamDeckMode(isSteamDeckMode);
|
||||||
await saveInstancesToStore(); toggleOptions(false); fetchGitHubData(); updatePlayButtonText(); showToast("Settings Saved");
|
await saveInstancesToStore(); toggleOptions(false); fetchGitHubData(); updatePlayButtonText(); showToast("Settings Saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1272,6 +1686,7 @@ async function saveProfile() {
|
||||||
|
|
||||||
function showToast(msg) {
|
function showToast(msg) {
|
||||||
const t = document.getElementById('toast'); if (!t) return;
|
const t = document.getElementById('toast'); if (!t) return;
|
||||||
|
UiSoundManager.playToast(msg);
|
||||||
t.textContent = msg; t.style.display = 'block'; t.style.animation = 'none'; t.offsetHeight; t.style.animation = 'slideUp 0.3s ease-out';
|
t.textContent = msg; t.style.display = 'block'; t.style.animation = 'none'; t.offsetHeight; t.style.animation = 'slideUp 0.3s ease-out';
|
||||||
setTimeout(() => { t.style.display = 'none'; }, 3000);
|
setTimeout(() => { t.style.display = 'none'; }, 3000);
|
||||||
}
|
}
|
||||||
|
|
@ -1282,20 +1697,81 @@ function scanCompatibilityLayers() {
|
||||||
const select = document.getElementById('compat-select'); if (!select) return;
|
const select = document.getElementById('compat-select'); if (!select) return;
|
||||||
const savedValue = currentInstance.compatLayer;
|
const savedValue = currentInstance.compatLayer;
|
||||||
const layers = [{ name: 'Default (Direct)', cmd: 'direct' }, { name: 'Wine64', cmd: 'wine64' }, { name: 'Wine', cmd: 'wine' }];
|
const layers = [{ name: 'Default (Direct)', cmd: 'direct' }, { name: 'Wine64', cmd: 'wine64' }, { name: 'Wine', cmd: 'wine' }];
|
||||||
|
|
||||||
// Add custom option
|
const seen = new Set(layers.map(l => l.cmd));
|
||||||
layers.push({ name: 'Custom (Linux)', cmd: 'custom' });
|
const foundProtonLayers = [];
|
||||||
|
const addLayer = (name, cmd) => {
|
||||||
const homeDir = require('os').homedir(); let steamPaths = [];
|
if (!name || !cmd || seen.has(cmd)) return;
|
||||||
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')];
|
seen.add(cmd);
|
||||||
else if (process.platform === 'darwin') steamPaths = [path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common')];
|
foundProtonLayers.push({ name, cmd });
|
||||||
for (const steamPath of steamPaths) {
|
};
|
||||||
if (fs.existsSync(steamPath)) { try { const dirs = fs.readdirSync(steamPath); dirs.filter(d => d.startsWith('Proton') || d.includes('Wine') || d.includes('CrossOver')).forEach(d => { const protonPath = path.join(steamPath, d, 'proton'); if (fs.existsSync(protonPath)) layers.push({ name: d, cmd: protonPath }); }); } catch (e) {} }
|
|
||||||
|
const homeDir = require('os').homedir();
|
||||||
|
const protonCandidates = [];
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
const steamRoots = [
|
||||||
|
path.join(homeDir, '.steam', 'steam'),
|
||||||
|
path.join(homeDir, '.local', 'share', 'Steam'),
|
||||||
|
path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam')
|
||||||
|
];
|
||||||
|
|
||||||
|
steamRoots.forEach((root) => {
|
||||||
|
protonCandidates.push(path.join(root, 'steamapps', 'common'));
|
||||||
|
protonCandidates.push(path.join(root, 'compatibilitytools.d'));
|
||||||
|
});
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
protonCandidates.push(path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common'));
|
||||||
|
protonCandidates.push(path.join(homeDir, 'Library', 'Application Support', 'Steam', 'compatibilitytools.d'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolNameMatches = (dir) => {
|
||||||
|
const n = dir.toLowerCase();
|
||||||
|
return n.startsWith('proton') || n.startsWith('ge-proton') || n.includes('proton-ge') || n.includes('wine') || n.includes('crossover') || n.includes('umu-proton');
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const basePath of protonCandidates) {
|
||||||
|
if (!fs.existsSync(basePath)) continue;
|
||||||
|
try {
|
||||||
|
const dirs = fs.readdirSync(basePath);
|
||||||
|
dirs.forEach((dirName) => {
|
||||||
|
if (!toolNameMatches(dirName)) return;
|
||||||
|
const protonPath = path.join(basePath, dirName, 'proton');
|
||||||
|
if (fs.existsSync(protonPath)) addLayer(dirName, protonPath);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Compatibility scan error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundProtonLayers.sort((a, b) => {
|
||||||
|
const aGe = /(^|\b)(ge-proton|proton-ge|umu-proton)/i.test(a.name) ? 1 : 0;
|
||||||
|
const bGe = /(^|\b)(ge-proton|proton-ge|umu-proton)/i.test(b.name) ? 1 : 0;
|
||||||
|
if (aGe !== bGe) return bGe - aGe;
|
||||||
|
return b.name.localeCompare(a.name, undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
|
||||||
|
layers.push(...foundProtonLayers);
|
||||||
|
|
||||||
|
// Add custom option at end so discovered runtimes are easier to browse first.
|
||||||
|
layers.push({ name: 'Custom (Linux)', cmd: 'custom' });
|
||||||
|
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
layers.forEach(l => { const opt = document.createElement('option'); opt.value = l.cmd; opt.textContent = l.name; select.appendChild(opt); if (l.cmd === savedValue) opt.selected = true; });
|
layers.forEach(l => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = l.cmd;
|
||||||
|
opt.textContent = l.name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
if (l.cmd === savedValue) opt.selected = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no saved compat layer or still on direct, prefer the newest detected GE/Proton on Linux.
|
||||||
|
if (process.platform === 'linux' && (savedValue === 'direct' || !savedValue) && foundProtonLayers.length > 0) {
|
||||||
|
select.value = foundProtonLayers[0].cmd;
|
||||||
|
}
|
||||||
|
|
||||||
updateCompatDisplay();
|
updateCompatDisplay();
|
||||||
|
|
||||||
const customPathInput = document.getElementById('custom-proton-path');
|
const customPathInput = document.getElementById('custom-proton-path');
|
||||||
if (customPathInput) customPathInput.value = currentInstance.customCompatPath || "";
|
if (customPathInput) customPathInput.value = currentInstance.customCompatPath || "";
|
||||||
}
|
}
|
||||||
|
|
@ -1363,8 +1839,9 @@ async function promptLauncherUpdate(version, changelog) {
|
||||||
modalText.style.display = 'block';
|
modalText.style.display = 'block';
|
||||||
}
|
}
|
||||||
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
|
document.activeElement?.blur(); modal.style.display = 'flex'; modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
const cleanup = (result) => {
|
const cleanup = (result) => {
|
||||||
modal.style.opacity = '0'; setTimeout(() => { modal.style.display = 'none'; if (modalText) modalText.style.display = 'none'; }, 300);
|
modal.style.opacity = '0'; UiSoundManager.play('popupClose'); setTimeout(() => { modal.style.display = 'none'; if (modalText) modalText.style.display = 'none'; }, 300);
|
||||||
confirmBtn.onclick = null; skipBtn.onclick = null; closeBtn.onclick = null; resolve(result);
|
confirmBtn.onclick = null; skipBtn.onclick = null; closeBtn.onclick = null; resolve(result);
|
||||||
};
|
};
|
||||||
confirmBtn.onclick = () => cleanup(true); skipBtn.onclick = () => cleanup(false); closeBtn.onclick = () => cleanup(false);
|
confirmBtn.onclick = () => cleanup(true); skipBtn.onclick = () => cleanup(false); closeBtn.onclick = () => cleanup(false);
|
||||||
|
|
@ -1418,12 +1895,36 @@ async function loadSplashText() {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function loadTheme() {
|
async function loadTheme() {
|
||||||
const isClassic = await Store.get('legacy_classic_theme', false);
|
// Force default UI on startup
|
||||||
|
const isClassic = false;
|
||||||
const cb = document.getElementById('classic-theme-checkbox');
|
const cb = document.getElementById('classic-theme-checkbox');
|
||||||
if (cb) cb.checked = isClassic;
|
if (cb) cb.checked = isClassic;
|
||||||
applyTheme(isClassic);
|
applyTheme(isClassic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSteamDeckMode() {
|
||||||
|
const autoSteamDeck = isSteamDeckEnvironment();
|
||||||
|
const saved = await Store.get('legacy_steamdeck_mode', null);
|
||||||
|
const enabled = saved === null ? autoSteamDeck : saved;
|
||||||
|
const cb = document.getElementById('steamdeck-mode-checkbox');
|
||||||
|
if (cb) cb.checked = enabled;
|
||||||
|
applySteamDeckMode(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadControllerLayoutMode() {
|
||||||
|
const mode = await Store.get('legacy_controller_layout_mode', 'auto');
|
||||||
|
GamepadManager.setControlLayoutMode(mode);
|
||||||
|
const select = document.getElementById('controller-layout-select');
|
||||||
|
if (select) {
|
||||||
|
select.value = mode;
|
||||||
|
applyControllerLayoutPresetState(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySteamDeckMode(enabled) {
|
||||||
|
document.body.classList.toggle('steamdeck-mode', !!enabled);
|
||||||
|
}
|
||||||
|
|
||||||
function applyTheme(isClassic) {
|
function applyTheme(isClassic) {
|
||||||
document.body.classList.toggle('classic-theme', isClassic);
|
document.body.classList.toggle('classic-theme', isClassic);
|
||||||
if (isClassic) {
|
if (isClassic) {
|
||||||
|
|
@ -1475,8 +1976,10 @@ async function toggleSnapshots(show, id = null) {
|
||||||
document.activeElement?.blur();
|
document.activeElement?.blur();
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
modal.style.opacity = '1';
|
modal.style.opacity = '1';
|
||||||
|
UiSoundManager.play('popupOpen');
|
||||||
} else {
|
} else {
|
||||||
modal.style.opacity = '0';
|
modal.style.opacity = '0';
|
||||||
|
UiSoundManager.play('popupClose');
|
||||||
setTimeout(() => modal.style.display = 'none', 300);
|
setTimeout(() => modal.style.display = 'none', 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1632,6 +2135,7 @@ window.checkForUpdatesManual = checkForUpdatesManual;
|
||||||
window.browseInstallDir = browseInstallDir;
|
window.browseInstallDir = browseInstallDir;
|
||||||
window.openGameDir = openGameDir;
|
window.openGameDir = openGameDir;
|
||||||
window.toggleMusic = toggleMusic;
|
window.toggleMusic = toggleMusic;
|
||||||
|
window.applyRepoPreset = applyRepoPreset;
|
||||||
window.getInstallDir = getInstallDir;
|
window.getInstallDir = getInstallDir;
|
||||||
window.showToast = showToast;
|
window.showToast = showToast;
|
||||||
window.toggleInstances = toggleInstances;
|
window.toggleInstances = toggleInstances;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
|
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
|
||||||
let isMainSkinDragging = false;
|
let isMainSkinDragging = false;
|
||||||
|
let mainMenuSkinRenderMode = '3d';
|
||||||
|
|
||||||
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
|
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
|
||||||
let isSkinDragging = false;
|
let isSkinDragging = false;
|
||||||
|
|
@ -19,6 +20,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
|
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
|
||||||
|
|
||||||
if (dropZone) {
|
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) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('border-green-500');
|
dropZone.classList.add('border-green-500');
|
||||||
|
|
@ -68,12 +76,13 @@ function initMainMenuSkinViewer() {
|
||||||
// Interaction for Main Menu Viewer
|
// Interaction for Main Menu Viewer
|
||||||
let prevX = 0;
|
let prevX = 0;
|
||||||
container.addEventListener('mousedown', (e) => {
|
container.addEventListener('mousedown', (e) => {
|
||||||
|
if (mainMenuSkinRenderMode !== '3d') return;
|
||||||
isMainSkinDragging = true;
|
isMainSkinDragging = true;
|
||||||
prevX = e.clientX;
|
prevX = e.clientX;
|
||||||
});
|
});
|
||||||
window.addEventListener('mouseup', () => isMainSkinDragging = false);
|
window.addEventListener('mouseup', () => isMainSkinDragging = false);
|
||||||
window.addEventListener('mousemove', (e) => {
|
window.addEventListener('mousemove', (e) => {
|
||||||
if (isMainSkinDragging && mainMenuPlayerGroup) {
|
if (mainMenuSkinRenderMode === '3d' && isMainSkinDragging && mainMenuPlayerGroup) {
|
||||||
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
|
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
|
||||||
prevX = e.clientX;
|
prevX = e.clientX;
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +91,9 @@ function initMainMenuSkinViewer() {
|
||||||
// Auto-rotate slowly
|
// Auto-rotate slowly
|
||||||
function animateMain() {
|
function animateMain() {
|
||||||
requestAnimationFrame(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);
|
if (mainMenuRenderer && mainMenuScene && mainMenuCamera) mainMenuRenderer.render(mainMenuScene, mainMenuCamera);
|
||||||
}
|
}
|
||||||
animateMain();
|
animateMain();
|
||||||
|
|
@ -116,7 +127,7 @@ async function loadMainMenuSkin() {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
const isLegacy = img.height === 32;
|
const isLegacy = img.height === 32;
|
||||||
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup);
|
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup, mainMenuSkinRenderMode);
|
||||||
};
|
};
|
||||||
img.src = url;
|
img.src = url;
|
||||||
} else {
|
} 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;
|
if (!targetGroup) return;
|
||||||
|
|
||||||
new THREE.TextureLoader().load(dataUrl, (texture) => {
|
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]
|
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
|
// 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 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);
|
const head = createBodyPart(8, 8, 8, texture, headUvs);
|
||||||
|
|
@ -211,6 +284,8 @@ function updateSkinModel(dataUrl, isLegacy, targetGroup) {
|
||||||
targetGroup.add(layer);
|
targetGroup.add(layer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
targetGroup.rotation.y = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,7 +386,7 @@ function processSkinImage(img, srcUrl, isInitialLoad = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skinScene) initPreviewEngine();
|
if (!skinScene) initPreviewEngine();
|
||||||
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup);
|
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup, '3d');
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPreviewEngine() {
|
function initPreviewEngine() {
|
||||||
|
|
@ -391,3 +466,4 @@ async function saveSkinToDisk() {
|
||||||
window.openSkinManager = openSkinManager;
|
window.openSkinManager = openSkinManager;
|
||||||
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
|
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
|
||||||
window.loadMainMenuSkin = loadMainMenuSkin;
|
window.loadMainMenuSkin = loadMainMenuSkin;
|
||||||
|
window.toggleMainSkinRenderMode = toggleMainSkinRenderMode;
|
||||||
|
|
|
||||||
440
style.css
|
|
@ -1,6 +1,6 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Minecraft';
|
font-family: 'Minecraft';
|
||||||
src: url('Minecraft.ttf') format('truetype');
|
src: url('assets/Minecraft.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,6 +94,15 @@ body {
|
||||||
background: #c42b1c;
|
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 {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
@ -104,15 +113,16 @@ body {
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 380px;
|
width: 380px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.75);
|
||||||
border-right: 4px solid #000;
|
border-right: 4px solid #000;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 10;
|
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;
|
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 {
|
.sidebar.collapsed {
|
||||||
|
|
@ -286,7 +296,7 @@ body {
|
||||||
|
|
||||||
.content-area {
|
.content-area {
|
||||||
flex-grow: 1;
|
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-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -336,7 +346,7 @@ body {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background-color: var(--mc-button-bg);
|
background: linear-gradient(to bottom, #7e7e7e, #6e6e6e);
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
@ -345,33 +355,43 @@ body {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
cursor: pointer;
|
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;
|
margin-bottom: 16px;
|
||||||
position: relative;
|
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 {
|
.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;
|
position: relative;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:focus {
|
.nav-item:focus {
|
||||||
transform: scale(1.05);
|
transform: scale(1.02);
|
||||||
z-index: 1000;
|
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 {
|
.nav-item.active-bump {
|
||||||
transform: scale(0.95) !important;
|
transform: scale(0.96) !important;
|
||||||
transition: transform 0.1s !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) {
|
.btn-mc:hover:not(.disabled) {
|
||||||
background-color: var(--mc-button-hover);
|
background: linear-gradient(to bottom, #8e8e8e, #7e7e7e);
|
||||||
color: #fff;
|
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 {
|
.btn-mc.controller-active {
|
||||||
|
|
@ -400,6 +420,58 @@ body {
|
||||||
text-shadow: 2px 2px 0 #000;
|
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 {
|
.version-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
|
@ -496,12 +568,71 @@ body {
|
||||||
transition: width 0.2s ease-out;
|
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 {
|
.progress-text {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-shadow: 2px 2px 0 #000;
|
text-shadow: 2px 2px 0 #000;
|
||||||
margin-bottom: 6px;
|
text-align: left;
|
||||||
text-align: center;
|
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 {
|
.modal-overlay {
|
||||||
|
|
@ -520,14 +651,14 @@ body {
|
||||||
max-width: 850px;
|
max-width: 850px;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
background: var(--mc-gui-bg);
|
background: #c6c6c6;
|
||||||
border: 4px solid #000;
|
border: 4px solid #000;
|
||||||
padding: clamp(20px, 4vw, 50px);
|
padding: clamp(20px, 4vw, 50px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: inset 4px 4px 0 #fff;
|
box-shadow: inset 4px 4px 0 #fff, inset -4px -4px 0 #555, 0 20px 50px rgba(0,0,0,0.8);
|
||||||
animation: modalPop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
animation: modalPop 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
@ -608,11 +739,100 @@ body {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
outline: none;
|
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 {
|
.mc-input:focus, .mc-input.focused {
|
||||||
border-color: #fff !important;
|
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 {
|
#server-checkbox.focused {
|
||||||
|
|
@ -630,7 +850,7 @@ body {
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
background-image: url('minecraft.jpg');
|
background-image: url('assets/minecraft.jpg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
@ -642,12 +862,18 @@ body {
|
||||||
background: rgba(0,0,0,0.8);
|
background: rgba(0,0,0,0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.loader-content {
|
.loader-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#loader-text {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.loader-spinner {
|
.loader-spinner {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
|
|
@ -718,6 +944,25 @@ select.hidden-select {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
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 {
|
::-webkit-scrollbar {
|
||||||
|
|
@ -739,6 +984,135 @@ select.hidden-select {
|
||||||
background: #7e7e7e;
|
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
|
CLASSIC MINECRAFT LAUNCHER THEME
|
||||||
Applied when <body class="classic-theme"> is set
|
Applied when <body class="classic-theme"> is set
|
||||||
|
|
@ -1024,3 +1398,27 @@ body.classic-theme .sidebar-title {
|
||||||
body.classic-theme .music-btn {
|
body.classic-theme .music-btn {
|
||||||
bottom: 92px;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||