site+server: add manual Zern Prologue pack variant (download zip + instructions) — site card with path %USERPROFILE%\.zernmc\instances\ZernPrologue, prism/multimc import, endpoint /pack/ZernPrologue/zip (requires pass)
This commit is contained in:
-469
@@ -1,469 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1364,6 +1364,32 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request, curren
|
||||
return await send_file_async(full_path, request, cache=True)
|
||||
|
||||
|
||||
@app.get("/pack/{pack_name}/zip")
|
||||
async def download_pack_zip(pack_name: str, request: Request, current_user: dict = Depends(get_current_user)):
|
||||
"""Download whole pack as zip for manual installation (requires pass)"""
|
||||
if not has_permission(current_user["role"], Permissions.DOWNLOAD_PACK):
|
||||
raise HTTPException(403, "Requires active pass")
|
||||
if is_pack_disabled(pack_name):
|
||||
raise HTTPException(403, "Pack is disabled")
|
||||
pack_path = PACKS_DIR / pack_name
|
||||
if not pack_path.exists() or not pack_path.is_dir():
|
||||
raise HTTPException(404, "Pack not found")
|
||||
import tempfile, zipfile
|
||||
tmp = Path(tempfile.gettempdir()) / f"{pack_name}.zip"
|
||||
# re-use if fresh (<5 min)
|
||||
if tmp.exists() and (datetime.utcnow().timestamp() - tmp.stat().st_mtime) < 300:
|
||||
return await send_file_async(tmp, request, content_type="application/zip", cache=False)
|
||||
# create zip
|
||||
def _zip():
|
||||
with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for f in pack_path.rglob("*"):
|
||||
if f.is_file():
|
||||
zf.write(f, f.relative_to(pack_path))
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, _zip)
|
||||
return await send_file_async(tmp, request, content_type="application/zip", cache=False)
|
||||
|
||||
|
||||
# ====================== ЭНДПОИНТЫ ДЛЯ ЛАУНЧЕРА ======================
|
||||
|
||||
def get_current_launcher_version() -> str:
|
||||
|
||||
@@ -438,6 +438,40 @@
|
||||
</div>
|
||||
<code style="font-size:12px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:8px 10px;color:var(--text-secondary);overflow:auto">git clone ssh://git@git.swe.zernmc.ru:2222/sasheg/launcher.git</code>
|
||||
</div>
|
||||
|
||||
<!-- Manual Zern Prologue pack -->
|
||||
<div class="manual-pack-card" style="margin-top:20px;background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:20px;display:flex;flex-direction:column;gap:12px">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<div style="width:48px;height:48px;border-radius:12px;background:rgba(233,69,96,0.12);display:flex;align-items:center;justify-content:center;color:var(--accent);flex-shrink:0">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="font-size:15px;font-weight:700">Zern Prologue — ручная установка</h4>
|
||||
<p style="font-size:12px;color:var(--text-secondary);margin-top:2px">1.21.1 NeoForge · Create: Aeronautics · без лаунчера</p>
|
||||
</div>
|
||||
<span style="margin-left:auto;font-size:11px;color:var(--accent);background:rgba(233,69,96,0.12);padding:4px 8px;border-radius:999px;white-space:nowrap">v4 · 1.8 ГБ</span>
|
||||
</div>
|
||||
<p style="font-size:13px;color:var(--text-secondary);line-height:1.5">Скачай архив модпака и распакуй вручную. Подходит если лаунчер не качает или хочешь поставить на Prism / MultiMC / ATLauncher.</p>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-family:var(--mono);font-size:12px;color:var(--text-secondary)">
|
||||
%USERPROFILE%\.zernmc\instances\ZernPrologue
|
||||
<span style="color:var(--text-muted)"> · ~/.zernmc/instances/ZernPrologue на Linux</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<a href="/pack/ZernPrologue/zip" class="btn btn-primary" style="flex:1;justify-content:center;min-width:180px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Скачать Prologue.zip
|
||||
</a>
|
||||
<button class="btn btn-ghost" onclick="navigator.clipboard.writeText('%USERPROFILE%\\.zernmc\\instances');var t=document.createElement('div');t.textContent='Скопировано';t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--bg-elevated);border:1px solid var(--border);padding:8px 16px;border-radius:8px;font-size:13px;z-index:9999';document.body.appendChild(t);setTimeout(()=>t.remove(),1500)" style="flex:0 0 auto">Копировать путь</button>
|
||||
</div>
|
||||
<details style="font-size:12px;color:var(--text-secondary)"><summary style="cursor:pointer;color:var(--text)">Инструкция</summary>
|
||||
<ol style="margin:8px 0 0 18px;display:flex;flex-direction:column;gap:4px">
|
||||
<li>Нажми «Скачать Prologue.zip» (требует проходку — войди в лаунчере хотя бы раз, иначе 403).</li>
|
||||
<li>Распаковать в папку выше (создай если нет). Если там уже есть Prologue — замени.</li>
|
||||
<li>Для Prism/MultiMC: Импорт → «Import from zip» или «Add instance → Import from folder» укажи распакованную папку, Java 21.</li>
|
||||
<li>Запусти через ZernMC лаунчер или свой — вход на <strong>mc.zernmc.ru</strong></li>
|
||||
</ol>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="download-note" data-i18n="download.note">Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.</p>
|
||||
|
||||
Reference in New Issue
Block a user