попытка оптимизации и ДЖЛЫВОСШФРСЖДЛВОФЖДЛОВСМДЖЛФ ИНТЕРФЕЙС ФИКСЕСССС БЛЯЯЯ
This commit is contained in:
@@ -66,10 +66,8 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void startCLI() throws IOException {
|
private static void startCLI() throws IOException {
|
||||||
// Проверка всех сервисов при старте
|
ZHttpClient.checkAllServicesOnStartup(true);
|
||||||
ZHttpClient.checkAllServicesOnStartup();
|
|
||||||
|
|
||||||
// === АВТОРИЗАЦИЯ (используем новый API) ===
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
|
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
|
||||||
var sessionResponse = api.checkSession();
|
var sessionResponse = api.checkSession();
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,14 @@ package me.sashegdev.zernmc.launcher.api;
|
|||||||
import me.sashegdev.zernmc.launcher.api.auth.AuthService;
|
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 me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.util.ArrayList;
|
||||||
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;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
|
||||||
* Центральный фасад для внутреннего API лаунчера.
|
|
||||||
* Используется как единая точка входа для UI и других компонентов.
|
|
||||||
*/
|
|
||||||
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;
|
||||||
@@ -82,115 +73,101 @@ public class LauncherAPI {
|
|||||||
|
|
||||||
public ApiResponse<List<String>> getMCVersions() {
|
public ApiResponse<List<String>> getMCVersions() {
|
||||||
try {
|
try {
|
||||||
URL url = new URL(LAUNCHER_SERVER + "/launcher/minecraft-versions");
|
org.json.JSONObject manifest = ZHttpClient.getMojangVersionManifest();
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
org.json.JSONArray versions = manifest.getJSONArray("versions");
|
||||||
conn.setConnectTimeout(5000);
|
List<String> mcVersions = new ArrayList<>();
|
||||||
conn.setReadTimeout(10000);
|
for (int i = 0; i < versions.length(); i++) {
|
||||||
if (conn.getResponseCode() == 200) {
|
mcVersions.add(versions.getJSONObject(i).getString("id"));
|
||||||
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(mcVersions);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("[API] MC versions fetch failed, using fallback: " + e.getMessage());
|
System.out.println("[API] MC versions fetch failed: " + 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"));
|
return ApiResponse.error("Не удалось загрузить версии Minecraft");
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
List<String> versions = new ArrayList<>();
|
||||||
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()) {
|
||||||
switch (loader.toLowerCase()) {
|
case "fabric":
|
||||||
case "fabric":
|
versions = ZHttpClient.getFabricLoaderVersions();
|
||||||
versions.add("0.15.11");
|
break;
|
||||||
versions.add("0.15.9");
|
case "forge":
|
||||||
versions.add("0.15.8");
|
String xml = ZHttpClient.downloadString("https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml");
|
||||||
versions.add("0.14.21");
|
int idx = 0;
|
||||||
break;
|
while ((idx = xml.indexOf("<version>", idx)) != -1) {
|
||||||
case "forge":
|
int start = idx + 9;
|
||||||
versions.add("47.1.0");
|
int end = xml.indexOf("</version>", start);
|
||||||
versions.add("46.0.1");
|
if (end == -1) break;
|
||||||
versions.add("45.0.2");
|
String fullVersion = xml.substring(start, end).trim();
|
||||||
break;
|
if (fullVersion.startsWith(mcVersion + "-")) {
|
||||||
case "neoforge":
|
versions.add(fullVersion.substring(mcVersion.length() + 1));
|
||||||
versions.add("1.21-rc.2");
|
}
|
||||||
versions.add("1.20.4-rc.4");
|
idx = end;
|
||||||
break;
|
}
|
||||||
default:
|
versions.sort((a, b) -> b.compareTo(a));
|
||||||
break;
|
break;
|
||||||
|
case "neoforge":
|
||||||
|
String neoforgeXml = ZHttpClient.downloadString("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml");
|
||||||
|
int neoidx = 0;
|
||||||
|
while ((neoidx = neoforgeXml.indexOf("<version>", neoidx)) != -1) {
|
||||||
|
int start = neoidx + 9;
|
||||||
|
int end = neoforgeXml.indexOf("</version>", start);
|
||||||
|
if (end == -1) break;
|
||||||
|
String fullVersion = neoforgeXml.substring(start, end).trim();
|
||||||
|
if (isNeoForgeCompatible(fullVersion, mcVersion)) {
|
||||||
|
versions.add(fullVersion);
|
||||||
|
}
|
||||||
|
neoidx = end;
|
||||||
|
}
|
||||||
|
versions.sort((a, b) -> b.compareTo(a));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ApiResponse.success(versions);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("[API] Loader versions fetch failed: " + e.getMessage());
|
||||||
|
return ApiResponse.error("Не удалось загрузить версии лоадера");
|
||||||
}
|
}
|
||||||
return ApiResponse.success(versions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApiResponse<List<java.util.Map<String, String>>> getZernMCPacks() {
|
private boolean isNeoForgeCompatible(String version, String mcVersion) {
|
||||||
|
if (mcVersion.startsWith("1.21")) {
|
||||||
|
return version.contains("1.21") && !version.contains("1.20");
|
||||||
|
} else if (mcVersion.startsWith("1.20") && !mcVersion.equals("1.20")) {
|
||||||
|
return version.contains("1.20.4") || version.contains("1.20.5") || version.contains("1.20.6");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse<List<Map<String, String>>> getZernMCPacks() {
|
||||||
try {
|
try {
|
||||||
String token = authService.getCurrentToken();
|
String token = authService.getCurrentToken();
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
return ApiResponse.error("Не авторизован");
|
return ApiResponse.error("Не авторизован");
|
||||||
}
|
}
|
||||||
|
|
||||||
URL url = new URL(LAUNCHER_SERVER + "/packs");
|
String response = ZHttpClient.get("/packs");
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
org.json.JSONArray arr = new org.json.JSONArray(response);
|
||||||
conn.setConnectTimeout(5000);
|
List<Map<String, String>> packs = new ArrayList<>();
|
||||||
conn.setReadTimeout(10000);
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
conn.setRequestProperty("Authorization", "Bearer " + token);
|
org.json.JSONObject pack = arr.getJSONObject(i);
|
||||||
|
Map<String, String> packInfo = new java.util.HashMap<>();
|
||||||
if (conn.getResponseCode() == 200) {
|
packInfo.put("name", pack.optString("name", ""));
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
packInfo.put("displayName", pack.optString("displayName", pack.optString("name", "")));
|
||||||
StringBuilder sb = new StringBuilder();
|
packInfo.put("version", pack.optString("version", ""));
|
||||||
String line;
|
packInfo.put("mcVersion", pack.optString("mcVersion", ""));
|
||||||
while ((line = br.readLine()) != null) sb.append(line);
|
packInfo.put("loader", pack.optString("loader", "vanilla"));
|
||||||
org.json.JSONArray arr = new org.json.JSONArray(sb.toString());
|
packInfo.put("description", pack.optString("description", ""));
|
||||||
List<java.util.Map<String, String>> packs = new java.util.ArrayList<>();
|
packs.add(packInfo);
|
||||||
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("Требуется проходка");
|
|
||||||
}
|
}
|
||||||
|
return ApiResponse.success(packs);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("[API] Packs fetch failed: " + e.getMessage());
|
System.out.println("[API] Packs fetch failed: " + e.getMessage());
|
||||||
return ApiResponse.error("Ошибка загрузки сборок: " + e.getMessage());
|
return ApiResponse.error("Ошибка загрузки сборок: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return ApiResponse.success(java.util.List.of());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-10
@@ -12,10 +12,25 @@ import java.io.BufferedReader;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class LaunchService {
|
public class LaunchService {
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<Long, Process> runningProcesses = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
System.out.println("[LAUNCH] Shutting down all running processes...");
|
||||||
|
runningProcesses.values().forEach(p -> {
|
||||||
|
try {
|
||||||
|
p.destroy();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
public ApiResponse<LaunchInfo> prepareLaunch(String instanceName) {
|
public ApiResponse<LaunchInfo> prepareLaunch(String instanceName) {
|
||||||
try {
|
try {
|
||||||
Instance instance = InstanceManager.getInstance(instanceName);
|
Instance instance = InstanceManager.getInstance(instanceName);
|
||||||
@@ -49,7 +64,6 @@ public class LaunchService {
|
|||||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||||
LaunchOptions options = new LaunchOptions();
|
LaunchOptions options = new LaunchOptions();
|
||||||
|
|
||||||
// Set auth info
|
|
||||||
options.setUsername(AuthManager.getUsername());
|
options.setUsername(AuthManager.getUsername());
|
||||||
options.setAccessToken(AuthManager.getAccessToken());
|
options.setAccessToken(AuthManager.getAccessToken());
|
||||||
options.setUuid(AuthManager.getUuid());
|
options.setUuid(AuthManager.getUuid());
|
||||||
@@ -61,27 +75,60 @@ public class LaunchService {
|
|||||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
processBuilder.directory(instance.getPath().toFile());
|
processBuilder.directory(instance.getPath().toFile());
|
||||||
processBuilder.redirectErrorStream(true);
|
processBuilder.redirectErrorStream(true);
|
||||||
// Не перехватываем вывод игры - пусть выводится напрямую в консоль
|
|
||||||
// Инициализируем лог файл для игры, но не дублируем вывод
|
|
||||||
Path logsDir = instance.getPath().resolve("logs");
|
Path logsDir = instance.getPath().resolve("logs");
|
||||||
java.nio.file.Files.createDirectories(logsDir);
|
java.nio.file.Files.createDirectories(logsDir);
|
||||||
Path gameLog = logsDir.resolve("game.log");
|
Path gameLog = logsDir.resolve("game.log");
|
||||||
processBuilder.redirectOutput(ProcessBuilder.Redirect.to(gameLog.toFile()));
|
|
||||||
|
|
||||||
Process process = processBuilder.start();
|
Process process = processBuilder.start();
|
||||||
System.out.println("[LAUNCH] Process started, pid=" + process.pid());
|
long pid = process.pid();
|
||||||
|
|
||||||
ProcessInfo info = new ProcessInfo(
|
runningProcesses.put(pid, process);
|
||||||
instanceName,
|
System.out.println("[LAUNCH] Process started, pid=" + pid);
|
||||||
process.pid(),
|
|
||||||
"RUNNING"
|
java.io.FileOutputStream logFileOut = new java.io.FileOutputStream(gameLog.toFile(), true);
|
||||||
);
|
|
||||||
|
Thread logReader = new Thread(() -> {
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
String timestamped = "[" + java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) + "] " + line;
|
||||||
|
JFXLauncher.appendGameLog(line);
|
||||||
|
try {
|
||||||
|
logFileOut.write((timestamped + "\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
logFileOut.flush();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
JFXLauncher.appendGameLog("[Ошибка чтения логов: " + e.getMessage() + "]");
|
||||||
|
} finally {
|
||||||
|
try { logFileOut.close(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}, "GameLogReader-" + instanceName);
|
||||||
|
logReader.setDaemon(true);
|
||||||
|
logReader.start();
|
||||||
|
|
||||||
|
process.onExit().thenRun(() -> {
|
||||||
|
runningProcesses.remove(pid);
|
||||||
|
JFXLauncher.appendGameLog("[Minecraft завершился с кодом: " + process.exitValue() + "]");
|
||||||
|
});
|
||||||
|
|
||||||
|
ProcessInfo info = new ProcessInfo(instanceName, pid, "RUNNING");
|
||||||
return ApiResponse.success(info);
|
return ApiResponse.success(info);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ApiResponse.error("Ошибка запуска: " + e.getMessage());
|
return ApiResponse.error("Ошибка запуска: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void killAllProcesses() {
|
||||||
|
runningProcesses.values().forEach(p -> {
|
||||||
|
try {
|
||||||
|
p.destroyForcibly();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
runningProcesses.clear();
|
||||||
|
}
|
||||||
|
|
||||||
public ApiResponse<Boolean> isReady(String instanceName) {
|
public ApiResponse<Boolean> isReady(String instanceName) {
|
||||||
try {
|
try {
|
||||||
Instance instance = InstanceManager.getInstance(instanceName);
|
Instance instance = InstanceManager.getInstance(instanceName);
|
||||||
|
|||||||
+129
-29
@@ -1,6 +1,7 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jfx;
|
package me.sashegdev.zernmc.launcher.ui.jfx;
|
||||||
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.web.WebView;
|
import javafx.scene.web.WebView;
|
||||||
import javafx.scene.web.WebEngine;
|
import javafx.scene.web.WebEngine;
|
||||||
@@ -8,6 +9,7 @@ import javafx.stage.Stage;
|
|||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
||||||
|
import me.sashegdev.zernmc.launcher.api.launch.LaunchService;
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
||||||
@@ -43,11 +45,43 @@ public class JFXLauncher extends Application {
|
|||||||
private final LauncherAPI api = new LauncherAPI();
|
private final LauncherAPI api = new LauncherAPI();
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
private HttpServer server;
|
private HttpServer server;
|
||||||
private StringBuilder logBuffer = new StringBuilder();
|
private static StringBuilder launcherLogBuffer = new StringBuilder();
|
||||||
private static StringBuilder gameLogBuffer = new StringBuilder();
|
private static StringBuilder gameLogBuffer = new StringBuilder();
|
||||||
private static Path gameLogFile;
|
private static Path gameLogFile;
|
||||||
private Stage mainStage;
|
private Stage mainStage;
|
||||||
|
|
||||||
|
private static final java.util.concurrent.CopyOnWriteArrayList<LogConsumer> logConsumers = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||||
|
private static final java.util.concurrent.CopyOnWriteArrayList<LogConsumer> gameLogConsumers = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
public interface LogConsumer {
|
||||||
|
void onLog(String line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addLogConsumer(LogConsumer consumer) {
|
||||||
|
logConsumers.add(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeLogConsumer(LogConsumer consumer) {
|
||||||
|
logConsumers.remove(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addGameLogConsumer(LogConsumer consumer) {
|
||||||
|
gameLogConsumers.add(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeGameLogConsumer(LogConsumer consumer) {
|
||||||
|
gameLogConsumers.remove(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void appendLauncherLog(String log) {
|
||||||
|
synchronized (launcherLogBuffer) {
|
||||||
|
launcherLogBuffer.append(log).append("\n");
|
||||||
|
}
|
||||||
|
for (LogConsumer consumer : logConsumers) {
|
||||||
|
try { consumer.onLog(log); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void appendGameLog(String log) {
|
public static void appendGameLog(String log) {
|
||||||
synchronized (gameLogBuffer) {
|
synchronized (gameLogBuffer) {
|
||||||
gameLogBuffer.append(log).append("\n");
|
gameLogBuffer.append(log).append("\n");
|
||||||
@@ -60,6 +94,9 @@ public class JFXLauncher extends Application {
|
|||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (LogConsumer consumer : gameLogConsumers) {
|
||||||
|
try { consumer.onLog(log); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initGameLog(Path instanceDir) {
|
public static void initGameLog(Path instanceDir) {
|
||||||
@@ -75,13 +112,29 @@ public class JFXLauncher extends Application {
|
|||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void clearGameLog() {
|
||||||
|
synchronized (gameLogBuffer) {
|
||||||
|
gameLogBuffer.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getGameLogs() {
|
public static String getGameLogs() {
|
||||||
synchronized (gameLogBuffer) {
|
synchronized (gameLogBuffer) {
|
||||||
return gameLogBuffer.toString();
|
return gameLogBuffer.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getLauncherLogs() {
|
||||||
|
synchronized (launcherLogBuffer) {
|
||||||
|
return launcherLogBuffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
System.out.println("[JFX] Shutdown hook triggered");
|
||||||
|
LaunchService.killAllProcesses();
|
||||||
|
}));
|
||||||
launch(args);
|
launch(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +292,8 @@ public class JFXLauncher extends Application {
|
|||||||
|
|
||||||
stage.setOnCloseRequest(e -> {
|
stage.setOnCloseRequest(e -> {
|
||||||
log("Закрытие...");
|
log("Закрытие...");
|
||||||
stopServer();
|
LaunchService.killAllProcesses();
|
||||||
|
if (server != null) server.stop(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -260,9 +314,11 @@ public class JFXLauncher extends Application {
|
|||||||
server.createContext("/api/logs", this::handleLogs);
|
server.createContext("/api/logs", this::handleLogs);
|
||||||
server.createContext("/api/logs/stream", this::handleLogsStream);
|
server.createContext("/api/logs/stream", this::handleLogsStream);
|
||||||
server.createContext("/api/game-logs", this::handleGameLogs);
|
server.createContext("/api/game-logs", this::handleGameLogs);
|
||||||
|
server.createContext("/api/game-logs/stream", this::handleGameLogsStream);
|
||||||
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/packs", this::handlePacks);
|
||||||
|
server.createContext("/api/shutdown", this::handleShutdown);
|
||||||
server.createContext("/api/exit", this::handleExit);
|
server.createContext("/api/exit", this::handleExit);
|
||||||
server.createContext("/assets/", this::handleStatic);
|
server.createContext("/assets/", this::handleStatic);
|
||||||
|
|
||||||
@@ -394,7 +450,7 @@ public class JFXLauncher extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogs(HttpExchange exchange) {
|
private void handleLogs(HttpExchange exchange) {
|
||||||
sendJson(exchange, Map.of("success", true, "data", logBuffer.toString()));
|
sendJson(exchange, Map.of("success", true, "data", getLauncherLogs()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogsStream(HttpExchange exchange) {
|
private void handleLogsStream(HttpExchange exchange) {
|
||||||
@@ -404,46 +460,75 @@ public class JFXLauncher extends Application {
|
|||||||
exchange.getResponseHeaders().set("Connection", "keep-alive");
|
exchange.getResponseHeaders().set("Connection", "keep-alive");
|
||||||
exchange.sendResponseHeaders(200, 0);
|
exchange.sendResponseHeaders(200, 0);
|
||||||
|
|
||||||
String lastLog = "";
|
|
||||||
int sameCount = 0;
|
|
||||||
final OutputStream os = exchange.getResponseBody();
|
final OutputStream os = exchange.getResponseBody();
|
||||||
|
int[] lastLength = {getLauncherLogs().length()};
|
||||||
|
|
||||||
Thread.currentThread().setName("LogStream-" + System.currentTimeMillis());
|
LogConsumer consumer = new LogConsumer() {
|
||||||
|
@Override
|
||||||
for (int i = 0; !Thread.currentThread().isInterrupted() && i < 3000; i++) {
|
public synchronized void onLog(String line) {
|
||||||
String currentLog = logBuffer.toString();
|
try {
|
||||||
if (!currentLog.equals(lastLog)) {
|
String data = "data: " + line.replace("\n", "").replace("\r", "") + "\n\n";
|
||||||
String newContent = currentLog.substring(lastLog.length());
|
os.write(data.getBytes(StandardCharsets.UTF_8));
|
||||||
if (!newContent.isEmpty()) {
|
|
||||||
for (String line : newContent.split("\n")) {
|
|
||||||
if (!line.trim().isEmpty()) {
|
|
||||||
String data = "data: " + line.replace("\n", "").replace("\r", "") + "\n\n";
|
|
||||||
os.write(data.getBytes(StandardCharsets.UTF_8));
|
|
||||||
os.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastLog = currentLog;
|
|
||||||
sameCount = 0;
|
|
||||||
} else {
|
|
||||||
sameCount++;
|
|
||||||
if (sameCount > 50) {
|
|
||||||
os.write("data: \n\n".getBytes(StandardCharsets.UTF_8));
|
|
||||||
os.flush();
|
os.flush();
|
||||||
sameCount = 0;
|
} catch (Exception e) {
|
||||||
|
removeLogConsumer(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Thread.sleep(100);
|
};
|
||||||
|
|
||||||
|
addLogConsumer(consumer);
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
Thread.sleep(10000);
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {} finally {
|
} catch (Exception ignored) {} finally {
|
||||||
|
removeLogConsumer(consumer);
|
||||||
try { exchange.getResponseBody().close(); } catch (Exception ignored) {}
|
try { exchange.getResponseBody().close(); } catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LogConsumer consumer = null;
|
||||||
|
|
||||||
private void handleGameLogs(HttpExchange exchange) {
|
private void handleGameLogs(HttpExchange exchange) {
|
||||||
sendJson(exchange, Map.of("success", true, "data", getGameLogs()));
|
sendJson(exchange, Map.of("success", true, "data", getGameLogs()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleGameLogsStream(HttpExchange exchange) {
|
||||||
|
try {
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "text/event-stream");
|
||||||
|
exchange.getResponseHeaders().set("Cache-Control", "no-cache");
|
||||||
|
exchange.getResponseHeaders().set("Connection", "keep-alive");
|
||||||
|
exchange.sendResponseHeaders(200, 0);
|
||||||
|
|
||||||
|
final OutputStream os = exchange.getResponseBody();
|
||||||
|
|
||||||
|
consumer = new LogConsumer() {
|
||||||
|
@Override
|
||||||
|
public synchronized void onLog(String line) {
|
||||||
|
try {
|
||||||
|
String data = "data: " + line.replace("\n", "").replace("\r", "") + "\n\n";
|
||||||
|
os.write(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
os.flush();
|
||||||
|
} catch (Exception e) {
|
||||||
|
removeGameLogConsumer(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addGameLogConsumer(consumer);
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
Thread.sleep(10000);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {} finally {
|
||||||
|
if (consumer != null) {
|
||||||
|
removeGameLogConsumer(consumer);
|
||||||
|
}
|
||||||
|
consumer = null;
|
||||||
|
try { exchange.getResponseBody().close(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleMCVersions(HttpExchange exchange) {
|
private void handleMCVersions(HttpExchange exchange) {
|
||||||
try {
|
try {
|
||||||
var versions = api.getMCVersions();
|
var versions = api.getMCVersions();
|
||||||
@@ -500,9 +585,19 @@ public class JFXLauncher extends Application {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleShutdown(HttpExchange exchange) {
|
||||||
|
log("Shutdown request received...");
|
||||||
|
LaunchService.killAllProcesses();
|
||||||
|
if (server != null) server.stop(1);
|
||||||
|
Platform.exit();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
private void handleExit(HttpExchange exchange) {
|
private void handleExit(HttpExchange exchange) {
|
||||||
log("Выход...");
|
log("Выход...");
|
||||||
|
LaunchService.killAllProcesses();
|
||||||
if (mainStage != null) mainStage.close();
|
if (mainStage != null) mainStage.close();
|
||||||
|
Platform.exit();
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +658,12 @@ public class JFXLauncher extends Application {
|
|||||||
|
|
||||||
private void log(String msg) {
|
private void log(String msg) {
|
||||||
String entry = "[" + java.time.LocalTime.now() + "] " + msg + "\n";
|
String entry = "[" + java.time.LocalTime.now() + "] " + msg + "\n";
|
||||||
logBuffer.append(entry);
|
synchronized (launcherLogBuffer) {
|
||||||
|
launcherLogBuffer.append(entry);
|
||||||
|
}
|
||||||
System.out.println("[JFX] " + msg);
|
System.out.println("[JFX] " + msg);
|
||||||
|
for (LogConsumer consumer : logConsumers) {
|
||||||
|
try { consumer.onLog(entry); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,9 +96,11 @@ public class ZHttpClient {
|
|||||||
* Вызывать один раз при запуске лаунчера
|
* Вызывать один раз при запуске лаунчера
|
||||||
*/
|
*/
|
||||||
public static void checkAllServicesOnStartup() {
|
public static void checkAllServicesOnStartup() {
|
||||||
if (proxyTested.get()) return;
|
checkAllServicesOnStartup(false);
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка доступности сервисов..."));
|
public static void checkAllServicesOnStartup(boolean verbose) {
|
||||||
|
if (proxyTested.get()) return;
|
||||||
|
|
||||||
List<ServiceType> servicesToCheck = List.of(
|
List<ServiceType> servicesToCheck = List.of(
|
||||||
ServiceType.ZERN_SERVER,
|
ServiceType.ZERN_SERVER,
|
||||||
@@ -116,14 +118,20 @@ public class ZHttpClient {
|
|||||||
serviceHealthy.put(service, isHealthy);
|
serviceHealthy.put(service, isHealthy);
|
||||||
|
|
||||||
if (service.isAlwaysDirect()) {
|
if (service.isAlwaysDirect()) {
|
||||||
System.out.println(isHealthy ?
|
if (verbose) {
|
||||||
ZAnsi.green(" " + service.name() + " - OK") :
|
System.out.println(isHealthy ?
|
||||||
ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
|
ZAnsi.green(" " + service.name() + " - OK") :
|
||||||
|
ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isHealthy) {
|
if (isHealthy) {
|
||||||
System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
|
if (verbose) {
|
||||||
|
System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow(" " + service.name() + " - НЕ ДОСТУПЕН, будет использован прокси"));
|
if (verbose) {
|
||||||
|
System.out.println(ZAnsi.yellow(" " + service.name() + " - НЕ ДОСТУПЕН, будет использован прокси"));
|
||||||
|
}
|
||||||
serviceProxyMode.put(service, true);
|
serviceProxyMode.put(service, true);
|
||||||
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
|
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
|
||||||
}
|
}
|
||||||
@@ -131,12 +139,16 @@ public class ZHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!serviceHealthy.get(ServiceType.ZERN_SERVER)) {
|
if (!serviceHealthy.get(ServiceType.ZERN_SERVER)) {
|
||||||
System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
|
if (verbose) {
|
||||||
|
System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyTested.set(true);
|
proxyTested.set(true);
|
||||||
startHealthCheckThread();
|
if (verbose) {
|
||||||
printStats();
|
startHealthCheckThread();
|
||||||
|
printStats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -99,6 +99,12 @@
|
|||||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-logout" id="close-btn" title="Закрыть" onclick="app.shutdownLauncher()">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -159,24 +165,38 @@
|
|||||||
<div id="tab-vanilla" class="tab-content">
|
<div id="tab-vanilla" class="tab-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Версия Minecraft</label>
|
<label>Версия Minecraft</label>
|
||||||
<select id="mc-version-select" class="select-input">
|
<div class="custom-dropdown" id="mc-version-dropdown">
|
||||||
<option value="">Выберите версию</option>
|
<div class="dropdown-trigger">
|
||||||
</select>
|
<span class="dropdown-value">Выберите версию</span>
|
||||||
|
<span class="dropdown-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Лоадер</label>
|
<label>Лоадер</label>
|
||||||
<select id="loader-select" class="select-input">
|
<div class="custom-dropdown" id="loader-dropdown">
|
||||||
<option value="vanilla">Vanilla (без лоадера)</option>
|
<div class="dropdown-trigger">
|
||||||
<option value="fabric">Fabric</option>
|
<span class="dropdown-value">Vanilla (без лоадера)</span>
|
||||||
<option value="forge">Forge</option>
|
<span class="dropdown-arrow">▼</span>
|
||||||
<option value="neoforge">NeoForge</option>
|
</div>
|
||||||
</select>
|
<div class="dropdown-list">
|
||||||
|
<div class="dropdown-item selected" data-value="vanilla">Vanilla (без лоадера)</div>
|
||||||
|
<div class="dropdown-item" data-value="fabric">Fabric</div>
|
||||||
|
<div class="dropdown-item" data-value="forge">Forge</div>
|
||||||
|
<div class="dropdown-item" data-value="neoforge">NeoForge</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="loader-version-group" class="form-group hidden">
|
<div id="loader-version-group" class="form-group hidden">
|
||||||
<label>Версия лоадера</label>
|
<label>Версия лоадера</label>
|
||||||
<select id="loader-version-select" class="select-input">
|
<div class="custom-dropdown" id="loader-version-dropdown">
|
||||||
<option value="">Загрузка...</option>
|
<div class="dropdown-trigger">
|
||||||
</select>
|
<span class="dropdown-value">Выберите версию</span>
|
||||||
|
<span class="dropdown-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Название сборки</label>
|
<label>Название сборки</label>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
let consoleEventSource = null;
|
let consoleEventSource = null;
|
||||||
|
let gameLogEventSource = null;
|
||||||
|
|
||||||
class LauncherApp {
|
class LauncherApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -17,9 +18,114 @@ class LauncherApp {
|
|||||||
async init() {
|
async init() {
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.initGridAnimation();
|
this.initGridAnimation();
|
||||||
|
this.initDropdowns();
|
||||||
await this.checkAuth();
|
await this.checkAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initDropdowns() {
|
||||||
|
document.querySelectorAll('.custom-dropdown').forEach(dropdown => {
|
||||||
|
const trigger = dropdown.querySelector('.dropdown-trigger');
|
||||||
|
const list = dropdown.querySelector('.dropdown-list');
|
||||||
|
let selectedValue = null;
|
||||||
|
|
||||||
|
trigger.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
document.querySelectorAll('.custom-dropdown .dropdown-list.open').forEach(d => {
|
||||||
|
if (d !== list) d.classList.remove('open');
|
||||||
|
});
|
||||||
|
list.classList.toggle('open');
|
||||||
|
trigger.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener('click', (e) => {
|
||||||
|
const item = e.target.closest('.dropdown-item');
|
||||||
|
if (item) {
|
||||||
|
const value = item.dataset.value;
|
||||||
|
const text = item.textContent;
|
||||||
|
|
||||||
|
list.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
|
||||||
|
item.classList.add('selected');
|
||||||
|
|
||||||
|
trigger.querySelector('.dropdown-value').textContent = text;
|
||||||
|
trigger.dataset.value = value;
|
||||||
|
list.classList.remove('open');
|
||||||
|
trigger.classList.remove('active');
|
||||||
|
|
||||||
|
dropdown.dispatchEvent(new CustomEvent('change', { detail: { value, text } }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.custom-dropdown .dropdown-list.open').forEach(list => {
|
||||||
|
list.classList.remove('open');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.custom-dropdown .dropdown-trigger.active').forEach(trigger => {
|
||||||
|
trigger.classList.remove('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateDropdown(id, items) {
|
||||||
|
const dropdown = document.getElementById(id);
|
||||||
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
const list = dropdown.querySelector('.dropdown-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'dropdown-item';
|
||||||
|
div.dataset.value = item.value || item;
|
||||||
|
div.textContent = item.label || item;
|
||||||
|
list.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectDropdownItem(id, value) {
|
||||||
|
const dropdown = document.getElementById(id);
|
||||||
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
const items = dropdown.querySelectorAll('.dropdown-item');
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.dataset.value === value) {
|
||||||
|
item.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDropdownValue(id) {
|
||||||
|
const dropdown = document.getElementById(id);
|
||||||
|
if (!dropdown) return null;
|
||||||
|
return dropdown.querySelector('.dropdown-trigger').dataset.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
startGameLogStream() {
|
||||||
|
if (gameLogEventSource) {
|
||||||
|
gameLogEventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
gameLogEventSource = new EventSource(API_BASE + '/game-logs/stream');
|
||||||
|
|
||||||
|
gameLogEventSource.onmessage = (event) => {
|
||||||
|
if (event.data && event.data.trim()) {
|
||||||
|
this.addLog(event.data, this.getLogType(event.data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gameLogEventSource.onerror = () => {
|
||||||
|
gameLogEventSource.close();
|
||||||
|
setTimeout(() => this.startGameLogStream(), 5000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
stopGameLogStream() {
|
||||||
|
if (gameLogEventSource) {
|
||||||
|
gameLogEventSource.close();
|
||||||
|
gameLogEventSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startConsoleLogStream() {
|
startConsoleLogStream() {
|
||||||
if (consoleEventSource) {
|
if (consoleEventSource) {
|
||||||
consoleEventSource.close();
|
consoleEventSource.close();
|
||||||
@@ -86,8 +192,8 @@ class LauncherApp {
|
|||||||
this.clearLogs();
|
this.clearLogs();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('loader-select').addEventListener('change', (e) => {
|
document.getElementById('loader-dropdown').addEventListener('change', (e) => {
|
||||||
this.onLoaderChange(e.target.value);
|
this.onLoaderChange(e.detail.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('install-zernmc-btn').addEventListener('click', () => {
|
document.getElementById('install-zernmc-btn').addEventListener('click', () => {
|
||||||
@@ -172,7 +278,9 @@ class LauncherApp {
|
|||||||
this.account = result.data;
|
this.account = result.data;
|
||||||
this.username = result.data.username;
|
this.username = result.data.username;
|
||||||
this.showMainScreen();
|
this.showMainScreen();
|
||||||
|
this.renderCurrentInstance();
|
||||||
this.startConsoleLogStream();
|
this.startConsoleLogStream();
|
||||||
|
this.startGameLogStream();
|
||||||
await this.loadInstances();
|
await this.loadInstances();
|
||||||
} else {
|
} else {
|
||||||
this.showLoginScreen();
|
this.showLoginScreen();
|
||||||
@@ -212,15 +320,35 @@ class LauncherApp {
|
|||||||
this.account = result.data;
|
this.account = result.data;
|
||||||
this.username = result.data.username;
|
this.username = result.data.username;
|
||||||
this.showMainScreen();
|
this.showMainScreen();
|
||||||
|
this.renderCurrentInstance();
|
||||||
this.startConsoleLogStream();
|
this.startConsoleLogStream();
|
||||||
|
this.startGameLogStream();
|
||||||
await this.loadInstances();
|
await this.loadInstances();
|
||||||
} else {
|
} else {
|
||||||
this.showError(result.error || 'Ошибка входа');
|
this.showError(result.error || 'Ошибка входа');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCurrentInstance() {
|
||||||
|
if (!this.account) return;
|
||||||
|
|
||||||
|
document.getElementById('username-display').textContent = this.account.username;
|
||||||
|
const statusEl = document.getElementById('account-status');
|
||||||
|
statusEl.textContent = this.account.passActive ? 'PRO' : 'FREE';
|
||||||
|
statusEl.className = 'badge ' + (this.account.passActive ? 'active' : 'inactive');
|
||||||
|
|
||||||
|
const roleEl = document.getElementById('account-role');
|
||||||
|
if (this.account.roleName) {
|
||||||
|
roleEl.textContent = this.account.roleName;
|
||||||
|
roleEl.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
roleEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleLogout() {
|
async handleLogout() {
|
||||||
this.stopConsoleLogStream();
|
this.stopConsoleLogStream();
|
||||||
|
this.stopGameLogStream();
|
||||||
this.username = null;
|
this.username = null;
|
||||||
this.account = null;
|
this.account = null;
|
||||||
this.currentInstance = null;
|
this.currentInstance = null;
|
||||||
@@ -229,6 +357,11 @@ class LauncherApp {
|
|||||||
this.addLog('Вы вышли из аккаунта', 'info');
|
this.addLog('Вы вышли из аккаунта', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async shutdownLauncher() {
|
||||||
|
const result = await this.request('/shutdown', { method: 'POST' });
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
showError(message) {
|
showError(message) {
|
||||||
const errorEl = document.getElementById('login-error');
|
const errorEl = document.getElementById('login-error');
|
||||||
errorEl.textContent = message;
|
errorEl.textContent = message;
|
||||||
@@ -331,42 +464,20 @@ class LauncherApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCurrentInstance(instance) {
|
renderCurrentInstance(instance) {
|
||||||
const container = document.getElementById('current-instance');
|
if (!this.account) return;
|
||||||
let version = instance.version || 'Vanilla';
|
|
||||||
if (instance.isServerPack) {
|
document.getElementById('username-display').textContent = this.account.username;
|
||||||
version = `v${instance.serverVersion}`;
|
const statusEl = document.getElementById('account-status');
|
||||||
|
statusEl.textContent = this.account.passActive ? 'PRO' : 'FREE';
|
||||||
|
statusEl.className = 'badge ' + (this.account.passActive ? 'active' : 'inactive');
|
||||||
|
|
||||||
|
const roleEl = document.getElementById('account-role');
|
||||||
|
if (this.account.roleName) {
|
||||||
|
roleEl.textContent = this.account.roleName;
|
||||||
|
roleEl.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
roleEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
container.innerHTML = `
|
|
||||||
<div class="instance-card-mini">
|
|
||||||
<span class="instance-name">${this.escapeHtml(instance.name)}</span>
|
|
||||||
<span class="instance-version">${this.escapeHtml(version)}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (this.account) {
|
|
||||||
document.getElementById('username-display').textContent = this.account.username;
|
|
||||||
const statusEl = document.getElementById('account-status');
|
|
||||||
statusEl.textContent = this.account.passActive ? 'PRO' : 'FREE';
|
|
||||||
statusEl.className = 'badge ' + (this.account.passActive ? 'active' : 'inactive');
|
|
||||||
|
|
||||||
const roleEl = document.getElementById('account-role');
|
|
||||||
if (this.account.roleName) {
|
|
||||||
roleEl.textContent = this.account.roleName;
|
|
||||||
roleEl.style.display = 'inline-block';
|
|
||||||
} else {
|
|
||||||
roleEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNoInstance() {
|
|
||||||
const container = document.getElementById('current-instance');
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="instance-card-mini">
|
|
||||||
<span class="instance-name" style="color: var(--text-muted)">Нет сборки</span>
|
|
||||||
<span class="instance-version" style="background: var(--bg-secondary)">Нажмите скачать</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchInstance() {
|
async launchInstance() {
|
||||||
@@ -394,36 +505,32 @@ class LauncherApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadDownloadModalData() {
|
async loadDownloadModalData() {
|
||||||
const mcSelect = document.getElementById('mc-version-select');
|
this.populateDropdown('mc-version-dropdown', [{ value: '', label: 'Загрузка...' }]);
|
||||||
mcSelect.innerHTML = '<option value="">Загрузка...</option>';
|
|
||||||
|
|
||||||
const mcResult = await this.request('/mc-versions');
|
const mcResult = await this.request('/mc-versions');
|
||||||
if (mcResult.success && mcResult.data) {
|
if (mcResult.success && mcResult.data) {
|
||||||
mcSelect.innerHTML = '<option value="">Выберите версию</option>';
|
const items = mcResult.data.map(v => ({ value: v, label: v }));
|
||||||
mcResult.data.forEach(v => {
|
this.populateDropdown('mc-version-dropdown', items);
|
||||||
const opt = document.createElement('option');
|
} else {
|
||||||
opt.value = v;
|
this.populateDropdown('mc-version-dropdown', [{ value: '', label: 'Ошибка загрузки' }]);
|
||||||
opt.textContent = v;
|
|
||||||
mcSelect.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const packSelect = document.getElementById('zernmc-pack-select');
|
const zernmcSelect = document.getElementById('zernmc-pack-select');
|
||||||
packSelect.innerHTML = '<option value="">Загрузка...</option>';
|
zernmcSelect.innerHTML = '<option value="">Загрузка...</option>';
|
||||||
|
|
||||||
const packResult = await this.request('/packs');
|
const packResult = await this.request('/packs');
|
||||||
if (packResult.success && packResult.data && packResult.data.length > 0) {
|
if (packResult.success && packResult.data && packResult.data.length > 0) {
|
||||||
packSelect.innerHTML = '<option value="">Выберите сборку</option>';
|
zernmcSelect.innerHTML = '<option value="">Выберите сборку</option>';
|
||||||
packResult.data.forEach(p => {
|
packResult.data.forEach(p => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = p.name;
|
opt.value = p.name;
|
||||||
opt.textContent = p.displayName + ' (' + p.version + ')';
|
opt.textContent = p.displayName + ' (' + p.version + ')';
|
||||||
packSelect.appendChild(opt);
|
zernmcSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
} else if (packResult.error && packResult.error.includes('проходка')) {
|
} else if (packResult.error && packResult.error.includes('проходка')) {
|
||||||
packSelect.innerHTML = '<option value="">Требуется проходка</option>';
|
zernmcSelect.innerHTML = '<option value="">Требуется проходка</option>';
|
||||||
} else {
|
} else {
|
||||||
packSelect.innerHTML = '<option value="">Сборки недоступны</option>';
|
zernmcSelect.innerHTML = '<option value="">Сборки недоступны</option>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const zernmcTab = document.querySelector('[data-tab="zernmc"]');
|
const zernmcTab = document.querySelector('[data-tab="zernmc"]');
|
||||||
@@ -491,9 +598,9 @@ class LauncherApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async installVanilla() {
|
async installVanilla() {
|
||||||
const mcVersion = document.getElementById('mc-version-select').value;
|
const mcVersion = this.getDropdownValue('mc-version-dropdown');
|
||||||
const loader = document.getElementById('loader-select').value;
|
const loader = this.getDropdownValue('loader-dropdown');
|
||||||
const loaderVersion = document.getElementById('loader-version-select').value;
|
const loaderVersion = this.getDropdownValue('loader-version-dropdown');
|
||||||
const instanceName = document.getElementById('vanilla-instance-name').value;
|
const instanceName = document.getElementById('vanilla-instance-name').value;
|
||||||
|
|
||||||
if (!mcVersion) {
|
if (!mcVersion) {
|
||||||
@@ -536,24 +643,21 @@ class LauncherApp {
|
|||||||
|
|
||||||
async 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 = this.getDropdownValue('mc-version-dropdown');
|
||||||
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');
|
||||||
|
this.populateDropdown('loader-version-dropdown', [{ value: '', label: 'Загрузка...' }]);
|
||||||
|
|
||||||
if (mcVersion) {
|
if (mcVersion) {
|
||||||
loaderVersionSelect.innerHTML = '<option value="">Загрузка...</option>';
|
|
||||||
const result = await this.request('/loader-versions?mc=' + mcVersion + '&loader=' + loader);
|
const result = await this.request('/loader-versions?mc=' + mcVersion + '&loader=' + loader);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
loaderVersionSelect.innerHTML = '<option value="">Выберите версию</option>';
|
const items = result.data.map(v => ({ value: v, label: v }));
|
||||||
result.data.forEach(v => {
|
this.populateDropdown('loader-version-dropdown', items);
|
||||||
const opt = document.createElement('option');
|
} else {
|
||||||
opt.value = v;
|
this.populateDropdown('loader-version-dropdown', [{ value: '', label: 'Ошибка загрузки' }]);
|
||||||
opt.textContent = v;
|
|
||||||
loaderVersionSelect.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,6 +490,10 @@ body {
|
|||||||
color: var(--error);
|
color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-logout#close-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* Main Content - Logs */
|
/* Main Content - Logs */
|
||||||
.main-content {
|
.main-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -900,3 +904,109 @@ body {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--text-muted);
|
background: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== CUSTOM DROPDOWN ==================== */
|
||||||
|
.custom-dropdown {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger:hover {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger.active {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: transform var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger.active .dropdown-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-list {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
display: none;
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-list.open {
|
||||||
|
display: block;
|
||||||
|
animation: slideUp var(--transition-fast) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: var(--bg-card-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.selected {
|
||||||
|
background: rgba(233, 69, 96, 0.15);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-search {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-search input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-search input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
+7
-14
@@ -822,20 +822,13 @@ async def activate_pass_page():
|
|||||||
# ====================== ЭНДПОИНТЫ ДЛЯ ПАКОВ ======================
|
# ====================== ЭНДПОИНТЫ ДЛЯ ПАКОВ ======================
|
||||||
|
|
||||||
@app.get("/packs")
|
@app.get("/packs")
|
||||||
async def list_packs(request: Request):
|
async def list_packs(
|
||||||
"""List all available packs - requires auth or master key for mirrors"""
|
request: Request,
|
||||||
# Check for master key
|
current_user: dict = Depends(get_current_user)
|
||||||
master_key = request.headers.get("X-Master-Key")
|
):
|
||||||
if master_key == MASTER_KEY:
|
"""List all available packs - requires auth"""
|
||||||
# Master key - allow access
|
if not has_permission(current_user["role"], Permissions.VIEW_PACKS):
|
||||||
pass
|
raise HTTPException(403, "Requires active pass")
|
||||||
else:
|
|
||||||
# Normal auth required
|
|
||||||
current_user = await get_current_user(request)
|
|
||||||
if not current_user:
|
|
||||||
raise HTTPException(401, "Authentication required")
|
|
||||||
if not has_permission(current_user["role"], Permissions.VIEW_PACKS):
|
|
||||||
raise HTTPException(403, "Requires active pass")
|
|
||||||
|
|
||||||
packs = []
|
packs = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user