-
-
-

Логи

- +
+ +
+
+
+

Select a pack

+

Choose a pack from the sidebar to get started

+
+
+ + +
-
-
Ожидание запуска...
+ +
+
+ +

No pack selected

+

Select a pack from the sidebar or add a new one

+
+ +
+ +
+
+ Select a pack +
+ +
+
+ + +
+
+

News

+
+
+
+
Coming Soon
+

ZernMC Server Updates

+

News and announcements will appear here. Stay tuned for the latest updates about the server and launcher.

+ +
+
+
Info
+

Launcher v1.0.9

+

English UI, JavaFX redesign, improved pack management, and more. Check the GitHub for the full changelog.

+ +
+
+
Guide
+

Getting Started

+

Install a pack, activate your pass via the website, and start playing. Need help? Contact a moderator.

+ +
+
+
+ + +
+
+

Settings

+
+
+
+
+

Activate Pass

+

Enter your pass code to access server packs

+
+
+ + +
+
+
+
+

Allocated RAM

+

Loading...

+
+
+ + 4 GB +
+
+
+
+

Game Resolution

+

Width x Height

+
+
+ + x + +
+
+
+
+

Extra JVM Arguments

+

Additional Java VM options

+
+
+ +
+
+
+
+

Java Path

+

~/.zernmc/jre/

+
+
+ +
+
+
+
+

Server

+

http://87.120.187.36:1582

