From 566559d6395118a3eeaba0beb31216c2fbc62b05 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Wed, 19 Aug 2026 19:54:52 +0000 Subject: [PATCH] =?UTF-8?q?v1.0.16.21=20=E2=80=94=20prefetch=20Forge/NeoFo?= =?UTF-8?q?rge=20libraries=20through=20smart=20proxy;=20UI=20polish=20(no?= =?UTF-8?q?=20text=20selection,=20log=20viewer=20copy/open,=20locale=20in?= =?UTF-8?q?=20settings,=20window=20icon,=20adaptive=20pack=20list)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minecraft/installer/ForgeInstaller.java | 33 +++--- .../installer/NeoForgeInstaller.java | 4 + .../zernmc/launcher/ui/jfx/JFXLauncher.java | 105 +++++++++++++++++- .../zernmc/launcher/utils/ZHttpClient.java | 77 +++++++++++++ .../src/resources/ui/icons/zernmc.ico | Bin 0 -> 105246 bytes .../src/resources/ui/icons/zernmc.png | Bin 0 -> 2968 bytes launcher/launcher/src/resources/ui/index.html | 6 +- .../launcher/src/resources/ui/launcher.js | 58 +++++++++- launcher/launcher/src/resources/ui/style.css | 25 ++++- launcher/pom.xml | 2 +- 10 files changed, 281 insertions(+), 29 deletions(-) create mode 100644 launcher/launcher/src/resources/ui/icons/zernmc.ico create mode 100644 launcher/launcher/src/resources/ui/icons/zernmc.png diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java index b6ff086..b015493 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java @@ -48,7 +48,16 @@ public class ForgeInstaller { LauncherLogger.info("Downloading Forge Installer..."); downloadFileWithProgress(installerUrl, installerJar); - // Step 4: Run Forge Installer and show its output + // Step 4: Pre-download the libraries the installer subprocess would fetch from + // maven.minecraftforge.net directly. On blocked networks that subprocess cannot + // reach the maven host, so we seed every library (version.json + install_profile.json) + // through the launcher smart proxy first. The installer reuses files whose checksums + // validate ("File exists: Checksum validated"), so no direct downloads are needed. + LauncherLogger.info("Prefetching Forge libraries via smart proxy..."); + int prefetched = ZHttpClient.prefetchInstallerLibraries(installerJar, instance.getPath()); + LauncherLogger.info("Prefetched " + prefetched + " Forge libraries"); + + // Step 5: Run Forge Installer and show its output LauncherLogger.info("Running Forge Installer..."); LauncherLogger.info("This may take a few minutes. Please wait..."); @@ -105,6 +114,13 @@ public class ForgeInstaller { while (attempt <= maxRetries) { LauncherLogger.info("Attempt " + attempt + " of " + maxRetries); + // Re-seed libraries each attempt: the installer may have left partial files, + // and prefetch deletes any with a bad checksum before re-downloading via proxy. + int seeded = ZHttpClient.prefetchInstallerLibraries(installerJar, instance.getPath()); + if (seeded > 0) { + LauncherLogger.info(" Prefetched " + seeded + " libraries for this attempt"); + } + ProcessBuilder pb = new ProcessBuilder( JavaResolver.getJavaExecutable(), "-jar", @@ -175,19 +191,8 @@ public class ForgeInstaller { if (attempt < maxRetries) { LauncherLogger.warn("Install error. Retrying in 5 seconds..."); Thread.sleep(5000); - - // Clean temp files before retry - Path librariesDir = instance.getPath().resolve("libraries"); - if (Files.exists(librariesDir)) { - // Удаляем только частично скачанные библиотеки Forge - 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 */ } - }); - } - } + // Prefetch at the top of the next attempt already re-seeds any library that + // was left partial or corrupted (SHA-1 mismatch -> delete + re-download via proxy). } else { LauncherLogger.error("Forge Installer exited with error code: " + exitCode); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java index 183c2f0..70d89d7 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java @@ -49,6 +49,10 @@ public class NeoForgeInstaller { LauncherLogger.info("Downloading NeoForge Installer..."); downloadFileWithProgress(installerUrl, installerJar); + LauncherLogger.info("Prefetching NeoForge libraries via smart proxy..."); + int prefetched = ZHttpClient.prefetchInstallerLibraries(installerJar, instance.getPath()); + LauncherLogger.info("Prefetched " + prefetched + " NeoForge libraries"); + LauncherLogger.info("Running NeoForge Installer..."); LauncherLogger.info("This may take a few minutes. Please wait..."); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java index 29f443e..06029e9 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java @@ -206,6 +206,77 @@ public class JFXLauncher extends Application { launch(args); } + private static final byte[] PNG_SIGNATURE = {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; + + private static javafx.scene.image.Image loadWindowIcon() { + try { + byte[] ico = readAssetBytes("assets/ui/icons/zernmc.ico"); + if (ico != null) { + byte[] png = extractIcoPngFrame(ico); + if (png != null) { + return new javafx.scene.image.Image(new java.io.ByteArrayInputStream(png)); + } + } + byte[] png = readAssetBytes("assets/ui/icons/zernmc.png"); + if (png != null) { + return new javafx.scene.image.Image(new java.io.ByteArrayInputStream(png)); + } + } catch (Exception e) { + log("Window icon load error: " + e.getMessage()); + } + return null; + } + + private static byte[] readAssetBytes(String resourcePath) { + try (InputStream in = JFXLauncher.class.getResourceAsStream("/" + resourcePath)) { + if (in != null) { + return in.readAllBytes(); + } + } catch (Exception ignored) {} + try { + Path file = Paths.get(resourcePath); + if (Files.exists(file)) { + return Files.readAllBytes(file); + } + } catch (Exception ignored) {} + return null; + } + + private static byte[] extractIcoPngFrame(byte[] ico) { + if (ico == null || ico.length < 6 || ico[0] != 0 || ico[1] != 0) { + return null; + } + int count = (ico[4] & 0xFF) | ((ico[5] & 0xFF) << 8); + int bestSize = -1; + int bestOffset = -1; + int bestLength = -1; + for (int i = 0; i < count; i++) { + int off = 6 + i * 16; + if (off + 16 > ico.length) break; + int w = ico[off] & 0xFF; + int h = ico[off + 1] & 0xFF; + int size = (w == 0 ? 256 : w) * (h == 0 ? 256 : h); + int len = (ico[off + 8] & 0xFF) | ((ico[off + 9] & 0xFF) << 8) + | ((ico[off + 10] & 0xFF) << 16) | ((ico[off + 11] & 0xFF) << 24); + int imgOff = (ico[off + 12] & 0xFF) | ((ico[off + 13] & 0xFF) << 8) + | ((ico[off + 14] & 0xFF) << 16) | ((ico[off + 15] & 0xFF) << 24); + if (imgOff < 0 || len <= 0 || imgOff + len > ico.length) continue; + boolean isPng = true; + for (int b = 0; b < PNG_SIGNATURE.length; b++) { + if (ico[imgOff + b] != PNG_SIGNATURE[b]) { isPng = false; break; } + } + if (isPng && size > bestSize) { + bestSize = size; + bestOffset = imgOff; + bestLength = len; + } + } + if (bestOffset < 0 || bestLength <= 0) return null; + byte[] png = new byte[bestLength]; + System.arraycopy(ico, bestOffset, png, 0, bestLength); + return png; + } + private static void extractAssets() { try { Path assetsDir = Paths.get("assets"); @@ -445,6 +516,11 @@ public class JFXLauncher extends Application { String url = "http://localhost:" + PORT + "/assets/ui/index.html"; engine.load(url); + javafx.scene.image.Image windowIcon = loadWindowIcon(); + if (windowIcon != null) { + stage.getIcons().add(windowIcon); + } + stage.setTitle(APP_TITLE); int winW = Config.getWindowWidth(); int winH = Config.getWindowHeight(); @@ -518,6 +594,7 @@ public class JFXLauncher extends Application { server.createContext("/api/exit-parent", this::handleExitParent); server.createContext("/api/system-info", this::handleSystemInfo); server.createContext("/api/open-log-file", this::handleOpenLogFile); + server.createContext("/api/clipboard", this::handleClipboard); server.createContext("/api/friends/list", this::handleFriendList); server.createContext("/api/friends/add", this::handleFriendAdd); server.createContext("/api/friends/remove", this::handleFriendRemove); @@ -561,21 +638,43 @@ public class JFXLauncher extends Application { private void handleOpenLogFile(HttpExchange exchange) { try { + Path target = null; if (gameLogFile != null && Files.exists(gameLogFile)) { + target = gameLogFile; + } else if (launcherLogFile != null && Files.exists(launcherLogFile)) { + target = launcherLogFile; + } + if (target != null) { if (Desktop.isDesktopSupported()) { - Desktop.getDesktop().open(gameLogFile.toFile()); - sendJson(exchange, Map.of("success", true)); + Desktop.getDesktop().open(target.toFile()); + sendJson(exchange, Map.of("success", true, "file", target.toString())); } else { sendJson(exchange, Map.of("success", false, "error", "Desktop not supported")); } } else { - sendJson(exchange, Map.of("success", false, "error", "No game log file found")); + sendJson(exchange, Map.of("success", false, "error", "No log file found")); } } catch (Exception e) { sendJson(exchange, Map.of("success", false, "error", e.getMessage())); } } + private void handleClipboard(HttpExchange exchange) { + try { + Map body = parseJson(exchange.getRequestBody()); + String text = body != null ? body.get("text") : null; + if (text == null) { + sendJson(exchange, Map.of("success", false, "error", "Empty clipboard text")); + return; + } + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents( + new java.awt.datatransfer.StringSelection(text), null); + sendJson(exchange, Map.of("success", true)); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + private void handleOpenUrl(HttpExchange exchange) { try { Map body = parseJson(exchange.getRequestBody()); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java index 29ed214..027fb5e 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java @@ -717,6 +717,83 @@ public class ZHttpClient { } } + /** + * Pre-downloads every library referenced by a loader installer jar (Forge/NeoForge). + * The installer later runs as a separate JVM subprocess that downloads libraries + * directly from the loader's maven repository, bypassing this launcher's smart proxy. + * On blocked networks (e.g. maven.minecraftforge.net unreachable) that subprocess + * fails with "Connection reset". Because the installer reuses any library already + * present with a matching checksum ("File exists: Checksum validated"), pre-seeding + * those files through the proxy lets the install complete without direct downloads. + * + * @return number of new library files downloaded + */ + public static int prefetchInstallerLibraries(Path installerJar, Path minecraftDir) { + int prefetched = 0; + try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(installerJar.toFile())) { + for (String jsonName : List.of("version.json", "install_profile.json")) { + java.util.zip.ZipEntry entry = zip.getEntry(jsonName); + if (entry == null) continue; + String content = new String(zip.getInputStream(entry).readAllBytes(), StandardCharsets.UTF_8); + JSONObject root = new JSONObject(content); + JSONArray libs = root.optJSONArray("libraries"); + if (libs == null) continue; + for (int i = 0; i < libs.length(); i++) { + JSONObject lib = libs.optJSONObject(i); + if (lib == null) continue; + JSONObject downloads = lib.optJSONObject("downloads"); + if (downloads == null) continue; + JSONObject artifact = downloads.optJSONObject("artifact"); + if (artifact == null) continue; + String url = artifact.optString("url", null); + String path = artifact.optString("path", null); + String sha1 = artifact.optString("sha1", null); + if (url == null || path == null) continue; + + Path target = minecraftDir.resolve("libraries").resolve(path); + try { + if (Files.exists(target)) { + if (sha1 == null || sha1.isEmpty() || sha1Matches(target, sha1)) { + continue; + } + Files.delete(target); + } + Files.createDirectories(target.getParent()); + downloadFileWithSmartProxy(url, target); + if (sha1 != null && !sha1.isEmpty() && !sha1Matches(target, sha1)) { + Files.delete(target); + LauncherLogger.warn("[LIB] Checksum mismatch after prefetch: " + path); + continue; + } + prefetched++; + LauncherLogger.info("[LIB] Prefetched " + path); + } catch (Exception e) { + LauncherLogger.warn("[LIB] Prefetch failed for " + path + ": " + e.getMessage()); + } + } + } + } catch (Exception e) { + LauncherLogger.warn("[LIB] Installer prefetch scan failed: " + e.getMessage()); + } + return prefetched; + } + + private static boolean sha1Matches(Path file, String expectedSha1) throws Exception { + java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1"); + try (java.io.InputStream in = Files.newInputStream(file)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = in.read(buffer)) != -1) { + digest.update(buffer, 0, read); + } + } + StringBuilder sb = new StringBuilder(); + for (byte b : digest.digest()) { + sb.append(String.format("%02x", b)); + } + return sb.toString().equalsIgnoreCase(expectedSha1); + } + private static String resolveCoordinatesUrl(JSONObject lib) { String name = lib.getString("name"); String base = lib.getString("url"); diff --git a/launcher/launcher/src/resources/ui/icons/zernmc.ico b/launcher/launcher/src/resources/ui/icons/zernmc.ico new file mode 100644 index 0000000000000000000000000000000000000000..6f60d98c8fc442d34c110148a63e0d930898581e GIT binary patch literal 105246 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 Zuy1~Vdtg6H?m_$3YTm=9JE->C{|7}#Q!)Sm literal 0 HcmV?d00001 diff --git a/launcher/launcher/src/resources/ui/icons/zernmc.png b/launcher/launcher/src/resources/ui/icons/zernmc.png new file mode 100644 index 0000000000000000000000000000000000000000..45e75c23285e434600f023e6d6a97ec768e10399 GIT binary patch literal 2968 zcmb_eVNg`p6}}5AT~t^R1tZ`pScwkJYBi!FKGUKbu}Xs#wK{B^G@>-RM8rjsa97r5 zwJ1B&(LtM%ZRUy>-4+1G*jE(2^rPB; zKDobY_r1SNblqxL{^Y27=J$I)nCi}(CZ={gS<#x>KDFS+Y}>BSkIntiG_Ff9{X%*x z{oIbmS!--V1y^k8>qq*WP%AG#bL!xx<8Z;qpDM5ApDxRLGZJ?6Sl0E!wA+7eJ-@T9 z@@D?2vc*R#@A;m779Q^U=+vS5+=nB{W$(4E-LAc*KC5f6)N9qf?eRcBN?*W!r1X8s zW5K`DHd+nrW0U6LDo@Z}d^;6r5sxqm4}gyu0YB9R5ua``9X|qovw$|M-fuDQ=0k7+_gafaxaAs2 zPWUg^wspJM{V$*x4=m&n`~D~~NgBF2mY?yEJnn2hvT|p0dtq!m(}ecH#ay!$1@wTT zvKb&H;ERL#Trn9v;enl0^0`5G#2&l#D)T-T-G7doCXnB9le0=FnF%ar@&OmjQA>Et z-bOf&J9`7UxszyAPU1)7VJ=rpL2dlRdF3uQ7>qb}j7g?7X$^>vXOCk}B z#UEw!QzmrX$v-Bc>v2ry=@JAkSbzp~!DT@&QXW?M1;0_vI=)^}w;YY`bHz5LgIBt> zD{Ry;gLk$qWd)ys2XqwO-yjQ8$%C)xf;s^*a5KO5IW@n4Ga?JeEJ+D82VpTjr{kJL zzTh^S73&y|>u7?wAfkk*V6(bTPJW=sp%ZmZmoSYH$TFGS3t5V1ofa5^S)|uZnLS-%D(YrAp8njJtjK`WGT45cqQyoonEY@pZz}v2#p1NySK)#< zC2Zua1!Zb6mvlu#~3h>A%@7+wrHfQHNisVt|PwHFcE^ zJSQhkaWsg9bk5*{sI?+xeVz9utvL!OvPcD7H{);V1}*;zr(4*JdrW?DA?F;yig$o! zZ-gO6>mj!|ac{tY59x@@2jR8V0KcvUSvM4P+HF#Egr3bLRV?WWEtn0+B0iIFORwAr zP`^rfLmt;i;*&MQkD7-R`+in{b?s}8l;WXQvl3QLzB4?Wt64A8&ccmK^QpsfDEJ`7 z1q*RB1&>j!&$iCiJgV=E9I`*O(U|0eY=FWSaW_Q!{Weow zegY(W6Py=EQOZHbskRW888J$!*PI97Y@pbi#C%g#oioN(nnU1czo#IdnKxfsC3Tixxl;;5W zHk()EwjzHgPU-WrnPtx+z;rRzCV05^VdewJ-=i7}Y+N}jZ zk*C1I)CJ{f1}!4M8>z_a7MCpDg-S1t#8Q!B-^WWZr1iJ>2j6_fD0xIVV{s>`)tZ;n z3VH7AX&`gd&>`yD+csz<(Ow_zRi%V|zZSKy@6d+6L>vrK25FS60imLWK?>KSShOkl zMa0E&b~DAs8O`^Q8$@d{!>b+A+OkznCTU&0%r1eI?G>+I4)*R z+7Hlq#Y&ePU9ou-#(+q7qsJen?NZ%qJ#qN4D3YjCtBzRkv4Ag;T*#ui1T5ppF4_PV z{Gr|%isveWpRR|+40^h`XR`UpHdbetZ$aN^s6QC8@F$z6GD|bQvyvZ4$BX!uT7u#` zMtSJFV6No2Xk-tB>aV{CnoDmMj5Sin!GLgqKPv+N!;T)gM#qPl&_(+R4||@L&i9_z z4PW7c^;(54@W3)III89H_=8u3$vn#WxRWQQszZE>hxUqkLT0R=*rfGygQrTqo zUYKN}p;mN;e@T~uK#}&1qI8Lb-X-7WDKAIu<#9}NjPI~-2<)f&$_xhh4ieBd<^S;D z2p{92@A-N?zgj+Kn1{}l#COpwRK-cVnm&dmqw5?!PbZ@j6+DjybwNO|a>2`dQ_weH zuZ6#`=&oiX)q)kahZ%AFe1`>m39LP-i& -
+
-
+
@@ -550,7 +550,7 @@

Game Log

- +
diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js index daa3075..3ba1fbe 100644 --- a/launcher/launcher/src/resources/ui/launcher.js +++ b/launcher/launcher/src/resources/ui/launcher.js @@ -58,6 +58,8 @@ const LOCALES = { 'logViewer.noLogs': 'No logs yet. Launch the game to see logs here.', 'logViewer.copy': 'Copy', 'logViewer.copied': 'Copied!', 'logViewer.openFile': 'Open File', 'logViewer.close': 'Close', + 'logViewer.opened': 'File opened', + 'logViewer.openFailed': 'Could not open the log file', 'logViewer.waiting': 'Waiting for game output...', 'install.title': 'Install Pack', 'install.tab.serverPack': 'Server Pack', 'install.tab.custom': 'Custom', @@ -267,6 +269,8 @@ const LOCALES = { 'logViewer.noLogs': 'Логов пока нет. Запустите игру, чтобы увидеть логи.', 'logViewer.copy': 'Копировать', 'logViewer.copied': 'Скопировано!', 'logViewer.openFile': 'Открыть файл', 'logViewer.close': 'Закрыть', + 'logViewer.opened': 'Файл открыт', + 'logViewer.openFailed': 'Не удалось открыть файл логов', 'logViewer.waiting': 'Ожидание вывода игры...', 'install.title': 'Установить сборку', 'install.tab.serverPack': 'Серверная сборка', 'install.tab.custom': 'Своя', @@ -475,7 +479,10 @@ class ZernMCLauncher { _localeData = LOCALES[lang] || LOCALES.en; applyLocale(); var sel = document.getElementById('locale-select'); - if (sel) sel.value = lang; + if (sel) { + sel.value = lang; + if (sel.dataset.enhanced) this.refreshCustomSelect(sel); + } } // ==================== BACKGROUND ==================== @@ -2477,6 +2484,7 @@ class ZernMCLauncher { openLogViewer() { var overlay = document.getElementById('log-viewer-overlay'); if (!overlay) return; + this._logRenderedLines = null; overlay.classList.remove('hidden'); this._pollLogViewer(); this._logPollInterval = setInterval(this._pollLogViewer.bind(this), 1500); @@ -2504,13 +2512,32 @@ class ZernMCLauncher { var content = document.getElementById('log-viewer-content'); if (!content) return; if (!raw || !raw.trim()) { + this._logRenderedLines = null; content.innerHTML = '
' + t('logViewer.noLogs') + '
'; return; } var lines = raw.split('\n').filter(function(l) { return l.trim(); }); - var html = lines.map(this._renderLogLine.bind(this)).join(''); - content.innerHTML = html; - content.scrollTop = content.scrollHeight; + var cleaned = lines.map(function(l) { return l.replace(/§[0-9a-fklmnor]/g, ''); }); + + var stick = content.scrollTop + content.clientHeight >= content.scrollHeight - 40; + + var prev = this._logRenderedLines; + var appendFrom = 0; + if (prev && prev.length > 0 && lines.length >= prev.length) { + var same = true; + for (var i = 0; i < prev.length; i++) { + if (cleaned[i] !== prev[i]) { same = false; break; } + } + if (same) appendFrom = prev.length; + } + + this._logRenderedLines = cleaned; + if (appendFrom > 0) { + content.insertAdjacentHTML('beforeend', lines.slice(appendFrom).map(this._renderLogLine.bind(this)).join('')); + } else { + content.innerHTML = lines.map(this._renderLogLine.bind(this)).join(''); + } + if (stick) content.scrollTop = content.scrollHeight; } _renderLogLine(line) { @@ -2524,10 +2551,18 @@ class ZernMCLauncher { return '
' + this.esc(text) + '
'; } - copyLogs() { + async copyLogs() { var content = document.getElementById('log-viewer-content'); if (!content) return; var text = content.textContent; + try { + var r = await this.req('/clipboard', { method: 'POST', body: JSON.stringify({ text: text }) }); + if (r && r.success) { + this.toast(t('logViewer.copied'), 'success'); + return; + } + } catch(e) {} + // Fallback for plain-browser use (outside the launcher) if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function() { app.toast(t('logViewer.copied'), 'success'); @@ -2547,6 +2582,19 @@ class ZernMCLauncher { } } + async openLogFile() { + try { + var r = await this.req('/open-log-file', { method: 'POST' }); + if (r && r.success) { + this.toast(t('logViewer.opened'), 'success'); + } else { + this.toast((r && r.error) || t('logViewer.openFailed'), 'error'); + } + } catch(e) { + this.toast(t('logViewer.openFailed'), 'error'); + } + } + esc(s) { if (!s) return ''; const d = document.createElement('div'); diff --git a/launcher/launcher/src/resources/ui/style.css b/launcher/launcher/src/resources/ui/style.css index 97c6021..c749faa 100644 --- a/launcher/launcher/src/resources/ui/style.css +++ b/launcher/launcher/src/resources/ui/style.css @@ -29,6 +29,9 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body, * { -webkit-user-select: none; user-select: none; } +input, textarea { -webkit-user-select: text; user-select: text; } + html { font-size: 14px; } body { @@ -226,15 +229,31 @@ body { } .section-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); transition: var(--transition); } .section-chevron { flex-shrink: 0; color: var(--text-muted); transition: transform .2s var(--ease, ease); } +.section-collapsible { display: flex; flex-direction: column; min-height: 0; } +.section-collapsible:not(.collapsed) { flex: 1 1 auto; } +.section-collapsible:not(.collapsed) .section-chevron { transform: rotate(0deg); } .section-collapsible.collapsed .section-chevron { transform: rotate(-90deg); } -.section-collapsible.collapsed .pack-list { display: none; } .section-collapsible.collapsed .section-header { margin-bottom: 0; } .section-collapsible.collapsed .section-title { color: var(--text-muted); } +.pack-list-collapse { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows .3s var(--ease, ease); + min-height: 0; + overflow: hidden; +} +.section-collapsible:not(.collapsed) .pack-list-collapse { + grid-template-rows: 1fr; + flex: 1; +} +.pack-list-collapse > .pack-list { + overflow-y: auto; + min-height: 0; +} + .pack-list { display: flex; flex-direction: column; gap: 3px; - overflow-y: auto; max-height: calc((100vh - 460px) / 2); - min-height: 40px; } .pack-list:empty::after { content: 'No packs'; display: block; padding: 12px 8px; diff --git a/launcher/pom.xml b/launcher/pom.xml index cd39f99..a5401cc 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -20,7 +20,7 @@ 1.0.16 - 18 + 21 21 21 UTF-8