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>
|
<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>
|
||||||
|
|
||||||
|
|||||||
+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>
|
<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
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user