Launcher UI: MC/loader versions from server, split instances, console log sync, disable ZernMC for FREE
This commit is contained in:
@@ -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,37 +82,115 @@ 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"));
|
||||
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) {
|
||||
return ApiResponse.error(e.getMessage());
|
||||
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 {
|
||||
java.util.List<String> versions = new java.util.ArrayList<>();
|
||||
switch (loader.toLowerCase()) {
|
||||
case "fabric":
|
||||
versions.add("0.15.11");
|
||||
versions.add("0.15.9");
|
||||
versions.add("0.15.8");
|
||||
versions.add("0.14.21");
|
||||
break;
|
||||
case "forge":
|
||||
versions.add("47.1.0");
|
||||
versions.add("46.0.1");
|
||||
versions.add("45.0.2");
|
||||
break;
|
||||
case "neoforge":
|
||||
versions.add("1.21-rc.2");
|
||||
versions.add("1.20.4-rc.4");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
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);
|
||||
}
|
||||
}
|
||||
return ApiResponse.success(versions);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error(e.getMessage());
|
||||
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":
|
||||
versions.add("0.15.11");
|
||||
versions.add("0.15.9");
|
||||
versions.add("0.15.8");
|
||||
versions.add("0.14.21");
|
||||
break;
|
||||
case "forge":
|
||||
versions.add("47.1.0");
|
||||
versions.add("46.0.1");
|
||||
versions.add("45.0.2");
|
||||
break;
|
||||
case "neoforge":
|
||||
versions.add("1.21-rc.2");
|
||||
versions.add("1.20.4-rc.4");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ApiResponse.success(versions);
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,39 +269,64 @@ 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 card = document.createElement('div');
|
||||
card.className = 'instance-card';
|
||||
if (this.selectedInstance && this.selectedInstance.name === inst.name) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
card.dataset.name = inst.name;
|
||||
card.onclick = () => this.selectInstance(inst.name);
|
||||
const zernmcInstances = this.instances.filter(i => i.category === 'zernmc');
|
||||
const localInstances = this.instances.filter(i => i.category === 'local');
|
||||
|
||||
let details = `<span class="instance-card-version">${inst.version || '?'}</span>`;
|
||||
if (inst.loaderType && inst.loaderType !== 'vanilla') {
|
||||
details += `<span class="instance-card-loader">${inst.loaderType}</span>`;
|
||||
}
|
||||
if (inst.isServerPack) {
|
||||
details += `<span class="instance-card-server">v${inst.serverVersion}</span>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="instance-card-name">${this.escapeHtml(inst.name)}</div>
|
||||
<div class="instance-card-details">${details}</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
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) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
card.dataset.name = inst.name;
|
||||
card.onclick = () => this.selectInstance(inst.name);
|
||||
|
||||
let details = `<span class="instance-card-version">${inst.version || '?'}</span>`;
|
||||
if (inst.loaderType && inst.loaderType !== 'vanilla') {
|
||||
details += `<span class="instance-card-loader">${inst.loaderType}</span>`;
|
||||
}
|
||||
if (inst.isServerPack) {
|
||||
details += `<span class="instance-card-server">v${inst.serverVersion}</span>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="instance-card-name">${this.escapeHtml(inst.name)}</div>
|
||||
<div class="instance-card-details">${details}</div>
|
||||
`;
|
||||
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>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user