Files
launcher/server/site/js/main.js
T
SashegDev 1900db3caf fix: 1.1.1.3 online installer 5.8M + mods toggle + ZGC vs G1GC conflict
- server/main.py BUILDS_DIR absolute + /launcher/download/online,offine-setup
- site index/js online->/download/online 5.8M
- LaunchCommandBuilder skip G1GC when systemBasedJvm (ZGC conflict -> Bootstrap crash)
- JFXLauncher toggle robust rename + FileAlreadyExists fix
2026-09-01 13:51:30 +00:00

775 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
'use strict';
const API = ''; // same origin (root -> launcher server)
const I18N = {
ru: {
'nav.server': 'Сервер',
'nav.highlights': 'Хайлайты',
'nav.season': 'Сезон',
'nav.features': 'Возможности',
'nav.showcase': 'Лаунчер',
'nav.download': 'Скачать',
'nav.news': 'Новости',
'nav.play': 'Играть сейчас',
'section.of': 'Раздел {n} из {m}',
'hero.kicker': 'КАСТОМНЫЙ MINECRAFT ЛАУНЧЕР',
'hero.title': 'Очнись от<br><span class="hero-title-accent">холода.</span>',
'hero.sub': 'Zern оживляет твои любимые модпаки. Один лаунчер, любой загрузчик, без лишних хлопот — создан для сервера ZernMC и не только.',
'hero.download': 'Скачать лаунчер',
'hero.explore': 'Обзор',
'hero.scroll': 'Листай вниз',
'server.title': 'Сервер <span class="accent">жив.</span>',
'server.sub': 'ZernMC — это кастомный Minecraft-сервер со своим лором, самописными плагинами и живым комьюнити. Вот что происходит прямо сейчас.',
'server.status': 'Статус',
'server.checking': 'Проверяем…',
'server.players': 'Игроков онлайн',
'server.mode': 'Режим',
'server.season': 'Сезон Prologue',
'server.online': 'Онлайн',
'server.offline': 'Оффлайн',
'server.unavailable': 'Недоступно',
'highlights.title': 'Хайлайты <span class="accent">сервера.</span>',
'highlights.sub': 'Моменты, запечатлённые на сервере ZernMC — войны, постройки и хаос.',
'carousel.prev': 'Назад',
'carousel.next': 'Вперёд',
'carousel.cap1': 'Zern',
'carousel.cap3': 'Это честно не я',
'carousel.cap5': 'Спавн ТЦ (старое)',
'carousel.cap6': 'Спавн склад (старое)',
'carousel.alt': 'Хайлайт Zern',
'features.title': 'Всё, что <span class="accent">тебе нужно.</span>',
'features.sub': 'Лаунчер, который делает всю тяжёлую работу, чтобы ты просто играл.',
'features.forge.title': 'Forge, NeoForge, Fabric',
'features.forge.sub': 'Все основные модлоадеры подхватываются автоматически. Библиотеки предзагружаются и проверяются, так что установка работает — даже на медленном интернете.',
'features.ui.title': 'Тёмный, быстрый нативный UI',
'features.ui.sub': 'Лёгкий интерфейс на JavaFX с чистым тёмным оформлением. Быстро запускается, мгновенно реагирует и не мешает.',
'features.proxy.title': 'Умные зеркала и прокси',
'features.proxy.sub': 'Загрузки идут через быстрые зеркала с автоматическим фолбэком. Заблокировали или зарезали скорость? Прокси подхватит — ты больше не зависнешь на 80%.',
'features.news.title': 'Новости и статус сервера',
'features.news.sub': 'Чейнджлоги, ивенты и обновления приходят прямо в лаунчер. Ты всегда знаешь, что нового, ещё до нажатия «Играть».',
'features.pack.title': 'Установка сборки в один клик',
'features.pack.sub': 'Выбери сборку и жми «Установить». Манифесты версий, ванильные ассеты и библиотеки подтягиваются и проверяются автоматически. Просто.',
'theme.title': 'Что внутри <span class="accent">сезона.</span>',
'theme.sub': 'Prologue — модовый сезон на базе Create: Aeronautics, война и выживание. ОБТ — полигон, где всё это обкатывается.',
'theme.zarya.tag': 'ОБТ · 1.20.1 Forge',
'theme.zarya.title': 'Война в Заре',
'theme.zarya.sub': 'Двухдневный ивент в городе Заря: группировка «Феникс» атакует, «Ласточка» обороняется, а жители выбирают сторону — или поднимают бунт. Военные правила.',
'theme.obt.tag': 'ОБТ · тестовая стадия',
'theme.obt.title': 'Открытый бета-сервер',
'theme.obt.sub': 'Бета — это ивентовый сервер: городские бои, сценарий атомного реактора и правила, которые лягут в основу настоящего сезона.',
'theme.air.tag': 'Сезон · 1.21.1 NeoForge',
'theme.air.title': 'Корабли в небе',
'theme.air.sub': 'Create: Aeronautics даёт дирижабли, субмарины и летающие механизмы. Поезда, дизели, большие пушки и атомный реактор — осваивай.',
'theme.year.tag': 'Сезон · Prologue',
'theme.year.title': 'Марафон на год',
'theme.year.sub': 'Сезон — это марафон, а не спринт. Начни эту главу с первого дня и строй её вместе с комьюнити.',
'theme.lore.tag': 'Сервер',
'theme.lore.title': 'Свой лор и плагины',
'theme.lore.sub': 'Самописные плагины, ивенты под историю сервера и системы, которые из коробки работают с лаунчером.',
'theme.community.tag': 'Комьюнити',
'theme.community.title': 'Голос и друзья',
'theme.community.sub': 'Simple Voice Chat в игре, система друзей в лаунчере и Telegram-комьюнити, которое реально рулит — решения принимаются голосованием игроков.',
'download.title': 'Скачать <span class="accent">Zern.</span>',
'download.sub': 'Скачай лаунчер для Windows и заходи на сервер ZernMC.',
'download.latest': 'Скачать актуальную версию',
'download.mirrors': 'Зеркала',
'download.note': 'Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.',
'showcase.title': 'Показываем <span class="accent">лаунчер</span>',
'showcase.sub': 'Никаких статичных скриншотов — живой тур по интерфейсу. Нужно просто наблюдать: лаунчер сам открывает свои экраны.',
'preview.aria': 'ZernMC Launcher preview, auto-rotating screens',
'preview.login': 'Вход',
'preview.packs': 'Сборки',
'preview.news': 'Новости',
'preview.settings': 'Настройки',
'preview.install': 'Установка',
'preview.signIn': 'Вход',
'preview.username': 'Логин',
'preview.password': 'Пароль',
'preview.nav.packs': 'Сборки',
'preview.nav.news': 'Новости',
'preview.nav.friends': 'Друзья',
'preview.nav.settings': 'Настройки',
'preview.serverPacks': 'Серверные сборки',
'preview.localPacks': 'Локальные сборки',
'preview.stat.loader': 'Загрузчик',
'preview.stat.files': 'Файлы',
'preview.stat.size': 'Размер',
'preview.ready': 'Готов к запуску',
'preview.play': 'Играть',
'preview.news.update': 'Обновление',
'preview.news.event': 'Событие',
'preview.news1.title': 'Сезон Prologue запущен',
'preview.news1.body': 'Небо принадлежит дирижаблям. Добро пожаловать в Prologue — 1.21.1 NeoForge, Create: Aeronautics.',
'preview.news2.title': 'Война в Заре',
'preview.news2.body': '«Феникс» атакует, «Ласточка» защищает. Жители выбирают сторону — или бунт.',
'preview.settings.ram': 'Выделено ОЗУ',
'preview.settings.resolution': 'Разрешение',
'preview.settings.language': 'Язык',
'preview.settings.server': 'Сервер',
'preview.online': 'Онлайн',
'preview.install.title': 'Установка сборки…',
'preview.install.stage': 'Загрузка модов 130/130 · 1.8 ГБ',
'news.title': 'Что <span class="accent">нового.</span>',
'news.sub': 'Последние чейнджлоги и обновления команды Zern.',
'news.empty': 'Новостей пока нет.',
'news.error': 'Новости временно недоступны.',
'news.update': 'Обновление',
'footer.note': 'Не аффилирован с Mojang или Microsoft. Minecraft — торговая марка Mojang AB.'
},
en: {
'nav.server': 'Server',
'nav.highlights': 'Highlights',
'nav.season': 'Season',
'nav.features': 'Features',
'nav.showcase': 'Launcher',
'nav.download': 'Download',
'nav.news': 'News',
'nav.play': 'Play now',
'section.of': 'Section {n} of {m}',
'hero.kicker': 'CUSTOM MINECRAFT LAUNCHER',
'hero.title': 'Awaken from<br><span class="hero-title-accent">the cold.</span>',
'hero.sub': 'Zern brings your favourite modpacks to life. One launcher, every loader, no hassle — built for the ZernMC server and beyond.',
'hero.download': 'Download launcher',
'hero.explore': 'Explore',
'hero.scroll': 'Scroll to explore',
'server.title': 'The server is <span class="accent">alive.</span>',
'server.sub': 'ZernMC is a custom Minecraft server with its own lore, hand-written plugins and a living community. This is what\'s happening right now.',
'server.status': 'Status',
'server.checking': 'Checking…',
'server.players': 'Players online',
'server.mode': 'Mode',
'server.season': 'Season Prologue',
'server.online': 'Online',
'server.offline': 'Offline',
'server.unavailable': 'Unavailable',
'highlights.title': 'Server <span class="accent">highlights.</span>',
'highlights.sub': 'Moments captured on the ZernMC server — wars, builds and chaos.',
'carousel.prev': 'Previous',
'carousel.next': 'Next',
'carousel.cap1': 'Zern',
'carousel.cap3': 'This is honestly not me',
'carousel.cap5': 'Spawn mall (old)',
'carousel.cap6': 'Spawn storage (old)',
'carousel.alt': 'Zern highlight',
'features.title': 'Everything <span class="accent">you need.</span>',
'features.sub': 'A launcher that does the heavy lifting, so you can just play.',
'features.forge.title': 'Forge, NeoForge, Fabric',
'features.forge.sub': 'Every major mod loader handled automatically. Libraries are prefetched and verified, so installs just work — even on slow networks.',
'features.ui.title': 'Dark, fast, native UI',
'features.ui.sub': 'A lightweight JavaFX interface with a clean dark theme. Starts quick, responds instantly, and stays out of your way.',
'features.proxy.title': 'Smart mirrors & proxy',
'features.proxy.sub': 'Downloads route through fast mirrors with automatic failover. Blocked or throttled? The proxy takes over so you never stall at 80%.',
'features.news.title': 'News & server status',
'features.news.sub': 'Changelogs, events and updates stream straight into the launcher. You always know what\'s new before you hit play.',
'features.pack.title': 'One-click pack install',
'features.pack.sub': 'Pick a pack, hit install. Version manifests, vanilla assets and libraries are pulled and verified automatically. Simple.',
'theme.title': 'What\'s inside <span class="accent">the season.</span>',
'theme.sub': 'Prologue — a modded season built around Create: Aeronautics, war and survival. OBT is the testing ground for the whole idea.',
'theme.zarya.tag': 'OBT · 1.20.1 Forge',
'theme.zarya.title': 'War in Zarya',
'theme.zarya.sub': 'Two-day event set in the city of Zarya: the Fenix group attacks, Lastochka defends, and civilians pick a side — or rise up. Military rules apply.',
'theme.obt.tag': 'OBT · test stage',
'theme.obt.title': 'Open beta server',
'theme.obt.sub': 'The beta is an event server: city battles, an atomic-reactor city-life scenario and rule sets that will shape the real season.',
'theme.air.tag': 'Season · 1.21.1 NeoForge',
'theme.air.title': 'Ships in the sky',
'theme.air.sub': 'Create: Aeronautics gives you airships, submarines and flying contraptions. Trains, diesel engines, big cannons and a nuclear reactor to master.',
'theme.year.tag': 'Season · Prologue',
'theme.year.title': 'A year-long run',
'theme.year.sub': 'Each season is a marathon, not a sprint. Start this chapter from day one and shape it with the community.',
'theme.lore.tag': 'Server',
'theme.lore.title': 'Own lore & plugins',
'theme.lore.sub': 'Hand-written plugins, events written for the server story, and systems the launcher integrates with out of the box.',
'theme.community.tag': 'Community',
'theme.community.title': 'Voice & friends',
'theme.community.sub': 'Simple Voice Chat in-game, a friends system in the launcher and a Telegram community that runs the show — decisions are voted on by players.',
'download.title': 'Get <span class="accent">Zern.</span>',
'download.sub': 'Download the launcher for Windows and join the ZernMC server.',
'download.latest': 'Download latest',
'download.mirrors': 'Mirrors',
'download.note': 'No paywalls, no ads. Just a launcher. System requirements: Windows 10+, 4 GB RAM.',
'showcase.title': 'Meet the <span class="accent">launcher</span>',
'showcase.sub': 'No static screenshots — a live tour of the UI. Just watch: the launcher shows its screens on its own.',
'preview.aria': 'ZernMC Launcher preview, auto-rotating screens',
'preview.login': 'Sign in',
'preview.packs': 'Packs',
'preview.news': 'News',
'preview.settings': 'Settings',
'preview.install': 'Installing',
'preview.signIn': 'Sign In',
'preview.username': 'Username',
'preview.password': 'Password',
'preview.nav.packs': 'Packs',
'preview.nav.news': 'News',
'preview.nav.friends': 'Friends',
'preview.nav.settings': 'Settings',
'preview.serverPacks': 'Server Packs',
'preview.localPacks': 'Local Packs',
'preview.stat.loader': 'Loader',
'preview.stat.files': 'Files',
'preview.stat.size': 'Size',
'preview.ready': 'Ready to launch',
'preview.play': 'Play',
'preview.news.update': 'Update',
'preview.news.event': 'Event',
'preview.news1.title': 'Season Prologue has launched',
'preview.news1.body': 'The sky belongs to airships. Welcome to Prologue — 1.21.1 NeoForge, Create: Aeronautics.',
'preview.news2.title': 'War in Zarya',
'preview.news2.body': 'Fenix attacks, Lastochka defends. Citizens pick a side — or rise up.',
'preview.settings.ram': 'Allocated RAM',
'preview.settings.resolution': 'Resolution',
'preview.settings.language': 'Language',
'preview.settings.server': 'Server',
'preview.online': 'Online',
'preview.install.title': 'Installing pack…',
'preview.install.stage': 'Downloading mods 130/130 · 1.8 GB',
'news.title': 'What\'s <span class="accent">new.</span>',
'news.sub': 'Latest changelogs and updates from the Zern team.',
'news.empty': 'No news yet.',
'news.error': 'News is temporarily unavailable.',
'news.update': 'Update',
'footer.note': 'Not affiliated with Mojang or Microsoft. Minecraft is a trademark of Mojang AB.'
}
};
const STORAGE_KEY = 'zern_lang';
function getLang() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === 'ru' || saved === 'en') return saved;
} catch (e) { /* ignore */ }
return 'ru'; // default: Russian
}
let currentLang = getLang();
function tr(key) {
const dict = I18N[currentLang] || I18N.ru;
return dict[key] != null ? dict[key] : key;
}
function applyI18n() {
document.documentElement.lang = currentLang;
const toggle = document.getElementById('langToggleLabel');
if (toggle) toggle.textContent = currentLang === 'ru' ? 'RU' : 'EN';
// Section indices: add a tooltip explaining what "02 / 07" means.
document.querySelectorAll('.section-index').forEach((el) => {
const m = (el.textContent || '').trim().match(/^(\d+)\s*\/\s*(\d+)$/);
if (m) el.setAttribute('title', tr('section.of').replace('{n}', m[1]).replace('{m}', m[2]));
});
document.querySelectorAll('[data-i18n]').forEach((el) => {
el.textContent = tr(el.dataset.i18n);
});
document.querySelectorAll('[data-i18n-html]').forEach((el) => {
el.innerHTML = tr(el.dataset.i18nHtml);
});
document.querySelectorAll('[data-i18n-aria]').forEach((el) => {
el.setAttribute('aria-label', tr(el.dataset.i18nAria));
});
// Carousel captions & alts are created dynamically with tr() at init
document.querySelectorAll('.carousel-caption[data-caption-key]').forEach((el) => {
el.textContent = tr(el.dataset.captionKey);
});
document.querySelectorAll('.carousel-slide img').forEach((img) => {
const slide = img.closest('.carousel-slide');
const cap = slide && slide.querySelector('.carousel-caption[data-caption-key]');
if (cap) img.alt = tr(cap.dataset.captionKey);
else img.alt = tr('carousel.alt');
});
// Live-update dynamic strings
const statusValue = document.getElementById('srv-status');
if (statusValue && statusValue.dataset.state) {
updateStatusUI(statusValue.dataset.state);
}
const caption = document.getElementById('previewCaption');
if (caption && caption.dataset.screen) {
const scene = SCENES.find((s) => s.key === caption.dataset.screen);
caption.textContent = scene ? tr(scene.label) : '';
}
}
function toggleLang() {
currentLang = currentLang === 'ru' ? 'en' : 'ru';
try { localStorage.setItem(STORAGE_KEY, currentLang); } catch (e) { /* ignore */ }
applyI18n();
}
function fmtSize(bytes) {
if (!bytes) return '—';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
let n = bytes;
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
return n.toFixed(1) + ' ' + units[i];
}
async function loadLauncherInfo() {
try {
const res = await fetch(API + '/launcher/info');
if (!res.ok) return;
const data = await res.json();
const version = data.current_version || '—';
const heroVersion = document.getElementById('hero-version');
const heroSize = document.getElementById('hero-size');
const dlVersion = document.getElementById('download-version');
const dlSize = document.getElementById('download-size');
const dlBtn = document.getElementById('download-btn');
const dlOnline = document.getElementById('download-online-btn');
const dlOffline = document.getElementById('download-offline-btn');
const pvVersion = document.getElementById('preview-version');
if (heroVersion) heroVersion.textContent = version;
if (dlVersion) dlVersion.textContent = version;
if (pvVersion) pvVersion.textContent = 'v' + version;
if (data.files && data.files.zips && data.files.zips.length) {
const latest = data.files.zips[0];
if (heroSize) heroSize.textContent = fmtSize(latest.size);
if (dlSize) dlSize.textContent = fmtSize(latest.size);
}
if (dlBtn && data.new_format && data.new_format.download_url) {
dlBtn.href = API + data.new_format.download_url;
}
// Online/Offline hybrid (Go, 1.1.3)
if (dlOnline) {
// online setup is Go exe (~5.8M) — try to discover via /launcher/version or static name
try {
const v = version && version !== '—' ? version : '1.1.1.2';
dlOnline.href = API + '/launcher/download/online';
dlOnline.setAttribute('data-version', v);
} catch(e){}
}
if (dlOffline) dlOffline.href = API + '/launcher/download/offline-setup';
if (dlOffline && data.new_format && data.new_format.download_url) {
}
} catch (e) {
console.warn('[site] failed to load launcher info', e);
}
}
async function loadMirrors() {
try {
const res = await fetch(API + '/launcher/mirrors');
if (!res.ok) return;
const data = await res.json();
const container = document.getElementById('mirrors');
if (!container || !data.mirrors || !data.mirrors.length) return;
const tags = data.mirrors.slice(0, 5).map((m) => {
const a = document.createElement('a');
a.className = 'mirror-tag';
a.href = (m.url || '') + '/launcher/download/latest';
a.textContent = m.name || m;
return a;
});
tags.forEach((t) => container.appendChild(t));
} catch (e) {
console.warn('[site] failed to load mirrors', e);
}
}
async function loadServerStatus() {
const statusEl = document.getElementById('srv-status');
const playersEl = document.getElementById('srv-players');
const versionEl = document.getElementById('srv-version');
if (!statusEl) return;
try {
const res = await fetch(API + '/api/status');
const data = await res.json();
if (data.online) {
statusEl.dataset.state = 'online';
if (playersEl) playersEl.textContent = `${data.players_online} / ${data.players_max}`;
if (versionEl && data.version) versionEl.textContent = data.version;
} else {
statusEl.dataset.state = 'offline';
if (playersEl) playersEl.textContent = '— / —';
}
} catch (e) {
statusEl.dataset.state = 'unavailable';
if (playersEl) playersEl.textContent = '— / —';
}
updateStatusUI(statusEl.dataset.state);
}
function updateStatusUI(state) {
const statusEl = document.getElementById('srv-status');
const dot = document.querySelector('#srv-status .status-dot');
if (!statusEl) return;
if (state === 'online') {
if (dot) dot.className = 'status-dot status-dot--online';
statusEl.innerHTML = '<span class="status-dot status-dot--online"></span><span class="status-text--online">' + tr('server.online') + '</span>';
} else if (state === 'offline') {
if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span class="status-text--offline">' + tr('server.offline') + '</span>';
} else {
if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span>' + tr('server.unavailable') + '</span>';
}
}
const HIGHLIGHTS = [
{ file: '1.jpg', type: 'image', captionKey: 'carousel.cap1' },
{ file: '2.mp4', type: 'video', captionKey: null },
{ file: '3.jpg', type: 'image', captionKey: 'carousel.cap3' },
{ file: '4.mp4', type: 'video', captionKey: null },
{ file: '5.jpg', type: 'image', captionKey: 'carousel.cap5' },
{ file: '6.jpg', type: 'image', captionKey: 'carousel.cap6' },
{ file: '7.jpg', type: 'image', captionKey: null },
{ file: '8.jpg', type: 'image', captionKey: null },
{ file: '9.jpg', type: 'image', captionKey: null },
{ file: '10.jpg', type: 'image', captionKey: null },
{ file: '11.jpg', type: 'image', captionKey: null },
{ file: '12.jpg', type: 'image', captionKey: null },
{ file: '13.jpg', type: 'image', captionKey: null },
{ file: '14.jpg', type: 'image', captionKey: null },
{ file: '15.jpg', type: 'image', captionKey: null },
];
function initCarousel() {
const track = document.getElementById('carousel');
const dotsWrap = document.getElementById('carouselDots');
const prev = document.getElementById('carouselPrev');
const next = document.getElementById('carouselNext');
if (!track) return;
const AUTO_MS = 6500; // auto-advance when idle
const MANUAL_MS = 22000; // hold much longer after manual navigation
const videos = [];
HIGHLIGHTS.forEach((h, i) => {
const slide = document.createElement('div');
slide.className = 'carousel-slide';
if (h.type === 'video') {
const vid = document.createElement('video');
vid.src = '/highlights/' + h.file;
vid.controls = true;
vid.muted = true;
vid.playsInline = true;
slide.appendChild(vid);
videos[i] = vid;
} else {
const img = document.createElement('img');
img.src = '/highlights/' + h.file;
img.alt = h.captionKey ? tr(h.captionKey) : tr('carousel.alt');
img.loading = 'lazy';
slide.appendChild(img);
}
if (h.captionKey) {
const cap = document.createElement('span');
cap.className = 'carousel-caption';
cap.dataset.captionKey = h.captionKey;
cap.textContent = tr(h.captionKey);
slide.appendChild(cap);
}
track.appendChild(slide);
const dot = document.createElement('span');
dot.className = 'carousel-dot' + (i === 0 ? ' active' : '');
dot.addEventListener('click', () => goto(i, true));
dotsWrap.appendChild(dot);
});
let index = 0;
let timer = null;
function schedule(delay) {
stop();
timer = setInterval(() => nextSlide(false), delay);
}
function isVideoAt(i) {
const h = HIGHLIGHTS[i];
return h && h.type === 'video';
}
function goto(i, manual) {
index = (i + HIGHLIGHTS.length) % HIGHLIGHTS.length;
track.style.transform = 'translateX(-' + index * 100 + '%)';
dotsWrap.querySelectorAll('.carousel-dot').forEach((d, k) => {
d.classList.toggle('active', k === index);
});
// Only the visible video plays; others pause to save bandwidth.
videos.forEach((vid, k) => {
if (vid) {
if (k === index) {
if (vid.paused) {
vid.currentTime = 0;
const p = vid.play();
if (p && p.catch) p.catch(() => {});
}
} else {
vid.pause();
}
}
});
if (isVideoAt(index)) {
// A video slide always advances on its 'ended' event, never on a timer.
stop();
return;
}
// Image slides: manual navigation holds much longer, idle auto-advance restores.
schedule(manual ? MANUAL_MS : AUTO_MS);
}
function nextSlide(manual) { goto(index + 1, !!manual); }
function prevSlide() { goto(index - 1, true); }
function start() { goto(index, false); }
function stop() {
if (timer) { clearInterval(timer); timer = null; }
}
// A video slide advances only once its playback actually finished.
videos.forEach((vid) => {
if (!vid) return;
vid.addEventListener('ended', () => nextSlide(false));
});
if (prev) prev.addEventListener('click', () => { prevSlide(); });
if (next) next.addEventListener('click', () => { nextSlide(true); });
track.addEventListener('mouseenter', stop);
track.addEventListener('mouseleave', start);
goto(0, false);
}
function stripMarkup(text) {
if (!text) return '';
return text
.replace(/\[[^\]]*photo=[^\]]*\]/g, '')
.replace(/\[[^\]]*\]/g, '')
.replace(/\*\*\*/g, '')
.replace(/\*\*/g, '')
.replace(/[*_~`#]/g, '')
.replace(/\n+/g, ' ')
.trim();
}
async function loadNews() {
const grid = document.getElementById('news-grid');
if (!grid) return;
try {
const res = await fetch(API + '/news');
if (!res.ok) throw new Error('news fetch failed');
const data = await res.json();
const news = (data.news || []).slice(0, 6);
if (!news.length) { grid.innerHTML = '<p class="section-sub">' + tr('news.empty') + '</p>'; return; }
grid.innerHTML = '';
news.forEach((n) => {
const card = document.createElement('article');
card.className = 'news-card';
const tags = document.createElement('div');
tags.className = 'news-tags';
if (n.type) {
const t = document.createElement('span');
t.className = 'news-tag';
t.textContent = n.type;
tags.appendChild(t);
}
if (n.version) {
const v = document.createElement('span');
v.className = 'news-tag news-tag--version';
v.textContent = n.version;
tags.appendChild(v);
}
const title = document.createElement('h3');
title.textContent = n.title || tr('news.update');
const body = document.createElement('p');
const text = stripMarkup(n.body);
body.textContent = text.length > 220 ? text.slice(0, 220) + '…' : text;
card.appendChild(tags);
card.appendChild(title);
card.appendChild(body);
grid.appendChild(card);
});
} catch (e) {
console.warn('[site] failed to load news', e);
grid.innerHTML = '<p class="section-sub">' + tr('news.error') + '</p>';
}
}
function initNav() {
const nav = document.getElementById('nav');
const onScroll = () => nav.classList.toggle('scrolled', window.scrollY > 24);
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
function initParallax() {
const els = document.querySelectorAll('[data-parallax]');
if (!els.length) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const onScroll = () => {
const y = window.scrollY;
if (y > window.innerHeight) return;
els.forEach((el) => {
const speed = parseFloat(el.dataset.parallax);
el.style.transform = 'translateY(' + (y * speed) + 'px)';
});
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
const SCENES = [
{ key: 'login', label: 'preview.login' },
{ key: 'packs', label: 'preview.packs' },
{ key: 'news', label: 'preview.news' },
{ key: 'settings', label: 'preview.settings' },
{ key: 'install', label: 'preview.install' },
];
let sceneIndex = 0;
let sceneTimer = null;
function showScene(i) {
const body = document.getElementById('launcherPreviewBody');
const dots = document.getElementById('previewDots');
const caption = document.getElementById('previewCaption');
if (!body) return;
sceneIndex = (i + SCENES.length) % SCENES.length;
body.querySelectorAll('.preview-scene').forEach((el, k) => {
el.classList.toggle('is-active', k === sceneIndex);
});
if (dots) {
dots.querySelectorAll('.preview-dot').forEach((d, k) => {
d.classList.toggle('active', k === sceneIndex);
});
}
if (caption) {
caption.textContent = tr(SCENES[sceneIndex].label);
caption.dataset.screen = SCENES[sceneIndex].key;
}
}
function nextScene() { showScene(sceneIndex + 1); }
function startPreview() {
if (!document.getElementById('launcherPreviewBody')) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (sceneTimer) clearInterval(sceneTimer);
sceneTimer = setInterval(nextScene, 4200);
showScene(0);
}
function initPreview() {
const body = document.getElementById('launcherPreviewBody');
const dots = document.getElementById('previewDots');
if (!body || !dots) return;
SCENES.forEach((_, k) => {
const d = document.createElement('span');
d.className = 'preview-dot' + (k === 0 ? ' active' : '');
dots.appendChild(d);
});
startPreview();
}
function initReveal() {
const els = document.querySelectorAll('.section-head, .feature-card, .download-card, .news-card, .server-card, .theme-card, .carousel, .launcher-window');
if (!('IntersectionObserver' in window)) {
els.forEach((el) => el.classList.add('visible'));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
io.unobserve(entry.target);
}
});
}, { threshold: 0.12 });
els.forEach((el) => { el.classList.add('reveal'); io.observe(el); });
}
function initYear() {
const el = document.getElementById('year');
if (el) el.textContent = new Date().getFullYear();
}
async function loadGithubLanguages() {
const bar = document.getElementById('lang-bar');
const legend = document.getElementById('lang-legend');
if (!bar || !legend) return;
const colors = { Java:'#b07219', Python:'#3572A5', JavaScript:'#f1e05a', TypeScript:'#3178c6', CSS:'#563d7c', HTML:'#e34c26', Go:'#00ADD8', Rust:'#dea584', Shell:'#89e051', Dockerfile:'#384d54' };
const tryFetch = async (url) => {
const r = await fetch(url, {headers:{'Accept':'application/vnd.github.v3+json'}});
if (!r.ok) throw new Error(url);
return r.json();
};
try {
let data = null;
try { data = await tryFetch('https://git.swe.zernmc.ru/api/v1/repos/sasheg/launcher/languages'); } catch(e) {
// Gitea fallback only, GitHub mirror removed
}
if (!data || typeof data !== 'object') return;
const total = Object.values(data).reduce((a,b)=>a+b,0);
if (!total) return;
const entries = Object.entries(data).map(([lang, bytes])=>({lang, bytes, pct: bytes/total*100})).sort((a,b)=>b.bytes-a.bytes).slice(0,6);
// update bar
bar.innerHTML = '';
bar.title = entries.map(e=>`${e.lang} ${e.pct.toFixed(1)}%`).join(' · ');
legend.innerHTML = '';
entries.forEach(e=>{
const seg = document.createElement('div');
seg.style.flex = String(e.pct);
seg.style.background = colors[e.lang] || 'var(--accent)';
seg.title = `${e.lang} ${e.pct.toFixed(1)}%`;
bar.appendChild(seg);
const item = document.createElement('span');
item.style.display='flex'; item.style.alignItems='center'; item.style.gap='6px';
item.innerHTML = `<i style="width:10px;height:10px;border-radius:50%;background:${colors[e.lang]||'var(--accent)'};display:inline-block"></i> ${e.lang} ${e.pct.toFixed(1)}%`;
legend.appendChild(item);
});
} catch(e) { /* keep static fallback */ }
}
document.addEventListener('DOMContentLoaded', () => {
applyI18n();
const toggle = document.getElementById('langToggle');
if (toggle) toggle.addEventListener('click', toggleLang);
loadLauncherInfo();
loadMirrors();
loadNews();
loadServerStatus();
setInterval(loadServerStatus, 15000);
loadGithubLanguages();
initCarousel();
initPreview();
initNav();
initParallax();
initReveal();
initYear();
});
})();