fix: автодетект --cli в Bootstrap, отображение локальных сборок в JFX, обработка ошибок установки и друзей

This commit is contained in:
SashegDev
2026-06-30 13:44:26 +00:00
parent 0a2b80ed06
commit e49e630afe
10 changed files with 569 additions and 50 deletions
@@ -27,7 +27,7 @@ import java.util.jar.Manifest;
public class Bootstrap {
private static final String JAR_NAME = "zernmclauncher.jar";
private static final String BASE_URL = "http://87.120.187.36:1582";
private static final String BASE_URL = "https://api.zernmc.ru";
private static List<String> MIRRORS = new ArrayList<>();
private static volatile boolean jfxChildExiting = false;
@@ -53,9 +53,12 @@ public class Bootstrap {
log("=== ZernMC Launcher ===");
List<String> argList = Arrays.asList(args);
isCliMode = argList.contains("--cli");
List<String> argList = new ArrayList<>(Arrays.asList(args));
isCliMode = argList.contains("--cli") || (System.console() != null && !argList.contains("--jfx"));
isJfxMode = !isCliMode;
if (isCliMode && !argList.contains("--cli")) {
argList.add("--cli");
}
log("Mode: " + (isCliMode ? "CLI" : "JFX"));
@@ -94,7 +97,7 @@ public class Bootstrap {
ui.close();
}
launchMain(args);
launchMain(argList.toArray(new String[0]));
}
private static void launchMain(String[] args) throws Exception {
@@ -14,7 +14,7 @@ import java.util.List;
public class Bootstrap {
private static final String VERSION_FILE = "build.version";
private static final String JAR_NAME = "ZernMCLauncher.jar";
private static final String BASE_URL = "http://87.120.187.36:1582";
private static final String BASE_URL = "https://api.zernmc.ru";
private static Path baseDir;
private static Path logDir;
+16 -12
View File
@@ -78,18 +78,22 @@
</div>
<nav class="sidebar-nav">
<button class="nav-btn active" data-view="packs" title="Packs">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<span data-i18n="nav.packs">Packs</span>
</button>
<button class="nav-btn" data-view="news" title="News">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9h4"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/></svg>
<span data-i18n="nav.news">News</span>
</button>
<button class="nav-btn" data-view="friends" title="Friends">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span data-i18n="nav.friends">Friends</span>
</button>
<div class="sidebar-nav-left">
<button class="nav-btn active" data-view="packs" title="Packs">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<span data-i18n="nav.packs">Packs</span>
</button>
<button class="nav-btn" data-view="news" title="News">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9h4"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/></svg>
<span data-i18n="nav.news">News</span>
</button>
</div>
<div class="sidebar-nav-right">
<button class="nav-btn" data-view="friends" title="Friends">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span data-i18n="nav.friends">Friends</span>
</button>
</div>
</nav>
<div class="sidebar-section">
+149 -23
View File
@@ -78,6 +78,7 @@ const LOCALES = {
'toast.launched': 'Launched! PID: {pid}',
'toast.installing': 'Installing...',
'toast.installComplete': 'Installation complete!',
'toast.installFailed': 'Installation failed',
'toast.enterPassCode': 'Enter a pass code',
'toast.passActivated': 'Pass activated!',
'toast.activationFailed': 'Activation failed',
@@ -86,6 +87,122 @@ const LOCALES = {
'pack.worldsCount': '{count} worlds',
'pack.logsCount': '{count} logs',
'locale.en': 'English', 'locale.ru': 'Russian',
'ram.gb': '{value} GB',
'ram.mbAllocated': '{value} MB allocated',
'nav.friends': 'Friends',
'friends.title': 'Friends',
'friends.add': 'Add',
'friends.addTitle': 'Add Friend',
'friends.addLabel': 'Username',
'friends.addPlaceholder': 'Enter username...',
'friends.search': 'Search friends...',
'friends.empty': 'No friends yet',
'friends.online': 'Online',
'friends.offline': 'Offline',
'friends.playing': 'Playing {pack}',
'friends.noPack': 'Idle',
'friends.requests': 'Friend Requests',
'friends.requestFrom': '{user} wants to add you as friend',
'friends.accept': 'Accept',
'friends.decline': 'Decline',
'friends.remove': 'Remove',
'friends.addSuccess': 'Request sent!',
'friends.addError': 'Failed to add friend',
'friends.removeSuccess': 'Friend removed',
'friends.acceptSuccess': 'Request accepted',
'friends.passActive': 'Pass Active',
'friends.passInactive': 'No Pass',
'playtime.label': 'Playtime',
'playtime.hours': '{h}h {m}m',
'playtime.total': 'Total: {h}h',
'playtime.syncing': 'Syncing...',
},
ru: {
'nav.packs': 'Сборки', 'nav.news': 'Новости', 'nav.settings': 'Настройки',
'login.title': 'Вход', 'login.username': 'Логин', 'login.password': 'Пароль',
'login.hint': 'Новый аккаунт создаётся автоматически при первом входе',
'login.signingIn': 'Вход...',
'loading.text': 'Загрузка...',
'sidebar.serverPacks': 'Серверные сборки', 'sidebar.localPacks': 'Локальные сборки',
'pack.empty.title': 'Выберите сборку',
'pack.empty.desc': 'Выберите сборку в боковой панели',
'pack.description.loading': 'Загрузка описания...',
'pack.noDescription': 'Описание отсутствует',
'stat.loaderVer': 'Версия загрузчика', 'stat.files': 'Файлы', 'stat.size': 'Размер',
'playBar.play': 'Играть', 'playBar.passRequired': 'Требуется проходка',
'settings.title': 'Настройки',
'settings.activatePass.title': 'Активировать проходку',
'settings.activatePass.desc': 'Введите код проходки для доступа к серверным сборкам',
'settings.activatePass.placeholder': 'Код проходки',
'settings.activatePass.button': 'Активировать',
'settings.ram.title': 'Выделенная память',
'settings.resolution.title': 'Разрешение экрана',
'settings.resolution.desc': 'Ширина x Высота',
'settings.jvmArgs.title': 'Дополнительные аргументы JVM',
'settings.jvmArgs.desc': 'Дополнительные параметры Java VM',
'settings.javaPath.title': 'Путь к Java',
'settings.server.title': 'Сервер',
'settings.server.checking': 'Проверка...',
'settings.server.connected': 'Подключено',
'settings.server.disconnected': 'Отключено',
'settings.language.title': 'Язык',
'settings.language.desc': 'Язык интерфейса',
'settings.systemJvm.title': 'Системная оптимизация JVM',
'settings.systemJvm.desc': 'Автонастройка флагов JVM под вашу систему (экспериментально)',
'settings.systemJvm.info': 'ЦП: {cores} ядер | ОЗУ: {ram} ГБ | {flags}',
'settings.systemJvm.enabled': 'Включено', 'settings.systemJvm.disabled': 'Выключено',
'settings.logViewer.title': 'Лог игры',
'settings.logViewer.desc': 'Просмотр логов игры в реальном времени',
'settings.logViewer.open': 'Открыть лог',
'logViewer.title': 'Лог игры',
'logViewer.noLogs': 'Логов пока нет. Запустите игру, чтобы увидеть логи.',
'logViewer.copy': 'Копировать', 'logViewer.copied': 'Скопировано!',
'logViewer.openFile': 'Открыть файл', 'logViewer.close': 'Закрыть',
'logViewer.waiting': 'Ожидание вывода игры...',
'install.title': 'Установить сборку',
'install.tab.serverPack': 'Серверная сборка', 'install.tab.custom': 'Своя',
'install.serverPack.label': 'Серверная сборка',
'install.localName.label': 'Локальное имя',
'install.downloadBtn': 'Скачать и установить',
'install.custom.unavailable': 'Пока недоступно',
'install.custom.desc': 'Установка своей сборки отключена в этой версии. Используйте вкладку "Серверная сборка".',
'install.progress.installing': 'Установка...',
'install.enterName': 'Введите имя',
'install.step': 'Шаг {current} из {total}: {name}',
'select.loading': 'Загрузка...',
'select.selectPack': 'Выберите сборку',
'select.passRequired': 'Требуется проходка',
'select.noPacks': 'Нет доступных сборок',
'select.selectVersion': 'Выберите версию',
'select.failedLoad': 'Ошибка загрузки',
'select.selectMCFirst': 'Сначала выберите версию',
'select.placeholder': 'Выберите...',
'select.search': 'Поиск...',
'packList.empty': 'Нет сборок',
'news.title': 'Новости', 'news.empty': 'Новостей пока нет', 'news.loading': 'Загрузка новостей...',
'pack.update': 'Обновить', 'pack.delete': 'Удалить',
'badge.free': 'БЕСПЛАТНО', 'badge.pro': 'ПРО',
'toast.welcomeBack': 'С возвращением, {username}!',
'toast.welcome': 'Добро пожаловать, {username}!',
'toast.accountCreated': 'Аккаунт создан! Добро пожаловать, {username}!',
'toast.loggedOut': 'Вы вышли',
'toast.enterCredentials': 'Введите логин и пароль',
'toast.loginFailed': 'Ошибка входа',
'toast.launching': 'Запуск {name}...',
'toast.launchFailed': 'Ошибка запуска',
'toast.noPackSelected': 'Сначала выберите сборку',
'toast.launched': 'Запущено! PID: {pid}',
'toast.installing': 'Установка...',
'toast.installComplete': 'Установка завершена!',
'toast.installFailed': 'Ошибка установки',
'toast.enterPassCode': 'Введите код проходки',
'toast.passActivated': 'Проходка активирована!',
'toast.activationFailed': 'Ошибка активации',
'error.network': 'Ошибка сети',
'pack.modsCount': '{count} модов',
'pack.worldsCount': '{count} миров',
'pack.logsCount': '{count} логов',
'locale.en': 'Английский', 'locale.ru': 'Русский',
'ram.gb': '{value} ГБ',
'ram.mbAllocated': '{value} МБ выделено',
'nav.friends': 'Друзья',
@@ -225,10 +342,7 @@ class ZernMCLauncher {
return await r.json();
} catch (fetchErr) {
clearTimeout(timeoutId);
if (fetchErr.name === 'AbortError') {
return { success: false, error: 'Request timeout' };
}
throw fetchErr;
return { success: false, error: fetchErr.message || 'Request failed' };
}
}
return await new Promise(function(resolve, reject) {
@@ -450,19 +564,26 @@ class ZernMCLauncher {
renderSidebar() {
const serverList = document.getElementById('server-packs-list');
const localList = document.getElementById('local-packs-list');
const localSection = document.getElementById('local-packs-section');
serverList.innerHTML = '';
localList.innerHTML = '';
if (localSection) localSection.classList.add('hidden');
if (!this.state.instances || this.state.instances.length === 0) return;
this.state.instances.forEach(inst => {
const isZern = inst.isServerPack || inst.category === 'zernmc';
if (!isZern) return;
const targetList = isZern ? serverList : localList;
const el = document.createElement('div');
el.className = 'pack-entry' + (this.state.selectedPack && this.state.selectedPack.name === inst.name ? ' selected' : '');
el.innerHTML = `
<div class="pack-entry-icon server">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/></svg>
<div class="pack-entry-icon ${isZern ? 'server' : 'local'}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${isZern
? '<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"/>'
: '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>'
}</svg>
</div>
<div class="pack-entry-info">
<div class="pack-entry-name">${this.esc(inst.name)}</div>
@@ -470,8 +591,12 @@ class ZernMCLauncher {
</div>
`;
el.addEventListener('click', () => this.selectPack(inst));
serverList.appendChild(el);
targetList.appendChild(el);
});
if (localSection && localList.children.length > 0) {
localSection.classList.remove('hidden');
}
}
selectPack(inst) {
@@ -984,8 +1109,9 @@ class ZernMCLauncher {
if (!r.data.inProgress) {
this.stopProgressPoll();
this.hideInstallModal();
this.toast(t('toast.installComplete'), 'success');
await this.loadInstances();
var failed = r.data.stageName === 'Failed' || r.data.stageName === 'Error' || (r.data.label && r.data.label.startsWith('Error'));
this.toast(failed ? (r.data.label || t('toast.installFailed')) : t('toast.installComplete'), failed ? 'error' : 'success');
if (!failed) await this.loadInstances();
}
}
}, 500);
@@ -1173,13 +1299,13 @@ class ZernMCLauncher {
btn.disabled = true;
try {
var r = await this.req('/friends/add', { method: 'POST', body: JSON.stringify({ username: username }) });
if (r.success !== false || r.message) {
if (r.detail) {
errEl.textContent = r.detail;
errEl.classList.remove('hidden');
} else {
this.toast(r.message || t('friends.addSuccess'), 'success');
this.closeAddFriend();
this.loadFriends();
} else {
errEl.textContent = r.detail || r.error || t('friends.addError');
errEl.classList.remove('hidden');
}
} catch (e) {
errEl.textContent = t('friends.addError');
@@ -1194,31 +1320,31 @@ class ZernMCLauncher {
async acceptFriendRequest(userId) {
var r = await this.req('/friends/accept', { method: 'POST', body: JSON.stringify({ user_id: userId }) });
if (r.success !== false || r.message) {
if (r.detail) {
this.toast(r.detail, 'error');
} else {
this.toast(r.message || t('friends.acceptSuccess'), 'success');
this.loadFriends();
} else {
this.toast(r.detail || r.error, 'error');
}
}
async declineFriendRequest(userId) {
var r = await this.req('/friends/remove', { method: 'POST', body: JSON.stringify({ user_id: userId }) });
if (r.success !== false || r.message) {
if (r.detail) {
this.toast(r.detail, 'error');
} else {
this.toast(r.message || 'Declined', 'info');
this.loadFriends();
} else {
this.toast(r.detail || r.error, 'error');
}
}
async removeFriend(userId) {
var r = await this.req('/friends/remove', { method: 'POST', body: JSON.stringify({ user_id: userId }) });
if (r.success !== false || r.message) {
if (r.detail) {
this.toast(r.detail, 'error');
} else {
this.toast(r.message || t('friends.removeSuccess'), 'success');
this.loadFriends();
} else {
this.toast(r.detail || r.error, 'error');
}
}
+5 -1
View File
@@ -185,9 +185,12 @@ body {
.sidebar-brand-ver { font-size: 11px; color: var(--text-muted); }
.sidebar-nav {
display: flex; gap: 4px;
display: flex; justify-content: space-between; gap: 4px;
padding-bottom: 16px; border-bottom: 1px solid var(--border);
}
.sidebar-nav-left, .sidebar-nav-right {
display: flex; gap: 4px;
}
.nav-btn {
flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px; background: transparent; border: 1px solid transparent;
@@ -670,6 +673,7 @@ body {
.sidebar-bottom .user-card .btn-icon:first-child { display: none; }
.sidebar-brand { justify-content: center; padding: 8px; }
.sidebar-nav { flex-direction: column; }
.sidebar-nav-left, .sidebar-nav-right { flex-direction: column; }
.nav-btn { padding: 8px; }
.pack-entry { justify-content: center; padding: 8px; }
.content { padding: 12px; }