diff --git a/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java b/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java index 3dc0af1..3f09365 100644 --- a/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java +++ b/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java @@ -3,7 +3,9 @@ package me.sashegdev.zernmc.launcher; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.*; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -22,10 +24,14 @@ public class Bootstrap { private static final String JAR_NAME = "zernmclauncher.jar"; private static final String BASE_URL = "http://87.120.187.36:1582"; private static List MIRRORS = new ArrayList<>(); + private static volatile boolean jfxChildExiting = false; private static Path baseDir; private static Path binDir; private static Path logDir; + private static Path javafxPath; + private static boolean isCliMode; + private static boolean isJfxMode; private static Path getLauncherJar() { return binDir.resolve(JAR_NAME); @@ -37,22 +43,22 @@ public class Bootstrap { Files.createDirectories(binDir); logDir = baseDir.resolve("logs"); Files.createDirectories(logDir); + javafxPath = baseDir.resolve("lib").resolve("javafx"); log("=== ZernMC Launcher ==="); - // Определяем режим запуска List argList = Arrays.asList(args); - boolean cliMode = argList.contains("--cli"); - boolean jfxMode = !cliMode; // по умолчанию JFX + isCliMode = argList.contains("--cli"); + isJfxMode = !isCliMode; + + log("Режим: " + (isCliMode ? "CLI" : "JFX")); - // Проверка и обновление лаунчера String currentVersion = readCurrentVersion(); String serverVersion = getServerVersion(); log("Локальная версия: " + currentVersion); log("Версия на сервере: " + serverVersion); - // Загружаем mirrors loadMirrors(); log("Основной сервер: " + BASE_URL); log("Mirrors доступны: " + (MIRRORS.size() + 1)); @@ -64,14 +70,120 @@ public class Bootstrap { log("Версия актуальна"); } - // Запуск в выбранном режиме - if (jfxMode) { - launchJFX(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log("Получен сигнал завершения..."); + })); + + launchMain(args); + } + + private static void launchMain(String[] args) throws Exception { + log("Загрузка лаунчера: " + getLauncherJar()); + + if (isCliMode) { + launchInProcess(args); } else { - launchCLI(); + launchInNewProcess(args); } } + private static void launchInProcess(String[] args) throws Exception { + ClassLoader parent = Bootstrap.class.getClassLoader(); + URL[] urls = { getLauncherJar().toUri().toURL() }; + URLClassLoader cl = new URLClassLoader(urls, parent); + + Thread.currentThread().setContextClassLoader(cl); + + try { + Class mainClass = cl.loadClass("me.sashegdev.zernmc.launcher.Main"); + java.lang.reflect.Method mainMethod = mainClass.getMethod("main", String[].class); + mainMethod.invoke(null, (Object) args); + } finally { + cl.close(); + } + } + + private static void launchInNewProcess(String[] args) throws Exception { + String os = System.getProperty("os.name").toLowerCase(); + String javaExe = os.contains("windows") ? "javaw.exe" : "java"; + + Path javaBin = findJava(false); + Path javafxPath = baseDir.resolve("lib").resolve("javafx"); + + List cmd = new ArrayList<>(); + cmd.add(javaBin.toAbsolutePath().toString()); + cmd.add("-Dfile.encoding=UTF-8"); + cmd.add("-Dsun.stdout.encoding=UTF-8"); + cmd.add("-Dsun.stderr.encoding=UTF-8"); + cmd.add("-Dlauncher.server=" + BASE_URL); + + if (Files.exists(javafxPath)) { + cmd.add("--module-path"); + cmd.add(javafxPath.toAbsolutePath().toString()); + cmd.add("--add-modules"); + cmd.add("javafx.controls,javafx.web"); + } + + cmd.add("-jar"); + cmd.add(getLauncherJar().toAbsolutePath().toString()); + cmd.add("--jfx"); + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(baseDir.toFile()); + pb.redirectErrorStream(true); + + Process p = pb.start(); + + Thread outputThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } catch (Exception ignored) {} + }); + outputThread.start(); + + int code = p.waitFor(); + outputThread.interrupt(); + + log("JFX процесс завершился с кодом: " + code); + System.exit(code); + } + + private static Path findJava(boolean preferConsole) { + String os = System.getProperty("os.name").toLowerCase(); + String javaExe; + + if (preferConsole || !os.contains("windows")) { + javaExe = "java"; + } else { + javaExe = "javaw.exe"; + } + + Path javaBin = baseDir.resolve("jre21").resolve("bin").resolve(javaExe); + if (!Files.exists(javaBin)) { + javaBin = Paths.get(System.getProperty("java.home"), "bin", javaExe); + } + if (!Files.exists(javaBin)) { + try { + Process p = new ProcessBuilder("where", javaExe).redirectErrorStream(true).start(); + if (p.waitFor() == 0) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String path = br.readLine(); + if (path != null) javaBin = Paths.get(path.trim()); + } + } + } catch (Exception ignored) {} + } + + if (!Files.exists(javaBin)) { + throw new RuntimeException("Java не найдена"); + } + return javaBin; + } + private static void log(String msg) { String entry = "[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "] " + msg; System.out.println(entry); @@ -353,127 +465,6 @@ public class Bootstrap { } } - private static void launchJFX() throws Exception { - Path javaBin = findJava(false); // false = use javaw for GUI mode - Path jarPath = getLauncherJar(); - - log("Запуск JFX режима..."); - log("Java: " + javaBin); - log("JAR: " + jarPath); - - Path consoleLog = logDir.resolve("console.log"); - - // JVM аргументы для UTF-8 и JavaFX - Path javafxPath = baseDir.resolve("lib").resolve("javafx"); - List jvmArgs; - if (Files.exists(javafxPath)) { - jvmArgs = List.of( - "-Dfile.encoding=UTF-8", - "-Dsun.stdout.encoding=UTF-8", - "-Dsun.stderr.encoding=UTF-8", - "-Dlauncher.server=" + BASE_URL, - "--module-path", javafxPath.toAbsolutePath().toString(), - "--add-modules", "javafx.controls,javafx.web" - ); - } else { - jvmArgs = List.of( - "-Dfile.encoding=UTF-8", - "-Dsun.stdout.encoding=UTF-8", - "-Dsun.stderr.encoding=UTF-8", - "-Dlauncher.server=" + BASE_URL - ); - } - - List cmd = new ArrayList<>(); - cmd.add(javaBin.toAbsolutePath().toString()); - cmd.addAll(jvmArgs); - cmd.add("-jar"); - cmd.add(jarPath.toAbsolutePath().toString()); - cmd.add("--jfx"); - - ProcessBuilder pb = new ProcessBuilder(cmd); - pb.directory(baseDir.toFile()); - pb.inheritIO(); - - Process p = pb.start(); - int code = p.waitFor(); - log("Завершено с кодом: " + code); - System.exit(code); - } - - private static void launchCLI() throws Exception { - Path javaBin = findJava(true); - Path jarPath = getLauncherJar(); // <-- added this line - - log("Запуск CLI режима..."); - log("Java: " + javaBin); - log("JAR: " + jarPath); - - // JVM аргументы для UTF-8 - List jvmArgs = List.of( - "-Dfile.encoding=UTF-8", - "-Dsun.stdout.encoding=UTF-8", - "-Dsun.stderr.encoding=UTF-8", - "-Dlauncher.server=" + BASE_URL - ); - - List cmd = new ArrayList<>(); - cmd.add(javaBin.toAbsolutePath().toString()); - cmd.addAll(jvmArgs); - cmd.add("-jar"); - cmd.add(jarPath.toAbsolutePath().toString()); - cmd.add("--cli"); - - ProcessBuilder pb = new ProcessBuilder(cmd); - pb.directory(baseDir.toFile()); - pb.redirectErrorStream(true); - - Process p = pb.inheritIO().start(); - int code = p.waitFor(); - log("Завершено с кодом: " + code); - System.exit(code); - } - - private static Path findJava(boolean needConsole) { - String os = System.getProperty("os.name").toLowerCase(); - String javaExe; - - if (needConsole || !os.contains("windows")) { - javaExe = "java"; - } else { - javaExe = "javaw.exe"; - } - - // Сначала ищем jre21/bin/java рядом с лаунчером - Path javaBin = baseDir.resolve("jre21").resolve("bin").resolve(javaExe); - - // Если нет, пробуем системную Java - if (!Files.exists(javaBin)) { - javaBin = Paths.get(System.getProperty("java.home"), "bin", javaExe); - } - - // Если и это не найдено - ищем java в PATH - if (!Files.exists(javaBin)) { - try { - Process p = new ProcessBuilder("which", javaExe).start(); - if (p.waitFor() == 0) { - try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) { - String path = br.readLine(); - if (path != null) { - javaBin = Paths.get(path.trim()); - } - } - } - } catch (Exception ignored) {} - } - - if (!Files.exists(javaBin)) { - throw new RuntimeException("Java не найдена. Убедитесь, что jre21 присутствует в папке с лаунчером или Java установлена в системе"); - } - - return javaBin; - } - private static void loadMirrors() { try { URL url = new URL(BASE_URL + "/launcher/mirrors"); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java index 6c76cf3..40282cc 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java @@ -15,24 +15,23 @@ public class Main { private static final LauncherAPI api = new LauncherAPI(); public static void main(String[] args) throws IOException { - // Настройка кодировки для Windows и Linux - System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true"); System.setProperty("file.encoding", "UTF-8"); - System.setProperty("sun.err.encoding", "UTF-8"); + System.setProperty("sun.stderr.encoding", "UTF-8"); System.setProperty("sun.stdout.encoding", "UTF-8"); + System.setProperty("java.stdout.encoding", "UTF-8"); + System.setProperty("java.stderr.encoding", "UTF-8"); + System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true"); - // Для Windows CMD - пытаемся переключить в UTF-8 режим - try { - if (System.getProperty("os.name").toLowerCase().contains("windows")) { + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + try { new ProcessBuilder("cmd", "/c", "chcp", "65001").inheritIO().start().waitFor(); - } - } catch (Exception ignored) {} - + } catch (Exception ignored) {} + } + ZAnsi.install(); System.out.print("\033[H\033[2J"); System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION)); - // Определяем режим запуска List argList = List.of(args); boolean jfxMode = argList.contains("--jfx"); boolean cliMode = argList.contains("--cli"); @@ -42,7 +41,6 @@ public class Main { return; } - // CLI режим (по умолчанию или с --cli) startCLI(); } diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java index eaf4eb5..b3c8be9 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java @@ -5,6 +5,7 @@ import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import org.jline.utils.InfoCmp; +import java.nio.charset.StandardCharsets; import java.io.IOException; import java.util.List; @@ -15,7 +16,7 @@ public class ArrowMenu { private int selected = 0; private final Terminal terminal; - private static final int VISIBLE_ITEMS = 7; // сколько строк показывать в списке + private static final int VISIBLE_ITEMS = 7; public ArrowMenu(String title, List options) throws IOException { this.title = title; @@ -23,6 +24,7 @@ public class ArrowMenu { this.terminal = TerminalBuilder.builder() .system(true) .jna(true) + .encoding(StandardCharsets.UTF_8) .build(); } @@ -37,28 +39,35 @@ public class ArrowMenu { int key = terminal.reader().read(); if (key == 'w' || key == 'W' || key == 'ц' || key == 'Ц' - || key == 'k' || key == 'K' || key == 'л' || key == 'Л') { // Up / Arrow Up + || key == 'k' || key == 'K' || key == 'л' || key == 'Л') { selected = (selected - 1 + options.size()) % options.size(); } else if (key == 's' || key == 'S' || key == 'ы' || key == 'Ы' - || key == 'j' || key == 'J' || key == 'о' || key == 'О') { // Down / Arrow Down + || key == 'j' || key == 'J' || key == 'о' || key == 'О') { selected = (selected + 1) % options.size(); } - else if (key == 13 || key == 10) { // Enter + else if (key == 13 || key == 10) { return selected; } - else if (key == 27) { // Esc or arrow escape seq - int next = terminal.reader().read(); - if (next == 91) { // '[' — start of arrow escape sequence - int arrow = terminal.reader().read(); - if (arrow == 65) { // 'A' — Up arrow + else if (key == 27) { + int next = nonBlockingRead(); + if (next == -1) { + return -1; + } + if (next == 91) { + int arrow = nonBlockingRead(); + if (arrow == 65) { selected = (selected - 1 + options.size()) % options.size(); - } else if (arrow == 66) { // 'B' — Down arrow + } else if (arrow == 66) { selected = (selected + 1) % options.size(); } - // else — unknown escape seq, ignore - } else { - return -1; // genuine Esc + } else if (next == 79) { + int arrow = nonBlockingRead(); + if (arrow == 68) { + selected = (selected - 1 + options.size()) % options.size(); + } else if (arrow == 70) { + return options.size() - 1; + } } } } @@ -67,20 +76,31 @@ public class ArrowMenu { terminal.close(); } } + + private int nonBlockingRead() throws IOException { + while (true) { + int available = terminal.reader().available(); + if (available > 0) { + return terminal.reader().read(); + } + try { + Thread.sleep(10); + } catch (InterruptedException e) { + return -1; + } + } + } private void printPagedMenu() { StringBuilder sb = new StringBuilder(); sb.append("\033[H\033[2J"); - // Заголовок (фиксированный) sb.append(ZAnsi.header("=== ZernMC Launcher ===")).append("\n\n"); sb.append(ZAnsi.yellow(title)).append("\n\n"); - // Вычисляем диапазон отображаемых элементов int start = Math.max(0, selected - (VISIBLE_ITEMS / 2)); int end = Math.min(options.size(), start + VISIBLE_ITEMS); - // Если в конце списка — подтягиваем вверх if (end - start < VISIBLE_ITEMS && start > 0) { start = Math.max(0, end - VISIBLE_ITEMS); } @@ -94,10 +114,10 @@ public class ArrowMenu { } } - // Подсказка внизу (фиксированная) sb.append("\n") .append(ZAnsi.white("W/S (Ц/Ы) или ↑/↓ - перемещение | Enter - выбрать | Esc - назад")); System.out.print(sb); + System.out.flush(); } } \ No newline at end of file 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 9a496e6..64b9f14 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 @@ -51,6 +51,11 @@ public class JFXLauncher extends Application { private static Path gameLogFile; private Stage mainStage; + private static volatile String installProgressLabel = ""; + private static volatile int installProgressCurrent = 0; + private static volatile int installProgressTotal = 100; + private static volatile boolean installInProgress = false; + private static final java.util.concurrent.CopyOnWriteArrayList logConsumers = new java.util.concurrent.CopyOnWriteArrayList<>(); private static final java.util.concurrent.CopyOnWriteArrayList gameLogConsumers = new java.util.concurrent.CopyOnWriteArrayList<>(); @@ -74,6 +79,20 @@ public class JFXLauncher extends Application { gameLogConsumers.remove(consumer); } + public static void setInstallProgress(String label, int current, int total) { + installProgressLabel = label; + installProgressCurrent = current; + installProgressTotal = total; + } + + public static void setInstallInProgress(boolean inProgress) { + installInProgress = inProgress; + } + + public static boolean isInstallInProgress() { + return installInProgress; + } + public static void appendLauncherLog(String log) { synchronized (launcherLogBuffer) { launcherLogBuffer.append(log).append("\n"); @@ -295,6 +314,17 @@ public class JFXLauncher extends Application { log("Закрытие..."); LaunchService.killAllProcesses(); if (server != null) server.stop(1); + + try { + java.net.HttpURLConnection conn = (java.net.HttpURLConnection) + new java.net.URL("http://localhost:" + PORT + "/api/exit-parent").openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(1000); + conn.getResponseCode(); + conn.disconnect(); + } catch (Exception ignored) {} + + Platform.exit(); }); } catch (Exception e) { @@ -308,10 +338,12 @@ public class JFXLauncher extends Application { server = HttpServer.create(new InetSocketAddress("localhost", PORT), 0); server.createContext("/api/login", this::handleLogin); + server.createContext("/api/auto-login", this::handleAutoLogin); server.createContext("/api/account", this::handleAccount); server.createContext("/api/instances", this::handleInstances); server.createContext("/api/launch", this::handleLaunch); server.createContext("/api/install", this::handleInstall); + server.createContext("/api/install/progress", this::handleInstallProgress); server.createContext("/api/logs", this::handleLogs); server.createContext("/api/logs/stream", this::handleLogsStream); server.createContext("/api/game-logs", this::handleGameLogs); @@ -321,6 +353,7 @@ public class JFXLauncher extends Application { server.createContext("/api/packs", this::handlePacks); server.createContext("/api/shutdown", this::handleShutdown); server.createContext("/api/exit", this::handleExit); + server.createContext("/api/exit-parent", this::handleExitParent); server.createContext("/assets/", this::handleStatic); server.setExecutor(Executors.newCachedThreadPool()); @@ -375,6 +408,24 @@ public class JFXLauncher extends Application { sendJson(exchange, Map.of("success", false, "error", e.getMessage())); } } + + private void handleAutoLogin(HttpExchange exchange) { + try { + if (AuthManager.loadSavedSession()) { + Map data = new HashMap<>(); + data.put("username", AuthManager.getUsername()); + data.put("passActive", AuthManager.hasActivePass()); + data.put("role", AuthManager.getRole()); + data.put("roleName", AuthManager.getRoleName()); + sendJson(exchange, Map.of("success", true, "data", data, "autoLogin", true)); + log("Автологин выполнен: " + AuthManager.getUsername()); + } else { + sendJson(exchange, Map.of("success", false, "autoLogin", false)); + } + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } private void handleInstances(HttpExchange exchange) { try { @@ -418,6 +469,11 @@ public class JFXLauncher extends Application { } private void handleInstall(HttpExchange exchange) { + if (installInProgress) { + sendJson(exchange, Map.of("success", false, "error", "Установка уже выполняется")); + return; + } + try { if (!api.isLoggedIn()) { sendJson(exchange, Map.of("success", false, "error", "Не авторизован")); @@ -446,26 +502,61 @@ public class JFXLauncher extends Application { instance.setLoaderVersion(loaderVersion); } - MinecraftLib lib = new MinecraftLib(instance); + sendJson(exchange, Map.of("success", true, "message", "Установка началась")); - boolean success = false; - if ("vanilla".equalsIgnoreCase(loader)) { - success = lib.installMinecraft(version); - } else { - success = lib.installPack(name, version, loader, loaderVersion != null ? loaderVersion : ""); - } + setInstallInProgress(true); + setInstallProgress("Подготовка...", 0, 100); - if (success) { - log("Установлено: " + name); - sendJson(exchange, Map.of("success", true, "data", true)); - } else { - sendJson(exchange, Map.of("success", false, "error", "Ошибка установки")); - } + Thread installThread = new Thread(() -> { + try { + MinecraftLib lib = new MinecraftLib(instance); + + boolean success = false; + if ("vanilla".equalsIgnoreCase(loader)) { + success = lib.installMinecraft(version); + } else { + success = lib.installPack(name, version, loader, loaderVersion != null ? loaderVersion : ""); + } + + setInstallInProgress(false); + if (success) { + log("Установлено: " + name); + } else { + log("Ошибка установки: " + name); + } + } catch (Exception e) { + log("Ошибка установки: " + e.getMessage()); + setInstallInProgress(false); + } + }); + installThread.setDaemon(true); + installThread.start(); } else { sendJson(exchange, Map.of("success", false, "error", "Instance not found")); } } catch (Exception e) { log("Ошибка установки: " + e.getMessage()); + setInstallInProgress(false); + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleInstallProgress(HttpExchange exchange) { + try { + Map progress = new java.util.HashMap<>(); + progress.put("label", installProgressLabel); + progress.put("current", installProgressCurrent); + progress.put("total", installProgressTotal); + progress.put("inProgress", installInProgress); + + if (installInProgress && installProgressTotal > 0) { + progress.put("percent", (int) ((installProgressCurrent * 100.0) / installProgressTotal)); + } else { + progress.put("percent", installInProgress ? 0 : 100); + } + + sendJson(exchange, Map.of("success", true, "data", progress)); + } catch (Exception e) { sendJson(exchange, Map.of("success", false, "error", e.getMessage())); } } @@ -621,6 +712,14 @@ public class JFXLauncher extends Application { Platform.exit(); System.exit(0); } + + private void handleExitParent(HttpExchange exchange) { + log("Завершение родительского процесса..."); + LaunchService.killAllProcesses(); + if (mainStage != null) mainStage.close(); + Platform.exit(); + System.exit(240); + } private void handleStatic(HttpExchange exchange) { try { diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Input.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Input.java index 988505e..cbd7cbc 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Input.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Input.java @@ -3,6 +3,7 @@ package me.sashegdev.zernmc.launcher.utils; import me.sashegdev.zernmc.launcher.ui.ArrowMenu; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Scanner; @@ -11,15 +12,14 @@ import java.util.Scanner; */ public class Input { - // Используем UTF-8 явно — это помогает на Windows - private static final Scanner scanner = new Scanner(System.in, "UTF-8"); + private static final Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8); public static String readLine() { return scanner.nextLine().trim(); } public static String readLine(String prompt) { - flushInput(); // Очищаем буфер + flushInput(); System.out.print(prompt); return scanner.nextLine().trim(); } @@ -50,7 +50,7 @@ public class Input { * @throws IOException */ public static boolean confirm(String question) throws IOException { - ConsoleUtils.clearScreen(); // опционально, можно убрать + ConsoleUtils.clearScreen(); List options = List.of( "Да", @@ -60,7 +60,7 @@ public class Input { ArrowMenu menu = new ArrowMenu(question, options); int choice = menu.show(); - return choice == 0; // 0 = "Да" + return choice == 0; } /** @@ -91,7 +91,6 @@ public class Input { System.in.read(); } } catch (IOException e) { - // Игнорируем } } } \ No newline at end of file diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java index d85bb2d..0fc863f 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java @@ -6,11 +6,23 @@ public class ProgressBar { private static final int BAR_LENGTH = 40; private static final DecimalFormat DF = new DecimalFormat("#.##"); + + private static String currentLabel = ""; + private static long currentTotal = 0; /** * Прогресс по количеству файлов (для библиотек и общего прогресса) */ public static void show(String label, long current, long total, String unit) { + currentLabel = label; + currentTotal = total; + + try { + Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); + java.lang.reflect.Method setProgress = jfxClass.getMethod("setInstallProgress", String.class, int.class, int.class); + setProgress.invoke(null, label, (int) current, (int) total); + } catch (Exception ignored) {} + if (total <= 0) { System.out.print("\r" + ZAnsi.cyan(label) + " ..."); return; @@ -31,6 +43,15 @@ public class ProgressBar { * Прогресс по байтам для одного файла (реальный прогресс) */ public static void showDownload(String label, long downloaded, long totalBytes) { + currentLabel = label; + currentTotal = totalBytes; + + try { + Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); + java.lang.reflect.Method setProgress = jfxClass.getMethod("setInstallProgress", String.class, int.class, int.class); + setProgress.invoke(null, label + " " + formatBytes(downloaded) + "/" + formatBytes(totalBytes), (int) downloaded, (int) totalBytes); + } catch (Exception ignored) {} + if (totalBytes <= 0) { System.out.print("\r" + ZAnsi.cyan(label) + " ..."); return; @@ -53,8 +74,16 @@ public class ProgressBar { } public static void showAnimated(String label, long current, long total, String unit) { + currentLabel = label; + currentTotal = total; + + try { + Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); + java.lang.reflect.Method setProgress = jfxClass.getMethod("setInstallProgress", String.class, int.class, int.class); + setProgress.invoke(null, label, (int) current, (int) (total > 0 ? total : 100)); + } catch (Exception ignored) {} + if (total <= 0) { - // Анимация для неизвестного размера char[] spinner = {'|', '/', '-', '\\'}; int idx = (int) (current / 1024) % 4; System.out.print("\r" + label + " [" + spinner[idx] + "] " + formatBytes(current)); @@ -64,6 +93,12 @@ public class ProgressBar { } public static void finish(String message) { + try { + Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); + java.lang.reflect.Method setInProgress = jfxClass.getMethod("setInstallInProgress", boolean.class); + setInProgress.invoke(null, false); + } catch (Exception ignored) {} + System.out.println("\r" + ZAnsi.brightGreen(message + " завершено ✓")); System.out.flush(); } diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js index 43abafb..df51e45 100644 --- a/launcher/launcher/src/resources/ui/launcher.js +++ b/launcher/launcher/src/resources/ui/launcher.js @@ -272,18 +272,30 @@ class LauncherApp { async checkAuth() { this.showLoading(true); - const result = await this.request('/account'); - - if (result.success) { - this.account = result.data; - this.username = result.data.username; + + const autoLoginResult = await this.request('/auto-login'); + if (autoLoginResult.success && autoLoginResult.autoLogin) { + this.account = autoLoginResult.data; + this.username = autoLoginResult.data.username; this.showMainScreen(); this.renderCurrentInstance(); this.startConsoleLogStream(); this.startGameLogStream(); await this.loadInstances(); + this.addLog('Автовход выполнен: ' + this.username, 'success'); } else { - this.showLoginScreen(); + const result = await this.request('/account'); + if (result.success) { + this.account = result.data; + this.username = result.data.username; + this.showMainScreen(); + this.renderCurrentInstance(); + this.startConsoleLogStream(); + this.startGameLogStream(); + await this.loadInstances(); + } else { + this.showLoginScreen(); + } } this.showLoading(false); @@ -358,7 +370,7 @@ class LauncherApp { } async shutdownLauncher() { - const result = await this.request('/shutdown', { method: 'POST' }); + await this.request('/exit-parent', { method: 'POST' }); window.close(); } @@ -520,17 +532,24 @@ class LauncherApp { const packResult = await this.request('/packs'); if (packResult.success && packResult.data && packResult.data.length > 0) { - zernmcSelect.innerHTML = ''; - packResult.data.forEach(p => { - const opt = document.createElement('option'); - opt.value = p.name; - opt.textContent = p.displayName + ' (' + p.version + ')'; - zernmcSelect.appendChild(opt); - }); + if (this.account && this.account.passActive) { + zernmcSelect.innerHTML = ''; + packResult.data.forEach(p => { + const opt = document.createElement('option'); + opt.value = p.name; + opt.textContent = p.displayName + ' (' + p.version + ')'; + zernmcSelect.appendChild(opt); + }); + } else { + zernmcSelect.innerHTML = ''; + zernmcSelect.disabled = true; + } } else if (packResult.error && packResult.error.includes('проходка')) { zernmcSelect.innerHTML = ''; + zernmcSelect.disabled = true; } else { zernmcSelect.innerHTML = ''; + zernmcSelect.disabled = true; } const zernmcTab = document.querySelector('[data-tab="zernmc"]'); @@ -564,7 +583,7 @@ class LauncherApp { const packName = document.getElementById('zernmc-pack-select').value; const instanceName = document.getElementById('zernmc-instance-name').value; - if (!packName) { + if (!packName || packName === '') { alert('Выберите сборку'); return; } @@ -582,7 +601,7 @@ class LauncherApp { body: JSON.stringify({ name: instanceName, version: 'latest', - loader: 'vanilla' + loader: 'zernmc' }) }); @@ -670,7 +689,34 @@ class LauncherApp { progress.classList.remove('hidden'); progressText.textContent = text; - progressFill.style.width = '50%'; + progressFill.style.width = '0%'; + this.progressInterval = setInterval(() => this.pollInstallProgress(), 500); + } + + async pollInstallProgress() { + try { + const result = await this.request('/install/progress'); + if (result.success && result.data) { + const { label, current, total, percent } = result.data; + const progressFill = document.getElementById('progress-fill'); + const progressText = document.getElementById('progress-text'); + + if (percent !== undefined) { + progressFill.style.width = percent + '%'; + } + if (label) { + progressText.textContent = label; + this.addLog(label, 'info'); + } + + if (!result.data.inProgress) { + clearInterval(this.progressInterval); + this.hideProgress(); + } + } + } catch (e) { + clearInterval(this.progressInterval); + } } hideProgress() { diff --git a/launcher/launcher/src/resources/ui/style.css b/launcher/launcher/src/resources/ui/style.css index e8b066e..667631e 100644 --- a/launcher/launcher/src/resources/ui/style.css +++ b/launcher/launcher/src/resources/ui/style.css @@ -22,12 +22,11 @@ --transition-fast: 150ms ease; --transition-normal: 300ms ease; --transition-slow: 500ms ease; + --base-font-size: 13px; } -* { - margin: 0; - padding: 0; - box-sizing: border-box; +html { + font-size: var(--base-font-size); } body { @@ -35,7 +34,7 @@ body { background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; - overflow-x: hidden; + overflow: hidden; } #grid-canvas { @@ -229,9 +228,9 @@ body { /* ==================== MAIN LAYOUT ==================== */ .main-layout { display: grid; - grid-template-columns: 280px 1fr 200px; + grid-template-columns: 240px 1fr 180px; + grid-template-rows: 1fr auto; width: 100%; - max-width: 1600px; height: calc(100vh - 40px); gap: 0; background: var(--bg-secondary); @@ -247,7 +246,8 @@ body { border-right: 1px solid var(--border-color); display: flex; flex-direction: column; - padding: 20px; + padding: 16px; + grid-row: 1; } .sidebar-header { @@ -498,9 +498,10 @@ body { .main-content { display: flex; flex-direction: column; - padding: 20px; + padding: 12px; background: var(--bg-primary); height: 100%; + grid-row: 1; } .logs-section { @@ -518,23 +519,23 @@ body { display: flex; justify-content: space-between; align-items: center; - padding: 16px 20px; + padding: 10px 14px; border-bottom: 1px solid var(--border-color); } .logs-header h2 { - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--text-primary); } .btn-clear-logs { - padding: 6px 12px; + padding: 4px 10px; background: transparent; border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-muted); - font-size: 12px; + font-size: 11px; cursor: pointer; transition: var(--transition-fast); } @@ -546,15 +547,15 @@ body { .logs-container { flex: 1; - padding: 16px 20px; + padding: 10px 14px; overflow-y: auto; font-family: 'JetBrains Mono', 'Consolas', monospace; - font-size: 12px; - line-height: 1.6; + font-size: 11px; + line-height: 1.5; } .log-entry { - padding: 4px 0; + padding: 2px 0; color: var(--text-secondary); animation: fadeIn var(--transition-fast) forwards; } @@ -580,25 +581,26 @@ body { display: flex; align-items: flex-end; justify-content: center; - padding: 30px; + padding: 20px; border-left: 1px solid var(--border-color); background: var(--bg-sidebar); + grid-row: 1 / 3; } .btn-play { width: 100%; - padding: 20px 30px; + padding: 16px 20px; background: linear-gradient(135deg, var(--success), #22c55e); border: none; border-radius: var(--radius-md); color: #0a0a0f; - font-size: 18px; + font-size: 16px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; - gap: 12px; + gap: 10px; transition: var(--transition-normal); box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4); } @@ -621,18 +623,18 @@ body { .btn-update { width: 100%; - padding: 20px 30px; + padding: 16px 20px; background: linear-gradient(135deg, var(--warning), #f59e0b); border: none; border-radius: var(--radius-md); color: #1a1a24; - font-size: 18px; + font-size: 16px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; - gap: 12px; + gap: 10px; transition: var(--transition-normal); box-shadow: 0 4px 20px rgba(251, 191, 36, 0.4); }