feat: 1.1.1.4 GUI bootstrap-Inno installer + standalone offline

- Installer.java: Swing dark Bootstrap (bg 0c0c12, accent e94560) Inno wizard (sidebar steps, Welcome->Dir->Progress->Finish), JRE + meta download via /launcher/file, offline via embedded /offline.zip + appended ZIP + adjacent fallback, shortcut/uninstall
- pom: hotfix 4, launch4j 2 exe ZernMC-Online/Offline-Setup-1.1.1.4.exe (icon, versionInfo, header gui, jar shaded), offline.zip resource
- server/main.py: Content-Disposition attachment filename for online/offline, BUILDS_DIR absolute
- Builds: Online 6.3M (downloads), Offline 103M (standalone embedded 98M zip), both signed, site headers fixed
This commit is contained in:
SashegDev
2026-09-01 14:14:30 +00:00
parent 1900db3caf
commit b864e17921
4 changed files with 518 additions and 4 deletions
+39
View File
@@ -112,6 +112,12 @@
<include>ui/**</include> <include>ui/**</include>
</includes> </includes>
</resource> </resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>offline.zip</include>
</includes>
</resource>
</resources> </resources>
<plugins> <plugins>
<plugin> <plugin>
@@ -257,6 +263,39 @@
</versionInfo> </versionInfo>
</configuration> </configuration>
</execution> </execution>
<!-- Online Setup GUI Bootstrap-Inno -->
<execution>
<id>l4j-online-setup</id>
<phase>package</phase>
<goals><goal>launch4j</goal></goals>
<configuration>
<outfile>../../server/builds/ZernMC-Online-Setup-${project.version}.${hotfix}.exe</outfile>
<jar>../../server/builds/zernmclauncher.jar</jar>
<icon>${project.basedir}/src/main/icons/zernmc.ico</icon>
<headerType>gui</headerType>
<dontWrapJar>false</dontWrapJar>
<classPath><mainClass>me.sashegdev.zernmc.launcher.installer.Installer</mainClass><addDependencies>false</addDependencies></classPath>
<jre><path>lib/jre21</path><minVersion>21</minVersion></jre>
<versionInfo><fileVersion>${project.version}.${hotfix}</fileVersion><txtFileVersion>${project.version}.${hotfix}</txtFileVersion><fileDescription>ZernMC Online Setup</fileDescription><copyright>Copyright (c) 2023-2026 ZernMC</copyright><productVersion>${project.version}.${hotfix}</productVersion><txtProductVersion>${project.version}.${hotfix}</txtProductVersion><productName>ZernMC</productName><companyName>ZernMC</companyName><internalName>zernmc-online-setup</internalName><originalFilename>ZernMC-Online-Setup.exe</originalFilename></versionInfo>
</configuration>
</execution>
<!-- Offline Setup GUI with embedded zip -->
<execution>
<id>l4j-offline-setup</id>
<phase>package</phase>
<goals><goal>launch4j</goal></goals>
<configuration>
<outfile>../../server/builds/ZernMC-Offline-Setup-${project.version}.${hotfix}.exe</outfile>
<jar>../../server/builds/zernmclauncher.jar</jar>
<icon>${project.basedir}/src/main/icons/zernmc.ico</icon>
<headerType>gui</headerType>
<dontWrapJar>false</dontWrapJar>
<classPath><mainClass>me.sashegdev.zernmc.launcher.installer.Installer</mainClass><addDependencies>false</addDependencies></classPath>
<cmdLine>--offline</cmdLine>
<jre><path>lib/jre21</path><minVersion>21</minVersion></jre>
<versionInfo><fileVersion>${project.version}.${hotfix}</fileVersion><txtFileVersion>${project.version}.${hotfix}</txtFileVersion><fileDescription>ZernMC Offline Setup</fileDescription><copyright>Copyright (c) 2023-2026 ZernMC</copyright><productVersion>${project.version}.${hotfix}</productVersion><txtProductVersion>${project.version}.${hotfix}</txtProductVersion><productName>ZernMC</productName><companyName>ZernMC</companyName><internalName>zernmc-offline-setup</internalName><originalFilename>ZernMC-Offline-Setup.exe</originalFilename></versionInfo>
</configuration>
</execution>
</executions> </executions>
</plugin> </plugin>
@@ -0,0 +1,469 @@
package me.sashegdev.zernmc.launcher.installer;
import javax.swing.*;
import javax.swing.plaf.basic.BasicProgressBarUI;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import com.google.gson.*;
public class Installer {
private static final String BASE_URL = "https://api.zern.cc";
private static JFrame frame;
private static CardLayout cards;
private static JPanel mainPanel;
private static JProgressBar progressBar;
private static JLabel statusLabel, speedLabel, titleLabel;
private static JTextField dirField;
private static JCheckBox shortcutCheck;
private static String installDir = "C:\\ZernMC";
private static boolean isOffline = false;
private static final Color bg = new Color(0x0c,0x0c,0x12);
private static final Color surface = new Color(0x16,0x16,0x1f);
private static final Color accent = new Color(0xe9,0x45,0x60);
private static final Color text = new Color(0xee,0xee,0xf0);
private static final Color muted = new Color(0x88,0x88,0x9a);
private static final Color sidebarBg = new Color(0x10,0x10,0x1e);
private static int currentStep = 0;
private static JLabel[] stepLabels;
public static void main(String[] args) throws Exception {
for (String a: args) if (a.equals("--offline")) isOffline = true;
SwingUtilities.invokeLater(() -> createUI());
}
private static void createUI() {
try { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); } catch(Exception ignored){}
frame = new JFrame("ZernMC Setup");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(640, 420);
frame.setLocationRelativeTo(null);
frame.setResizable(false);
frame.setUndecorated(false);
JPanel root = new JPanel(new BorderLayout());
root.setBackground(bg);
JPanel sidebar = new JPanel();
sidebar.setBackground(sidebarBg);
sidebar.setPreferredSize(new Dimension(180, 420));
sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS));
sidebar.setBorder(BorderFactory.createEmptyBorder(24, 18, 24, 18));
JLabel logo = new JLabel("ZernMC");
logo.setFont(new Font("Segoe UI", Font.BOLD, 22));
logo.setForeground(text);
logo.setAlignmentX(Component.LEFT_ALIGNMENT);
sidebar.add(logo);
JLabel sub = new JLabel("Setup");
sub.setFont(new Font("Segoe UI", Font.PLAIN, 12));
sub.setForeground(accent);
sub.setAlignmentX(Component.LEFT_ALIGNMENT);
sidebar.add(sub);
sidebar.add(Box.createVerticalStrut(24));
JSeparator sep = new JSeparator();
sep.setMaximumSize(new Dimension(144,1));
sep.setForeground(new Color(0x2a,0x2a,0x3a));
sidebar.add(sep);
sidebar.add(Box.createVerticalStrut(18));
String[] steps = {"Приветствие","Папка установки","Установка","Готово"};
stepLabels = new JLabel[steps.length];
for (int i=0;i<steps.length;i++) {
JLabel l = new JLabel((i+1)+". "+steps[i]);
l.setFont(new Font("Segoe UI", Font.PLAIN, 12));
l.setForeground(i==0?text:muted);
l.setAlignmentX(Component.LEFT_ALIGNMENT);
l.setBorder(BorderFactory.createEmptyBorder(4,8,4,0));
stepLabels[i]=l;
sidebar.add(l);
}
sidebar.add(Box.createVerticalGlue());
JLabel ver = new JLabel("v1.1.1.4 • Zern.cc");
ver.setFont(new Font("Segoe UI", Font.PLAIN, 10));
ver.setForeground(muted);
sidebar.add(ver);
root.add(sidebar, BorderLayout.WEST);
JPanel content = new JPanel(new BorderLayout());
content.setBackground(surface);
content.setBorder(BorderFactory.createEmptyBorder(18, 20, 0, 20));
cards = new CardLayout();
mainPanel = new JPanel(cards);
mainPanel.setBackground(surface);
mainPanel.add(createWelcome(), "welcome");
mainPanel.add(createDirPanel(), "dir");
mainPanel.add(createProgressPanel(), "progress");
mainPanel.add(createFinishPanel(), "finish");
content.add(mainPanel, BorderLayout.CENTER);
JPanel nav = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 10));
nav.setBackground(surface);
JButton cancel = new JButton("Отмена");
styleNav(cancel, false);
cancel.addActionListener(e -> System.exit(0));
JButton next = new JButton("Далее >");
styleNav(next, true);
next.addActionListener(e -> onNext());
next.putClientProperty("next", true);
nav.add(cancel);
nav.add(next);
content.add(nav, BorderLayout.SOUTH);
root.add(content, BorderLayout.CENTER);
frame.setContentPane(root);
frame.setVisible(true);
}
private static void styleNav(JButton b, boolean primary) {
b.setFont(new Font("Segoe UI", Font.PLAIN, 12));
b.setFocusPainted(false);
b.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(primary?accent:new Color(0x3a,0x3a,0x4a),1),
BorderFactory.createEmptyBorder(6, 16, 6, 16)));
b.setBackground(primary?accent:surface);
b.setForeground(primary?Color.WHITE:text);
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
private static JPanel createWelcome() {
JPanel p = new JPanel();
p.setBackground(surface);
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
JLabel t = new JLabel("Добро пожаловать в ZernMC");
t.setFont(new Font("Segoe UI", Font.BOLD, 16));
t.setForeground(text);
t.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(t);
p.add(Box.createVerticalStrut(10));
JLabel d = new JLabel("<html>Мастер установит ZernMC Launcher на ваш компьютер.<br>Рекомендуется закрыть другие приложения.<br><br>Нажмите «Далее», чтобы продолжить.</html>");
d.setFont(new Font("Segoe UI", Font.PLAIN, 12));
d.setForeground(muted);
d.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(d);
p.add(Box.createVerticalStrut(12));
JLabel note = new JLabel("<html><b>Онлайн</b> — скачает 47М JRE + 50М лаунчер &nbsp;|&nbsp; <b>Оффлайн</b> — распакует встроенный архив</html>");
note.setFont(new Font("Segoe UI", Font.PLAIN, 11));
note.setForeground(muted);
note.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(note);
return p;
}
private static JPanel createDirPanel() {
JPanel p = new JPanel();
p.setBackground(surface);
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
JLabel t = new JLabel("Выберите папку установки");
t.setFont(new Font("Segoe UI", Font.BOLD, 14));
t.setForeground(text);
t.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(t);
p.add(Box.createVerticalStrut(12));
JPanel row = new JPanel(new BorderLayout(8,0));
row.setBackground(surface);
row.setMaximumSize(new Dimension(420, 28));
row.setAlignmentX(Component.LEFT_ALIGNMENT);
dirField = new JTextField(installDir);
dirField.setFont(new Font("Segoe UI", Font.PLAIN, 12));
dirField.setBackground(bg);
dirField.setForeground(text);
dirField.setCaretColor(text);
dirField.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(0x2a,0x2a,0x3a)), BorderFactory.createEmptyBorder(4,6,4,6)));
JButton browse = new JButton("Обзор...");
browse.setFont(new Font("Segoe UI", Font.PLAIN, 11));
browse.addActionListener(e -> {
JFileChooser fc = new JFileChooser();
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fc.setDialogTitle("Куда установить ZernMC");
if (fc.showOpenDialog(frame)==JFileChooser.APPROVE_OPTION) {
String sel = fc.getSelectedFile().getAbsolutePath();
if (!sel.toLowerCase().contains("zernmc")) sel = sel + File.separator + "ZernMC";
dirField.setText(sel);
}
});
row.add(dirField, BorderLayout.CENTER);
row.add(browse, BorderLayout.EAST);
p.add(row);
p.add(Box.createVerticalStrut(10));
JLabel hint = new JLabel("По умолчанию: C:\\ZernMC • Данные в %USERPROFILE%\\.zernmc");
hint.setFont(new Font("Segoe UI", Font.PLAIN, 10));
hint.setForeground(muted);
hint.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(hint);
p.add(Box.createVerticalStrut(12));
shortcutCheck = new JCheckBox("Создать ярлык на рабочем столе", true);
shortcutCheck.setBackground(surface);
shortcutCheck.setForeground(text);
shortcutCheck.setFont(new Font("Segoe UI", Font.PLAIN, 12));
shortcutCheck.setAlignmentX(Component.LEFT_ALIGNMENT);
p.add(shortcutCheck);
return p;
}
private static JPanel createProgressPanel() {
JPanel p = new JPanel();
p.setBackground(surface);
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
titleLabel = new JLabel("Установка...");
titleLabel.setFont(new Font("Segoe UI", Font.PLAIN, 12));
titleLabel.setForeground(muted);
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
p.add(titleLabel);
p.add(Box.createVerticalStrut(18));
statusLabel = new JLabel("Подготовка...");
statusLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13));
statusLabel.setForeground(text);
statusLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
p.add(statusLabel);
p.add(Box.createVerticalStrut(12));
progressBar = new JProgressBar(0,100);
progressBar.setPreferredSize(new Dimension(400,6));
progressBar.setMaximumSize(new Dimension(400,6));
progressBar.setAlignmentX(Component.CENTER_ALIGNMENT);
progressBar.setBackground(new Color(0x2a,0x2a,0x3a));
progressBar.setForeground(accent);
progressBar.setBorderPainted(false);
progressBar.setUI(new BasicProgressBarUI(){ protected Color getSelectionBackground(){return accent;} protected Color getSelectionForeground(){return accent;} });
p.add(progressBar);
p.add(Box.createVerticalStrut(6));
speedLabel = new JLabel(" ");
speedLabel.setFont(new Font("Segoe UI", Font.PLAIN, 11));
speedLabel.setForeground(muted);
speedLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
p.add(speedLabel);
return p;
}
private static JPanel createFinishPanel() {
JPanel p = new JPanel();
p.setBackground(surface);
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
JLabel t = new JLabel("Установка завершена!");
t.setFont(new Font("Segoe UI", Font.BOLD, 16));
t.setForeground(text);
t.setAlignmentX(Component.CENTER_ALIGNMENT);
p.add(t);
p.add(Box.createVerticalStrut(10));
JLabel d = new JLabel("<html><center>ZernMC Launcher установлен.<br>Запустите с ярлыка или из C:\\ZernMC\\zernmc.exe<br>Удаление: C:\\ZernMC\\uninstall.exe</center></html>");
d.setFont(new Font("Segoe UI", Font.PLAIN, 12));
d.setForeground(muted);
d.setAlignmentX(Component.CENTER_ALIGNMENT);
p.add(d);
p.add(Box.createVerticalStrut(18));
JButton launch = new JButton("Запустить ZernMC");
styleNav(launch, true);
launch.setAlignmentX(Component.CENTER_ALIGNMENT);
launch.addActionListener(e -> {
try { Runtime.getRuntime().exec(new String[]{dirField.getText()+File.separator+"zernmc.exe"}); } catch(Exception ignored){}
System.exit(0);
});
p.add(launch);
return p;
}
private static void onNext() {
if (currentStep==0) { currentStep=1; updateSteps(); cards.show(mainPanel,"dir"); }
else if (currentStep==1) {
installDir = dirField.getText().trim();
if (installDir.isEmpty()) { JOptionPane.showMessageDialog(frame,"Укажите папку"); return; }
currentStep=2; updateSteps(); cards.show(mainPanel,"progress");
// disable nav
new Thread(() -> {
try { doInstall(installDir, shortcutCheck.isSelected()); SwingUtilities.invokeLater(() -> { currentStep=3; updateSteps(); cards.show(mainPanel,"finish"); }); }
catch(Exception ex){ SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog(frame,"Ошибка: "+ex.getMessage()); currentStep=1; updateSteps(); cards.show(mainPanel,"dir"); }); }
}).start();
} else if (currentStep==3) System.exit(0);
}
private static void updateSteps() {
for (int i=0;i<stepLabels.length;i++) stepLabels[i].setForeground(i==currentStep?text:i<currentStep?accent:muted);
}
private static void doInstall(String dir, boolean shortcut) throws Exception {
Path base = Paths.get(dir);
Files.createDirectories(base);
setStatus("Проверка JRE...",0);
if (!isOffline) {
// online: download JRE + meta files
Path jreExe = base.resolve("lib/jre21/bin/java.exe");
if (!Files.exists(jreExe)) {
setStatus("Скачивание JRE 47М...", 5);
Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "jre.zip");
downloadFile(BASE_URL+"/launcher/download/jre", tmp, -1);
setStatus("Распаковка JRE...", 10);
unzip(tmp, base);
Path cand = base.resolve("jre21");
if (Files.exists(cand)) {
Path target = base.resolve("lib/jre21");
Files.createDirectories(target.getParent());
if (Files.exists(target)) deleteRecursively(target);
Files.move(cand, target);
}
Files.deleteIfExists(tmp);
}
setStatus("Получение списка файлов...", 15);
JsonObject meta = fetchMeta();
JsonArray files = meta.getAsJsonArray("files");
int total = files.size();
int idx=0;
for (JsonElement el: files) {
JsonObject f = el.getAsJsonObject();
String rel = f.get("path").getAsString();
if (rel.startsWith("jre21/")||rel.startsWith("lib/jre21")) continue;
String hash = f.get("hash").getAsString().replace("sha256:","");
long size = f.get("size").getAsLong();
Path dest = base.resolve(rel.replace("/","\\"));
boolean need = true;
if (Files.exists(dest)) {
String h = sha256(dest);
if (h.equals(hash)) need=false;
}
if (!need) { idx++; continue; }
int pct = 15 + (int)((idx+1)*70.0/total);
setStatus(rel, pct);
Files.createDirectories(dest.getParent());
downloadFile(BASE_URL+"/launcher/file/"+meta.get("version").getAsString()+"/"+rel, dest, size);
idx++;
}
Files.writeString(base.resolve("build.version"), meta.get("version").getAsString());
} else {
// offline: try embedded resource, then appended ZIP, then adjacent file
setStatus("Распаковка встроенного архива...", 20);
InputStream in = Installer.class.getResourceAsStream("/offline.zip");
Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "offline-"+System.currentTimeMillis()+".zip");
boolean found = false;
if (in!=null) {
Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING);
found = true;
} else {
// try appended ZIP at end of self exe (for standalone offline)
try {
String self = Installer.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path selfPath = Paths.get(self);
if (Files.exists(selfPath)) {
long[] off = findAppendedZipOffset(selfPath);
if (off!=null) {
try (RandomAccessFile raf = new RandomAccessFile(selfPath.toFile(), "r")) {
raf.seek(off[0]);
try (OutputStream out = Files.newOutputStream(tmp)) {
byte[] buf = new byte[8192]; long rem = off[1];
while (rem>0) { int toRead = (int)Math.min(buf.length, rem); int r = raf.read(buf,0,toRead); if(r<=0) break; out.write(buf,0,r); rem-=r; }
}
found = true;
}
}
}
} catch(Exception ignored){}
if (!found) {
// try adjacent ZernMC-win-*.zip next to exe
try {
String self = Installer.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path exeDir = Paths.get(self).getParent();
if (exeDir!=null) {
try (var stream = Files.list(exeDir)) {
for (Path cand : stream.toList()) {
String n = cand.getFileName().toString();
if (n.startsWith("ZernMC-win-") && n.endsWith(".zip") || n.startsWith("ZernMC-Offline") && n.endsWith(".zip")) {
Files.copy(cand, tmp, StandardCopyOption.REPLACE_EXISTING);
found = true; break;
}
}
}
if (!found) {
// also check current dir
try (var s2 = Files.list(Paths.get("."))) {
for (Path cand : s2.toList()) {
String n = cand.getFileName().toString();
if (n.startsWith("ZernMC-win-") && n.endsWith(".zip")) { Files.copy(cand, tmp, StandardCopyOption.REPLACE_EXISTING); found=true; break; }
}
}
}
}
} catch(Exception ignored){}
}
}
if (!found) throw new IOException("offline.zip не найден. Положите ZernMC-win-1.1.1.4.zip рядом с установщиком или соберите standalone offline");
unzip(tmp, base);
Files.deleteIfExists(tmp);
}
setStatus("Создание ярлыка...", 90);
if (shortcut) createShortcut(dir);
createUninstaller(dir);
setStatus("Готово!",100);
}
private static long[] findAppendedZipOffset(Path exe) throws Exception {
try (java.io.RandomAccessFile raf = new java.io.RandomAccessFile(exe.toFile(), "r")) {
long len = raf.length();
long scan = Math.min(len, 1024*1024);
for (long pos = len - 22; pos >= len - scan; pos--) {
raf.seek(pos);
int b0 = raf.read(); int b1 = raf.read(); int b2 = raf.read(); int b3 = raf.read();
if (b0==0x50 && b1==0x4b && b2==0x05 && b3==0x06) {
for (long s = pos; s >= Math.max(0, pos - 110*1024*1024); s--) {
raf.seek(s);
int a0=raf.read(); int a1=raf.read(); int a2=raf.read(); int a3=raf.read();
if (a0==0x50 && a1==0x4b && a2==0x03 && a3==0x04) { return new long[]{s, len - s}; }
}
}
}
}
return null;
}
private static void setStatus(String s, int pct){ SwingUtilities.invokeLater(() -> { statusLabel.setText(s); progressBar.setValue(pct); titleLabel.setText(pct+"%"); }); }
private static JsonObject fetchMeta() throws Exception {
String ver = httpGet(BASE_URL+"/launcher/version");
JsonObject jo = JsonParser.parseString(ver).getAsJsonObject();
String v = jo.get("version").getAsString();
String metaStr = httpGet(BASE_URL+"/launcher/meta/"+v);
JsonObject meta = JsonParser.parseString(metaStr).getAsJsonObject();
if (!meta.has("version")) meta.addProperty("version", v);
return meta;
}
private static String httpGet(String url) throws Exception {
HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection();
c.setConnectTimeout(5000); c.setReadTimeout(10000);
try(BufferedReader br=new BufferedReader(new InputStreamReader(c.getInputStream()))){ StringBuilder sb=new StringBuilder(); String l; while((l=br.readLine())!=null) sb.append(l); return sb.toString(); }
}
private static void downloadFile(String url, Path dest, long expected) throws Exception {
HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection();
c.setConnectTimeout(8000); c.setReadTimeout(60000);
if (c.getResponseCode()!=200) throw new IOException("HTTP "+c.getResponseCode()+" "+url);
long total = expected>0?expected:c.getContentLengthLong();
try(InputStream in=c.getInputStream(); OutputStream out=Files.newOutputStream(dest)){ byte[] buf=new byte[65536]; long done=0; long start=System.currentTimeMillis(); int n; while((n=in.read(buf))>0){ out.write(buf,0,n); done+=n; if(System.currentTimeMillis()-start>200){ double pct= total>0? done*100.0/total:0; speedLabel.setText(String.format("%.1f MB / %.1f MB", done/1024.0/1024, total/1024.0/1024)); } } }
}
private static void unzip(Path zip, Path dest) throws Exception {
try(ZipInputStream zis=new ZipInputStream(Files.newInputStream(zip))){ ZipEntry e; while((e=zis.getNextEntry())!=null){ Path p=dest.resolve(e.getName()); if (!p.normalize().startsWith(dest.normalize())) throw new IOException("ZipSlip"); if(e.isDirectory()) Files.createDirectories(p); else { Files.createDirectories(p.getParent()); try(OutputStream o=Files.newOutputStream(p)){ zis.transferTo(o); }} zis.closeEntry(); } }
}
private static String sha256(Path p) throws Exception { MessageDigest d=MessageDigest.getInstance("SHA-256"); try(InputStream in=Files.newInputStream(p)){ byte[] b=new byte[8192]; int n; while((n=in.read(b))>0) d.update(b,0,n);} StringBuilder sb=new StringBuilder(); for(byte x:d.digest()) sb.append(String.format("%02x",x)); return sb.toString(); }
private static void deleteRecursively(Path p) throws Exception { if(Files.isDirectory(p)) try(var s=Files.list(p)){ for(Path c: s.toList()) deleteRecursively(c);} Files.deleteIfExists(p);}
private static void createShortcut(String dir) throws Exception {
String desktop = System.getenv("USERPROFILE")+"\\Desktop";
if(!Files.exists(Paths.get(desktop))) desktop = System.getenv("USERPROFILE")+"\\OneDrive\\Desktop";
String lnk = desktop+"\\ZernMC Launcher.lnk";
String target = dir+"\\zernmc.exe";
String ps = "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('"+lnk+"'); $Shortcut.TargetPath = '"+target+"'; $Shortcut.WorkingDirectory = '"+dir+"'; $Shortcut.IconLocation = '"+target+"'; $Shortcut.Save()";
Path tmp = Paths.get(System.getProperty("java.io.tmpdir"),"sc.ps1");
Files.writeString(tmp, ps);
new ProcessBuilder("powershell","-ExecutionPolicy","Bypass","-File",tmp.toString()).start().waitFor();
Files.deleteIfExists(tmp);
}
private static void createUninstaller(String dir) throws Exception {
Path exe = Paths.get(dir,"uninstall.exe");
// copy self as uninstall.exe
String self = Installer.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path selfPath = Paths.get(new URI("file://"+self));
if(Files.exists(selfPath) && !selfPath.equals(exe)) try{ Files.copy(selfPath, exe, StandardCopyOption.REPLACE_EXISTING);}catch(Exception ignored){}
Files.writeString(Paths.get(dir,"uninstall.bat"), "@echo off\ndel \"%USERPROFILE%\\Desktop\\ZernMC Launcher.lnk\"\n echo data in %USERPROFILE%\\.zernmc kept\n");
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
<properties> <properties>
<revision>1.1.1</revision> <revision>1.1.1</revision>
<hotfix>3</hotfix> <hotfix>4</hotfix>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+9 -3
View File
@@ -1835,7 +1835,9 @@ async def download_online_setup(request: Request):
"""Download online Go installer (5.8M, STANDALONE EXE without bundled JRE)""" """Download online Go installer (5.8M, STANDALONE EXE without bundled JRE)"""
p = _find_latest_setup("ZernMC-Online-Setup-*.exe") p = _find_latest_setup("ZernMC-Online-Setup-*.exe")
if p and p.exists(): if p and p.exists():
return await send_file_async(p, request, content_type="application/vnd.microsoft.portable-executable", cache=True) resp = await send_file_async(p, request, content_type="application/vnd.microsoft.portable-executable", cache=True)
resp.headers["Content-Disposition"] = f'attachment; filename="{p.name}"'
return resp
raise HTTPException(404, "Online installer not found") raise HTTPException(404, "Online installer not found")
@app.get("/launcher/download/offline-setup") @app.get("/launcher/download/offline-setup")
@@ -1843,11 +1845,15 @@ async def download_offline_setup(request: Request):
"""Download offline Go installer (STANDALONE EXE with embedded build, ~100M)""" """Download offline Go installer (STANDALONE EXE with embedded build, ~100M)"""
p = _find_latest_setup("ZernMC-Offline-Setup-*.exe") p = _find_latest_setup("ZernMC-Offline-Setup-*.exe")
if p and p.exists(): if p and p.exists():
return await send_file_async(p, request, content_type="application/vnd.microsoft.portable-executable", cache=True) resp = await send_file_async(p, request, content_type="application/vnd.microsoft.portable-executable", cache=True)
resp.headers["Content-Disposition"] = f'attachment; filename="{p.name}"'
return resp
# fallback: try legacy offline zip embed name without -Setup # fallback: try legacy offline zip embed name without -Setup
p2 = _find_latest_setup("ZernMC-Offline-*.exe") p2 = _find_latest_setup("ZernMC-Offline-*.exe")
if p2 and p2.exists(): if p2 and p2.exists():
return await send_file_async(p2, request, content_type="application/vnd.microsoft.portable-executable", cache=True) resp = await send_file_async(p2, request, content_type="application/vnd.microsoft.portable-executable", cache=True)
resp.headers["Content-Disposition"] = f'attachment; filename="{p2.name}"'
return resp
raise HTTPException(404, "Offline installer not found") raise HTTPException(404, "Offline installer not found")
@app.get("/launcher/download/zip/{filename}") @app.get("/launcher/download/zip/{filename}")