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:
@@ -112,6 +112,12 @@
|
||||
<include>ui/**</include>
|
||||
</includes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<includes>
|
||||
<include>offline.zip</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
@@ -257,6 +263,39 @@
|
||||
</versionInfo>
|
||||
</configuration>
|
||||
</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>
|
||||
</plugin>
|
||||
|
||||
|
||||
+469
@@ -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М лаунчер | <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
@@ -20,7 +20,7 @@
|
||||
|
||||
<properties>
|
||||
<revision>1.1.1</revision>
|
||||
<hotfix>3</hotfix>
|
||||
<hotfix>4</hotfix>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
+9
-3
@@ -1835,7 +1835,9 @@ async def download_online_setup(request: Request):
|
||||
"""Download online Go installer (5.8M, STANDALONE EXE without bundled JRE)"""
|
||||
p = _find_latest_setup("ZernMC-Online-Setup-*.exe")
|
||||
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")
|
||||
|
||||
@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)"""
|
||||
p = _find_latest_setup("ZernMC-Offline-Setup-*.exe")
|
||||
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
|
||||
p2 = _find_latest_setup("ZernMC-Offline-*.exe")
|
||||
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")
|
||||
|
||||
@app.get("/launcher/download/zip/{filename}")
|
||||
|
||||
Reference in New Issue
Block a user