Launcher UI: MC/loader versions from server, split instances, console log sync, disable ZernMC for FREE

This commit is contained in:
SashegDev
2026-05-09 23:55:08 +00:00
parent a8f3ca5049
commit e17b1d073a
5 changed files with 314 additions and 73 deletions
@@ -4,7 +4,13 @@ import me.sashegdev.zernmc.launcher.api.auth.AuthService;
import me.sashegdev.zernmc.launcher.api.instance.InstanceService;
import me.sashegdev.zernmc.launcher.api.launch.LaunchService;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
/**
* Центральный фасад для внутреннего API лаунчера.
@@ -12,6 +18,8 @@ import java.util.List;
*/
public class LauncherAPI {
private static final String LAUNCHER_SERVER = System.getProperty("launcher.server", "http://87.120.187.36:1582");
private final AuthService authService;
private final InstanceService instanceService;
private final LaunchService launchService;
@@ -74,14 +82,52 @@ public class LauncherAPI {
public ApiResponse<List<String>> getMCVersions() {
try {
return ApiResponse.success(java.util.List.of("1.21", "1.20.6", "1.20.4", "1.20.1", "1.19.4", "1.18.2", "1.17.1", "1.16.5"));
} catch (Exception e) {
return ApiResponse.error(e.getMessage());
URL url = new URL(LAUNCHER_SERVER + "/launcher/minecraft-versions");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
if (conn.getResponseCode() == 200) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
org.json.JSONArray arr = new org.json.JSONArray(sb.toString());
List<String> versions = new java.util.ArrayList<>();
for (int i = 0; i < arr.length(); i++) {
versions.add(arr.getString(i));
}
return ApiResponse.success(versions);
}
}
} catch (Exception e) {
System.out.println("[API] MC versions fetch failed, using fallback: " + e.getMessage());
}
return ApiResponse.success(java.util.List.of("1.21", "1.20.6", "1.20.4", "1.20.1", "1.19.4", "1.18.2", "1.17.1", "1.16.5"));
}
public ApiResponse<List<String>> getLoaderVersions(String mcVersion, String loader) {
try {
URL url = new URL(LAUNCHER_SERVER + "/launcher/loader-versions?mc=" + mcVersion + "&loader=" + loader.toLowerCase());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
if (conn.getResponseCode() == 200) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
org.json.JSONArray arr = new org.json.JSONArray(sb.toString());
List<String> versions = new java.util.ArrayList<>();
for (int i = 0; i < arr.length(); i++) {
versions.add(arr.getString(i));
}
return ApiResponse.success(versions);
}
}
} catch (Exception e) {
System.out.println("[API] Loader versions fetch failed, using fallback: " + e.getMessage());
}
java.util.List<String> versions = new java.util.ArrayList<>();
switch (loader.toLowerCase()) {
case "fabric":
@@ -103,8 +149,48 @@ public class LauncherAPI {
break;
}
return ApiResponse.success(versions);
} catch (Exception e) {
return ApiResponse.error(e.getMessage());
}
public ApiResponse<List<java.util.Map<String, String>>> getZernMCPacks() {
try {
String token = authService.getCurrentToken();
if (token == null) {
return ApiResponse.error("Не авторизован");
}
URL url = new URL(LAUNCHER_SERVER + "/packs");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
conn.setRequestProperty("Authorization", "Bearer " + token);
if (conn.getResponseCode() == 200) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
org.json.JSONArray arr = new org.json.JSONArray(sb.toString());
List<java.util.Map<String, String>> packs = new java.util.ArrayList<>();
for (int i = 0; i < arr.length(); i++) {
org.json.JSONObject pack = arr.getJSONObject(i);
java.util.Map<String, String> packInfo = new java.util.HashMap<>();
packInfo.put("name", pack.optString("name", ""));
packInfo.put("displayName", pack.optString("displayName", pack.optString("name", "")));
packInfo.put("version", pack.optString("version", ""));
packInfo.put("mcVersion", pack.optString("mcVersion", ""));
packInfo.put("loader", pack.optString("loader", "vanilla"));
packInfo.put("description", pack.optString("description", ""));
packs.add(packInfo);
}
return ApiResponse.success(packs);
}
} else if (conn.getResponseCode() == 403) {
return ApiResponse.error("Требуется проходка");
}
} catch (Exception e) {
System.out.println("[API] Packs fetch failed: " + e.getMessage());
return ApiResponse.error("Ошибка загрузки сборок: " + e.getMessage());
}
return ApiResponse.success(java.util.List.of());
}
}
@@ -105,6 +105,10 @@ public class AuthService {
return AuthManager.getUsername();
}
public String getCurrentToken() {
return AuthManager.getAccessToken();
}
public static class LoginResult {
private String username;
private String token;
@@ -261,6 +261,7 @@ public class JFXLauncher extends Application {
server.createContext("/api/game-logs", this::handleGameLogs);
server.createContext("/api/mc-versions", this::handleMCVersions);
server.createContext("/api/loader-versions", this::handleLoaderVersions);
server.createContext("/api/packs", this::handlePacks);
server.createContext("/api/exit", this::handleExit);
server.createContext("/assets/", this::handleStatic);
@@ -429,6 +430,19 @@ public class JFXLauncher extends Application {
}
}
private void handlePacks(HttpExchange exchange) {
try {
var result = api.getZernMCPacks();
if (result.isSuccess()) {
sendJson(exchange, Map.of("success", true, "data", result.getData()));
} else {
sendJson(exchange, Map.of("success", false, "error", result.getError()));
}
} catch (Exception e) {
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
}
}
private Map<String, String> parseQuery(String query) {
Map<String, String> params = new HashMap<>();
if (query != null && !query.isEmpty()) {
+7 -10
View File
@@ -62,20 +62,17 @@
</div>
<div class="sidebar-content">
<div class="current-instance-section">
<h3 class="section-label">Текущая сборка</h3>
<div id="current-instance" class="current-instance">
<div class="instance-card-mini">
<span class="instance-name">Загрузка...</span>
<span class="instance-version">-</span>
</div>
<div class="instances-section">
<h3 class="section-label">ZernMC сборки</h3>
<div id="zernmc-instances-list" class="instances-list">
<!-- ZernMC instances -->
</div>
</div>
<div class="instances-section">
<h3 class="section-label">Все сборки</h3>
<div id="instances-list" class="instances-list">
<!-- Dynamic content -->
<h3 class="section-label">Локальные сборки</h3>
<div id="local-instances-list" class="instances-list">
<!-- Local instances -->
</div>
</div>
+162 -22
View File
@@ -1,4 +1,5 @@
const API_BASE = '/api';
let consoleLogPollingInterval = null;
class LauncherApp {
constructor() {
@@ -10,15 +11,79 @@ class LauncherApp {
this.selectedInstance = null;
this.hasUpdate = false;
this.hasMismatches = false;
this.consoleLogBuffer = '';
this.init();
}
async init() {
this.bindEvents();
this.initGridAnimation();
this.startConsoleLogPolling();
await this.checkAuth();
}
startConsoleLogPolling() {
this.pollConsoleLogs();
consoleLogPollingInterval = setInterval(() => this.pollConsoleLogs(), 2000);
}
stopConsoleLogPolling() {
if (consoleLogPollingInterval) {
clearInterval(consoleLogPollingInterval);
consoleLogPollingInterval = null;
}
}
async pollConsoleLogs() {
try {
const response = await fetch(`${API_BASE}/logs`);
const result = await response.json();
if (result.success && result.data) {
const newLogs = result.data;
if (newLogs !== this.consoleLogBuffer) {
this.consoleLogBuffer = newLogs;
this.syncConsoleLogs(newLogs);
}
}
} catch (e) {
// Silent fail for polling
}
}
syncConsoleLogs(fullLogs) {
const lines = fullLogs.split('\n').filter(l => l.trim());
const container = document.getElementById('logs-container');
if (!container) return;
const existingLines = Array.from(container.querySelectorAll('.log-entry')).length;
const newLines = lines.slice(existingLines);
newLines.forEach(line => {
if (!line.trim()) return;
const entry = document.createElement('div');
entry.className = 'log-entry ' + this.getLogType(line);
const timestampMatch = line.match(/^\[(\d{2}:\d{2}:\d{2})\]/);
if (timestampMatch) {
entry.textContent = line;
} else {
const time = new Date().toLocaleTimeString();
entry.textContent = '[' + time + '] ' + line;
}
container.appendChild(entry);
});
container.scrollTop = container.scrollHeight;
}
getLogType(line) {
if (line.includes('[STDOUT]') || line.includes('Render thread/ERROR') || line.includes('/ERROR]:')) return 'error';
if (line.includes('[STDOUT]') || line.includes('Render thread/WARN') || line.includes('/WARN]:')) return 'warning';
if (line.includes('[STDOUT]') || line.includes('Render thread/INFO') || line.includes('/INFO]:')) return 'info';
if (line.includes(' успешно') || line.includes('Started') || line.includes(' запущен') || line.includes('done')) return 'success';
return 'info';
}
bindEvents() {
document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault();
@@ -133,12 +198,11 @@ class LauncherApp {
this.showLoading(true);
const result = await this.request('/account');
if (result.success && result.data) {
if (result.success) {
this.account = result.data;
this.username = result.data.username;
this.showMainScreen();
await this.loadInstances();
await this.loadCurrentInstance();
} else {
this.showLoginScreen();
}
@@ -178,7 +242,6 @@ class LauncherApp {
this.username = result.data.username;
this.showMainScreen();
await this.loadInstances();
await this.loadCurrentInstance();
} else {
this.showError(result.error || 'Ошибка входа');
}
@@ -206,16 +269,43 @@ class LauncherApp {
this.instances = result.data;
this.renderInstances();
this.addLog('Загружено ' + result.data.length + ' сборок', 'success');
if (this.instances.length > 0 && !this.selectedInstance) {
this.currentInstance = this.instances[0];
this.selectedInstance = this.currentInstance;
this.addLog('Сборка загружена: ' + this.currentInstance.name, 'success');
}
this.updatePlayButton();
} else {
this.addLog('Ошибка загрузки: ' + (result.error || 'неизвестная ошибка'), 'error');
}
}
renderInstances() {
const container = document.getElementById('instances-list');
container.innerHTML = '';
const zernmcContainer = document.getElementById('zernmc-instances-list');
const localContainer = document.getElementById('local-instances-list');
zernmcContainer.innerHTML = '';
localContainer.innerHTML = '';
this.instances.forEach(inst => {
const zernmcInstances = this.instances.filter(i => i.category === 'zernmc');
const localInstances = this.instances.filter(i => i.category === 'local');
zernmcInstances.forEach(inst => {
zernmcContainer.appendChild(this.createInstanceCard(inst));
});
localInstances.forEach(inst => {
localContainer.appendChild(this.createInstanceCard(inst));
});
if (zernmcInstances.length === 0) {
zernmcContainer.innerHTML = '<div class="log-entry info" style="padding:8px;font-size:12px">Нет сборок</div>';
}
if (localInstances.length === 0) {
localContainer.innerHTML = '<div class="log-entry info" style="padding:8px;font-size:12px">Нет сборок</div>';
}
}
createInstanceCard(inst) {
const card = document.createElement('div');
card.className = 'instance-card';
if (this.selectedInstance && this.selectedInstance.name === inst.name) {
@@ -236,9 +326,7 @@ class LauncherApp {
<div class="instance-card-name">${this.escapeHtml(inst.name)}</div>
<div class="instance-card-details">${details}</div>
`;
container.appendChild(card);
});
return card;
}
selectInstance(name) {
@@ -247,18 +335,6 @@ class LauncherApp {
this.updatePlayButton();
}
async loadCurrentInstance() {
if (this.instances.length > 0) {
this.currentInstance = this.instances[0];
this.selectedInstance = this.currentInstance;
this.renderCurrentInstance(this.currentInstance);
this.addLog('Сборка загружена: ' + this.currentInstance.name, 'success');
} else {
this.renderNoInstance();
}
this.updatePlayButton();
}
updatePlayButton() {
const btn = document.getElementById('play-btn');
const instance = this.selectedInstance || this.currentInstance;
@@ -341,6 +417,53 @@ class LauncherApp {
async showDownloadModal() {
document.getElementById('download-modal').classList.remove('hidden');
await this.loadDownloadModalData();
}
async loadDownloadModalData() {
const mcSelect = document.getElementById('mc-version-select');
mcSelect.innerHTML = '<option value="">Загрузка...</option>';
const mcResult = await this.request('/mc-versions');
if (mcResult.success && mcResult.data) {
mcSelect.innerHTML = '<option value="">Выберите версию</option>';
mcResult.data.forEach(v => {
const opt = document.createElement('option');
opt.value = v;
opt.textContent = v;
mcSelect.appendChild(opt);
});
}
const packSelect = document.getElementById('zernmc-pack-select');
packSelect.innerHTML = '<option value="">Загрузка...</option>';
const packResult = await this.request('/packs');
if (packResult.success && packResult.data && packResult.data.length > 0) {
packSelect.innerHTML = '<option value="">Выберите сборку</option>';
packResult.data.forEach(p => {
const opt = document.createElement('option');
opt.value = p.name;
opt.textContent = p.displayName + ' (' + p.version + ')';
packSelect.appendChild(opt);
});
} else if (packResult.error && packResult.error.includes('проходка')) {
packSelect.innerHTML = '<option value="">Требуется проходка</option>';
} else {
packSelect.innerHTML = '<option value="">Сборки недоступны</option>';
}
const zernmcTab = document.querySelector('[data-tab="zernmc"]');
if (this.account && !this.account.passActive) {
zernmcTab.disabled = true;
zernmcTab.style.opacity = '0.5';
zernmcTab.style.cursor = 'not-allowed';
this.switchTab('vanilla');
} else {
zernmcTab.disabled = false;
zernmcTab.style.opacity = '1';
zernmcTab.style.cursor = 'pointer';
}
}
hideDownloadModal() {
@@ -438,12 +561,28 @@ class LauncherApp {
}
}
onLoaderChange(loader) {
async onLoaderChange(loader) {
const loaderVersionGroup = document.getElementById('loader-version-group');
const loaderVersionSelect = document.getElementById('loader-version-select');
const mcVersion = document.getElementById('mc-version-select').value;
if (loader === 'vanilla') {
loaderVersionGroup.classList.add('hidden');
} else {
loaderVersionGroup.classList.remove('hidden');
if (mcVersion) {
loaderVersionSelect.innerHTML = '<option value="">Загрузка...</option>';
const result = await this.request('/loader-versions?mc=' + mcVersion + '&loader=' + loader);
if (result.success && result.data) {
loaderVersionSelect.innerHTML = '<option value="">Выберите версию</option>';
result.data.forEach(v => {
const opt = document.createElement('option');
opt.value = v;
opt.textContent = v;
loaderVersionSelect.appendChild(opt);
});
}
}
}
}
@@ -471,6 +610,7 @@ class LauncherApp {
}
clearLogs() {
this.consoleLogBuffer = '';
const container = document.getElementById('logs-container');
container.innerHTML = '<div class="log-entry info">Логи очищены</div>';
}