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.instance.InstanceService;
import me.sashegdev.zernmc.launcher.api.launch.LaunchService; 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.List;
import java.util.stream.Collectors;
/** /**
* Центральный фасад для внутреннего API лаунчера. * Центральный фасад для внутреннего API лаунчера.
@@ -12,6 +18,8 @@ import java.util.List;
*/ */
public class LauncherAPI { 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 AuthService authService;
private final InstanceService instanceService; private final InstanceService instanceService;
private final LaunchService launchService; private final LaunchService launchService;
@@ -74,14 +82,52 @@ public class LauncherAPI {
public ApiResponse<List<String>> getMCVersions() { public ApiResponse<List<String>> getMCVersions() {
try { 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")); URL url = new URL(LAUNCHER_SERVER + "/launcher/minecraft-versions");
} catch (Exception e) { HttpURLConnection conn = (HttpURLConnection) url.openConnection();
return ApiResponse.error(e.getMessage()); 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) { public ApiResponse<List<String>> getLoaderVersions(String mcVersion, String loader) {
try { 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<>(); java.util.List<String> versions = new java.util.ArrayList<>();
switch (loader.toLowerCase()) { switch (loader.toLowerCase()) {
case "fabric": case "fabric":
@@ -103,8 +149,48 @@ public class LauncherAPI {
break; break;
} }
return ApiResponse.success(versions); 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(); return AuthManager.getUsername();
} }
public String getCurrentToken() {
return AuthManager.getAccessToken();
}
public static class LoginResult { public static class LoginResult {
private String username; private String username;
private String token; private String token;
@@ -261,6 +261,7 @@ public class JFXLauncher extends Application {
server.createContext("/api/game-logs", this::handleGameLogs); server.createContext("/api/game-logs", this::handleGameLogs);
server.createContext("/api/mc-versions", this::handleMCVersions); server.createContext("/api/mc-versions", this::handleMCVersions);
server.createContext("/api/loader-versions", this::handleLoaderVersions); server.createContext("/api/loader-versions", this::handleLoaderVersions);
server.createContext("/api/packs", this::handlePacks);
server.createContext("/api/exit", this::handleExit); server.createContext("/api/exit", this::handleExit);
server.createContext("/assets/", this::handleStatic); 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) { private Map<String, String> parseQuery(String query) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
if (query != null && !query.isEmpty()) { if (query != null && !query.isEmpty()) {
+7 -10
View File
@@ -62,20 +62,17 @@
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">
<div class="current-instance-section"> <div class="instances-section">
<h3 class="section-label">Текущая сборка</h3> <h3 class="section-label">ZernMC сборки</h3>
<div id="current-instance" class="current-instance"> <div id="zernmc-instances-list" class="instances-list">
<div class="instance-card-mini"> <!-- ZernMC instances -->
<span class="instance-name">Загрузка...</span>
<span class="instance-version">-</span>
</div>
</div> </div>
</div> </div>
<div class="instances-section"> <div class="instances-section">
<h3 class="section-label">Все сборки</h3> <h3 class="section-label">Локальные сборки</h3>
<div id="instances-list" class="instances-list"> <div id="local-instances-list" class="instances-list">
<!-- Dynamic content --> <!-- Local instances -->
</div> </div>
</div> </div>
+162 -22
View File
@@ -1,4 +1,5 @@
const API_BASE = '/api'; const API_BASE = '/api';
let consoleLogPollingInterval = null;
class LauncherApp { class LauncherApp {
constructor() { constructor() {
@@ -10,15 +11,79 @@ class LauncherApp {
this.selectedInstance = null; this.selectedInstance = null;
this.hasUpdate = false; this.hasUpdate = false;
this.hasMismatches = false; this.hasMismatches = false;
this.consoleLogBuffer = '';
this.init(); this.init();
} }
async init() { async init() {
this.bindEvents(); this.bindEvents();
this.initGridAnimation(); this.initGridAnimation();
this.startConsoleLogPolling();
await this.checkAuth(); 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() { bindEvents() {
document.getElementById('login-form').addEventListener('submit', (e) => { document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
@@ -133,12 +198,11 @@ class LauncherApp {
this.showLoading(true); this.showLoading(true);
const result = await this.request('/account'); const result = await this.request('/account');
if (result.success && result.data) { if (result.success) {
this.account = result.data; this.account = result.data;
this.username = result.data.username; this.username = result.data.username;
this.showMainScreen(); this.showMainScreen();
await this.loadInstances(); await this.loadInstances();
await this.loadCurrentInstance();
} else { } else {
this.showLoginScreen(); this.showLoginScreen();
} }
@@ -178,7 +242,6 @@ class LauncherApp {
this.username = result.data.username; this.username = result.data.username;
this.showMainScreen(); this.showMainScreen();
await this.loadInstances(); await this.loadInstances();
await this.loadCurrentInstance();
} else { } else {
this.showError(result.error || 'Ошибка входа'); this.showError(result.error || 'Ошибка входа');
} }
@@ -206,16 +269,43 @@ class LauncherApp {
this.instances = result.data; this.instances = result.data;
this.renderInstances(); this.renderInstances();
this.addLog('Загружено ' + result.data.length + ' сборок', 'success'); 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 { } else {
this.addLog('Ошибка загрузки: ' + (result.error || 'неизвестная ошибка'), 'error'); this.addLog('Ошибка загрузки: ' + (result.error || 'неизвестная ошибка'), 'error');
} }
} }
renderInstances() { renderInstances() {
const container = document.getElementById('instances-list'); const zernmcContainer = document.getElementById('zernmc-instances-list');
container.innerHTML = ''; 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'); const card = document.createElement('div');
card.className = 'instance-card'; card.className = 'instance-card';
if (this.selectedInstance && this.selectedInstance.name === inst.name) { 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-name">${this.escapeHtml(inst.name)}</div>
<div class="instance-card-details">${details}</div> <div class="instance-card-details">${details}</div>
`; `;
return card;
container.appendChild(card);
});
} }
selectInstance(name) { selectInstance(name) {
@@ -247,18 +335,6 @@ class LauncherApp {
this.updatePlayButton(); 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() { updatePlayButton() {
const btn = document.getElementById('play-btn'); const btn = document.getElementById('play-btn');
const instance = this.selectedInstance || this.currentInstance; const instance = this.selectedInstance || this.currentInstance;
@@ -341,6 +417,53 @@ class LauncherApp {
async showDownloadModal() { async showDownloadModal() {
document.getElementById('download-modal').classList.remove('hidden'); 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() { hideDownloadModal() {
@@ -438,12 +561,28 @@ class LauncherApp {
} }
} }
onLoaderChange(loader) { async onLoaderChange(loader) {
const loaderVersionGroup = document.getElementById('loader-version-group'); 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') { if (loader === 'vanilla') {
loaderVersionGroup.classList.add('hidden'); loaderVersionGroup.classList.add('hidden');
} else { } else {
loaderVersionGroup.classList.remove('hidden'); 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() { clearLogs() {
this.consoleLogBuffer = '';
const container = document.getElementById('logs-container'); const container = document.getElementById('logs-container');
container.innerHTML = '<div class="log-entry info">Логи очищены</div>'; container.innerHTML = '<div class="log-entry info">Логи очищены</div>';
} }