mirror of
https://github.com/gradenGnostic/LegacyLauncher.git
synced 2026-04-24 07:56:40 +00:00
1182 lines
40 KiB
HTML
1182 lines
40 KiB
HTML
<!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>
|