LegacyLauncher/index.html
2026-03-04 22:05:15 +01:00

1182 lines
40 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LegacyLauncher</title>
<link rel="icon" type="image/png" href="256x256.png">
<script src="https://cdn.tailwindcss.com"></script>
<style>
@font-face {
font-family: 'Minecraft';
src: url('Minecraft.ttf') format('truetype');
}
:root {
--mc-gui-bg: #c6c6c6;
--mc-progress-bg: #313131;
--mc-progress-fill: #55ff55;
--mc-border-dark: #555;
--mc-border-light: #fff;
--mc-button-bg: #6e6e6e;
--mc-button-hover: #7e7e7e;
}
* {
box-sizing: border-box;
-webkit-font-smoothing: none;
}
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
background-color: var(--mc-gui-bg);
font-family: 'Minecraft', monospace;
overflow: hidden;
image-rendering: pixelated;
-webkit-user-select: none;
user-select: none;
border: 2px solid #000;
}
/* Title Bar */
.title-bar {
height: 32px;
background: #222;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
-webkit-app-region: drag;
z-index: 1000;
border-bottom: 2px solid #000;
}
.title-bar-text {
color: #aaa;
font-size: 16px;
text-shadow: 1px 1px 0 #000;
}
.window-controls {
display: flex;
gap: 5px;
-webkit-app-region: no-drag;
}
.win-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #aaa;
font-size: 18px;
transition: all 0.1s;
}
.win-btn:hover {
background: #444;
color: #fff;
}
.win-btn.close:hover {
background: #c42b1c;
}
.content-area {
flex-grow: 1;
background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('minecraft.jpg');
background-size: cover;
background-position: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.mc-logo-img {
width: 500px;
max-width: 90%;
filter: drop-shadow(6px 6px 0px rgba(0,0,0,0.6));
margin-bottom: 40px;
transition: transform 0.3s ease;
}
.mc-logo-img:hover {
transform: scale(1.02);
}
.btn-mc {
width: 400px;
max-width: 90%;
height: 52px;
background-color: var(--mc-button-bg);
border: 2px solid #000;
color: #e0e0e0;
font-size: 26px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: inset -3px -3px 0px #333, inset 3px 3px 0px #aaa;
margin-bottom: 14px;
position: relative;
transition: transform 0.05s;
}
.btn-mc:hover:not(.disabled) {
background-color: var(--mc-button-hover);
color: #fff;
outline: 2px solid #fff;
z-index: 5;
}
.btn-mc:active:not(.disabled) {
box-shadow: inset 3px 3px 0px #333, inset -3px -3px 0px #aaa;
transform: translateY(2px);
}
.btn-mc.disabled, .btn-mc.running {
background-color: #4e4e4e;
color: #888;
cursor: default;
box-shadow: inset -3px -3px 0px #222, inset 3px 3px 0px #666;
opacity: 0.8;
transform: none !important;
}
.btn-mc.running {
color: #55ff55;
text-shadow: 2px 2px 0 #000;
}
.btn-play {
height: 80px;
font-size: 48px;
margin-bottom: 30px;
color: #fff;
text-shadow: 2px 2px 0 #000;
}
.version-row {
display: flex;
width: 400px;
max-width: 90%;
margin-bottom: 24px;
align-items: center;
}
.version-label {
color: white;
font-size: 24px;
margin-right: 15px;
text-shadow: 2px 2px 0 #000;
white-space: nowrap;
}
.version-select-box {
flex-grow: 1;
height: 48px;
background: #000;
border: 2px solid #555;
display: flex;
align-items: center;
padding: 0 15px;
color: white;
font-size: 22px;
cursor: pointer;
position: relative;
transition: border-color 0.2s;
}
.version-select-box:hover {
border-color: #fff;
}
.select-arrow {
width: 40px;
height: 44px;
background: #6e6e6e;
border-left: 2px solid #555;
display: flex;
align-items: center;
justify-content: center;
color: white;
position: absolute;
right: 0;
}
/* Progress Bar Styling */
.progress-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background: rgba(0,0,0,0.9);
border-top: 4px solid #000;
display: none;
flex-direction: column;
justify-content: center;
padding: 0 30px;
z-index: 50;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.progress-bar-bg {
width: 100%;
height: 16px;
background: var(--mc-progress-bg);
border: 2px solid #000;
position: relative;
}
.progress-bar-fill {
width: 0%;
height: 100%;
background: var(--mc-progress-fill);
box-shadow: inset 0 3px 0 rgba(255,255,255,0.4);
transition: width 0.2s ease-out;
}
.progress-text {
color: #fff;
font-size: 20px;
text-shadow: 2px 2px 0 #000;
margin-bottom: 6px;
text-align: center;
}
/* Settings Modal */
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.9);
display: none;
align-items: center;
justify-content: center;
z-index: 150;
backdrop-filter: blur(4px);
}
.modal-box {
width: 600px;
max-width: 95%;
background: var(--mc-gui-bg);
border: 4px solid #000;
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: inset 4px 4px 0 #fff;
animation: modalPop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes modalPop {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal-title {
font-size: 42px;
color: #333;
margin-bottom: 30px;
text-shadow: 2px 2px 0 #fff;
text-align: center;
}
.mc-input-group {
width: 100%;
margin-bottom: 20px;
}
.mc-label {
display: block;
color: #444;
font-size: 22px;
margin-bottom: 6px;
}
.mc-input {
width: 100%;
background: #000;
border: 2px solid #555;
color: white;
padding: 12px;
font-size: 22px;
outline: none;
transition: border-color 0.2s;
}
.mc-input:focus {
border-color: #fff;
}
#loader {
position: absolute;
inset: 0;
background: rgba(0,0,0,1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 36px;
z-index: 2000;
background-image: url('minecraft.jpg');
background-size: cover;
background-position: center;
}
#loader::before {
content: '';
position: absolute;
inset: 0;
background: rgba(0,0,0,0.8);
}
.loader-content {
position: relative;
z-index: 1;
text-align: center;
}
.loader-spinner {
width: 80px;
height: 80px;
border: 8px solid #333;
border-top: 8px solid var(--mc-progress-fill);
border-radius: 50%;
margin: 0 auto 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.95);
color: #fff;
padding: 15px 40px;
border: 3px solid #fff;
font-size: 24px;
display: none;
z-index: 3000;
text-align: center;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
}
select.hidden-select {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #333;
border-left: 2px solid #000;
}
::-webkit-scrollbar-thumb {
background: #6e6e6e;
border: 2px solid #000;
box-shadow: inset -2px -2px 0 #333, inset 2px 2px 0 #aaa;
}
::-webkit-scrollbar-thumb:hover {
background: #7e7e7e;
}
</style>
</head>
<body>
<div class="title-bar">
<div class="title-bar-text">LegacyLauncher</div>
<div class="window-controls">
<div class="win-btn" onclick="minimizeWindow()"></div>
<div class="win-btn" id="maximize-btn" onclick="toggleMaximize()"></div>
<div class="win-btn close" onclick="closeWindow()">×</div>
</div>
</div>
<div id="loader">
<div class="loader-content">
<div class="loader-spinner"></div>
<div id="loader-text">CONNECTING...</div>
</div>
</div>
<div class="content-area">
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="btn-play-main" class="btn-mc btn-play" onclick="launchGame()">PLAY</div>
<div class="version-row">
<span class="version-label">Version:</span>
<div class="version-select-box">
<span id="current-version-display">Loading...</span>
<div class="select-arrow"></div>
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
<option>Loading...</option>
</select>
</div>
</div>
<div class="flex gap-4 w-[400px] max-w-[90%]">
<div class="btn-mc flex-grow" id="btn-profile" onclick="toggleProfile(true)">PROFILE</div>
<div class="btn-mc flex-grow" id="btn-options" onclick="toggleOptions(true)">OPTIONS</div>
</div>
<!-- Progress Bar at bottom -->
<div class="progress-container" id="progress-container">
<div class="progress-text" id="progress-text">Downloading...</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progress-bar-fill"></div>
</div>
</div>
</div>
<!-- Options Modal -->
<div class="modal-overlay" id="options-modal">
<div class="modal-box">
<div class="modal-title">LAUNCHER OPTIONS</div>
<div class="mc-input-group">
<label class="mc-label">GitHub Repository Source:</label>
<input type="text" id="repo-input" class="mc-input" placeholder="user/repo">
</div>
<div class="mc-input-group">
<label class="mc-label">Client Executable Name:</label>
<input type="text" id="exec-input" class="mc-input" placeholder="Minecraft.Client.exe">
</div>
<div id="compat-option-container" class="mc-input-group" style="display: none;">
<label class="mc-label">Compatibility Layer (Linux):</label>
<div class="version-select-box" style="width: 100%; height: 48px; margin-top: 8px;">
<span id="current-compat-display">Default (Direct)</span>
<div class="select-arrow" style="height: 44px;"></div>
<select id="compat-select" class="hidden-select" onchange="updateCompatDisplay()">
<option value="direct">Default (Direct)</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-4 w-full mt-2">
<div class="mc-input-group">
<label class="mc-label">Connect/Bind IP:</label>
<input type="text" id="ip-input" class="mc-input" placeholder="Optional">
</div>
<div class="mc-input-group">
<label class="mc-label">Port:</label>
<input type="text" id="port-input" class="mc-input" placeholder="Optional">
</div>
</div>
<div class="mc-input-group">
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="server-checkbox" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;">
Launch as Headless Server (-server)
</label>
</div>
<div class="flex gap-4 w-full mt-4">
<div class="btn-mc flex-grow" onclick="saveOptions()">DONE</div>
<div class="btn-mc flex-grow" onclick="toggleOptions(false)">CANCEL</div>
</div>
</div>
</div>
<!-- Profile Modal -->
<div class="modal-overlay" id="profile-modal">
<div class="modal-box">
<div class="modal-title">PLAYER PROFILE</div>
<div class="mc-input-group">
<label class="mc-label">In-Game Username:</label>
<input type="text" id="username-input" class="mc-input" placeholder="Steve">
</div>
<div class="mc-input-group mt-4">
<label class="mc-label">Total Playtime:</label>
<div id="playtime-display" style="color: #222; font-size: 26px; text-shadow: 1px 1px 0 #fff;">0h 0m 0s</div>
</div>
<div class="flex gap-4 w-full mt-4">
<div class="btn-mc flex-grow" onclick="saveProfile()">SAVE</div>
<div class="btn-mc flex-grow" onclick="toggleProfile(false)">CANCEL</div>
</div>
</div>
</div>
<!-- Update Confirmation Modal -->
<div class="modal-overlay" id="update-modal">
<div class="modal-box">
<div class="modal-title">UPDATE AVAILABLE</div>
<p id="update-modal-text" style="color: #333; font-size: 22px; text-align: center; margin-bottom: 30px; text-shadow: 1px 1px 0 #fff; line-height: 1.4;">
A new version is available. Would you like to update now?
</p>
<div class="flex flex-col gap-4 w-full">
<div class="btn-mc w-full" id="btn-confirm-update">UPDATE NOW</div>
<div class="btn-mc w-full" id="btn-skip-update">LATER (LAUNCH OLD)</div>
</div>
</div>
</div>
<div id="toast">NOTIFICATION</div>
<script>
const DEFAULT_REPO = "smartcmd/MinecraftConsoles";
const DEFAULT_EXEC = "Minecraft.Client.exe";
const TARGET_FILE = "LCEWindows64.zip";
let releasesData = [];
let currentReleaseIndex = 0;
let isProcessing = false;
let isGameRunning = false;
const isElectron = window.process && window.process.type === 'renderer';
let fs, path, https, extractZip, childProcess;
if (isElectron) {
fs = require('fs');
path = require('path');
https = require('https');
extractZip = require('extract-zip');
childProcess = require('child_process');
}
window.onload = () => {
document.getElementById('repo-input').value = getRepo();
document.getElementById('exec-input').value = getExecPath();
document.getElementById('username-input').value = getUsername();
document.getElementById('ip-input').value = getIP();
document.getElementById('port-input').value = getPort();
document.getElementById('server-checkbox').checked = getIsServer();
if (isElectron && process.platform === 'linux') {
document.getElementById('compat-option-container').style.display = 'block';
scanCompatibilityLayers();
}
if (isElectron) {
const { ipcRenderer } = require('electron');
ipcRenderer.on('window-is-maximized', (event, isMaximized) => {
document.getElementById('maximize-btn').textContent = isMaximized ? '❐' : '▢';
});
}
fetchGitHubData();
};
function getRepo() {
return localStorage.getItem('legacy_repo') || DEFAULT_REPO;
}
function getExecPath() {
return localStorage.getItem('legacy_exec_path') || DEFAULT_EXEC;
}
function getUsername() {
return localStorage.getItem('legacy_username') || "";
}
function getIP() {
return localStorage.getItem('legacy_ip') || "";
}
function getPort() {
return localStorage.getItem('legacy_port') || "";
}
function getIsServer() {
return localStorage.getItem('legacy_is_server') === 'true';
}
function getPlaytime() {
return parseInt(localStorage.getItem('legacy_playtime')) || 0;
}
function formatPlaytime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h}h ${m}m ${s}s`;
}
function updatePlaytimeDisplay() {
const el = document.getElementById('playtime-display');
if (el) el.textContent = formatPlaytime(getPlaytime());
}
function getSavedCompat() {
return localStorage.getItem('legacy_compat_layer') || 'direct';
}
function updateCompatDisplay() {
const select = document.getElementById('compat-select');
const display = document.getElementById('current-compat-display');
display.textContent = select.options[select.selectedIndex].text;
}
async function scanCompatibilityLayers() {
const select = document.getElementById('compat-select');
const savedValue = getSavedCompat();
// Basic options
const layers = [
{ name: 'Default (Direct)', cmd: 'direct' },
{ name: 'Wine64', cmd: 'wine64' },
{ name: 'Wine', cmd: 'wine' }
];
// Scan for Proton in common locations
const homeDir = require('os').homedir();
const steamPaths = [
path.join(homeDir, '.steam', 'steam', 'steamapps', 'common'),
path.join(homeDir, '.local', 'share', 'Steam', 'steamapps', 'common'),
path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam', 'steamapps', 'common')
];
for (const steamPath of steamPaths) {
if (fs.existsSync(steamPath)) {
try {
const dirs = fs.readdirSync(steamPath);
dirs.filter(d => d.startsWith('Proton')).forEach(d => {
const protonPath = path.join(steamPath, d, 'proton');
if (fs.existsSync(protonPath)) {
layers.push({ name: d, cmd: protonPath });
}
});
} catch (e) {}
}
}
// Populate select
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;
});
updateCompatDisplay();
}
function getInstalledPath() {
if (!isElectron) return "";
const homeDir = require('os').homedir();
return path.join(homeDir, 'Downloads', 'LegacyClient', getExecPath());
}
function checkIsInstalled(tag) {
if (!isElectron) return false;
const fullPath = getInstalledPath();
const installedTag = localStorage.getItem('installed_version_tag');
return fs.existsSync(fullPath) && installedTag === tag;
}
function updatePlayButtonText() {
const btn = document.getElementById('btn-play-main');
if (isProcessing) return;
if (isGameRunning) {
btn.textContent = "GAME RUNNING";
btn.classList.add('running');
return;
} else {
btn.classList.remove('running');
}
const release = releasesData[currentReleaseIndex];
if (!release) {
btn.textContent = "PLAY";
return;
}
if (checkIsInstalled(release.tag_name)) {
btn.textContent = "PLAY";
} else {
const installedTag = localStorage.getItem('installed_version_tag');
const fullPath = getInstalledPath();
if (fs && fs.existsSync(fullPath)) {
btn.textContent = "UPDATE";
} else {
btn.textContent = "INSTALL";
}
}
}
function updateRPC(details, state, startTime = null) {
if (isElectron) {
const { ipcRenderer } = require('electron');
ipcRenderer.send('update-rpc', { details, state, startTime });
}
}
function setGameRunning(running) {
isGameRunning = running;
updatePlayButtonText();
if (!running) {
updateRPC('In Menus', 'Ready to Play');
}
}
async function monitorProcess(proc) {
if (!proc) return;
const sessionStart = Date.now();
setGameRunning(true);
// Update RPC for playing
const release = releasesData[currentReleaseIndex];
const version = release ? release.tag_name : 'Unknown';
updateRPC(`Playing Legacy (${version})`, getIsServer() ? 'Running Headless Server' : 'In Game', sessionStart);
proc.on('exit', () => {
const sessionDuration = Math.floor((Date.now() - sessionStart) / 1000);
const total = getPlaytime() + sessionDuration;
localStorage.setItem('legacy_playtime', total);
setGameRunning(false);
});
proc.on('error', (err) => {
console.error("Process error:", err);
setGameRunning(false);
});
}
function minimizeWindow() {
if (isElectron) {
const { ipcRenderer } = require('electron');
ipcRenderer.send('window-minimize');
}
}
function toggleMaximize() {
if (isElectron) {
const { ipcRenderer } = require('electron');
ipcRenderer.send('window-maximize');
}
}
function closeWindow() {
if (isElectron) {
const { ipcRenderer } = require('electron');
ipcRenderer.send('window-close');
} else {
window.close();
}
}
async function fetchGitHubData() {
const repo = getRepo();
const loader = document.getElementById('loader');
const loaderText = document.getElementById('loader-text');
loader.style.display = 'flex';
loaderText.textContent = "SYNCING: " + repo;
try {
const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
if (!response.ok) throw new Error("Rate Limited");
releasesData = await response.json();
populateVersions();
setTimeout(() => {
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 300);
}, 500);
} catch (err) {
loaderText.textContent = "REPO NOT FOUND OR API ERROR";
showToast("Check repository name in Options.");
setTimeout(() => {
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 300);
}, 2500);
}
}
function populateVersions() {
const select = document.getElementById('version-select');
const display = document.getElementById('current-version-display');
select.innerHTML = '';
if(releasesData.length === 0) {
display.textContent = "No Releases Found";
return;
}
releasesData.forEach((rel, index) => {
const opt = document.createElement('option');
opt.value = index;
opt.textContent = `Legacy (${rel.tag_name})`;
select.appendChild(opt);
if(index === 0) display.textContent = opt.textContent;
});
currentReleaseIndex = 0;
updatePlayButtonText();
}
function updateSelectedRelease() {
const select = document.getElementById('version-select');
currentReleaseIndex = select.value;
document.getElementById('current-version-display').textContent = select.options[select.selectedIndex].text;
updatePlayButtonText();
}
async function launchGame() {
if (isProcessing || isGameRunning) return;
const release = releasesData[currentReleaseIndex];
if (!release) return;
const asset = release.assets.find(a => a.name === TARGET_FILE);
if (!asset) {
showToast("ZIP Asset missing in this version!");
return;
}
if (isElectron) {
const isInstalled = checkIsInstalled(release.tag_name);
if (isInstalled) {
setProcessingState(true);
updateProgress(100, "Launching...");
await launchLocalClient();
setProcessingState(false);
} else {
const fullPath = getInstalledPath();
if (fs.existsSync(fullPath)) {
// Different version installed
const choice = await promptUpdate(release.tag_name);
if (choice === 'update') {
setProcessingState(true);
await handleElectronFlow(asset.browser_download_url);
setProcessingState(false);
} else {
// Launch existing version
setProcessingState(true);
updateProgress(100, "Launching Existing...");
await launchLocalClient();
setProcessingState(false);
}
} else {
// Nothing installed
setProcessingState(true);
await handleElectronFlow(asset.browser_download_url);
setProcessingState(false);
}
}
} else {
setProcessingState(true);
await simulateWorkflow();
setProcessingState(false);
}
updatePlayButtonText();
}
function promptUpdate(newTag) {
return new Promise((resolve) => {
const modal = document.getElementById('update-modal');
const confirmBtn = document.getElementById('btn-confirm-update');
const skipBtn = document.getElementById('btn-skip-update');
const installedTag = localStorage.getItem('installed_version_tag') || "Unknown";
document.getElementById('update-modal-text').innerHTML =
`New version <b>${newTag}</b> is available.<br><br>` +
`Currently installed: <b>${installedTag}</b>.<br><br>` +
`Would you like to update now?`;
modal.style.display = 'flex';
modal.style.opacity = '1';
const cleanup = (result) => {
modal.style.opacity = '0';
setTimeout(() => modal.style.display = 'none', 300);
confirmBtn.onclick = null;
skipBtn.onclick = null;
resolve(result);
};
confirmBtn.onclick = () => cleanup('update');
skipBtn.onclick = () => cleanup('launch');
});
}
async function launchLocalClient() {
const fullPath = getInstalledPath();
if (!fs.existsSync(fullPath)) {
throw new Error("Executable not found! Try reinstalling.");
}
return new Promise((resolve, reject) => {
const compat = getSavedCompat();
const username = getUsername();
const ip = getIP();
const port = getPort();
const isServer = getIsServer();
let args = [];
if (username) args.push("-name", username);
if (isServer) args.push("-server");
if (ip) args.push("-ip", ip);
if (port) args.push("-port", port);
const argString = args.map(a => `"${a}"`).join(" ");
let cmd = `"${fullPath}" ${argString}`;
if (isElectron && process.platform === 'linux') {
if (compat === 'wine64' || compat === 'wine') {
cmd = `${compat} "${fullPath}" ${argString}`;
} else if (compat.includes('Proton')) {
// Launching with Proton requires prefix and specific environment
const prefix = path.join(path.dirname(fullPath), 'pfx');
if (!fs.existsSync(prefix)) fs.mkdirSync(prefix, { recursive: true });
cmd = `STEAM_COMPAT_CLIENT_INSTALL_PATH="" STEAM_COMPAT_DATA_PATH="${prefix}" "${compat}" run "${fullPath}" ${argString}`;
}
}
console.log("Launching command:", cmd);
const startTime = Date.now();
const proc = childProcess.exec(cmd, (error) => {
const duration = Date.now() - startTime;
if (error && duration < 2000) { // Only show error if it failed in less than 2 seconds
showToast("Failed to launch: " + error.message);
reject(error);
} else {
resolve();
}
});
monitorProcess(proc);
});
}
function setProcessingState(active) {
isProcessing = active;
const playBtn = document.getElementById('btn-play-main');
const optionsBtn = document.getElementById('btn-options');
const progressContainer = document.getElementById('progress-container');
if (active) {
playBtn.classList.add('disabled');
optionsBtn.classList.add('disabled');
progressContainer.style.display = 'flex';
updateProgress(0, "Preparing...");
} else {
playBtn.classList.remove('disabled');
optionsBtn.classList.remove('disabled');
progressContainer.style.display = 'none';
}
}
function updateProgress(percent, text) {
document.getElementById('progress-bar-fill').style.width = percent + "%";
if (text) document.getElementById('progress-text').textContent = text;
}
// Electron-specific flow (Requires Node integration in Electron)
async function handleElectronFlow(url) {
try {
const homeDir = require('os').homedir();
const downloadDir = path.join(homeDir, 'Downloads');
const zipPath = path.join(downloadDir, TARGET_FILE);
const extractDir = path.join(downloadDir, 'LegacyClient');
// 1. Download the file
updateProgress(5, "Downloading " + TARGET_FILE + "...");
await downloadFile(url, zipPath);
// 2. Extract the archive
updateProgress(75, "Extracting Archive...");
if (!fs.existsSync(extractDir)) {
fs.mkdirSync(extractDir, { recursive: true });
}
await extractZip(zipPath, { dir: extractDir });
// 3. Launch the executable
const execName = getExecPath();
const fullPath = path.join(extractDir, execName);
if (!fs.existsSync(fullPath)) {
showToast("Executable not found at: " + execName);
return;
}
updateProgress(100, "Launching...");
// Save installed version tag
localStorage.setItem('installed_version_tag', releasesData[currentReleaseIndex].tag_name);
await new Promise(r => setTimeout(r, 800));
await launchLocalClient();
} catch (e) {
showToast("Error: " + e.message);
}
}
// Helper function to download file with progress tracking
function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
// Ensure directory exists
const dir = path.dirname(destPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const file = fs.createWriteStream(destPath);
let totalSize = 0;
let downloadedSize = 0;
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
downloadFile(response.headers.location, destPath).then(resolve).catch(reject);
return;
}
totalSize = parseInt(response.headers['content-length'], 10);
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const percent = Math.floor((downloadedSize / totalSize) * 70) + 5;
updateProgress(percent, `Downloading... ${percent}%`);
});
response.pipe(file);
file.on('finish', () => {
file.close(() => resolve());
});
file.on('error', (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
}).on('error', (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
});
}
async function simulateWorkflow() {
for (let i = 0; i <= 70; i += Math.floor(Math.random() * 8) + 2) {
updateProgress(i, `Downloading Assets... ${i}%`);
await new Promise(r => setTimeout(r, 80));
}
updateProgress(75, "Extracting Archive...");
await new Promise(r => setTimeout(r, 1200));
for (let i = 75; i <= 100; i += 5) {
updateProgress(i, `Extracting... ${i}%`);
await new Promise(r => setTimeout(r, 60));
}
updateProgress(100, "Starting " + getExecPath() + "...");
await new Promise(r => setTimeout(r, 800));
showToast("Launched " + getExecPath());
}
function toggleOptions(show) {
if (isProcessing) return;
const modal = document.getElementById('options-modal');
if (show) {
modal.style.display = 'flex';
modal.style.opacity = '1';
} else {
modal.style.opacity = '0';
setTimeout(() => modal.style.display = 'none', 300);
}
}
function toggleProfile(show) {
if (isProcessing) return;
const modal = document.getElementById('profile-modal');
if (show) {
updatePlaytimeDisplay();
modal.style.display = 'flex';
modal.style.opacity = '1';
} else {
modal.style.opacity = '0';
setTimeout(() => modal.style.display = 'none', 300);
}
}
function saveOptions() {
const newRepo = document.getElementById('repo-input').value.trim();
const newExec = document.getElementById('exec-input').value.trim();
const compatSelect = document.getElementById('compat-select');
const ip = document.getElementById('ip-input').value.trim();
const port = document.getElementById('port-input').value.trim();
const isServer = document.getElementById('server-checkbox').checked;
if (newRepo) localStorage.setItem('legacy_repo', newRepo);
if (newExec) localStorage.setItem('legacy_exec_path', newExec);
localStorage.setItem('legacy_ip', ip);
localStorage.setItem('legacy_port', port);
localStorage.setItem('legacy_is_server', isServer);
if (compatSelect) {
localStorage.setItem('legacy_compat_layer', compatSelect.value);
}
toggleOptions(false);
fetchGitHubData();
updatePlayButtonText();
showToast("Settings Saved");
}
function saveProfile() {
const username = document.getElementById('username-input').value.trim();
localStorage.setItem('legacy_username', username);
toggleProfile(false);
showToast("Profile Updated");
}
function showToast(msg) {
const t = document.getElementById('toast');
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);
}
</script>
</body>
</html>