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:
SashegDev
2026-09-02 00:57:43 +00:00
parent 48f73ef0d4
commit 3006408447
3 changed files with 60 additions and 469 deletions
@@ -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М лаунчер &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");
}
}
+26
View File
@@ -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:
+34
View File
@@ -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>