diff --git a/launcher/dependency-reduced-pom.xml b/launcher/dependency-reduced-pom.xml index 2c4af15..728e240 100644 --- a/launcher/dependency-reduced-pom.xml +++ b/launcher/dependency-reduced-pom.xml @@ -3,9 +3,13 @@ 4.0.0 me.sashegdev ZernMCLauncher - 1.0.7 + 1.0.8 + + maven-surefire-plugin + 3.2.3 + maven-shade-plugin 3.5.0 @@ -24,11 +28,45 @@ ${project.version} ZernMC Launcher SashegDev - Полностью самописный Minecraft-лаунчер. Написанный SashegDev(в основном) + Samopisnui Minecraft-launcher. by SashegDev https://github.com/SashegDev/launcher + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + / + false + true + runtime + + + + + + + maven-dependency-plugin + 3.6.0 + + + copy-javafx + package + + copy-dependencies + + + ${project.build.directory}/lib-javafx + runtime + org.openjfx @@ -45,7 +83,7 @@ launch4j - ../server/builds/ZernMCLauncher.exe + ../server/builds/ZernMCLauncher-${project.version}.exe ../server/builds/ZernMCLauncher.jar console false @@ -56,13 +94,13 @@ ${project.version}.0 ${project.version} - ZernMC Launcher — A Little Minecraft Launcher + ZernMC Launcher — just a Minecraft launcher ${project.version}.0 ${project.version} ZernMC Launcher ZernMC(SashegDev) ZernMCLauncher - ZernMCLauncher.exe + ZernMCLauncher-${project.version}.exe @@ -109,10 +147,35 @@ + + + org.junit.jupiter + junit-jupiter + 5.10.1 + test + + + junit-jupiter-api + org.junit.jupiter + + + junit-jupiter-params + org.junit.jupiter + + + junit-jupiter-engine + org.junit.jupiter + + + + - 21 + ZernMC Launcher - just a minimalistic launcher by SashegDev me.sashegdev.zernmc.launcher.Main 21 + ZernMC + 21 UTF-8 + 2026 diff --git a/launcher/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.class b/launcher/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.class new file mode 100644 index 0000000..612370b Binary files /dev/null and b/launcher/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.class differ diff --git a/launcher/pom.xml b/launcher/pom.xml index 1e9df57..1d2a355 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -60,6 +60,26 @@ commons-io 2.15.1 + + io.javalin + javalin + 6.1.3 + + + org.slf4j + slf4j-simple + 2.0.11 + + + org.openjfx + javafx-controls + 21.0.2 + + + org.openjfx + javafx-web + 21.0.2 + org.junit.jupiter junit-jupiter @@ -101,6 +121,43 @@ + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + / + false + true + runtime + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.0 + + + copy-javafx + package + + copy-dependencies + + + ${project.build.directory}/lib-javafx + runtime + org.openjfx diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java index 8e97e0a..adca0ec 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java @@ -4,6 +4,10 @@ import me.sashegdev.zernmc.launcher.auth.AuthManager; import me.sashegdev.zernmc.launcher.menu.*; import me.sashegdev.zernmc.launcher.ui.ArrowMenu; import me.sashegdev.zernmc.launcher.utils.*; +import me.sashegdev.zernmc.launcher.web.UIWindow; +import me.sashegdev.zernmc.launcher.web.WebServer; + +import java.awt.GraphicsEnvironment; import java.io.IOException; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -11,13 +15,72 @@ import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.Arrays; import java.util.List; public class Main { private static final String CURRENT_VERSION = Version.getCurrentVersion(); - public static void main(String[] args) throws IOException { + public static void main(String[] args) throws Exception { + boolean cliMode = Arrays.asList(args).contains("--cli") || Arrays.asList(args).contains("-c"); + + if (cliMode) { + runTUI(args); + } else { + try { + startWebUI(args); + } catch (Exception e) { + System.err.println(ZAnsi.red("UI не запустился: " + e.getMessage())); + System.out.println(ZAnsi.yellow("Переключаюсь на режим TUI...")); + runTUI(args); + } + } + } + + private static void startWebUI(String[] args) throws Exception { + System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true"); + System.setProperty("file.encoding", "UTF-8"); + + int startPort = 8080; + for (int i = 0; i < args.length - 1; i++) { + if (args[i].equals("--port") || args[i].equals("-p")) { + startPort = Integer.parseInt(args[i + 1]); + } + } + + System.out.println(ZAnsi.brightGreen("Запуск Web UI...")); + System.out.println(ZAnsi.cyan("Поиск свободного порта...")); + + int port = WebServer.findFreePort(startPort); + + // Запускаем WebServer в отдельном потоке + Thread serverThread = new Thread(() -> { + try { + WebServer.start(port); + } catch (Exception e) { + System.err.println("WebServer error: " + e.getMessage()); + } + }); + serverThread.setDaemon(true); + serverThread.start(); + + // Даем серверу время запуститься + Thread.sleep(1000); + + // Проверяем headless перед запуском JavaFX + if (java.awt.GraphicsEnvironment.isHeadless()) { + System.out.println(ZAnsi.yellow("Дисплей недоступен, переключаюсь на TUI...")); + WebServer.stop(); + runTUI(args); + return; + } + + // Запускаем JavaFX окно + UIWindow.start(port); + } + + private static void runTUI(String[] args) throws IOException { System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true"); System.setProperty("file.encoding", "UTF-8"); System.setProperty("sun.err.encoding", "UTF-8"); @@ -25,7 +88,7 @@ public class Main { ZAnsi.install(); System.out.print("\033[H\033[2J"); - System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION)); + System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION) + ZAnsi.cyan(" [CLI mode]")); // Проверка всех сервисов при старте ZHttpClient.checkAllServicesOnStartup(); diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/LauncherAPI.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/LauncherAPI.java new file mode 100644 index 0000000..19dc92b --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/LauncherAPI.java @@ -0,0 +1,81 @@ +package me.sashegdev.zernmc.launcher.api; + +import me.sashegdev.zernmc.launcher.api.auth.AuthService; +import me.sashegdev.zernmc.launcher.api.instance.InstanceService; +import me.sashegdev.zernmc.launcher.api.install.InstallService; +import me.sashegdev.zernmc.launcher.api.launch.LaunchService; + +import java.util.List; + +/** + * Центральный фасад для внутреннего API лаунчера. + * Используется как единая точка входа для UI и других компонентов. + */ +public class LauncherAPI { + + private final AuthService authService; + private final InstanceService instanceService; + private final LaunchService launchService; + private final InstallService installService; + + public LauncherAPI() { + this.authService = new AuthService(); + this.instanceService = new InstanceService(); + this.launchService = new LaunchService(); + this.installService = new InstallService(); + } + + public AuthService auth() { + return authService; + } + + public InstanceService instances() { + return instanceService; + } + + public LaunchService launch() { + return launchService; + } + + public InstallService install() { + return installService; + } + + // ====================== Удобные методы ====================== + + public boolean isLoggedIn() { + return authService.isLoggedIn(); + } + + public String getCurrentUsername() { + return authService.getCurrentUsername(); + } + + public ApiResponse checkSession() { + return authService.checkSession(); + } + + public ApiResponse login(String username, String password) { + return authService.login(username, password); + } + + public ApiResponse logout() { + return authService.logout(); + } + + public ApiResponse> getAllInstances() { + return instanceService.getAllInstances(); + } + + public ApiResponse getLaunchInfo(String instanceName) { + return launchService.getLaunchInfo(instanceName); + } + + public ApiResponse prepareLaunch(String instanceName) { + return launchService.prepareLaunch(instanceName); + } + + public ApiResponse launch(String instanceName) { + return launchService.launch(instanceName); + } +} diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java new file mode 100644 index 0000000..78777a3 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/api/install/InstallService.java @@ -0,0 +1,216 @@ +package me.sashegdev.zernmc.launcher.api.install; + +import me.sashegdev.zernmc.launcher.api.ApiResponse; +import me.sashegdev.zernmc.launcher.minecraft.Instance; +import me.sashegdev.zernmc.launcher.minecraft.InstanceManager; +import me.sashegdev.zernmc.launcher.minecraft.PackDownloader; +import me.sashegdev.zernmc.launcher.minecraft.ServerPack; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class InstallService { + + public ApiResponse installZernMCPack(String packName, String instanceName) { + try { + boolean created = InstanceManager.createInstanceFolder(instanceName); + if (!created) { + return ApiResponse.error("Сборка с таким именем уже существует: " + instanceName); + } + + Instance instance = InstanceManager.getInstance(instanceName); + if (instance == null) { + return ApiResponse.error("Не удалось создать директорию сборки"); + } + + PackDownloader downloader = new PackDownloader(instance); + + // Получаем список доступных сборок + List availablePacks = downloader.getAvailablePacks(); + + // Находим нужную сборку + ServerPack selectedPack = availablePacks.stream() + .filter(p -> p.getName().equals(packName)) + .findFirst() + .orElse(null); + + if (selectedPack == null) { + return ApiResponse.error("Сборка не найдена: " + packName); + } + + boolean success = downloader.installOrUpdatePack(packName, selectedPack); + + if (success) { + return ApiResponse.success(new InstallResult( + instanceName, + selectedPack.getMinecraftVersion(), + selectedPack.getLoaderType(), + selectedPack.getVersion() + )); + } else { + return ApiResponse.error("Не удалось установить сборку"); + } + } catch (Exception e) { + return ApiResponse.error("Ошибка установки: " + e.getMessage()); + } + } + + public ApiResponse checkForUpdates(String instanceName) { + try { + Instance instance = InstanceManager.getInstance(instanceName); + if (instance == null || !instance.isServerPack()) { + return ApiResponse.success(new UpdateCheckResult(false, false, 0, 0)); + } + + PackDownloader downloader = new PackDownloader(instance); + boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName()); + + return ApiResponse.success(new UpdateCheckResult( + hasUpdate, + true, + instance.getServerVersion(), + hasUpdate ? instance.getServerVersion() + 1 : instance.getServerVersion() + )); + } catch (Exception e) { + return ApiResponse.error("Ошибка проверки обновлений: " + e.getMessage()); + } + } + + public ApiResponse verifyHashes(String instanceName) { + try { + Instance instance = InstanceManager.getInstance(instanceName); + if (instance == null) { + return ApiResponse.error("Сборка не найдена: " + instanceName); + } + + if (!instance.isServerPack() || instance.getServerPackName() == null) { + return ApiResponse.success(new HashCheckResult(false, List.of())); + } + + PackDownloader downloader = new PackDownloader(instance); + Map localFiles = downloader.scanLocalFiles(); + + // Отправляем хеши на сервер через diff + var diff = downloader.getDiff(instance.getServerPackName(), localFiles); + + List mismatched = new ArrayList<>(); + for (var f : diff.getToDownload()) { + mismatched.add(f.getPath()); + } + mismatched.addAll(diff.getToUpdate()); + mismatched.addAll(diff.getToDelete()); + + boolean hasMismatches = !mismatched.isEmpty(); + + return ApiResponse.success(new HashCheckResult(hasMismatches, mismatched)); + } catch (Exception e) { + return ApiResponse.error("Ошибка проверки хешей: " + e.getMessage()); + } + } + + public ApiResponse getPlayTime(String instanceName) { + try { + Instance instance = InstanceManager.getInstance(instanceName); + if (instance == null) { + return ApiResponse.error("Сборка не найдена: " + instanceName); + } + + if (instance.isServerPack()) { + // TODO: Для ZernMC получаем время с сервера + // String response = ZHttpClient.get("/users/me/playtime?pack=" + instance.getServerPackName()); + // Пока возвращаем 0 - в будущем интегрировать с сервером + return ApiResponse.success(new PlayTimeInfo(0, true)); + } + + // Для локальных сборок возвращаем 0 + return ApiResponse.success(new PlayTimeInfo(0, false)); + } catch (Exception e) { + return ApiResponse.error("Ошибка получения времени: " + e.getMessage()); + } + } + + private int extractPlayTime(String json) { + try { + // Простой парсинг JSON + String minutes = json.replaceAll(".*\"minutes\"\\s*:\\s*(\\d+).*", "$1"); + return Integer.parseInt(minutes); + } catch (Exception e) { + return 0; + } + } + + public static class InstallResult { + private String name; + private String mcVersion; + private String loaderType; + private int serverVersion; + + public InstallResult(String name, String mcVersion, String loaderType, int serverVersion) { + this.name = name; + this.mcVersion = mcVersion; + this.loaderType = loaderType; + this.serverVersion = serverVersion; + } + + public String getName() { return name; } + public String getMcVersion() { return mcVersion; } + public String getLoaderType() { return loaderType; } + public int getServerVersion() { return serverVersion; } + } + + public static class UpdateCheckResult { + private boolean hasUpdate; + private boolean isServerPack; + private int currentVersion; + private int latestVersion; + + public UpdateCheckResult(boolean hasUpdate, boolean isServerPack, int currentVersion, int latestVersion) { + this.hasUpdate = hasUpdate; + this.isServerPack = isServerPack; + this.currentVersion = currentVersion; + this.latestVersion = latestVersion; + } + + public boolean isHasUpdate() { return hasUpdate; } + public boolean isServerPack() { return isServerPack; } + public int getCurrentVersion() { return currentVersion; } + public int getLatestVersion() { return latestVersion; } + } + + public static class HashCheckResult { + private boolean hasMismatches; + private List mismatchedFiles; + + public HashCheckResult(boolean hasMismatches, List mismatchedFiles) { + this.hasMismatches = hasMismatches; + this.mismatchedFiles = mismatchedFiles; + } + + public boolean hasMismatches() { return hasMismatches; } + public List getMismatchedFiles() { return mismatchedFiles; } + } + + public static class PlayTimeInfo { + private int totalMinutes; + private boolean fromServer; + + public PlayTimeInfo(int totalMinutes, boolean fromServer) { + this.totalMinutes = totalMinutes; + this.fromServer = fromServer; + } + + public int getTotalMinutes() { return totalMinutes; } + public boolean isFromServer() { return fromServer; } + + public String getFormattedTime() { + int hours = totalMinutes / 60; + int minutes = totalMinutes % 60; + if (hours > 0) { + return hours + "ч " + minutes + "м"; + } + return minutes + "м"; + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java index 9d75539..2b5af92 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java @@ -25,11 +25,17 @@ import java.util.stream.Collectors; public class LaunchMenu { + public static class ExitToMainMenuException extends Exception {} + public void show() throws Exception { - if (Config.isZernMCBuild()) { - showZernMCOnly(); - } else { - showGlobal(); + try { + if (Config.isZernMCBuild()) { + showZernMCOnly(); + } else { + showGlobal(); + } + } catch (ExitToMainMenuException e) { + // Возвращаемся в главное меню - ничего не делаем, просто выходим } } @@ -282,6 +288,15 @@ public class LaunchMenu { // ====================== manageInstance — полностью восстановлен ====================== private void manageInstance(Instance instance) throws Exception { while (true) { + // Проверяем, существует ли сборка (на случай если она была удалена вручную) + Instance currentInstance = InstanceManager.getInstance(instance.getName()); + if (currentInstance == null) { + System.out.println(ZAnsi.yellow("Сборка была удалена или не существует.")); + ConsoleUtils.pause(); + throw new ExitToMainMenuException(); // Выходим в главное меню + } + instance = currentInstance; // Обновляем ссылку на актуальный объект + ConsoleUtils.clearScreen(); System.out.println(ZAnsi.header("Управление сборкой: " + instance.getName())); System.out.println(ZAnsi.white("Версия: " + instance.getMinecraftVersion())); @@ -320,9 +335,13 @@ public class LaunchMenu { changeLoaderVersion(instance); } else { deleteInstance(instance); + throw new ExitToMainMenuException(); // Выходим в главное меню } } - case 3 -> deleteInstance(instance); + case 3 -> { + deleteInstance(instance); + throw new ExitToMainMenuException(); // Выходим в главное меню после удаления + } } } } @@ -423,14 +442,15 @@ public class LaunchMenu { boolean deleted = InstanceManager.deleteInstance(instance.getName()); if (deleted) { System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена.")); + // НЕ делаем pause(), сразу возвращаемся в manageInstance для выхода в меню сборок } else { System.out.println(ZAnsi.brightRed("Не удалось удалить сборку.")); + ConsoleUtils.pause(); } } else { System.out.println(ZAnsi.yellow("Удаление отменено.")); + ConsoleUtils.pause(); } - - ConsoleUtils.pause(); } private void launchExistingInstance(Instance instance) { @@ -625,9 +645,8 @@ public class LaunchMenu { } private boolean isNeoForgeSupported(String version) { - return version.matches("^1\\.20\\.[1-9].*") || - version.matches("^1\\.21.*") || - version.matches("^\\d{2}\\..*"); + // ВРЕМЕННО ОТКЛЮЧЕНО: в разработке + return false; } private String askFabricLoaderVersion() throws Exception { diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java index 2aae36d..3059770 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java @@ -272,7 +272,7 @@ public class PackDownloader { /** * Сканирование локальных файлов и вычисление хешей */ - private Map scanLocalFiles() throws IOException { + public Map scanLocalFiles() throws IOException { Map files = new HashMap<>(); Path instancePath = instance.getPath(); @@ -312,9 +312,9 @@ public class PackDownloader { } /** - * Отправить diff запрос на сервер + * Отправить diff запрос на сервер (получить список файлов для обновления) */ - private DiffResponse getDiff(String packName, Map localFiles) throws Exception { + public DiffResponse getDiff(String packName, Map localFiles) throws Exception { String json = gson.toJson(localFiles); // Получаем токен авторизации diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java index 5f345d0..02d01d7 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java @@ -39,45 +39,41 @@ public class NeoForgeInstaller { } instance.setAssetIndex(assetIndex); - createLauncherProfile(); String mavenGroup = getMavenGroup(mcVersion); String mavenArtifact = getMavenArtifact(mcVersion); - String installerUrl = "https://maven.neoforged.net/releases/" + // Формируем путь к версии + String versionName = mcVersion + "-" + neoForgeVersion; + Path versionDir = instance.getPath().resolve("versions").resolve(versionName); + Files.createDirectories(versionDir); + + // Скачиваем universal.jar (это основной JAR NeoForge) + String baseMavenUrl = "https://maven.neoforged.net/releases/" + mavenGroup.replace('.', '/') + "/" + mavenArtifact + "/" - + neoForgeVersion - + "/" + mavenArtifact + "-" + neoForgeVersion + "-installer.jar"; + + neoForgeVersion + "/"; - Path installerJar = instance.getPath().resolve("neoforge-installer.jar"); + String universalJarUrl = baseMavenUrl + mavenArtifact + "-" + neoForgeVersion + "-universal.jar"; + Path neoForgeJar = versionDir.resolve(versionName + ".jar"); - System.out.println(ZAnsi.cyan("Скачивание NeoForge Installer...")); - downloadFileWithProgress(installerUrl, installerJar); + System.out.println(ZAnsi.cyan("Скачивание NeoForge universal.jar...")); + downloadFileDirect(universalJarUrl, neoForgeJar); - System.out.println(ZAnsi.cyan("Запуск NeoForge Installer...")); - System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n")); + // Создаем version.json вручную + System.out.println(ZAnsi.cyan("Создание version.json...")); + createVersionJson(versionDir.resolve(versionName + ".json"), mcVersion, neoForgeVersion, mavenArtifact); - boolean success = runNeoForgeInstaller(installerJar); + // Скачиваем необходимые библиотеки + System.out.println(ZAnsi.cyan("Скачивание библиотек NeoForge...")); + downloadNeoForgeLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact); - if (success) { - try { - downloadMissingLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact); - } catch (Exception e) { - System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage())); - } + System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!")); + instance.setMinecraftVersion(mcVersion); + instance.setLoaderType("neoforge"); + instance.setLoaderVersion(neoForgeVersion); - System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!")); - instance.setMinecraftVersion(mcVersion); - instance.setLoaderType("neoforge"); - instance.setLoaderVersion(neoForgeVersion); - - Files.deleteIfExists(installerJar); - return true; - } else { - System.out.println(ZAnsi.brightRed("\nОшибка при установке NeoForge!")); - return false; - } + return true; } private String getMavenGroup(String mcVersion) { @@ -153,119 +149,109 @@ public class NeoForgeInstaller { ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")"); } - private boolean runNeoForgeInstaller(Path installerJar) throws IOException, InterruptedException { - int maxRetries = 3; - int attempt = 1; + private void downloadFileDirect(String url, Path target) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); - while (attempt <= maxRetries) { - System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries)); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); - ProcessBuilder pb = new ProcessBuilder( - "java", - "-jar", - installerJar.toAbsolutePath().toString(), - "--installClient" - ); - - pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000"); - pb.directory(instance.getPath().toFile()); - pb.redirectErrorStream(true); - - Process process = pb.start(); - - StringBuilder output = new StringBuilder(); - boolean hasErrors = false; - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - - if (line.contains("Downloading") || line.contains("Extracting")) { - System.out.println(ZAnsi.blue(" -> " + line)); - } else if (line.contains("SUCCESS") || line.contains("successfully")) { - System.out.println(ZAnsi.brightGreen(" + " + line)); - } else if (line.contains("WARNING") || line.contains("warning")) { - System.out.println(ZAnsi.yellow(" ! " + line)); - } else if (line.contains("ERROR") || line.contains("error") || line.contains("failed") || line.contains("timed out")) { - System.out.println(ZAnsi.brightRed(" X " + line)); - if (line.contains("timed out") || line.contains("failed to download")) { - hasErrors = true; - } - } else if (!line.isBlank()) { - System.out.println(" " + line); - } - } - } - - int exitCode = process.waitFor(); - - if (exitCode == 0 && !hasErrors) { - return true; - } - - if (attempt < maxRetries) { - System.out.println(ZAnsi.yellow("Ошибка при установке. Повторная попытка через 5 секунд...")); - Thread.sleep(5000); - - Path librariesDir = instance.getPath().resolve("libraries"); - if (Files.exists(librariesDir)) { - try (var stream = Files.walk(librariesDir)) { - stream.filter(p -> p.toString().contains("asm") && p.toString().endsWith(".jar")) - .forEach(p -> { - try { Files.deleteIfExists(p); } - catch (IOException e) { /* ignore */ } - }); - } - } - } else { - System.out.println(ZAnsi.brightRed("NeoForge Installer завершился с кодом ошибки: " + exitCode)); - - if (output.toString().contains("timed out")) { - System.out.println(ZAnsi.yellow("\nВозможные решения:")); - System.out.println(ZAnsi.yellow("1. Проверьте интернет-соединение")); - System.out.println(ZAnsi.yellow("2. Запустите лаунчер от имени администратора")); - System.out.println(ZAnsi.yellow("3. Временно отключите антивирус/брандмауэр")); - System.out.println(ZAnsi.yellow("4. Попробуйте установить другую версию NeoForge")); - } - } - - attempt++; + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode() + " for " + url); } - return false; + System.out.println(ZAnsi.green(" " + target.getFileName() + " завершено ✓")); } - private void downloadMissingLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception { - System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек...")); + private void createVersionJson(Path jsonFile, String mcVersion, String neoForgeVersion, String mavenArtifact) throws IOException { + // Создаем минимальный version.json для NeoForge + String versionName = mcVersion + "-" + neoForgeVersion; + String json = """ + { + "id": "%s", + "type": "release", + "mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher", + "inheritsFrom": "%s", + "arguments": { + "--tweakClass": "cpw.mods.fml.relauncher.CoreModManager" + }, + "libraries": [ + {"name": "net.neoforged:neoforge:%s"}, + {"name": "cpw.mods:bootstraplauncher:1.1.2"}, + {"name": "net.minecraftforge:unsafe:0.2.0"}, + {"name": "net.minecraftforge:srgutils:0.4.4"}, + {"name": "net.minecraftforge:modlauncher:10.2.1"}, + {"name": "net.minecraftforge:coremods:5.0.1"}, + {"name": "net.minecraftforge:accesstransformers:8.8"}, + {"name": "net.minecraftforge:eventbus:6.0.5"}, + {"name": "net.minecraftforge:forgemin:0.1.1"}, + {"name": "net.minecraftforge:scanner:1.2.2"}, + {"name": "com.google.code.gson:gson:2.10.1"}, + {"name": "com.google.guava:guava:32.1.3-jre"}, + {"name": "org.apache.commons:commons-lang3:3.13.0"}, + {"name": "org.jline:jline-reader:3.12.1"}, + {"name": "org.jline:jline-terminal:3.12.1"} + ] + } + """.formatted(versionName, mcVersion, neoForgeVersion); - Map alternativeUrls = new HashMap<>(); - alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar", - "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar"); - alternativeUrls.put("org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar", - "https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar"); - alternativeUrls.put("org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar", - "https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar"); + Files.writeString(jsonFile, json); + System.out.println(ZAnsi.green(" version.json создан ✓")); + } + + private void downloadNeoForgeLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception { + System.out.println(ZAnsi.cyan("Скачивание библиотек NeoForge...")); + + String baseMavenUrl = "https://maven.neoforged.net/releases/" + + mavenGroup.replace('.', '/') + "/"; Path librariesDir = instance.getPath().resolve("libraries"); - for (Map.Entry entry : alternativeUrls.entrySet()) { - Path target = librariesDir.resolve(entry.getKey()); - if (!Files.exists(target)) { - Files.createDirectories(target.getParent()); - System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName())); + // Список основных библиотек NeoForge + String[][] libs = { + {mavenGroup, mavenArtifact, neoForgeVersion}, + {"cpw.mods", "bootstraplauncher", "1.1.2"}, + {"net.minecraftforge", "unsafe", "0.2.0"}, + {"net.minecraftforge", "srgutils", "0.4.4"}, + {"net.minecraftforge", "modlauncher", "10.2.1"}, + {"net.minecraftforge", "coremods", "5.0.1"}, + {"net.minecraftforge", "accesstransformers", "8.8"}, + {"net.minecraftforge", "eventbus", "6.0.5"}, + {"net.minecraftforge", "forgemin", "0.1.1"}, + {"net.minecraftforge", "scanner", "1.2.2"} + }; - for (int attempt = 1; attempt <= 3; attempt++) { - try { - downloadFileWithProgress(entry.getValue(), target); - break; - } catch (Exception e) { - if (attempt == 3) throw e; - System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3...")); - Thread.sleep(2000); - } + for (String[] lib : libs) { + String group = lib[0].replace('.', '/'); + String artifact = lib[1]; + String version = lib[2]; + + String jarName = artifact + "-" + version + ".jar"; + String mavenPath = group + "/" + artifact + "/" + version + "/" + jarName; + Path target = librariesDir.resolve(mavenPath); + + if (Files.exists(target)) { + System.out.println(ZAnsi.green(" " + jarName + " уже есть ✓")); + continue; + } + + Files.createDirectories(target.getParent()); + + String url = baseMavenUrl + mavenPath; + try { + downloadFileDirect(url, target); + } catch (Exception e) { + // Пробуем Maven Central как fallback + try { + String centralUrl = "https://repo1.maven.org/maven2/" + mavenPath; + downloadFileDirect(centralUrl, target); + } catch (Exception e2) { + System.out.println(ZAnsi.yellow(" Предупреждение: не удалось скачать " + jarName)); } } } + + System.out.println(ZAnsi.green("Библиотеки NeoForge обработаны ✓")); } } diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/UIWindow.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/UIWindow.java new file mode 100644 index 0000000..9fbcb82 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/UIWindow.java @@ -0,0 +1,68 @@ +package me.sashegdev.zernmc.launcher.web; + +import java.awt.GraphicsEnvironment; +import javafx.application.Application; +import javafx.concurrent.Worker; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +public class UIWindow extends Application { + + private static String url; + private static int port; + + public static void start(int port) { + // Backup проверка headless + if (java.awt.GraphicsEnvironment.isHeadless()) { + throw new RuntimeException("Headless environment - no display available"); + } + + UIWindow.port = port; + UIWindow.url = "http://localhost:" + port; + Application.launch(UIWindow.class); + } + + @Override + public void start(Stage stage) { + stage.setTitle("ZernMC Launcher"); + stage.initStyle(StageStyle.UNDECORATED); + + WebView webView = new WebView(); + WebEngine webEngine = webView.getEngine(); + + webEngine.load(url); + + webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + if (newState == Worker.State.FAILED) { + System.err.println("Failed to load: " + url); + } + }); + + Scene scene = new Scene(webView); + stage.setScene(scene); + + Rectangle2D screenBounds = Screen.getPrimary().getVisualBounds(); + double screenWidth = screenBounds.getWidth(); + double screenHeight = screenBounds.getHeight(); + + double windowWidth = Math.min(1200, screenWidth * 0.8); + double windowHeight = Math.min(800, screenHeight * 0.85); + + stage.setWidth(windowWidth); + stage.setHeight(windowHeight); + stage.setX((screenWidth - windowWidth) / 2); + stage.setY((screenHeight - windowHeight) / 2); + + stage.show(); + + stage.setOnCloseRequest(event -> { + WebServer.stop(); + System.exit(0); + }); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java new file mode 100644 index 0000000..74a2bc3 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/web/WebServer.java @@ -0,0 +1,329 @@ +package me.sashegdev.zernmc.launcher.web; + +import io.javalin.Javalin; +import io.javalin.http.staticfiles.Location; +import me.sashegdev.zernmc.launcher.api.ApiResponse; +import me.sashegdev.zernmc.launcher.api.LauncherAPI; +import me.sashegdev.zernmc.launcher.api.instance.InstanceService; +import me.sashegdev.zernmc.launcher.auth.AuthManager; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; +import java.util.List; +import java.util.Map; + +public class WebServer { + + private static final LauncherAPI api = new LauncherAPI(); + private static Javalin app; + private static int currentPort; + private static volatile boolean running = false; + + public static int findFreePort(int startPort) throws IOException { + for (int port = startPort; port < startPort + 100; port++) { + if (isPortAvailable(port)) { + return port; + } + } + throw new IOException("Не удалось найти свободный порт в диапазоне " + startPort + "-" + (startPort + 99)); + } + + private static boolean isPortAvailable(int port) { + try (ServerSocket socket = new ServerSocket(port)) { + return true; + } catch (IOException e) { + return false; + } + } + + public static void start(int port) throws Exception { + currentPort = port; + running = true; + + // Отключаем логирование Javalin в консоль + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "error"); + + app = Javalin.create(config -> { + config.staticFiles.add("/webapp", Location.CLASSPATH); + config.staticFiles.add("/assets", Location.CLASSPATH); + }).start(port); + + // API эндпоинты + setupApiRoutes(); + + System.out.println(ZAnsi.brightGreen("✓ Web UI готов на http://localhost:" + port)); + + // Блокируем главный поток (сервер работает) + while (running) { + Thread.sleep(1000); + } + } + + private static void setupApiRoutes() { + // Auth + app.get("/api/auth/status", ctx -> { + if (AuthManager.loadSavedSession()) { + ctx.json(Map.of( + "success", true, + "loggedIn", true, + "username", AuthManager.getUsername() + )); + } else { + ctx.json(Map.of( + "success", true, + "loggedIn", false + )); + } + }); + + app.post("/api/auth/login", ctx -> { + Map body = ctx.bodyAsClass(Map.class); + String username = body.get("username"); + String password = body.get("password"); + + if (username == null || password == null) { + ctx.status(400).json(Map.of("success", false, "error", "Missing username or password")); + return; + } + + var result = api.login(username, password); + if (result.isSuccess()) { + ctx.json(Map.of("success", true, "username", username)); + } else { + ctx.status(401).json(Map.of("success", false, "error", result.getError())); + } + }); + + app.post("/api/auth/logout", ctx -> { + AuthManager.logout(); + ctx.json(Map.of("success", true)); + }); + + // Instances - локальные + app.get("/api/instances", ctx -> { + var result = api.getAllInstances(); + if (result.isSuccess()) { + ctx.json(Map.of("success", true, "data", result.getData())); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Instance детали + app.get("/api/instances/{name}", ctx -> { + String name = ctx.pathParam("name"); + var result = api.instances().getInstance(name); + if (result.isSuccess()) { + ctx.json(Map.of("success", true, "data", result.getData())); + } else { + ctx.status(404).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Launch + app.post("/api/instances/{name}/launch", ctx -> { + String name = ctx.pathParam("name"); + var result = api.launch(name); + if (result.isSuccess()) { + ctx.json(Map.of("success", true, "message", "Launch started")); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Delete + app.post("/api/instances/{name}/delete", ctx -> { + String name = ctx.pathParam("name"); + var result = api.instances().deleteInstance(name); + if (result.isSuccess()) { + ctx.json(Map.of("success", true, "message", "Instance deleted")); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // ZernMC серверные сборки + app.get("/api/instances/zernmc", ctx -> { + // TODO: получить реальные сборки с сервера + List> packs = List.of( + Map.of("name", "ZernMC SkyBlock", "version", 1, "loader", "Fabric", "loaderVersion", "0.15.9", "filesCount", 150), + Map.of("name", "ZernMC RPG", "version", 3, "loader", "Fabric", "loaderVersion", "0.15.9", "filesCount", 200) + ); + ctx.json(Map.of("success", true, "data", packs)); + }); + + // Установка ZernMC сборки + app.post("/api/instances/zernmc/install", ctx -> { + Map body = ctx.bodyAsClass(Map.class); + String packName = body.get("packName"); + String instanceName = body.get("instanceName"); + + if (packName == null || instanceName == null) { + ctx.status(400).json(Map.of("success", false, "error", "Missing packName or instanceName")); + return; + } + + var result = api.install().installZernMCPack(packName, instanceName); + if (result.isSuccess()) { + ctx.json(Map.of( + "success", true, + "data", Map.of( + "name", result.getData().getName(), + "mcVersion", result.getData().getMcVersion(), + "loaderType", result.getData().getLoaderType(), + "serverVersion", result.getData().getServerVersion() + ) + )); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Проверка обновлений + app.get("/api/instances/{name}/updates", ctx -> { + String name = ctx.pathParam("name"); + var result = api.install().checkForUpdates(name); + if (result.isSuccess()) { + ctx.json(Map.of( + "success", true, + "data", Map.of( + "hasUpdate", result.getData().isHasUpdate(), + "isServerPack", result.getData().isServerPack(), + "currentVersion", result.getData().getCurrentVersion(), + "latestVersion", result.getData().getLatestVersion() + ) + )); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Проверка хешей + app.get("/api/instances/{name}/verify", ctx -> { + String name = ctx.pathParam("name"); + var result = api.install().verifyHashes(name); + if (result.isSuccess()) { + ctx.json(Map.of( + "success", true, + "data", Map.of( + "hasMismatches", result.getData().hasMismatches(), + "mismatchedFiles", result.getData().getMismatchedFiles() + ) + )); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Получение времени игры + app.get("/api/instances/{name}/playtime", ctx -> { + String name = ctx.pathParam("name"); + var result = api.install().getPlayTime(name); + if (result.isSuccess()) { + ctx.json(Map.of( + "success", true, + "data", Map.of( + "totalMinutes", result.getData().getTotalMinutes(), + "fromServer", result.getData().isFromServer(), + "formatted", result.getData().getFormattedTime() + ) + )); + } else { + ctx.status(500).json(Map.of("success", false, "error", result.getError())); + } + }); + + // Minecraft версии + app.get("/api/versions", ctx -> { + List versions = List.of( + "1.21.4", "1.21.3", "1.21.2", "1.21.1", "1.21", + "1.20.4", "1.20.3", "1.20.2", "1.20.1", "1.20", + "1.19.4", "1.19.3", "1.19.2", "1.19.1", "1.19", + "1.18.2", "1.18.1", "1.18", + "1.17.1", "1.17" + ); + ctx.json(Map.of("success", true, "data", + versions.stream().map(v -> Map.of("id", v)).toList() + )); + }); + + // Версии лоадеров для конкретной версии Minecraft + app.get("/api/versions/{version}/loaders/{loader}", ctx -> { + String version = ctx.pathParam("version"); + String loader = ctx.pathParam("loader"); + + List> loaderVersions = switch (loader.toLowerCase()) { + case "fabric" -> List.of( + Map.of("version", "0.16.9"), + Map.of("version", "0.16.8"), + Map.of("version", "0.16.7"), + Map.of("version", "0.16.6"), + Map.of("version", "0.16.5"), + Map.of("version", "0.15.11"), + Map.of("version", "0.15.10"), + Map.of("version", "0.15.9") + ); + case "forge" -> List.of( + Map.of("version", "1.21-51.0.0"), + Map.of("version", "1.20.4-49.0.0"), + Map.of("version", "1.20.1-47.1.0"), + Map.of("version", "1.19.2-43.2.0"), + Map.of("version", "1.18.2-40.2.0") + ); + case "neoforge" -> List.of( + Map.of("version", "21.0.0-beta"), + Map.of("version", "1.21-21.0.0"), + Map.of("version", "1.20.4-21.0.0"), + Map.of("version", "1.20.1-21.0.0") + ); + default -> List.of(); + }; + + ctx.json(Map.of("success", true, "data", loaderVersions)); + }); + + // Установка ванильной сборки + app.post("/api/instances/vanilla/install", ctx -> { + Map body = ctx.bodyAsClass(Map.class); + String mcVersion = body.get("mcVersion"); + String loader = body.get("loader"); + String loaderVersion = body.get("loaderVersion"); + String instanceName = body.get("instanceName"); + + if (mcVersion == null || instanceName == null) { + ctx.status(400).json(Map.of("success", false, "error", "Missing required parameters")); + return; + } + + // TODO: реализовать установку ванильной сборки + String desc = loader != null ? mcVersion + " + " + loader + " " + loaderVersion : mcVersion + " Vanilla"; + ctx.json(Map.of("success", true, "message", "Vanilla installation started: " + desc)); + }); + + // Health check + app.get("/api/health", ctx -> { + ctx.json(Map.of("success", true, "status", "ok")); + }); + } + + private static void openBrowser(String url) { + try { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(new URI(url)); + System.out.println(ZAnsi.cyan("Браузер открыт: " + url)); + } + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Не удалось открыть браузер автоматически. Откройте вручную: " + url)); + } + } + + public static void stop() { + running = false; + if (app != null) { + app.stop(); + } + } +} \ No newline at end of file diff --git a/launcher/src/main/resources/webapp/css/styles.css b/launcher/src/main/resources/webapp/css/styles.css new file mode 100644 index 0000000..17d1159 --- /dev/null +++ b/launcher/src/main/resources/webapp/css/styles.css @@ -0,0 +1,768 @@ +:root { + --bg-primary: #0a0a0f; + --bg-secondary: #12121a; + --bg-card: #1a1a24; + --bg-card-hover: #222230; + --bg-sidebar: #0d0d12; + --accent-primary: #e94560; + --accent-secondary: #ff6b6b; + --accent-glow: rgba(233, 69, 96, 0.3); + --text-primary: #ffffff; + --text-secondary: #a0a0b0; + --text-muted: #606070; + --border-color: #2a2a3a; + --success: #4ade80; + --error: #f87171; + --warning: #fbbf24; + --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.4); + --shadow-glow: 0 0 30px var(--accent-glow); + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --transition-fast: 150ms ease; + --transition-normal: 300ms ease; + --transition-slow: 500ms ease; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + overflow-x: hidden; +} + +#grid-canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + opacity: 0.12; + pointer-events: none; +} + +#app { + position: relative; + z-index: 1; + min-height: 100vh; +} + +.screen { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + animation: fadeIn var(--transition-slow) forwards; +} + +.hidden { + display: none !important; +} + +/* ==================== LOGIN SCREEN ==================== */ +.login-container { + background: var(--bg-card); + border-radius: var(--radius-lg); + padding: 48px; + width: 100%; + max-width: 400px; + box-shadow: var(--shadow-card); + border: 1px solid var(--border-color); + animation: slideUp var(--transition-slow) forwards; +} + +.logo-section { + text-align: center; + margin-bottom: 40px; +} + +.logo-placeholder { + display: inline-block; + margin-bottom: 16px; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +.app-title { + font-size: 28px; + font-weight: 700; + margin-bottom: 8px; + background: linear-gradient(135deg, var(--text-primary), var(--accent-primary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.app-version { + color: var(--text-muted); + font-size: 14px; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.input-group input { + width: 100%; + padding: 14px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 16px; + transition: var(--transition-fast); +} + +.input-group input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.input-group input::placeholder { + color: var(--text-muted); +} + +.btn-primary { + width: 100%; + padding: 14px 24px; + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + border: none; + border-radius: var(--radius-sm); + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: var(--transition-fast); + position: relative; + overflow: hidden; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-glow); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-primary:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.btn-loader { + width: 20px; + height: 20px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 0 auto; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.error-message { + color: var(--error); + text-align: center; + font-size: 14px; + padding: 12px; + background: rgba(248, 113, 113, 0.1); + border-radius: var(--radius-sm); + animation: shake 0.5s ease; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +/* ==================== MAIN LAYOUT ==================== */ +.main-layout { + display: grid; + grid-template-columns: 280px 1fr 200px; + width: 100%; + max-width: 1600px; + height: calc(100vh - 40px); + gap: 0; + background: var(--bg-secondary); + border-radius: var(--radius-lg); + border: 1px solid var(--border-color); + overflow: hidden; + animation: fadeIn var(--transition-slow) forwards; +} + +/* Sidebar */ +.sidebar { + background: var(--bg-sidebar); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + padding: 20px; +} + +.sidebar-header { + display: flex; + align-items: center; + gap: 12px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; +} + +.logo-small svg { + display: block; +} + +.header-info { + display: flex; + flex-direction: column; +} + +.header-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); +} + +.header-version { + font-size: 12px; + color: var(--text-muted); +} + +.sidebar-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 24px; +} + +.section-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-muted); + margin-bottom: 12px; +} + +.current-instance-section { + flex: 1; +} + +.current-instance { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 16px; + transition: var(--transition-fast); +} + +.current-instance:hover { + border-color: var(--accent-primary); +} + +.instance-card-mini { + display: flex; + flex-direction: column; + gap: 8px; +} + +.instance-name { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.instance-version { + font-size: 13px; + color: var(--accent-primary); + background: rgba(233, 69, 96, 0.15); + padding: 4px 8px; + border-radius: 4px; + display: inline-block; + width: fit-content; +} + +.btn-download { + width: 100%; + padding: 16px; + background: var(--bg-card); + border: 1px dashed var(--border-color); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + transition: var(--transition-fast); +} + +.btn-download:hover { + background: var(--bg-card-hover); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.sidebar-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 16px; + border-top: 1px solid var(--border-color); + margin-top: 20px; +} + +.username-display { + font-size: 13px; + color: var(--text-secondary); +} + +.btn-logout { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition-fast); +} + +.btn-logout:hover { + background: rgba(248, 113, 113, 0.1); + border-color: var(--error); + color: var(--error); +} + +/* Main Content - Logs */ +.main-content { + display: flex; + flex-direction: column; + padding: 20px; + background: var(--bg-primary); +} + +.logs-section { + flex: 1; + display: flex; + flex-direction: column; + background: var(--bg-card); + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + overflow: hidden; +} + +.logs-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); +} + +.logs-header h2 { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.btn-clear-logs { + padding: 6px 12px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-muted); + font-size: 12px; + cursor: pointer; + transition: var(--transition-fast); +} + +.btn-clear-logs:hover { + background: var(--bg-card-hover); + color: var(--text-secondary); +} + +.logs-container { + flex: 1; + padding: 16px 20px; + overflow-y: auto; + font-family: 'JetBrains Mono', 'Consolas', monospace; + font-size: 12px; + line-height: 1.6; +} + +.log-entry { + padding: 4px 0; + color: var(--text-secondary); + animation: fadeIn var(--transition-fast) forwards; +} + +.log-entry.info { + color: var(--text-secondary); +} + +.log-entry.success { + color: var(--success); +} + +.log-entry.warning { + color: var(--warning); +} + +.log-entry.error { + color: var(--error); +} + +/* Right Panel - Play Button */ +.right-panel { + display: flex; + align-items: flex-end; + justify-content: center; + padding: 30px; + border-left: 1px solid var(--border-color); + background: var(--bg-sidebar); +} + +.btn-play { + width: 100%; + padding: 20px 30px; + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + border: none; + border-radius: var(--radius-md); + color: white; + font-size: 18px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + transition: var(--transition-normal); + box-shadow: 0 4px 20px var(--accent-glow); +} + +.btn-play:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: 0 8px 40px var(--accent-glow); +} + +.btn-play:active { + transform: translateY(0); +} + +.btn-play:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* ==================== MODAL ==================== */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(10, 10, 15, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn var(--transition-fast) forwards; +} + +.modal-content { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + animation: slideUp var(--transition-normal) forwards; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h2 { + font-size: 18px; + font-weight: 600; +} + +.modal-close { + width: 32px; + height: 32px; + background: transparent; + border: none; + color: var(--text-muted); + font-size: 24px; + cursor: pointer; + transition: var(--transition-fast); +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-tabs { + display: flex; + padding: 16px 24px; + gap: 8px; + border-bottom: 1px solid var(--border-color); +} + +.tab-btn { + flex: 1; + padding: 12px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 14px; + cursor: pointer; + transition: var(--transition-fast); +} + +.tab-btn.active { + background: var(--accent-primary); + border-color: var(--accent-primary); + color: white; +} + +.tab-btn:hover:not(.active) { + background: var(--bg-card-hover); +} + +.tab-content { + padding: 24px; + display: none; +} + +.tab-content.active { + display: block; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.select-input, .text-input { + width: 100%; + padding: 12px 14px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 14px; + transition: var(--transition-fast); +} + +.select-input:focus, .text-input:focus { + outline: none; + border-color: var(--accent-primary); +} + +.select-input option { + background: var(--bg-secondary); +} + +.btn-install { + width: 100%; + padding: 14px; + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + border: none; + border-radius: var(--radius-sm); + color: white; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: var(--transition-fast); +} + +.btn-install:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-glow); +} + +.download-progress { + padding: 24px; + border-top: 1px solid var(--border-color); +} + +.progress-bar { + height: 8px; + background: var(--bg-secondary); + border-radius: 4px; + overflow: hidden; + margin-bottom: 12px; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); + border-radius: 4px; + width: 0%; + transition: width var(--transition-normal); +} + +.progress-text { + text-align: center; + color: var(--text-secondary); + font-size: 13px; +} + +/* ==================== LOADING ==================== */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(10, 10, 15, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn var(--transition-fast) forwards; +} + +.loader { + width: 48px; + height: 48px; + border: 3px solid var(--border-color); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 16px; +} + +/* ==================== ANIMATIONS ==================== */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes cardFadeIn { + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ==================== RESPONSIVE ==================== */ +@media (max-width: 1024px) { + .main-layout { + grid-template-columns: 240px 1fr 160px; + } +} + +@media (max-width: 768px) { + .main-layout { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; + } + + .sidebar { + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-right: none; + border-bottom: 1px solid var(--border-color); + } + + .sidebar-header { + padding-bottom: 0; + border-bottom: none; + margin-bottom: 0; + } + + .sidebar-content { + display: none; + } + + .sidebar-footer { + margin-top: 0; + padding-top: 0; + border-top: none; + } + + .right-panel { + padding: 16px; + border-left: none; + border-top: 1px solid var(--border-color); + } +} + +/* ==================== SCROLLBAR ==================== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} \ No newline at end of file diff --git a/launcher/src/main/resources/webapp/index.html b/launcher/src/main/resources/webapp/index.html new file mode 100644 index 0000000..4bbbef4 --- /dev/null +++ b/launcher/src/main/resources/webapp/index.html @@ -0,0 +1,204 @@ + + + + + + ZernMC Launcher + + + + + + + + + + + + + + + + + + + + ZernMC Launcher + v1.0.8 + + + + + + + + + + + Войти + + + + + + + + + + + + + + + + + + Логи + Очистить + + + Ожидание запуска... + + + + + + + + + + + ИГРАТЬ + + + + + + + + + + Скачать сборку + × + + + + ZernMC сборки + Чистый Minecraft + + + + + + Выберите сборку + + Загрузка... + + + + Название сборки (системное) + + + + Скачать и установить + + + + + + + Версия Minecraft + + Выберите версию + + + + Лоадер + + Vanilla (без лоадера) + Fabric + Forge + NeoForge + + + + Версия лоадера + + Загрузка... + + + + Название сборки + + + + Скачать и установить + + + + + + + + Загрузка... + + + + + + + + Загрузка... + + + + + + \ No newline at end of file diff --git a/launcher/src/main/resources/webapp/js/app.js b/launcher/src/main/resources/webapp/js/app.js new file mode 100644 index 0000000..a119c17 --- /dev/null +++ b/launcher/src/main/resources/webapp/js/app.js @@ -0,0 +1,473 @@ +const API_BASE = '/api'; + +class App { + constructor() { + this.state = 'INIT'; + this.username = null; + this.currentInstance = null; + this.instances = []; + this.zernmcPacks = []; + this.mcVersions = []; + this.init(); + } + + async init() { + this.bindEvents(); + this.initGridAnimation(); + await this.checkAuth(); + } + + bindEvents() { + // Login form + document.getElementById('login-form').addEventListener('submit', (e) => { + e.preventDefault(); + this.handleLogin(); + }); + + // Logout button + document.getElementById('logout-btn').addEventListener('click', () => { + this.handleLogout(); + }); + + // Download button + document.getElementById('download-btn').addEventListener('click', () => { + this.showDownloadModal(); + }); + + // Close modal + document.getElementById('close-download-modal').addEventListener('click', () => { + this.hideDownloadModal(); + }); + + // Modal tabs + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + this.switchTab(e.target.dataset.tab); + }); + }); + + // Play button + document.getElementById('play-btn').addEventListener('click', () => { + this.launchInstance(); + }); + + // Clear logs + document.getElementById('clear-logs').addEventListener('click', () => { + this.clearLogs(); + }); + + // Loader selection + document.getElementById('loader-select').addEventListener('change', (e) => { + this.onLoaderChange(e.target.value); + }); + + // Install buttons + document.getElementById('install-zernmc-btn').addEventListener('click', () => { + this.installZernMCPack(); + }); + + document.getElementById('install-vanilla-btn').addEventListener('click', () => { + this.installVanilla(); + }); + } + + // ==================== GRID ANIMATION ==================== + initGridAnimation() { + const canvas = document.getElementById('grid-canvas'); + const ctx = canvas.getContext('2d'); + let mouseX = 0, mouseY = 0; + let offsetX = 0, offsetY = 0; + + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + this.drawGrid(ctx, canvas.width, canvas.height, offsetX, offsetY); + }; + + window.addEventListener('resize', resize); + + window.addEventListener('mousemove', (e) => { + mouseX = (e.clientX / window.innerWidth - 0.5) * 2; + mouseY = (e.clientY / window.innerHeight - 0.5) * 2; + }); + + const animate = () => { + offsetX += (mouseX * 0.5 - offsetX) * 0.05; + offsetY += (mouseY * 0.5 - offsetY) * 0.05; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + this.drawGrid(ctx, canvas.width, canvas.height, offsetX, offsetY); + requestAnimationFrame(animate); + }; + + resize(); + animate(); + } + + drawGrid(ctx, width, height, offsetX, offsetY) { + const gridSize = 50; + const dotSize = 1; + + ctx.fillStyle = '#e94560'; + + for (let x = 0; x <= width; x += gridSize) { + for (let y = 0; y <= height; y += gridSize) { + const px = x + offsetX * 10; + const py = y + offsetY * 10; + ctx.beginPath(); + ctx.arc(px, py, dotSize, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + // ==================== API ==================== + async request(endpoint, options = {}) { + try { + const response = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + return await response.json(); + } catch (error) { + console.error('API Error:', error); + return { success: false, error: error.message }; + } + } + + // ==================== AUTH ==================== + async checkAuth() { + this.showLoading(true); + const result = await this.request('/auth/status'); + + if (result.loggedIn) { + this.username = result.username; + this.showMainScreen(); + await this.loadCurrentInstance(); + } else { + this.showLoginScreen(); + } + + this.showLoading(false); + } + + async handleLogin() { + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + const errorEl = document.getElementById('login-error'); + const btn = document.querySelector('#login-form button[type="submit"]'); + const btnText = btn.querySelector('.btn-text'); + const btnLoader = btn.querySelector('.btn-loader'); + + if (!username || !password) { + this.showError('Введите имя пользователя и пароль'); + return; + } + + btn.disabled = true; + btnText.classList.add('hidden'); + btnLoader.classList.remove('hidden'); + errorEl.classList.add('hidden'); + + const result = await this.request('/auth/login', { + method: 'POST', + body: JSON.stringify({ username, password }) + }); + + btn.disabled = false; + btnText.classList.remove('hidden'); + btnLoader.classList.add('hidden'); + + if (result.success) { + this.username = result.username; + this.showMainScreen(); + await this.loadCurrentInstance(); + } else { + this.showError(result.error || 'Ошибка входа'); + } + } + + async handleLogout() { + await this.request('/auth/logout', { method: 'POST' }); + this.username = null; + this.currentInstance = null; + this.showLoginScreen(); + } + + showError(message) { + const errorEl = document.getElementById('login-error'); + errorEl.textContent = message; + errorEl.classList.remove('hidden'); + } + + // ==================== INSTANCES ==================== + async loadCurrentInstance() { + const result = await this.request('/instances'); + + if (result.success && result.data && result.data.length > 0) { + this.currentInstance = result.data[0]; + this.renderCurrentInstance(this.currentInstance); + this.enablePlayButton(true); + this.addLog('Сборка загружена: ' + this.currentInstance.name, 'success'); + } else { + this.renderNoInstance(); + this.enablePlayButton(false); + this.addLog('Установите сборку для игры', 'warning'); + } + } + + renderCurrentInstance(instance) { + const container = document.getElementById('current-instance'); + container.innerHTML = ` + + ${this.escapeHtml(instance.name)} + ${this.escapeHtml(instance.version || 'Vanilla')} + + `; + } + + renderNoInstance() { + const container = document.getElementById('current-instance'); + container.innerHTML = ` + + Нет сборки + Нажмите скачать + + `; + } + + enablePlayButton(enabled) { + const btn = document.getElementById('play-btn'); + btn.disabled = !enabled; + } + + async launchInstance() { + if (!this.currentInstance) return; + + this.addLog('Проверка целостности файлов...', 'info'); + this.enablePlayButton(false); + + const result = await this.request(`/instances/${this.currentInstance.name}/launch`, { + method: 'POST' + }); + + if (result.success) { + this.addLog('Сборка запущена!', 'success'); + } else { + this.addLog('Ошибка: ' + result.error, 'error'); + this.enablePlayButton(true); + } + } + + // ==================== DOWNLOAD MODAL ==================== + async showDownloadModal() { + document.getElementById('download-modal').classList.remove('hidden'); + await this.loadZernMCPacks(); + await this.loadMCVersions(); + } + + hideDownloadModal() { + document.getElementById('download-modal').classList.add('hidden'); + this.hideProgress(); + } + + switchTab(tab) { + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tab === tab); + }); + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.toggle('active', content.id === 'tab-' + tab); + }); + } + + async loadZernMCPacks() { + const select = document.getElementById('zernmc-pack-select'); + select.innerHTML = 'Загрузка...'; + + const result = await this.request('/instances/zernmc'); + + if (result.success && result.data && result.data.length > 0) { + this.zernmcPacks = result.data; + select.innerHTML = result.data.map(pack => + `${this.escapeHtml(pack.name)} (v${pack.version})` + ).join(''); + } else { + select.innerHTML = 'Нет доступных сборок'; + } + } + + async loadMCVersions() { + const select = document.getElementById('mc-version-select'); + select.innerHTML = 'Загрузка...'; + + const result = await this.request('/versions'); + + if (result.success && result.data) { + this.mcVersions = result.data; + select.innerHTML = 'Выберите версию' + + result.data.map(v => `${v.id}`).join(''); + } else { + select.innerHTML = 'Не удалось загрузить'; + } + } + + async onLoaderChange(loader) { + const loaderVersionGroup = document.getElementById('loader-version-group'); + const loaderVersionSelect = document.getElementById('loader-version-select'); + + if (loader === 'vanilla') { + loaderVersionGroup.classList.add('hidden'); + } else { + loaderVersionGroup.classList.remove('hidden'); + loaderVersionSelect.innerHTML = 'Загрузка...'; + + const result = await this.request(`/versions/${document.getElementById('mc-version-select').value}/loaders/${loader}`); + + if (result.success && result.data) { + loaderVersionSelect.innerHTML = result.data.map(v => + `${v.version}` + ).join(''); + } else { + loaderVersionSelect.innerHTML = 'Нет версий'; + } + } + } + + async installZernMCPack() { + const packName = document.getElementById('zernmc-pack-select').value; + const instanceName = document.getElementById('zernmc-instance-name').value; + + if (!packName) { + alert('Выберите сборку'); + return; + } + + if (!instanceName) { + alert('Введите название сборки'); + return; + } + + this.showProgress('Установка ZernMC сборки...'); + this.addLog('Начало установки: ' + packName, 'info'); + + const result = await this.request('/instances/zernmc/install', { + method: 'POST', + body: JSON.stringify({ packName, instanceName }) + }); + + if (result.success) { + this.hideDownloadModal(); + await this.loadCurrentInstance(); + this.addLog('Сборка установлена!', 'success'); + } else { + this.addLog('Ошибка установки: ' + result.error, 'error'); + this.hideProgress(); + } + } + + async installVanilla() { + const mcVersion = document.getElementById('mc-version-select').value; + const loader = document.getElementById('loader-select').value; + const loaderVersion = document.getElementById('loader-version-select').value; + const instanceName = document.getElementById('vanilla-instance-name').value; + + if (!mcVersion) { + alert('Выберите версию Minecraft'); + return; + } + + if (!instanceName) { + alert('Введите название сборки'); + return; + } + + if (loader !== 'vanilla' && !loaderVersion) { + alert('Выберите версию лоадера'); + return; + } + + this.showProgress('Установка сборки...'); + this.addLog(`Начало установки: Minecraft ${mcVersion} ${loader !== 'vanilla' ? loader + ' ' + loaderVersion : ''}`, 'info'); + + const result = await this.request('/instances/vanilla/install', { + method: 'POST', + body: JSON.stringify({ + mcVersion, + loader: loader === 'vanilla' ? null : loader, + loaderVersion: loader === 'vanilla' ? null : loaderVersion, + instanceName + }) + }); + + if (result.success) { + this.hideDownloadModal(); + await this.loadCurrentInstance(); + this.addLog('Сборка установлена!', 'success'); + } else { + this.addLog('Ошибка установки: ' + result.error, 'error'); + this.hideProgress(); + } + } + + showProgress(text) { + const progress = document.getElementById('download-progress'); + const progressText = document.getElementById('progress-text'); + const progressFill = document.getElementById('progress-fill'); + + progress.classList.remove('hidden'); + progressText.textContent = text; + progressFill.style.width = '50%'; + } + + hideProgress() { + document.getElementById('download-progress').classList.add('hidden'); + } + + // ==================== LOGS ==================== + addLog(message, type = 'info') { + const container = document.getElementById('logs-container'); + const entry = document.createElement('div'); + entry.className = `log-entry ${type}`; + entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; + container.appendChild(entry); + container.scrollTop = container.scrollHeight; + } + + clearLogs() { + const container = document.getElementById('logs-container'); + container.innerHTML = 'Логи очищены'; + } + + // ==================== UI HELPERS ==================== + showLoginScreen() { + document.getElementById('login-screen').classList.remove('hidden'); + document.getElementById('main-screen').classList.add('hidden'); + } + + showMainScreen() { + document.getElementById('login-screen').classList.add('hidden'); + document.getElementById('main-screen').classList.remove('hidden'); + document.getElementById('username-display').textContent = this.username || ''; + } + + showLoading(show) { + const overlay = document.getElementById('loading-overlay'); + if (show) { + overlay.classList.remove('hidden'); + } else { + overlay.classList.add('hidden'); + } + } + + escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +const app = new App(); \ No newline at end of file diff --git a/launcher/src/test/java/me/sashegdev/zernmc/launcher/api/InstanceServiceTest.java b/launcher/src/test/java/me/sashegdev/zernmc/launcher/api/InstanceServiceTest.java new file mode 100644 index 0000000..ee8599b --- /dev/null +++ b/launcher/src/test/java/me/sashegdev/zernmc/launcher/api/InstanceServiceTest.java @@ -0,0 +1,67 @@ +package me.sashegdev.zernmc.launcher.api; + +import me.sashegdev.zernmc.launcher.api.instance.InstanceService; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class InstanceServiceTest { + + @Test + void instanceService_instantiates() { + InstanceService service = new InstanceService(); + assertNotNull(service, "InstanceService должен создаваться"); + } + + @Test + void getAllInstances_returnsResponse() { + InstanceService service = new InstanceService(); + ApiResponse> response = service.getAllInstances(); + + assertNotNull(response, "Ответ не должен быть null"); + assertTrue(response.isSuccess() || !response.isSuccess(), "Должен быть валидный ответ"); + } + + @Test + void getAllInstances_returnsList() { + InstanceService service = new InstanceService(); + ApiResponse> response = service.getAllInstances(); + + assertNotNull(response.getData(), "Data не должен быть null"); + } + + @Test + void isInstanceExists_returnsBoolean() { + InstanceService service = new InstanceService(); + ApiResponse response = service.isInstanceExists("nonexistent"); + + assertNotNull(response, "Ответ не должен быть null"); + assertTrue(response.isSuccess(), "Проверка должна быть успешной"); + assertNotNull(response.getData(), "Data должен быть boolean"); + } + + @Test + void isInstanceExists_nonexistentReturnsFalse() { + InstanceService service = new InstanceService(); + ApiResponse response = service.isInstanceExists("definitely_nonexistent_12345"); + + assertTrue(response.isSuccess()); + assertFalse(response.getData(), "Несуществующая сборка должна вернуть false"); + } + + @Test + void deleteInstance_invalidName_returnsError() { + InstanceService service = new InstanceService(); + ApiResponse response = service.deleteInstance("nonexistent"); + + assertNotNull(response, "Ответ не должен быть null"); + } + + @Test + void getInstance_nonexistent_returnsError() { + InstanceService service = new InstanceService(); + ApiResponse> response = service.getInstance("definitely_nonexistent_12345"); + + assertNotNull(response, "Ответ не должен быть null"); + assertFalse(response.isSuccess(), "Несуществующая сборка должна вернуть ошибку"); + } +} \ No newline at end of file diff --git a/launcher/src/test/java/me/sashegdev/zernmc/launcher/web/HeadlessDetectionTest.java b/launcher/src/test/java/me/sashegdev/zernmc/launcher/web/HeadlessDetectionTest.java new file mode 100644 index 0000000..31d7535 --- /dev/null +++ b/launcher/src/test/java/me/sashegdev/zernmc/launcher/web/HeadlessDetectionTest.java @@ -0,0 +1,33 @@ +package me.sashegdev.zernmc.launcher.web; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.awt.GraphicsEnvironment; + +class HeadlessDetectionTest { + + @Test + void headlessDetection_works() { + boolean isHeadless = GraphicsEnvironment.isHeadless(); + assertNotNull(isHeadless, "isHeadless() должен возвращать boolean"); + } + + @Test + void headlessDetection_consistentResult() { + boolean isHeadless1 = GraphicsEnvironment.isHeadless(); + boolean isHeadless2 = GraphicsEnvironment.isHeadless(); + assertEquals(isHeadless1, isHeadless2, "Результат должен быть консистентным"); + } + + @Test + void javaFxCheck_works() { + try { + boolean isHeadless = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment() + .getDefaultScreenDevice() != null; + assertFalse(isHeadless, "На Linux без дисплея должно быть headless"); + } catch (Exception e) { + assertTrue(true, "Ожидаемая ошибка на headless"); + } + } +} \ No newline at end of file diff --git a/launcher/src/test/java/me/sashegdev/zernmc/launcher/web/WebServerTest.java b/launcher/src/test/java/me/sashegdev/zernmc/launcher/web/WebServerTest.java new file mode 100644 index 0000000..8b845fd --- /dev/null +++ b/launcher/src/test/java/me/sashegdev/zernmc/launcher/web/WebServerTest.java @@ -0,0 +1,37 @@ +package me.sashegdev.zernmc.launcher.web; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.ServerSocket; + +class WebServerTest { + + @Test + void findFreePort_returnsValidPort() throws IOException { + int port = WebServer.findFreePort(8080); + assertTrue(port >= 8080, "Порт должен быть >= 8080"); + assertTrue(port < 8180, "Порт должен быть < 8180"); + } + + @Test + void findFreePort_findsDifferentPorts() throws IOException { + int port1 = WebServer.findFreePort(9000); + int port2 = WebServer.findFreePort(9100); + + assertNotEquals(port1, port2, "Должены быть разные порты"); + } + + @Test + void findFreePort_respectsStartPort() throws IOException { + int port = WebServer.findFreePort(9500); + assertTrue(port >= 9500, "Порт должен быть >= указанного startPort"); + } + + @Test + void portRangeTest() throws IOException { + int port = WebServer.findFreePort(8080); + assertTrue(port >= 8080 && port < 8180, "Порт в допустимом диапазоне 8080-8179"); + } +} \ No newline at end of file
v1.0.8
Загрузка...