чиним cli + ui..... ДА БЛЯ НУ СКОЛЬКО МОЖНО ТО А

This commit is contained in:
SashegDev
2026-05-10 02:48:13 +00:00
parent 1d5241075b
commit a765d064c4
8 changed files with 409 additions and 219 deletions
@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> jvmArgs = List.of(
"-Dfile.encoding=UTF-8",
"-Dsun.stdout.encoding=UTF-8",
"-Dsun.stderr.encoding=UTF-8",
"-Dlauncher.server=" + BASE_URL
);
List<String> 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");
@@ -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<String> 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();
}
@@ -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<String> 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();
}
}
@@ -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<LogConsumer> logConsumers = new java.util.concurrent.CopyOnWriteArrayList<>();
private static final java.util.concurrent.CopyOnWriteArrayList<LogConsumer> 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<String, Object> 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<String, Object> 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 {
@@ -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<String> 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) {
// Игнорируем
}
}
}
@@ -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();
}
+63 -17
View File
@@ -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 = '<option value="">Выберите сборку</option>';
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 = '<option value="">Выберите сборку</option>';
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 = '<option value="">Требуется проходка</option>';
zernmcSelect.disabled = true;
}
} else if (packResult.error && packResult.error.includes('проходка')) {
zernmcSelect.innerHTML = '<option value="">Требуется проходка</option>';
zernmcSelect.disabled = true;
} else {
zernmcSelect.innerHTML = '<option value="">Сборки недоступны</option>';
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() {
+26 -24
View File
@@ -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);
}