+
+
+ Checking... +
+
+
+
- -
- + + - - - - - + +
- \ No newline at end of file + diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js index df51e45..f63dbbe 100644 --- a/launcher/launcher/src/resources/ui/launcher.js +++ b/launcher/launcher/src/resources/ui/launcher.js @@ -1,772 +1,568 @@ -const API_BASE = '/api'; -let consoleEventSource = null; -let gameLogEventSource = null; +const API = '/api'; -class LauncherApp { +class ZernMCLauncher { constructor() { - this.state = 'INIT'; - this.username = null; - this.account = null; - this.currentInstance = null; - this.instances = []; - this.selectedInstance = null; - this.hasUpdate = false; - this.hasMismatches = false; + this.state = { account: null, instances: [], selectedPack: null, installing: false, serverPacks: [] }; + this.toastTimer = null; + this.progressPoller = null; this.init(); } async init() { this.bindEvents(); - this.initGridAnimation(); - this.initDropdowns(); + this.initBg(); await this.checkAuth(); } - initDropdowns() { - document.querySelectorAll('.custom-dropdown').forEach(dropdown => { - const trigger = dropdown.querySelector('.dropdown-trigger'); - const list = dropdown.querySelector('.dropdown-list'); - let selectedValue = null; + // ==================== BACKGROUND ==================== + initBg() { + const c = document.getElementById('bg-canvas'); + const ctx = c.getContext('2d'); + let mx = 0, my = 0, ox = 0, oy = 0; - trigger.addEventListener('click', (e) => { - e.stopPropagation(); - document.querySelectorAll('.custom-dropdown .dropdown-list.open').forEach(d => { - if (d !== list) d.classList.remove('open'); - }); - list.classList.toggle('open'); - trigger.classList.toggle('active'); - }); - - list.addEventListener('click', (e) => { - const item = e.target.closest('.dropdown-item'); - if (item) { - const value = item.dataset.value; - const text = item.textContent; - - list.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected')); - item.classList.add('selected'); - - trigger.querySelector('.dropdown-value').textContent = text; - trigger.dataset.value = value; - list.classList.remove('open'); - trigger.classList.remove('active'); - - dropdown.dispatchEvent(new CustomEvent('change', { detail: { value, text } })); - } - }); - }); - - document.addEventListener('click', () => { - document.querySelectorAll('.custom-dropdown .dropdown-list.open').forEach(list => { - list.classList.remove('open'); - }); - document.querySelectorAll('.custom-dropdown .dropdown-trigger.active').forEach(trigger => { - trigger.classList.remove('active'); - }); - }); - } - - populateDropdown(id, items) { - const dropdown = document.getElementById(id); - if (!dropdown) return; - - const list = dropdown.querySelector('.dropdown-list'); - list.innerHTML = ''; - - items.forEach(item => { - const div = document.createElement('div'); - div.className = 'dropdown-item'; - div.dataset.value = item.value || item; - div.textContent = item.label || item; - list.appendChild(div); - }); - } - - selectDropdownItem(id, value) { - const dropdown = document.getElementById(id); - if (!dropdown) return; - - const items = dropdown.querySelectorAll('.dropdown-item'); - items.forEach(item => { - if (item.dataset.value === value) { - item.click(); - } - }); - } - - getDropdownValue(id) { - const dropdown = document.getElementById(id); - if (!dropdown) return null; - return dropdown.querySelector('.dropdown-trigger').dataset.value; - } - - startGameLogStream() { - if (gameLogEventSource) { - gameLogEventSource.close(); - } - - gameLogEventSource = new EventSource(API_BASE + '/game-logs/stream'); - - gameLogEventSource.onmessage = (event) => { - if (event.data && event.data.trim()) { - this.addLog(event.data, this.getLogType(event.data)); - } + const resize = () => { c.width = window.innerWidth; c.height = window.innerHeight; draw(); }; + const draw = () => { + ctx.clearRect(0, 0, c.width, c.height); + const gs = 48, r = 1.2; + ctx.fillStyle = '#e94560'; + for (let x = 0; x <= c.width; x += gs) + for (let y = 0; y <= c.height; y += gs) + ctx.beginPath(), ctx.arc(x + ox * 8, y + oy * 8, r, 0, Math.PI * 2), ctx.fill(); }; - - gameLogEventSource.onerror = () => { - gameLogEventSource.close(); - setTimeout(() => this.startGameLogStream(), 5000); - }; - } - - stopGameLogStream() { - if (gameLogEventSource) { - gameLogEventSource.close(); - gameLogEventSource = null; - } - } - - startConsoleLogStream() { - if (consoleEventSource) { - consoleEventSource.close(); - } - - consoleEventSource = new EventSource(API_BASE + '/logs/stream'); - - consoleEventSource.onmessage = (event) => { - if (event.data && event.data.trim()) { - this.addLog(event.data, this.getLogType(event.data)); - } - }; - - consoleEventSource.onerror = () => { - consoleEventSource.close(); - setTimeout(() => this.startConsoleLogStream(), 3000); - }; - } - - stopConsoleLogStream() { - if (consoleEventSource) { - consoleEventSource.close(); - consoleEventSource = null; - } - } - - getLogType(line) { - if (line.includes('[STDOUT]') || line.includes('Render thread/ERROR') || line.includes('/ERROR]:')) return 'error'; - if (line.includes('[STDOUT]') || line.includes('Render thread/WARN') || line.includes('/WARN]:')) return 'warning'; - if (line.includes('[STDOUT]') || line.includes('Render thread/INFO') || line.includes('/INFO]:')) return 'info'; - if (line.includes(' успешно') || line.includes('Started') || line.includes(' запущен') || line.includes('done')) return 'success'; - return 'info'; - } - - bindEvents() { - document.getElementById('login-form').addEventListener('submit', (e) => { - e.preventDefault(); - this.handleLogin(); - }); - - document.getElementById('logout-btn').addEventListener('click', () => { - this.handleLogout(); - }); - - document.getElementById('download-btn').addEventListener('click', () => { - this.showDownloadModal(); - }); - - document.getElementById('close-download-modal').addEventListener('click', () => { - this.hideDownloadModal(); - }); - - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - this.switchTab(e.target.dataset.tab); - }); - }); - - document.getElementById('play-btn').addEventListener('click', () => { - this.launchInstance(); - }); - - document.getElementById('clear-logs').addEventListener('click', () => { - this.clearLogs(); - }); - - document.getElementById('loader-dropdown').addEventListener('change', (e) => { - this.onLoaderChange(e.detail.value); - }); - - document.getElementById('install-zernmc-btn').addEventListener('click', () => { - this.installZernMCPack(); - }); - - document.getElementById('install-vanilla-btn').addEventListener('click', () => { - this.installVanilla(); - }); - } - - initGridAnimation() { - const canvas = document.getElementById('grid-canvas'); - const ctx = canvas.getContext('2d'); - let mouseX = 0, mouseY = 0; - let offsetX = 0, offsetY = 0; - - const resize = () => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - this.drawGrid(ctx, canvas.width, canvas.height, offsetX, offsetY); - }; - window.addEventListener('resize', resize); - - window.addEventListener('mousemove', (e) => { - mouseX = (e.clientX / window.innerWidth - 0.5) * 2; - mouseY = (e.clientY / window.innerHeight - 0.5) * 2; + window.addEventListener('mousemove', e => { + mx = (e.clientX / innerWidth - 0.5) * 2; + my = (e.clientY / innerHeight - 0.5) * 2; }); - - const animate = () => { - offsetX += (mouseX * 0.5 - offsetX) * 0.05; - offsetY += (mouseY * 0.5 - offsetY) * 0.05; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - this.drawGrid(ctx, canvas.width, canvas.height, offsetX, offsetY); - requestAnimationFrame(animate); + const anim = () => { + ox += (mx * 0.3 - ox) * 0.04; + oy += (my * 0.3 - oy) * 0.04; + draw(); + requestAnimationFrame(anim); }; - resize(); - animate(); + anim(); } - drawGrid(ctx, width, height, offsetX, offsetY) { - const gridSize = 50; - const dotSize = 1; - - ctx.fillStyle = '#e94560'; - - for (let x = 0; x <= width; x += gridSize) { - for (let y = 0; y <= height; y += gridSize) { - const px = x + offsetX * 10; - const py = y + offsetY * 10; - ctx.beginPath(); - ctx.arc(px, py, dotSize, 0, Math.PI * 2); - ctx.fill(); - } - } - } - - async request(endpoint, options = {}) { + // ==================== API ==================== + async req(endpoint, opts = {}) { try { - const response = await fetch(`${API_BASE}${endpoint}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } + const r = await fetch(`${API}${endpoint}`, { + ...opts, + headers: { 'Content-Type': 'application/json', ...opts.headers } }); - return await response.json(); - } catch (error) { - console.error('API Error:', error); - return { success: false, error: error.message }; + return await r.json(); + } catch (e) { + return { success: false, error: e.message }; } } + // ==================== AUTH ==================== async checkAuth() { this.showLoading(true); - - const autoLoginResult = await this.request('/auto-login'); - if (autoLoginResult.success && autoLoginResult.autoLogin) { - this.account = autoLoginResult.data; - this.username = autoLoginResult.data.username; - this.showMainScreen(); - this.renderCurrentInstance(); - this.startConsoleLogStream(); - this.startGameLogStream(); - await this.loadInstances(); - this.addLog('Автовход выполнен: ' + this.username, 'success'); + const auto = await this.req('/auto-login'); + if (auto.success && auto.autoLogin) { + this.state.account = auto.data; + this.enterMain(); + this.toast(`Welcome back, ${auto.data.username}`, 'success'); } else { - const result = await this.request('/account'); - if (result.success) { - this.account = result.data; - this.username = result.data.username; - this.showMainScreen(); - this.renderCurrentInstance(); - this.startConsoleLogStream(); - this.startGameLogStream(); - await this.loadInstances(); + const acct = await this.req('/account'); + if (acct.success) { + this.state.account = acct.data; + this.enterMain(); } else { - this.showLoginScreen(); + this.showLogin(); } } - this.showLoading(false); } - async handleLogin() { - const username = document.getElementById('username').value; + async handleLogin(e) { + e.preventDefault(); + const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value; - const errorEl = document.getElementById('login-error'); - const btn = document.querySelector('#login-form button[type="submit"]'); - const btnText = btn.querySelector('.btn-text'); - const btnLoader = btn.querySelector('.btn-loader'); + const errEl = document.getElementById('login-error'); + const btn = document.getElementById('login-btn'); + const label = btn.querySelector('.btn-label'); + const spinner = btn.querySelector('.spinner'); - if (!username || !password) { - this.showError('Введите имя пользователя и пароль'); - return; - } + if (!username || !password) { this.showLoginError('Enter username and password'); return; } btn.disabled = true; - btnText.classList.add('hidden'); - btnLoader.classList.remove('hidden'); - errorEl.classList.add('hidden'); + label.textContent = 'Signing in...'; + spinner.classList.remove('hidden'); - const result = await this.request('/login', { - method: 'POST', - body: JSON.stringify({ username, password }) - }); + const r = await this.req('/login', { method: 'POST', body: JSON.stringify({ username, password }) }); btn.disabled = false; - btnText.classList.remove('hidden'); - btnLoader.classList.add('hidden'); + label.textContent = 'Sign In'; + spinner.classList.add('hidden'); - if (result.success) { - this.account = result.data; - this.username = result.data.username; - this.showMainScreen(); - this.renderCurrentInstance(); - this.startConsoleLogStream(); - this.startGameLogStream(); - await this.loadInstances(); + if (r.success) { + this.state.account = r.data; + this.enterMain(); + this.toast(`Welcome, ${r.data.username}!`, 'success'); } else { - this.showError(result.error || 'Ошибка входа'); - } - } - - renderCurrentInstance() { - if (!this.account) return; - - document.getElementById('username-display').textContent = this.account.username; - const statusEl = document.getElementById('account-status'); - statusEl.textContent = this.account.passActive ? 'PRO' : 'FREE'; - statusEl.className = 'badge ' + (this.account.passActive ? 'active' : 'inactive'); - - const roleEl = document.getElementById('account-role'); - if (this.account.roleName) { - roleEl.textContent = this.account.roleName; - roleEl.style.display = 'inline-block'; - } else { - roleEl.style.display = 'none'; - } - } - - async handleLogout() { - this.stopConsoleLogStream(); - this.stopGameLogStream(); - this.username = null; - this.account = null; - this.currentInstance = null; - this.instances = []; - this.showLoginScreen(); - this.addLog('Вы вышли из аккаунта', 'info'); - } - - async shutdownLauncher() { - await this.request('/exit-parent', { method: 'POST' }); - window.close(); - } - - showError(message) { - const errorEl = document.getElementById('login-error'); - errorEl.textContent = message; - errorEl.classList.remove('hidden'); - } - - async loadInstances() { - const result = await this.request('/instances'); - - if (result.success && result.data) { - this.instances = result.data; - this.renderInstances(); - this.addLog('Загружено ' + result.data.length + ' сборок', 'success'); - - if (this.instances.length > 0 && !this.selectedInstance) { - this.currentInstance = this.instances[0]; - this.selectedInstance = this.currentInstance; - this.addLog('Сборка загружена: ' + this.currentInstance.name, 'success'); - } - this.updatePlayButton(); - } else { - this.addLog('Ошибка загрузки: ' + (result.error || 'неизвестная ошибка'), 'error'); - } - } - - renderInstances() { - const zernmcContainer = document.getElementById('zernmc-instances-list'); - const localContainer = document.getElementById('local-instances-list'); - zernmcContainer.innerHTML = ''; - localContainer.innerHTML = ''; - - const zernmcInstances = this.instances.filter(i => i.category === 'zernmc'); - const localInstances = this.instances.filter(i => i.category === 'local'); - - zernmcInstances.forEach(inst => { - zernmcContainer.appendChild(this.createInstanceCard(inst)); - }); - localInstances.forEach(inst => { - localContainer.appendChild(this.createInstanceCard(inst)); - }); - - if (zernmcInstances.length === 0) { - zernmcContainer.innerHTML = '
Нет сборок
'; - } - if (localInstances.length === 0) { - localContainer.innerHTML = '
Нет сборок
'; - } - } - - createInstanceCard(inst) { - const card = document.createElement('div'); - card.className = 'instance-card'; - if (this.selectedInstance && this.selectedInstance.name === inst.name) { - card.classList.add('selected'); - } - card.dataset.name = inst.name; - card.onclick = () => this.selectInstance(inst.name); - - let details = `${inst.version || '?'}`; - if (inst.loaderType && inst.loaderType !== 'vanilla') { - details += `${inst.loaderType}`; - } - if (inst.isServerPack) { - details += `v${inst.serverVersion}`; - } - - card.innerHTML = ` -
${this.escapeHtml(inst.name)}
-
${details}
- `; - return card; - } - - selectInstance(name) { - this.selectedInstance = this.instances.find(i => i.name === name); - this.renderInstances(); - this.updatePlayButton(); - } - - updatePlayButton() { - const btn = document.getElementById('play-btn'); - const instance = this.selectedInstance || this.currentInstance; - - if (!instance) { - btn.disabled = true; - btn.className = 'btn-play'; - btn.innerHTML = 'ВЫБЕРИТЕ СБОРКУ'; - return; - } - - if (this.hasUpdate || this.hasMismatches) { - btn.disabled = false; - btn.className = 'btn-update'; - btn.innerHTML = 'ОБНОВИТЬ'; - } else { - btn.disabled = false; - btn.className = 'btn-play'; - btn.innerHTML = 'ИГРАТЬ'; - } - } - - renderCurrentInstance(instance) { - if (!this.account) return; - - document.getElementById('username-display').textContent = this.account.username; - const statusEl = document.getElementById('account-status'); - statusEl.textContent = this.account.passActive ? 'PRO' : 'FREE'; - statusEl.className = 'badge ' + (this.account.passActive ? 'active' : 'inactive'); - - const roleEl = document.getElementById('account-role'); - if (this.account.roleName) { - roleEl.textContent = this.account.roleName; - roleEl.style.display = 'inline-block'; - } else { - roleEl.style.display = 'none'; - } - } - - async launchInstance() { - const instance = this.selectedInstance || this.currentInstance; - if (!instance) return; - - const name = instance.name; - this.addLog('Запуск сборки: ' + name, 'info'); - - const result = await this.request('/launch', { - method: 'POST', - body: JSON.stringify({ name }) - }); - - if (result.success) { - this.addLog('Сборка запущена! PID: ' + (result.data?.pid || ''), 'success'); - } else { - this.addLog('Ошибка: ' + (result.error || 'неизвестная ошибка'), 'error'); - } - } - - async showDownloadModal() { - document.getElementById('download-modal').classList.remove('hidden'); - await this.loadDownloadModalData(); - } - - async loadDownloadModalData() { - this.populateDropdown('mc-version-dropdown', [{ value: '', label: 'Загрузка...' }]); - - const mcResult = await this.request('/mc-versions'); - if (mcResult.success && mcResult.data) { - const items = mcResult.data.map(v => ({ value: v, label: v })); - this.populateDropdown('mc-version-dropdown', items); - } else { - this.populateDropdown('mc-version-dropdown', [{ value: '', label: 'Ошибка загрузки' }]); - } - - const zernmcSelect = document.getElementById('zernmc-pack-select'); - zernmcSelect.innerHTML = ''; - - const packResult = await this.request('/packs'); - if (packResult.success && packResult.data && packResult.data.length > 0) { - if (this.account && this.account.passActive) { - zernmcSelect.innerHTML = ''; - packResult.data.forEach(p => { - const opt = document.createElement('option'); - opt.value = p.name; - opt.textContent = p.displayName + ' (' + p.version + ')'; - zernmcSelect.appendChild(opt); - }); - } else { - zernmcSelect.innerHTML = ''; - zernmcSelect.disabled = true; - } - } else if (packResult.error && packResult.error.includes('проходка')) { - zernmcSelect.innerHTML = ''; - zernmcSelect.disabled = true; - } else { - zernmcSelect.innerHTML = ''; - zernmcSelect.disabled = true; - } - - const zernmcTab = document.querySelector('[data-tab="zernmc"]'); - if (this.account && !this.account.passActive) { - zernmcTab.disabled = true; - zernmcTab.style.opacity = '0.5'; - zernmcTab.style.cursor = 'not-allowed'; - this.switchTab('vanilla'); - } else { - zernmcTab.disabled = false; - zernmcTab.style.opacity = '1'; - zernmcTab.style.cursor = 'pointer'; - } - } - - hideDownloadModal() { - document.getElementById('download-modal').classList.add('hidden'); - this.hideProgress(); - } - - switchTab(tab) { - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.toggle('active', btn.dataset.tab === tab); - }); - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.toggle('active', content.id === 'tab-' + tab); - }); - } - - async installZernMCPack() { - const packName = document.getElementById('zernmc-pack-select').value; - const instanceName = document.getElementById('zernmc-instance-name').value; - - if (!packName || packName === '') { - alert('Выберите сборку'); - return; - } - - if (!instanceName) { - alert('Введите название сборки'); - return; - } - - this.showProgress('Установка ZernMC сборки...'); - this.addLog('Начало установки: ' + packName, 'info'); - - const result = await this.request('/install', { - method: 'POST', - body: JSON.stringify({ - name: instanceName, - version: 'latest', - loader: 'zernmc' - }) - }); - - if (result.success) { - this.hideDownloadModal(); - await this.loadInstances(); - await this.loadCurrentInstance(); - this.addLog('Сборка установлена!', 'success'); - } else { - this.addLog('Ошибка установки: ' + (result.error || 'неизвестная ошибка'), 'error'); - this.hideProgress(); - } - } - - async installVanilla() { - const mcVersion = this.getDropdownValue('mc-version-dropdown'); - const loader = this.getDropdownValue('loader-dropdown'); - const loaderVersion = this.getDropdownValue('loader-version-dropdown'); - const instanceName = document.getElementById('vanilla-instance-name').value; - - if (!mcVersion) { - alert('Выберите версию Minecraft'); - return; - } - - if (!instanceName) { - alert('Введите название сборки'); - return; - } - - if (loader !== 'vanilla' && !loaderVersion) { - alert('Выберите версию лоадера'); - return; - } - - this.showProgress('Установка сборки...'); - this.addLog(`Начало установки: Minecraft ${mcVersion} ${loader !== 'vanilla' ? loader + ' ' + loaderVersion : ''}`, 'info'); - - const result = await this.request('/install', { - method: 'POST', - body: JSON.stringify({ - name: instanceName, - version: mcVersion, - loader: loader === 'vanilla' ? 'vanilla' : loader - }) - }); - - if (result.success) { - this.hideDownloadModal(); - await this.loadInstances(); - await this.loadCurrentInstance(); - this.addLog('Сборка установлена!', 'success'); - } else { - this.addLog('Ошибка установки: ' + (result.error || 'неизвестная ошибка'), 'error'); - this.hideProgress(); - } - } - - async onLoaderChange(loader) { - const loaderVersionGroup = document.getElementById('loader-version-group'); - const mcVersion = this.getDropdownValue('mc-version-dropdown'); - - if (loader === 'vanilla') { - loaderVersionGroup.classList.add('hidden'); - } else { - loaderVersionGroup.classList.remove('hidden'); - this.populateDropdown('loader-version-dropdown', [{ value: '', label: 'Загрузка...' }]); - - if (mcVersion) { - const result = await this.request('/loader-versions?mc=' + mcVersion + '&loader=' + loader); - if (result.success && result.data) { - const items = result.data.map(v => ({ value: v, label: v })); - this.populateDropdown('loader-version-dropdown', items); - } else { - this.populateDropdown('loader-version-dropdown', [{ value: '', label: 'Ошибка загрузки' }]); + // If login fails, try register (auto-create account) + if (r.error && (r.error.includes('not found') || r.error.includes('Invalid'))) { + const reg = await this.req('/register', { method: 'POST', body: JSON.stringify({ username, password }) }); + if (reg.success) { + this.state.account = reg.data; + this.enterMain(); + this.toast(`Account created! Welcome, ${reg.data.username}!`, 'success'); + return; } } + this.showLoginError(r.error || 'Login failed'); } } - showProgress(text) { - const progress = document.getElementById('download-progress'); - const progressText = document.getElementById('progress-text'); - const progressFill = document.getElementById('progress-fill'); - - progress.classList.remove('hidden'); - progressText.textContent = text; - progressFill.style.width = '0%'; - this.progressInterval = setInterval(() => this.pollInstallProgress(), 500); - } - - async pollInstallProgress() { - try { - const result = await this.request('/install/progress'); - if (result.success && result.data) { - const { label, current, total, percent } = result.data; - const progressFill = document.getElementById('progress-fill'); - const progressText = document.getElementById('progress-text'); - - if (percent !== undefined) { - progressFill.style.width = percent + '%'; - } - if (label) { - progressText.textContent = label; - this.addLog(label, 'info'); - } - - if (!result.data.inProgress) { - clearInterval(this.progressInterval); - this.hideProgress(); - } - } - } catch (e) { - clearInterval(this.progressInterval); - } + showLoginError(msg) { + const el = document.getElementById('login-error'); + el.textContent = msg; + el.classList.remove('hidden'); } - hideProgress() { - document.getElementById('download-progress').classList.add('hidden'); - } - - addLog(message, type = 'info') { - const container = document.getElementById('logs-container'); - const entry = document.createElement('div'); - entry.className = 'log-entry ' + type; - entry.textContent = '[' + new Date().toLocaleTimeString() + '] ' + message; - container.appendChild(entry); - - while (container.children.length > 500) { - container.removeChild(container.firstChild); - } - - container.scrollTop = container.scrollHeight; - } - - clearLogs() { - const container = document.getElementById('logs-container'); - container.innerHTML = '
Логи очищены (консоль продолжает)
'; - } - - showLoginScreen() { + showLogin() { document.getElementById('login-screen').classList.remove('hidden'); document.getElementById('main-screen').classList.add('hidden'); } - showMainScreen() { - document.getElementById('login-screen').classList.add('hidden'); - document.getElementById('main-screen').classList.remove('hidden'); + async logout() { + this.state.selectedPack = null; + this.state.instances = []; + this.state.account = null; + this.toast('Logged out'); + document.getElementById('login-screen').classList.remove('hidden'); + document.getElementById('main-screen').classList.add('hidden'); } - showLoading(show) { - const overlay = document.getElementById('loading-overlay'); - if (show) { - overlay.classList.remove('hidden'); + enterMain() { + document.getElementById('login-screen').classList.add('hidden'); + document.getElementById('main-screen').classList.remove('hidden'); + const a = this.state.account; + const avatar = document.getElementById('user-avatar'); + avatar.textContent = (a.username || 'Z')[0].toUpperCase(); + document.getElementById('username-display').textContent = a.username; + const status = document.getElementById('account-status'); + if (a.passActive) { + status.textContent = 'PRO'; + status.className = 'badge badge-pro'; } else { - overlay.classList.add('hidden'); + status.textContent = 'FREE'; + status.className = 'badge badge-free'; + } + const role = document.getElementById('account-role'); + if (a.roleName) { + role.textContent = a.roleName; + role.classList.remove('hidden'); + } else { + role.classList.add('hidden'); + } + document.getElementById('header-version').textContent = document.getElementById('version').textContent; + this.switchView('packs'); + this.loadInstances(); + this.loadSettings(); + this.loadServerPacksList(); + } + + async loadServerPacksList() { + const r = await this.req('/packs'); + if (r.success && r.data) { + this.state.serverPacks = r.data; } } - escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + // ==================== NAV ==================== + bindEvents() { + document.getElementById('login-form').addEventListener('submit', e => this.handleLogin(e)); + document.getElementById('logout-btn').addEventListener('click', () => this.logout()); + + document.querySelectorAll('.nav-btn').forEach(btn => { + btn.addEventListener('click', () => this.switchView(btn.dataset.view)); + }); + + document.getElementById('add-pack-btn').addEventListener('click', () => this.showInstallModal()); + document.getElementById('close-modal-btn').addEventListener('click', () => this.hideInstallModal()); + document.querySelectorAll('.modal-tab').forEach(t => { + t.addEventListener('click', () => this.switchInstallTab(t.dataset.tab)); + }); + document.getElementById('loader-select').addEventListener('change', e => this.onLoaderChange(e.target.value)); + document.getElementById('install-zernmc-btn').addEventListener('click', () => this.installZernMCPack()); + document.getElementById('install-custom-btn').addEventListener('click', () => this.installCustom()); + document.getElementById('play-btn').addEventListener('click', () => this.launchPack()); + document.getElementById('ram-slider').addEventListener('input', e => { + document.getElementById('ram-value').textContent = (e.target.value / 1024).toFixed(1) + ' GB'; + }); + document.getElementById('ram-slider').addEventListener('change', e => this.saveSettings()); + document.getElementById('activate-pass-btn').addEventListener('click', () => this.activatePass()); + + let saveTimer; + const debouncedSave = () => { clearTimeout(saveTimer); saveTimer = setTimeout(() => this.saveSettings(), 500); }; + document.getElementById('win-width').addEventListener('change', debouncedSave); + document.getElementById('win-height').addEventListener('change', debouncedSave); + document.getElementById('jvm-args').addEventListener('change', debouncedSave); + document.getElementById('java-path-input').addEventListener('change', debouncedSave); + + document.querySelectorAll('.modal-backdrop').forEach(m => { + m.addEventListener('click', e => { if (e.target === m) this.hideInstallModal(); }); + }); + } + + switchView(view) { + document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); + document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view)); + } + + // ==================== INSTANCES ==================== + async loadInstances() { + const r = await this.req('/instances'); + if (r.success && r.data) { + this.state.instances = r.data; + this.renderSidebar(); + } + } + + renderSidebar() { + const serverList = document.getElementById('server-packs-list'); + serverList.innerHTML = ''; + + 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 el = document.createElement('div'); + el.className = 'pack-entry' + (this.state.selectedPack && this.state.selectedPack.name === inst.name ? ' selected' : ''); + el.innerHTML = ` +
+ +
+ + `; + el.addEventListener('click', () => this.selectPack(inst)); + serverList.appendChild(el); + }); + } + + selectPack(inst) { + this.state.selectedPack = inst; + this.renderSidebar(); + this.showPackDetail(inst); + } + + showPackDetail(inst) { + document.getElementById('pack-empty-state').classList.add('hidden'); + const detail = document.getElementById('pack-detail-content'); + detail.classList.remove('hidden'); + + document.getElementById('detail-name').textContent = inst.name; + document.getElementById('detail-mc').textContent = inst.version || '?'; + const loader = document.getElementById('detail-loader'); + if (inst.loaderType && inst.loaderType !== 'vanilla') { + loader.textContent = inst.loaderType; + loader.classList.remove('hidden'); + } else { + loader.classList.add('hidden'); + } + const serverTag = document.getElementById('detail-server'); + if (inst.isServerPack && inst.serverVersion) { + serverTag.textContent = 'v' + inst.serverVersion; + serverTag.classList.remove('hidden'); + } else { + serverTag.classList.add('hidden'); + } + document.getElementById('detail-loader-ver').textContent = inst.loaderVersion || '-'; + document.getElementById('detail-files').textContent = inst.filesCount || '0'; + + document.getElementById('selected-pack-title').textContent = inst.name; + document.getElementById('selected-pack-meta').textContent = + (inst.version || '?') + (inst.loaderType && inst.loaderType !== 'vanilla' ? ' \u00b7 ' + inst.loaderType : ''); + + const playBar = document.getElementById('play-bar-name'); + playBar.textContent = inst.name; + + const playBtn = document.getElementById('play-btn'); + if (inst.isServerPack && !this.state.account.passActive) { + playBtn.disabled = true; + playBtn.innerHTML = ' Pass Required'; + } else { + playBtn.disabled = false; + playBtn.innerHTML = ' Play'; + } + + // Load pack description from API + const descEl = document.getElementById('pack-description-text'); + const galleryEl = document.getElementById('pack-gallery'); + galleryEl.innerHTML = ''; + if (inst.isServerPack && inst.serverPackName) { + descEl.textContent = 'Loading description...'; + this.loadPackDescription(inst.serverPackName, descEl, galleryEl); + } else { + descEl.textContent = ''; + } + } + + async loadPackDescription(packName, descEl, galleryEl) { + const packs = await this.req('/packs'); + if (packs.success && packs.data) { + const pack = packs.data.find(p => p.name === packName); + if (pack && pack.description) { + descEl.textContent = pack.description; + } else { + descEl.textContent = 'No description available'; + } + } else { + descEl.textContent = 'Failed to load description'; + } + } + + // ==================== LAUNCH ==================== + async launchPack() { + const inst = this.state.selectedPack; + if (!inst) return; + this.toast(`Launching ${inst.name}...`, 'info'); + const r = await this.req('/launch', { method: 'POST', body: JSON.stringify({ name: inst.name }) }); + if (r.success) { + this.toast(`Launched! PID: ${r.data?.pid || ''}`, 'success'); + } else { + this.toast(r.error || 'Launch failed', 'error'); + } + } + + // ==================== INSTALL ==================== + async showInstallModal() { + document.getElementById('install-modal').classList.remove('hidden'); + document.getElementById('zernmc-pack-select').innerHTML = ''; + document.getElementById('mc-version-select').innerHTML = ''; + + const packs = await this.req('/packs'); + const zernmcSel = document.getElementById('zernmc-pack-select'); + if (packs.success && packs.data && packs.data.length > 0) { + if (this.state.account && this.state.account.passActive) { + zernmcSel.innerHTML = ''; + zernmcSel.disabled = false; + packs.data.forEach(p => { + const o = document.createElement('option'); + o.value = p.name; + o.textContent = (p.displayName || p.name) + ' (' + (p.version || '') + ')'; + zernmcSel.appendChild(o); + }); + } else { + zernmcSel.innerHTML = ''; + zernmcSel.disabled = true; + } + } else { + zernmcSel.innerHTML = ''; + zernmcSel.disabled = true; + } + + if (this.state.account && !this.state.account.passActive) { + document.querySelector('[data-tab="zernmc"]').style.opacity = '0.5'; + } else { + document.querySelector('[data-tab="zernmc"]').style.opacity = '1'; + } + + const mc = await this.req('/mc-versions'); + const mcSel = document.getElementById('mc-version-select'); + if (mc.success && mc.data) { + mcSel.innerHTML = ''; + mc.data.forEach(v => { + const o = document.createElement('option'); + o.value = v; o.textContent = v; + mcSel.appendChild(o); + }); + } else { + mcSel.innerHTML = ''; + } + } + + hideInstallModal() { + document.getElementById('install-modal').classList.add('hidden'); + document.getElementById('install-progress').classList.add('hidden'); + this.stopProgressPoll(); + } + + switchInstallTab(tab) { + document.querySelectorAll('.modal-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); + document.querySelectorAll('.modal-tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + tab)); + } + + async onLoaderChange(loader) { + const f = document.getElementById('loader-ver-field'); + if (loader === 'vanilla') { f.classList.add('hidden'); return; } + f.classList.remove('hidden'); + const sel = document.getElementById('loader-ver-select'); + sel.innerHTML = ''; + const mc = document.getElementById('mc-version-select').value; + if (!mc) { sel.innerHTML = ''; return; } + const r = await this.req(`/loader-versions?mc=${mc}&loader=${loader}`); + if (r.success && r.data) { + sel.innerHTML = ''; + r.data.forEach(v => { + const o = document.createElement('option'); + o.value = v; o.textContent = v; + sel.appendChild(o); + }); + } else { + sel.innerHTML = ''; + } + } + + async installZernMCPack() { + const packName = document.getElementById('zernmc-pack-select').value; + const instanceName = document.getElementById('zernmc-instance-name').value.trim(); + if (!packName) { this.toast('Select a pack', 'error'); return; } + if (!instanceName) { this.toast('Enter a name', 'error'); return; } + + const r = await this.req('/install', { + method: 'POST', + body: JSON.stringify({ name: instanceName, version: 'latest', loader: 'zernmc' }) + }); + if (r.success) { + this.toast('Installing...', 'info'); + this.startProgressPoll(); + } else { + this.toast(r.error || 'Install failed', 'error'); + } + } + + async installCustom() { + const mc = document.getElementById('mc-version-select').value; + const loader = document.getElementById('loader-select').value; + const loaderVer = document.getElementById('loader-ver-select').value; + const name = document.getElementById('custom-instance-name').value.trim(); + + if (!mc) { this.toast('Select MC version', 'error'); return; } + if (!name) { this.toast('Enter a name', 'error'); return; } + + const r = await this.req('/install', { + method: 'POST', + body: JSON.stringify({ name, version: mc, loader, loaderVersion: loaderVer }) + }); + if (r.success) { + this.toast('Installing...', 'info'); + this.startProgressPoll(); + } else { + this.toast(r.error || 'Install failed', 'error'); + } + } + + startProgressPoll() { + this.progressPoller = setInterval(async () => { + const r = await this.req('/install/progress'); + if (r.success && r.data) { + const p = document.getElementById('install-progress'); + p.classList.remove('hidden'); + document.getElementById('progress-fill').style.width = (r.data.percent || 0) + '%'; + document.getElementById('progress-label').textContent = r.data.label || 'Installing...'; + if (!r.data.inProgress) { + this.stopProgressPoll(); + this.hideInstallModal(); + this.toast('Installation complete!', 'success'); + await this.loadInstances(); + } + } + }, 500); + } + + stopProgressPoll() { + if (this.progressPoller) { + clearInterval(this.progressPoller); + this.progressPoller = null; + } + } + + // ==================== SETTINGS ==================== + async loadSettings() { + const r = await this.req('/settings'); + if (r.success && r.data) { + const ram = r.data.maxMemory || 4096; + document.getElementById('ram-slider').value = ram; + document.getElementById('ram-value').textContent = (ram / 1024).toFixed(1) + ' GB'; + document.getElementById('ram-info').textContent = ram + ' MB allocated'; + document.getElementById('server-url').textContent = r.data.serverUrl || 'http://87.120.187.36:1582'; + + if (r.data.windowWidth) { + document.getElementById('win-width').value = r.data.windowWidth; + } + if (r.data.windowHeight) { + document.getElementById('win-height').value = r.data.windowHeight; + } + if (r.data.extraJvmArgs !== undefined) { + document.getElementById('jvm-args').value = r.data.extraJvmArgs || ''; + } + if (r.data.javaPath) { + document.getElementById('java-path-input').value = r.data.javaPath; + } + } else { + document.getElementById('ram-value').textContent = '4 GB'; + document.getElementById('ram-slider').value = '4096'; + } + const sr = await this.req('/instances'); + if (sr.success) { + document.getElementById('server-status').textContent = 'Connected'; + document.getElementById('server-status').style.color = 'var(--success)'; + } else { + document.getElementById('server-status').textContent = 'Disconnected'; + document.getElementById('server-status').style.color = 'var(--error)'; + } + } + + async saveSettings() { + const ram = document.getElementById('ram-slider').value; + const w = parseInt(document.getElementById('win-width').value) || 1280; + const h = parseInt(document.getElementById('win-height').value) || 720; + const jvm = document.getElementById('jvm-args').value.trim(); + const jp = document.getElementById('java-path-input').value.trim(); + await this.req('/settings', { method: 'POST', body: JSON.stringify({ maxMemory: ram, windowWidth: w, windowHeight: h, extraJvmArgs: jvm, javaPath: jp }) }); + } + + async activatePass() { + const code = document.getElementById('pass-code').value.trim(); + if (!code) { this.toast('Enter a pass code', 'error'); return; } + const r = await this.req('/activate-pass', { method: 'POST', body: JSON.stringify({ code }) }); + if (r.success) { + this.toast('Pass activated!', 'success'); + document.getElementById('pass-code').value = ''; + // Refresh account info + const acct = await this.req('/account'); + if (acct.success) { + this.state.account = acct.data; + const status = document.getElementById('account-status'); + status.textContent = acct.data.passActive ? 'PRO' : 'FREE'; + status.className = 'badge ' + (acct.data.passActive ? 'badge-pro' : 'badge-free'); + } + } else { + this.toast(r.error || 'Activation failed', 'error'); + } + } + + // ==================== TOAST ==================== + toast(msg, type = 'info') { + const el = document.getElementById('toast'); + el.textContent = msg; + el.className = 'toast ' + type; + el.classList.remove('hidden'); + clearTimeout(this.toastTimer); + this.toastTimer = setTimeout(() => el.classList.add('hidden'), 3000); + } + + // ==================== LOADING ==================== + showLoading(show) { + document.getElementById('loading-overlay').classList.toggle('hidden', !show); + } + + esc(s) { + if (!s) return ''; + const d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; } } -const app = new LauncherApp(); \ No newline at end of file +const app = new ZernMCLauncher(); diff --git a/launcher/launcher/src/resources/ui/style.css b/launcher/launcher/src/resources/ui/style.css index 667631e..c55d3e1 100644 --- a/launcher/launcher/src/resources/ui/style.css +++ b/launcher/launcher/src/resources/ui/style.css @@ -1,1016 +1,531 @@ :root { - --bg-primary: #0a0a0f; - --bg-secondary: #12121a; - --bg-card: #1a1a24; - --bg-card-hover: #222230; - --bg-sidebar: #0d0d12; - --accent-primary: #e94560; - --accent-secondary: #ff6b6b; - --accent-glow: rgba(233, 69, 96, 0.3); - --text-primary: #ffffff; - --text-secondary: #a0a0b0; - --text-muted: #606070; - --border-color: #2a2a3a; + --bg-deep: #07070a; + --bg-surface: #0c0c12; + --bg-elevated: #111118; + --bg-card: #16161f; + --bg-card-hover: #1c1c28; + --bg-inset: #0a0a0f; + --accent: #e94560; + --accent-glow: rgba(233, 69, 96, 0.25); + --accent-soft: rgba(233, 69, 96, 0.1); + --text: #eeeef0; + --text-secondary: #88889a; + --text-muted: #555566; + --border: #1e1e2a; + --border-light: #2a2a3a; --success: #4ade80; --error: #f87171; --warning: #fbbf24; - --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.4); - --shadow-glow: 0 0 30px var(--accent-glow); - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --transition-fast: 150ms ease; - --transition-normal: 300ms ease; - --transition-slow: 500ms ease; - --base-font-size: 13px; + --info: #60a5fa; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --shadow: 0 4px 24px rgba(0,0,0,0.5); + --shadow-glow: 0 0 40px var(--accent-glow); + --transition: 200ms ease; + --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --mono: 'JetBrains Mono', 'Consolas', monospace; } -html { - font-size: var(--base-font-size); -} +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { font-size: 14px; } body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background: var(--bg-primary); - color: var(--text-primary); + font-family: var(--font); + background: var(--bg-deep); + color: var(--text); min-height: 100vh; overflow: hidden; + -webkit-font-smoothing: antialiased; } -#grid-canvas { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - opacity: 0.12; - pointer-events: none; +#bg-canvas { + position: fixed; inset: 0; width: 100%; height: 100%; + z-index: 0; opacity: 0.08; pointer-events: none; } -#app { - position: relative; - z-index: 1; - min-height: 100vh; -} +#app { position: relative; z-index: 1; height: 100vh; display: flex; } .screen { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - animation: fadeIn var(--transition-slow) forwards; + position: absolute; inset: 0; + display: flex; align-items: center; justify-content: center; + transition: opacity 0.4s ease, transform 0.4s ease; } -.hidden { - display: none !important; -} +.screen.hidden { opacity: 0; transform: scale(0.97); pointer-events: none; } -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} +.hidden { display: none !important; } -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ==================== LOGIN SCREEN ==================== */ +/* ========== LOGIN ========== */ .login-container { - background: var(--bg-card); + background: var(--bg-elevated); + border: 1px solid var(--border); border-radius: var(--radius-lg); - padding: 48px; + padding: 48px 40px 40px; width: 100%; - max-width: 400px; - box-shadow: var(--shadow-card); - border: 1px solid var(--border-color); - animation: slideUp var(--transition-slow) forwards; + max-width: 380px; + box-shadow: var(--shadow); + animation: floatIn 0.5s ease forwards; } -.logo-section { - text-align: center; - margin-bottom: 40px; +@keyframes floatIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } } -.logo-placeholder { - display: inline-block; - margin-bottom: 16px; - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.05); } -} - -.app-title { - font-size: 28px; - font-weight: 700; - margin-bottom: 8px; - background: linear-gradient(135deg, var(--text-primary), var(--accent-primary)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; +.login-brand { text-align: center; margin-bottom: 36px; } +.brand-icon { margin-bottom: 16px; } +.brand-title { + font-size: 28px; font-weight: 800; + background: linear-gradient(135deg, #fff 60%, var(--accent)); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } +.brand-sub { color: var(--text-muted); font-size: 13px; margin-top: 4px; } -.app-version { - color: var(--text-muted); - font-size: 14px; +.login-form { display: flex; flex-direction: column; gap: 20px; } + +.field { position: relative; } +.field label { + position: absolute; top: 50%; left: 14px; transform: translateY(-50%); + font-size: 13px; color: var(--text-muted); + transition: var(--transition); pointer-events: none; + background: var(--bg-elevated); padding: 0 4px; } - -.login-form { - display: flex; - flex-direction: column; - gap: 16px; +.field input:focus + label, +.field input:not(:placeholder-shown) + label { + top: 0; font-size: 11px; color: var(--accent); } - -.input-group input { - width: 100%; - padding: 14px 16px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 16px; - transition: var(--transition-fast); -} - -.input-group input:focus { +.field input { + width: 100%; padding: 14px 14px; font-size: 14px; + background: var(--bg-surface); border: 1px solid var(--border-light); + border-radius: var(--radius-sm); color: var(--text); + font-family: var(--font); transition: var(--transition); outline: none; - border-color: var(--accent-primary); +} +.field input:focus { + border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } - -.input-group input::placeholder { - color: var(--text-muted); +.field select { + width: 100%; padding: 12px 14px; font-size: 14px; + background: var(--bg-surface); border: 1px solid var(--border-light); + border-radius: var(--radius-sm); color: var(--text); + font-family: var(--font); cursor: pointer; outline: none; } +.field select:focus { border-color: var(--accent); } .btn-primary { - width: 100%; - padding: 14px 24px; - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); - border: none; - border-radius: var(--radius-sm); - color: white; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: var(--transition-fast); - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - min-height: 48px; + width: 100%; padding: 14px; border: none; border-radius: var(--radius-sm); + background: linear-gradient(135deg, var(--accent), #ff6b6b); + color: #fff; font-size: 15px; font-weight: 600; cursor: pointer; + font-family: var(--font); transition: var(--transition); + display: flex; align-items: center; justify-content: center; gap: 8px; + min-height: 48px; position: relative; } +.btn-primary:hover { transform: translateY(-1px); box-shadow: var(--shadow-glow); } +.btn-primary:active { transform: translateY(0); } +.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; } -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-glow); +.error-msg { + color: var(--error); font-size: 13px; text-align: center; + padding: 10px; background: rgba(248,113,113,0.1); + border-radius: var(--radius-sm); animation: shake 0.4s ease; } - -.btn-primary:active { - transform: translateY(0); -} - -.btn-primary:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; -} - -.btn-text { - transition: opacity var(--transition-fast); -} - -.btn-text.hidden { - opacity: 0; -} - -.btn-loader { - position: absolute; - width: 20px; - height: 20px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-top-color: white; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.error-message { - color: var(--error); - text-align: center; - font-size: 14px; - padding: 12px; - background: rgba(248, 113, 113, 0.1); - border-radius: var(--radius-sm); - animation: shake 0.5s ease; -} - @keyframes shake { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-5px); } - 75% { transform: translateX(5px); } + 0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)} } -/* ==================== MAIN LAYOUT ==================== */ -.main-layout { - display: grid; - grid-template-columns: 240px 1fr 180px; - grid-template-rows: 1fr auto; - width: 100%; - height: calc(100vh - 40px); - gap: 0; - background: var(--bg-secondary); - border-radius: var(--radius-lg); - border: 1px solid var(--border-color); - overflow: hidden; - animation: fadeIn var(--transition-slow) forwards; +.login-hint { text-align: center; font-size: 12px; color: var(--text-muted); margin-top: 4px; } + +.spinner { + position: absolute; width: 20px; height: 20px; + border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; + border-radius: 50%; animation: spin 0.7s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* ========== OVERLAY ========== */ +.overlay { + position: fixed; inset: 0; background: rgba(7,7,10,0.92); + display: flex; flex-direction: column; align-items: center; justify-content: center; + z-index: 100; animation: fadeIn 0.3s ease; +} +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + +.loader-ring { + width: 48px; height: 48px; + border: 3px solid var(--border-light); border-top-color: var(--accent); + border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 16px; +} +.loader-text { color: var(--text-secondary); font-size: 14px; } + +/* ========== MAIN SHELL ========== */ +.shell { + display: flex; width: 100%; height: 100vh; + background: var(--bg-surface); } -/* Sidebar */ +/* ========== SIDEBAR ========== */ .sidebar { - background: var(--bg-sidebar); - border-right: 1px solid var(--border-color); - display: flex; - flex-direction: column; - padding: 16px; - grid-row: 1; + width: 260px; min-width: 260px; + background: var(--bg-deep); + border-right: 1px solid var(--border); + display: flex; flex-direction: column; + padding: 16px 12px; } -.sidebar-header { - display: flex; - align-items: center; - gap: 12px; - padding-bottom: 20px; - border-bottom: 1px solid var(--border-color); - margin-bottom: 20px; +.sidebar-top { flex: 1; display: flex; flex-direction: column; gap: 20px; overflow: hidden; } + +.sidebar-brand { + display: flex; align-items: center; gap: 10px; + padding: 4px 8px 16px; border-bottom: 1px solid var(--border); +} +.sidebar-brand-text { display: flex; flex-direction: column; } +.sidebar-brand-name { font-size: 16px; font-weight: 700; } +.sidebar-brand-ver { font-size: 11px; color: var(--text-muted); } + +.sidebar-nav { + display: flex; gap: 4px; + padding-bottom: 16px; border-bottom: 1px solid var(--border); +} +.nav-btn { + flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; + padding: 8px; background: transparent; border: 1px solid transparent; + border-radius: var(--radius-sm); color: var(--text-muted); font-size: 11px; + font-weight: 500; cursor: pointer; font-family: var(--font); + transition: var(--transition); +} +.nav-btn:hover { color: var(--text-secondary); background: var(--bg-card); } +.nav-btn.active { color: var(--accent); background: var(--accent-soft); border-color: rgba(233,69,96,0.2); } + +.section-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 8px; padding: 0 4px; +} +.section-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); } + +.pack-list { + display: flex; flex-direction: column; gap: 3px; + overflow-y: auto; max-height: calc((100vh - 460px) / 2); + min-height: 40px; +} +.pack-list:empty::after { + content: 'No packs'; display: block; padding: 12px 8px; + font-size: 12px; color: var(--text-muted); text-align: center; } -.logo-small svg { - display: block; +.pack-entry { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; border-radius: var(--radius-sm); + cursor: pointer; transition: var(--transition); + border: 1px solid transparent; } +.pack-entry:hover { background: var(--bg-card); } +.pack-entry.selected { background: var(--accent-soft); border-color: rgba(233,69,96,0.25); } -.header-info { - display: flex; - flex-direction: column; -} - -.header-title { - font-size: 18px; - font-weight: 700; - color: var(--text-primary); -} - -.header-version { - font-size: 12px; - color: var(--text-muted); -} - -.sidebar-content { - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; - overflow: hidden; -} - -.section-label { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; - color: var(--text-muted); - margin-bottom: 12px; -} - -.current-instance-section { +.pack-entry-icon { + width: 32px; height: 32px; border-radius: 6px; + display: flex; align-items: center; justify-content: center; flex-shrink: 0; } +.pack-entry-icon.server { background: rgba(251,191,36,0.15); color: var(--warning); } +.pack-entry-icon.local { background: var(--accent-soft); color: var(--accent); } -.current-instance { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: 16px; - transition: var(--transition-fast); +.pack-entry-info { flex: 1; min-width: 0; } +.pack-entry-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.pack-entry-meta { font-size: 11px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.btn-icon { + width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; + background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); + color: var(--text-muted); cursor: pointer; transition: var(--transition); flex-shrink: 0; } +.btn-icon:hover { color: var(--text); background: var(--bg-card); border-color: var(--border-light); } -.current-instance:hover { - border-color: var(--accent-primary); +/* Sidebar bottom */ +.sidebar-bottom { padding-top: 12px; border-top: 1px solid var(--border); } +.user-card { + display: flex; align-items: center; gap: 10px; + padding: 8px; border-radius: var(--radius-sm); } - -.instance-card-mini { - display: flex; - flex-direction: column; - gap: 8px; +.user-avatar { + width: 32px; height: 32px; border-radius: 8px; + background: linear-gradient(135deg, var(--accent), #ff6b6b); + display: flex; align-items: center; justify-content: center; + font-weight: 700; font-size: 14px; color: #fff; flex-shrink: 0; } - -.instance-name { - font-size: 16px; - font-weight: 600; - color: var(--text-primary); -} - -.instance-version { - font-size: 13px; - color: var(--accent-primary); - background: rgba(233, 69, 96, 0.15); - padding: 4px 8px; - border-radius: 4px; - display: inline-block; - width: fit-content; -} - -.instances-section { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} - -.instances-list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 8px; -} - -.instance-card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - padding: 12px; - cursor: pointer; - transition: var(--transition-fast); -} - -.instance-card:hover { - background: var(--bg-card-hover); - border-color: var(--accent-primary); -} - -.instance-card.selected { - border-color: var(--accent-primary); - background: rgba(233, 69, 96, 0.1); -} - -.instance-card-name { - font-size: 14px; - font-weight: 600; - margin-bottom: 6px; -} - -.instance-card-details { - display: flex; - gap: 6px; - flex-wrap: wrap; -} - -.instance-card-version, -.instance-card-loader, -.instance-card-server { - font-size: 11px; - padding: 3px 6px; - border-radius: 4px; -} - -.instance-card-version { - background: var(--bg-secondary); - color: var(--text-secondary); -} - -.instance-card-loader { - background: rgba(99, 102, 241, 0.2); - color: #818cf8; -} - -.instance-card-server { - background: rgba(251, 191, 36, 0.2); - color: var(--warning); -} - -.btn-download { - width: 100%; - padding: 16px; - background: var(--bg-card); - border: 1px dashed var(--border-color); - border-radius: var(--radius-md); - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - transition: var(--transition-fast); - flex-shrink: 0; -} - -.btn-download:hover { - background: var(--bg-card-hover); - border-color: var(--accent-primary); - color: var(--accent-primary); -} - -.sidebar-footer { - display: flex; - align-items: center; - gap: 12px; - padding-top: 16px; - border-top: 1px solid var(--border-color); - margin-top: 20px; -} - -.username-display { - font-size: 13px; - color: var(--text-secondary); - flex: 1; -} - -.account-badges { - display: flex; - gap: 6px; -} - +.user-info { flex: 1; min-width: 0; } +.user-name { font-size: 13px; font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.user-badges { display: flex; gap: 4px; margin-top: 2px; } .badge { - padding: 4px 8px; - border-radius: 4px; - font-size: 11px; - font-weight: 500; + font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 3px; + text-transform: uppercase; letter-spacing: 0.5px; +} +.badge-pro { background: rgba(74,222,128,0.15); color: var(--success); } +.badge-free { background: rgba(248,113,113,0.12); color: var(--error); } +.badge-role { background: rgba(96,165,250,0.15); color: var(--info); } + +/* ========== CONTENT ========== */ +.content { + flex: 1; display: flex; flex-direction: column; + padding: 24px 32px; min-width: 0; + position: relative; } -.badge.active { - background: rgba(74, 222, 128, 0.2); - color: var(--success); -} +.view { display: none; flex-direction: column; height: 100%; } +.view.active { display: flex; } -.badge.inactive { - background: rgba(239, 68, 68, 0.2); - color: var(--error); +.view-header { + display: flex; align-items: flex-start; justify-content: space-between; + margin-bottom: 24px; gap: 16px; } +.view-title { font-size: 22px; font-weight: 700; } +.view-subtitle { font-size: 13px; color: var(--text-secondary); margin-top: 4px; } +.view-actions { display: flex; gap: 8px; flex-shrink: 0; } -.role-badge { - background: rgba(99, 102, 241, 0.2); - color: #818cf8; +.btn-secondary { + display: flex; align-items: center; gap: 6px; + padding: 8px 16px; background: var(--bg-card); border: 1px solid var(--border-light); + border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 13px; + font-weight: 500; cursor: pointer; font-family: var(--font); + transition: var(--transition); } +.btn-secondary:hover { background: var(--bg-card-hover); color: var(--text); border-color: var(--border); } +.btn-secondary.btn-danger:hover { color: var(--error); border-color: rgba(248,113,113,0.3); background: rgba(248,113,113,0.08); } -.btn-logout { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-secondary); - cursor: pointer; - transition: var(--transition-fast); +/* ========== PACK DETAIL ========== */ +.pack-detail { flex: 1; display: flex; } +.pack-empty { + flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 12px; color: var(--text-muted); } +.pack-empty h3 { font-size: 18px; font-weight: 600; color: var(--text-secondary); } +.pack-empty p { font-size: 13px; } -.btn-logout:hover { - background: rgba(248, 113, 113, 0.1); - border-color: var(--error); - color: var(--error); +.pack-detail-content { flex: 1; display: flex; flex-direction: column; gap: 24px; } +.pack-hero { display: flex; align-items: center; gap: 16px; } +.pack-icon { + width: 56px; height: 56px; border-radius: var(--radius-md); + background: var(--bg-card); border: 1px solid var(--border-light); + display: flex; align-items: center; justify-content: center; color: var(--accent); } - -.btn-logout#close-btn:hover { - background: rgba(239, 68, 68, 0.2); +.detail-name { font-size: 20px; font-weight: 700; } +.detail-tags { display: flex; gap: 6px; margin-top: 6px; } +.tag { + font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 4px; } +.tag-mc { background: var(--bg-card); color: var(--text-secondary); } +.tag-loader { background: rgba(99,102,241,0.15); color: #818cf8; } +.tag-server { background: rgba(251,191,36,0.15); color: var(--warning); } -/* Main Content - Logs */ -.main-content { - display: flex; - flex-direction: column; - padding: 12px; - background: var(--bg-primary); - height: 100%; - grid-row: 1; +.pack-stats { + display: flex; gap: 24px; padding: 16px; + background: var(--bg-card); border-radius: var(--radius-md); + border: 1px solid var(--border); } +.stat { display: flex; flex-direction: column; gap: 2px; } +.stat-value { font-size: 18px; font-weight: 700; color: var(--text); } +.stat-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; } -.logs-section { - flex: 1; - display: flex; - flex-direction: column; - background: var(--bg-card); +/* ========== PLAY BAR ========== */ +.play-bar { + display: flex; align-items: center; justify-content: space-between; + padding: 16px 20px; margin-top: auto; + background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-md); - border: 1px solid var(--border-color); +} +.play-bar-info { font-size: 14px; font-weight: 500; color: var(--text-secondary); } + +/* ========== PACK DESCRIPTION ========== */ +.pack-description { + padding: 16px; background: var(--bg-card); + border: 1px solid var(--border); border-radius: var(--radius-md); +} +.pack-description-text { + font-size: 13px; color: var(--text-secondary); line-height: 1.6; +} +.pack-gallery { + display: flex; gap: 12px; margin-top: 12px; flex-wrap: wrap; +} +.pack-gallery-item { + width: 120px; height: 80px; border-radius: var(--radius-sm); + background: var(--bg-elevated); border: 1px solid var(--border-light); + display: flex; align-items: center; justify-content: center; + color: var(--text-muted); font-size: 11px; overflow: hidden; - min-height: 0; } - -.logs-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 14px; - border-bottom: 1px solid var(--border-color); -} - -.logs-header h2 { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); -} - -.btn-clear-logs { - padding: 4px 10px; - background: transparent; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-muted); - font-size: 11px; - cursor: pointer; - transition: var(--transition-fast); -} - -.btn-clear-logs:hover { - background: var(--bg-card-hover); - color: var(--text-secondary); -} - -.logs-container { - flex: 1; - padding: 10px 14px; - overflow-y: auto; - font-family: 'JetBrains Mono', 'Consolas', monospace; - font-size: 11px; - line-height: 1.5; -} - -.log-entry { - padding: 2px 0; - color: var(--text-secondary); - animation: fadeIn var(--transition-fast) forwards; -} - -.log-entry.info { - color: var(--text-secondary); -} - -.log-entry.success { - color: var(--success); -} - -.log-entry.warning { - color: var(--warning); -} - -.log-entry.error { - color: var(--error); -} - -/* Right Panel - Play Button */ -.right-panel { - display: flex; - align-items: flex-end; - justify-content: center; - padding: 20px; - border-left: 1px solid var(--border-color); - background: var(--bg-sidebar); - grid-row: 1 / 3; +.pack-gallery-item img { + width: 100%; height: 100%; object-fit: cover; } .btn-play { - width: 100%; - padding: 16px 20px; + display: flex; align-items: center; gap: 8px; + padding: 12px 28px; border: none; border-radius: var(--radius-sm); background: linear-gradient(135deg, var(--success), #22c55e); - border: none; - border-radius: var(--radius-md); - color: #0a0a0f; - font-size: 16px; - font-weight: 700; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - transition: var(--transition-normal); - box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4); + color: #07070a; font-size: 15px; font-weight: 700; cursor: pointer; + font-family: var(--font); transition: var(--transition); + box-shadow: 0 4px 20px rgba(74,222,128,0.35); } +.btn-play:hover:not(:disabled) { transform: translateY(-2px) scale(1.02); box-shadow: 0 8px 32px rgba(74,222,128,0.45); } +.btn-play:active:not(:disabled) { transform: translateY(0); } +.btn-play:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; } -.btn-play:hover:not(:disabled) { - transform: translateY(-4px) scale(1.02); - box-shadow: 0 8px 40px rgba(74, 222, 128, 0.5); +/* ========== NEWS ========== */ +.news-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; overflow-y: auto; padding-bottom: 24px; } - -.btn-play:active { - transform: translateY(0); +.news-card { + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius-md); padding: 24px; display: flex; + flex-direction: column; gap: 12px; transition: var(--transition); } - -.btn-play:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - box-shadow: none; +.news-card:hover { border-color: var(--border-light); } +.news-card-badge { + align-self: flex-start; font-size: 10px; font-weight: 600; text-transform: uppercase; + letter-spacing: 1px; padding: 4px 10px; border-radius: 4px; } +.news-placeholder .news-card-badge { background: var(--accent-soft); color: var(--accent); } +.news-card h3 { font-size: 16px; font-weight: 600; } +.news-card p { font-size: 13px; color: var(--text-secondary); line-height: 1.5; } +.news-card time { font-size: 11px; color: var(--text-muted); margin-top: auto; } -.btn-update { - width: 100%; - padding: 16px 20px; - background: linear-gradient(135deg, var(--warning), #f59e0b); - border: none; - border-radius: var(--radius-md); - color: #1a1a24; - font-size: 16px; - font-weight: 700; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - transition: var(--transition-normal); - box-shadow: 0 4px 20px rgba(251, 191, 36, 0.4); +/* ========== SETTINGS ========== */ +.settings-grid { display: flex; flex-direction: column; gap: 12px; } +.setting-card { + display: flex; align-items: center; justify-content: space-between; + padding: 16px 20px; background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius-md); gap: 24px; } - -.btn-update:hover:not(:disabled) { - transform: translateY(-4px) scale(1.02); - box-shadow: 0 8px 40px rgba(251, 191, 36, 0.5); +.setting-info h4 { font-size: 14px; font-weight: 600; } +.setting-info p { font-size: 12px; color: var(--text-secondary); margin-top: 2px; } +.setting-control { display: flex; align-items: center; gap: 12px; flex-shrink: 0; } +.setting-control input[type="range"] { + width: 160px; height: 4px; -webkit-appearance: none; appearance: none; + background: var(--border); border-radius: 2px; outline: none; } - -.btn-update:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; +.setting-control input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; + background: var(--accent); cursor: pointer; border: 2px solid var(--bg-deep); } +.setting-value { font-size: 14px; font-weight: 600; color: var(--text); min-width: 48px; text-align: right; } +.setting-badge { + font-size: 12px; padding: 4px 10px; border-radius: 4px; + background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-light); +} +.setting-pass { display: flex; align-items: center; gap: 8px; } +.pass-input { + width: 160px; padding: 6px 12px; border-radius: var(--radius-sm); + background: var(--bg-inset); border: 1px solid var(--border-light); + color: var(--text); font-size: 13px; outline: none; +} +.pass-input:focus { border-color: var(--accent); } +.setting-input { + padding: 6px 10px; border-radius: var(--radius-sm); + background: var(--bg-inset); border: 1px solid var(--border-light); + color: var(--text); font-size: 13px; outline: none; font-family: var(--mono); +} +.setting-input:focus { border-color: var(--accent); } +.btn-sm { padding: 6px 14px !important; font-size: 12px !important; } -/* ==================== MODAL ==================== */ +/* ========== MODAL ========== */ +.modal-backdrop { + position: fixed; inset: 0; background: rgba(7,7,10,0.85); + display: flex; align-items: center; justify-content: center; z-index: 50; + animation: fadeIn 0.2s ease; +} .modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(10, 10, 15, 0.9); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fadeIn var(--transition-fast) forwards; + background: var(--bg-elevated); border: 1px solid var(--border); + border-radius: var(--radius-lg); width: 90%; max-width: 480px; + max-height: 85vh; overflow-y: auto; box-shadow: var(--shadow); + animation: floatIn 0.3s ease; } - -.modal-content { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - width: 90%; - max-width: 500px; - max-height: 80vh; - overflow-y: auto; - animation: slideUp var(--transition-normal) forwards; +.modal-head { + display: flex; align-items: center; justify-content: space-between; + padding: 20px 24px; border-bottom: 1px solid var(--border); } - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 24px; - border-bottom: 1px solid var(--border-color); -} - -.modal-header h2 { - font-size: 18px; - font-weight: 600; -} - +.modal-head h3 { font-size: 17px; font-weight: 600; } .modal-close { - width: 32px; - height: 32px; - background: transparent; - border: none; - color: var(--text-muted); - font-size: 24px; - cursor: pointer; - transition: var(--transition-fast); + width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; + background: transparent; border: none; color: var(--text-muted); + font-size: 22px; cursor: pointer; border-radius: var(--radius-sm); transition: var(--transition); +} +.modal-close:hover { color: var(--text); background: var(--bg-card); } + +.modal-body { padding: 20px 24px 24px; } +.modal-tabs { display: flex; gap: 8px; margin-bottom: 20px; } +.modal-tab { + flex: 1; padding: 10px; background: transparent; border: 1px solid var(--border-light); + border-radius: var(--radius-sm); color: var(--text-muted); font-size: 13px; + font-weight: 500; cursor: pointer; font-family: var(--font); transition: var(--transition); +} +.modal-tab.active { background: var(--accent-soft); border-color: rgba(233,69,96,0.3); color: var(--accent); } +.modal-tab:hover:not(.active) { background: var(--bg-card); color: var(--text-secondary); } + +.modal-tab-content { display: none; flex-direction: column; gap: 16px; } +.modal-tab-content.active { display: flex; } +.modal-tab-content .field label { + display: block; font-size: 12px; font-weight: 500; color: var(--text-secondary); + margin-bottom: 6px; position: static; transform: none; + background: none; padding: 0; } -.modal-close:hover { - color: var(--text-primary); +.select-wrap select { + width: 100%; padding: 10px 12px; font-size: 13px; + background: var(--bg-surface); border: 1px solid var(--border-light); + border-radius: var(--radius-sm); color: var(--text); + font-family: var(--font); cursor: pointer; outline: none; } +.select-wrap select:focus { border-color: var(--accent); } -.modal-tabs { - display: flex; - padding: 16px 24px; - gap: 8px; - border-bottom: 1px solid var(--border-color); +.install-progress { padding-top: 16px; border-top: 1px solid var(--border); } +.progress-track { + height: 6px; background: var(--bg-surface); border-radius: 3px; overflow: hidden; } - -.tab-btn { - flex: 1; - padding: 12px; - background: transparent; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-secondary); - font-size: 14px; - cursor: pointer; - transition: var(--transition-fast); -} - -.tab-btn.active { - background: var(--accent-primary); - border-color: var(--accent-primary); - color: white; -} - -.tab-btn:hover:not(.active) { - background: var(--bg-card-hover); -} - -.tab-content { - padding: 24px; - display: none; -} - -.tab-content.active { - display: block; -} - -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - font-size: 13px; - font-weight: 500; - color: var(--text-secondary); - margin-bottom: 8px; -} - -.select-input, .text-input { - width: 100%; - padding: 12px 14px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 14px; - transition: var(--transition-fast); -} - -.select-input:focus, .text-input:focus { - outline: none; - border-color: var(--accent-primary); -} - -.select-input option { - background: var(--bg-secondary); -} - -.btn-install { - width: 100%; - padding: 14px; - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); - border: none; - border-radius: var(--radius-sm); - color: white; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: var(--transition-fast); -} - -.btn-install:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-glow); -} - -.download-progress { - padding: 24px; - border-top: 1px solid var(--border-color); -} - -.progress-bar { - height: 8px; - background: var(--bg-secondary); - border-radius: 4px; - overflow: hidden; - margin-bottom: 12px; -} - .progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); - border-radius: 4px; - width: 0%; - transition: width var(--transition-normal); + height: 100%; width: 0%; + background: linear-gradient(90deg, var(--accent), #ff6b6b); + border-radius: 3px; transition: width 0.3s ease; } +.progress-label { font-size: 13px; color: var(--text-secondary); margin-top: 8px; text-align: center; } -.progress-text { - text-align: center; - color: var(--text-secondary); - font-size: 13px; +/* ========== TOAST ========== */ +.toast { + position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); + padding: 12px 24px; border-radius: var(--radius-sm); + font-size: 13px; font-weight: 500; z-index: 200; + background: var(--bg-elevated); border: 1px solid var(--border); + color: var(--text); box-shadow: var(--shadow); + animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards; } +.toast.error { border-color: rgba(248,113,113,0.3); color: var(--error); } +.toast.success { border-color: rgba(74,222,128,0.3); color: var(--success); } +@keyframes toastIn { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } +@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } } -/* ==================== LOADING ==================== */ -.loading-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(10, 10, 15, 0.9); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fadeIn var(--transition-fast) forwards; +/* ========== SCROLLBAR ========== */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* ========== RESPONSIVE ========== */ +@media (max-width: 900px) { + .sidebar { width: 200px; min-width: 200px; } + .content { padding: 16px; } } - -.loader { - width: 48px; - height: 48px; - border: 3px solid var(--border-color); - border-top-color: var(--accent-primary); - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 16px; +@media (max-width: 700px) { + .sidebar { width: 56px; min-width: 56px; } + .sidebar-brand-text, .sidebar-nav .nav-btn span, + .section-header, .pack-entry-info, .user-info, + .sidebar-bottom .user-card .btn-icon:first-child { display: none; } + .sidebar-brand { justify-content: center; padding: 8px; } + .sidebar-nav { flex-direction: column; } + .nav-btn { padding: 8px; } + .pack-entry { justify-content: center; padding: 8px; } + .content { padding: 12px; } + .play-bar { flex-direction: column; gap: 12px; } + .view-header { flex-direction: column; } } - -/* ==================== RESPONSIVE ==================== */ -@media (max-width: 1024px) { - .main-layout { - grid-template-columns: 240px 1fr 160px; - } -} - -@media (max-width: 768px) { - .main-layout { - grid-template-columns: 1fr; - grid-template-rows: auto 1fr auto; - } - - .sidebar { - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border-right: none; - border-bottom: 1px solid var(--border-color); - } - - .sidebar-header { - padding-bottom: 0; - border-bottom: none; - margin-bottom: 0; - } - - .sidebar-content { - display: none; - } - - .sidebar-footer { - margin-top: 0; - padding-top: 0; - border-top: none; - } - - .right-panel { - padding: 16px; - border-left: none; - border-top: 1px solid var(--border-color); - } -} - -/* ==================== SCROLLBAR ==================== */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--bg-secondary); -} - -::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--text-muted); -} - -/* ==================== CUSTOM DROPDOWN ==================== */ -.custom-dropdown { - position: relative; - width: 100%; -} - -.dropdown-trigger { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 14px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - cursor: pointer; - transition: var(--transition-fast); -} - -.dropdown-trigger:hover { - border-color: var(--accent-primary); -} - -.dropdown-trigger.active { - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px var(--accent-glow); -} - -.dropdown-value { - font-size: 14px; - color: var(--text-primary); -} - -.dropdown-arrow { - font-size: 10px; - color: var(--text-muted); - transition: transform var(--transition-fast); -} - -.dropdown-trigger.active .dropdown-arrow { - transform: rotate(180deg); -} - -.dropdown-list { - position: absolute; - top: 100%; - left: 0; - right: 0; - margin-top: 4px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - max-height: 240px; - overflow-y: auto; - z-index: 100; - display: none; - box-shadow: var(--shadow-card); -} - -.dropdown-list.open { - display: block; - animation: slideUp var(--transition-fast) forwards; -} - -.dropdown-item { - padding: 12px 14px; - font-size: 14px; - color: var(--text-secondary); - cursor: pointer; - transition: var(--transition-fast); - border-bottom: 1px solid var(--border-color); -} - -.dropdown-item:last-child { - border-bottom: none; -} - -.dropdown-item:hover { - background: var(--bg-card-hover); - color: var(--text-primary); -} - -.dropdown-item.selected { - background: rgba(233, 69, 96, 0.15); - color: var(--accent-primary); -} - -.dropdown-search { - padding: 10px; - border-bottom: 1px solid var(--border-color); -} - -.dropdown-search input { - width: 100%; - padding: 10px 12px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 14px; -} - -.dropdown-search input:focus { - outline: none; - border-color: var(--accent-primary); -} \ No newline at end of file