(() => { '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': 'Очнись от
холода.', 'hero.sub': 'Zern оживляет твои любимые модпаки. Один лаунчер, любой загрузчик, без лишних хлопот — создан для сервера ZernMC и не только.', 'hero.download': 'Скачать лаунчер', 'hero.explore': 'Обзор', 'hero.scroll': 'Листай вниз', 'server.title': 'Сервер жив.', 'server.sub': 'ZernMC — это кастомный Minecraft-сервер со своим лором, самописными плагинами и живым комьюнити. Вот что происходит прямо сейчас.', 'server.status': 'Статус', 'server.checking': 'Проверяем…', 'server.players': 'Игроков онлайн', 'server.mode': 'Режим', 'server.season': 'Сезон Prologue', 'server.online': 'Онлайн', 'server.offline': 'Оффлайн', 'server.unavailable': 'Недоступно', 'highlights.title': 'Хайлайты сервера.', 'highlights.sub': 'Моменты, запечатлённые на сервере ZernMC — войны, постройки и хаос.', 'carousel.prev': 'Назад', 'carousel.next': 'Вперёд', 'carousel.cap1': 'Zern', 'carousel.cap3': 'Это честно не я', 'carousel.cap5': 'Спавн ТЦ (старое)', 'carousel.cap6': 'Спавн склад (старое)', 'carousel.alt': 'Хайлайт Zern', 'features.title': 'Всё, что тебе нужно.', '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': 'Что внутри сезона.', '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': 'Скачать Zern.', 'download.sub': 'Скачай лаунчер для Windows и заходи на сервер ZernMC.', 'download.latest': 'Скачать актуальную версию', 'download.mirrors': 'Зеркала', 'download.note': 'Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.', 'showcase.title': 'Показываем лаунчер', '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': 'Что нового.', '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
the cold.', '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 alive.', '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 highlights.', '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 you need.', '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 the season.', '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 Zern.', '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 launcher', '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 new.', '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/jre'; // JRE endpoint validated, exe name is ZernMC-Online-Setup-.exe dlOnline.setAttribute('data-version', v); // probe if online exe exists fetch(API + '/launcher/file/' + v + '/ZernMC-Online-Setup-' + v + '.exe', {method:'HEAD'}).then(r=>{ if (r.ok) dlOnline.href = API + '/launcher/file/' + v + '/ZernMC-Online-Setup-' + v + '.exe'; }).catch(()=>{}); } catch(e){} } if (dlOffline && data.new_format && data.new_format.download_url) { dlOffline.href = API + 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 = '' + tr('server.online') + ''; } else if (state === 'offline') { if (dot) dot.className = 'status-dot status-dot--offline'; statusEl.innerHTML = '' + tr('server.offline') + ''; } else { if (dot) dot.className = 'status-dot status-dot--offline'; statusEl.innerHTML = '' + tr('server.unavailable') + ''; } } 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 = '

' + tr('news.empty') + '

'; 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 = '

' + tr('news.error') + '

'; } } 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 = ` ${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(); }); })();