diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java index b5c0251..3dcf791 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java @@ -474,6 +474,7 @@ public class JFXLauncher extends Application { server.createContext("/api/launch", this::handleLaunch); server.createContext("/api/install", this::handleInstall); server.createContext("/api/install/progress", this::handleInstallProgress); + server.createContext("/api/network/status", this::handleNetworkStatus); server.createContext("/api/logs", this::handleLogs); server.createContext("/api/logs/stream", this::handleLogsStream); server.createContext("/api/game-logs", this::handleGameLogs); @@ -842,6 +843,14 @@ public class JFXLauncher extends Application { } } + private void handleNetworkStatus(HttpExchange exchange) { + try { + sendJson(exchange, Map.of("success", true, "data", ZHttpClient.getNetworkStatus())); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + private void handleLogs(HttpExchange exchange) { sendJson(exchange, Map.of("success", true, "data", getLauncherLogs())); } diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java index a9763f0..d775cf9 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -34,6 +35,7 @@ public class ZHttpClient { private static final AtomicBoolean useProxyMode = new AtomicBoolean(false); private static final AtomicBoolean proxyTested = new AtomicBoolean(false); + private static final AtomicBoolean healthThreadStarted = new AtomicBoolean(false); public static void setBaseUrl(String url) { BASE_URL = url; @@ -137,8 +139,8 @@ public class ZHttpClient { } proxyTested.set(true); + startHealthCheckThread(); if (verbose) { - startHealthCheckThread(); printStats(); } } @@ -192,6 +194,8 @@ public class ZHttpClient { } private static void startHealthCheckThread() { + if (!healthThreadStarted.compareAndSet(false, true)) return; + Thread healthThread = new Thread(() -> { while (true) { try { @@ -575,6 +579,29 @@ public class ZHttpClient { return useProxyMode.get(); } + public static Map getNetworkStatus() { + Map status = new HashMap<>(); + status.put("initialized", proxyTested.get()); + status.put("globalProxy", useProxyMode.get()); + status.put("directSuccess", directSuccessCount); + status.put("directFail", directFailCount); + status.put("proxySuccess", proxySuccessCount); + + List> services = new ArrayList<>(); + for (ServiceType type : ServiceType.values()) { + if (type.isAlwaysDirect()) continue; + Map s = new HashMap<>(); + s.put("name", type.name()); + s.put("baseUrl", type.getBaseUrl()); + s.put("healthy", serviceHealthy.getOrDefault(type, false)); + s.put("proxy", serviceProxyMode.getOrDefault(type, false)); + s.put("fails", serviceFailCount.getOrDefault(type, 0)); + services.add(s); + } + status.put("services", services); + return status; + } + public static void printStats() { System.out.println(ZAnsi.cyan("\n=== Network Stats ===")); System.out.println(ZAnsi.white("Global proxy: ") + (useProxyMode.get() ? "ON" : "OFF")); diff --git a/launcher/launcher/src/resources/ui/index.html b/launcher/launcher/src/resources/ui/index.html index f391088..e88d969 100644 --- a/launcher/launcher/src/resources/ui/index.html +++ b/launcher/launcher/src/resources/ui/index.html @@ -438,6 +438,15 @@ Checking... +
+
+

Network

+

Per-service connection mode (direct / via proxy)

+
+
+ Checking... +
+

Language

diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js index 72bf2fa..54a5957 100644 --- a/launcher/launcher/src/resources/ui/launcher.js +++ b/launcher/launcher/src/resources/ui/launcher.js @@ -38,6 +38,13 @@ const LOCALES = { 'settings.server.checking': 'Checking...', 'settings.server.connected': 'Connected', 'settings.server.disconnected': 'Disconnected', + 'settings.network.title': 'Network', + 'settings.network.desc': 'Per-service connection mode (direct / via proxy)', + 'settings.network.direct': 'Direct', + 'settings.network.proxy': 'Proxy', + 'settings.network.down': 'Unavailable', + 'settings.network.loading': 'Checking...', + 'settings.network.unavailable': 'Not available', 'settings.language.title': 'Language', 'settings.language.desc': 'Interface language', 'settings.systemJvm.title': 'System-based JVM Optimization', @@ -222,6 +229,13 @@ const LOCALES = { 'settings.server.checking': 'Проверка...', 'settings.server.connected': 'Подключено', 'settings.server.disconnected': 'Отключено', + 'settings.network.title': 'Сеть', + 'settings.network.desc': 'Режим подключения по сервисам (напрямую / через прокси)', + 'settings.network.direct': 'Напрямую', + 'settings.network.proxy': 'Прокси', + 'settings.network.down': 'Недоступен', + 'settings.network.loading': 'Проверка...', + 'settings.network.unavailable': 'Недоступно', 'settings.language.title': 'Язык', 'settings.language.desc': 'Язык интерфейса', 'settings.systemJvm.title': 'Системная оптимизация JVM', @@ -759,6 +773,40 @@ class ZernMCLauncher { switchView(view) { document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view)); + if (view === 'settings') { + this.loadNetworkStatus(); + if (!this.networkPoller) { + this.networkPoller = setInterval(() => this.loadNetworkStatus(), 15000); + } + } else if (this.networkPoller) { + clearInterval(this.networkPoller); + this.networkPoller = null; + } + } + + async loadNetworkStatus() { + const list = document.getElementById('network-list'); + if (!list) return; + try { + const r = await this.req('/network/status'); + if (!r.success || !r.data) return; + const services = r.data.services || []; + if (!services.length) { + list.innerHTML = '' + t('settings.network.unavailable') + ''; + return; + } + let html = ''; + services.forEach(function(s) { + const state = s.proxy ? 'proxy' : (s.healthy ? 'direct' : 'down'); + const labels = { direct: t('settings.network.direct'), proxy: t('settings.network.proxy'), down: t('settings.network.down') }; + const colors = { direct: 'var(--success)', proxy: 'var(--warning)', down: 'var(--error)' }; + html += '
' + + '' + s.name.toLowerCase() + '' + + '' + labels[state] + '' + + '
'; + }); + list.innerHTML = html; + } catch (e) {} } // ==================== INSTANCES ==================== @@ -1860,7 +1908,8 @@ class ZernMCLauncher { stopProgressPoll() { if (this.progressPoller) { clearInterval(this.progressPoller); - this.progressPoller = null; + this.progressPoller = null; + this.networkPoller = null; } } diff --git a/launcher/launcher/src/resources/ui/style.css b/launcher/launcher/src/resources/ui/style.css index 4d9bf59..26f3597 100644 --- a/launcher/launcher/src/resources/ui/style.css +++ b/launcher/launcher/src/resources/ui/style.css @@ -527,6 +527,18 @@ body { background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-light); } .setting-pass { display: flex; align-items: center; gap: 8px; } +.setting-network-list { + display: flex; flex-direction: column; gap: 6px; min-width: 260px; flex-shrink: 0; +} +.network-row { + display: flex; align-items: center; justify-content: space-between; gap: 12px; + font-size: 12px; padding: 4px 10px; border-radius: 4px; + background: var(--bg-surface); border: 1px solid var(--border-light); +} +.network-name { + font-family: var(--mono); color: var(--text-secondary); text-transform: uppercase; +} +.network-state { font-weight: 600; } .pass-input { width: 160px; padding: 6px 12px; border-radius: var(--radius-sm); background: var(--bg-inset); border: 1px solid var(--border-light); diff --git a/launcher/pom.xml b/launcher/pom.xml index 180e2d0..a9497da 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -19,7 +19,7 @@ 1.0.14 - 3 + 4 21 21 UTF-8