+
+
+
+
+
-
diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js
index 18f9b2d..72bf2fa 100644
--- a/launcher/launcher/src/resources/ui/launcher.js
+++ b/launcher/launcher/src/resources/ui/launcher.js
@@ -6,7 +6,13 @@ const LOCALES = {
'login.title': 'Sign In', 'login.username': 'Username', 'login.password': 'Password',
'login.subtitle': 'Welcome back to ZernMC',
'login.hint': 'Need an account?',
- 'login.autoHint': "It's free \u2014 just sign in",
+ 'login.register': 'Register',
+ 'login.registerTitle': 'Create Account',
+ 'login.registerSubtitle': 'Join ZernMC today',
+ 'login.hasAccount': 'Already have an account?',
+ 'login.confirm': 'Confirm Password',
+ 'login.passMismatch': 'Passwords do not match',
+ 'login.passTooShort': 'Password must be at least 3 characters',
'login.signingIn': 'Signing in...',
'loading.text': 'Loading...',
'sidebar.serverPacks': 'Server Packs', 'sidebar.localPacks': 'Local Packs',
@@ -184,7 +190,13 @@ const LOCALES = {
'login.title': 'Вход', 'login.username': 'Логин', 'login.password': 'Пароль',
'login.subtitle': 'С возвращением в ZernMC',
'login.hint': 'Нет аккаунта?',
- 'login.autoHint': 'Это бесплатно \u2014 просто войдите',
+ 'login.register': 'Регистрация',
+ 'login.registerTitle': 'Создать аккаунт',
+ 'login.registerSubtitle': 'Присоединяйтесь к ZernMC',
+ 'login.hasAccount': 'Уже есть аккаунт?',
+ 'login.confirm': 'Подтвердите пароль',
+ 'login.passMismatch': 'Пароли не совпадают',
+ 'login.passTooShort': 'Пароль должен быть минимум 3 символа',
'login.signingIn': 'Вход...',
'loading.text': 'Загрузка...',
'sidebar.serverPacks': 'Серверные сборки', 'sidebar.localPacks': 'Локальные сборки',
@@ -419,28 +431,43 @@ class ZernMCLauncher {
initBg() {
const c = document.getElementById('bg-canvas');
const ctx = c.getContext('2d');
- let mx = 0, my = 0, ox = 0, oy = 0;
+ let t = 0, mode = 'login', modePulse = 0;
+
+ this.setWaveMode = m => { mode = m; modePulse = 1; };
+
+ const resize = () => { c.width = window.innerWidth; c.height = window.innerHeight; };
+ window.addEventListener('resize', resize);
- 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();
- };
- window.addEventListener('resize', resize);
- window.addEventListener('mousemove', e => {
- mx = (e.clientX / innerWidth - 0.5) * 2;
- my = (e.clientY / innerHeight - 0.5) * 2;
- });
- const anim = () => {
- ox += (mx * 0.3 - ox) * 0.04;
- oy += (my * 0.3 - oy) * 0.04;
- draw();
- requestAnimationFrame(anim);
+ const w = c.width, h = c.height;
+ modePulse += (0 - modePulse) * 0.03;
+
+ const baseAlpha = 0.04 + modePulse * 0.03;
+ const amp = 20 + modePulse * 10;
+ const freq = 0.008 + modePulse * 0.003;
+ const speed = 0.008;
+ const lines = 5;
+
+ for (let i = 0; i < lines; i++) {
+ const yOff = (h / (lines + 1)) * (i + 1);
+ const alpha = baseAlpha * (1 - i * 0.12);
+ ctx.beginPath();
+ for (let x = 0; x <= w; x += 2) {
+ const wave = Math.sin(x * freq + t * speed + i * 1.8) * amp;
+ const wave2 = Math.sin(x * freq * 0.5 + t * speed * 0.7 + i * 2.5) * amp * 0.5;
+ const y = yOff + wave + wave2;
+ x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
+ }
+ ctx.strokeStyle = `rgba(233, 69, 96, ${alpha})`;
+ ctx.lineWidth = 1.2;
+ ctx.stroke();
+ }
+
+ t++;
};
+
+ const anim = () => { draw(); requestAnimationFrame(anim); };
resize();
anim();
}
@@ -513,30 +540,37 @@ class ZernMCLauncher {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
- console.log('handleLogin: attempting login for', username);
const errEl = document.getElementById('login-error');
const btn = document.getElementById('login-btn');
- const label = btn.querySelector('.btn-label');
+ const label = document.getElementById('login-btn-label');
const spinner = btn.querySelector('.spinner');
if (!username || !password) { this.showLoginError(t('toast.enterCredentials')); return; }
+ if (this._registerMode) {
+ const confirm = document.getElementById('confirm-password').value;
+ if (password !== confirm) { this.showLoginError(t('login.passMismatch')); return; }
+ if (password.length < 3) { this.showLoginError(t('login.passTooShort')); return; }
+ }
+
btn.disabled = true;
label.textContent = t('login.signingIn');
spinner.classList.remove('hidden');
- const r = await this.req('/login', { method: 'POST', body: JSON.stringify({ username, password }) });
+ const endpoint = this._registerMode ? '/register' : '/login';
+ const r = await this.req(endpoint, { method: 'POST', body: JSON.stringify({ username, password }) });
btn.disabled = false;
- label.textContent = t('login.title');
+ label.textContent = this._registerMode ? t('login.register') : t('login.title');
spinner.classList.add('hidden');
if (r.success) {
this.state.account = r.data;
this.enterMain();
- this.toast(tr('toast.welcome', null, {username: r.data.username}), 'success');
+ const key = this._registerMode ? 'toast.accountCreated' : 'toast.welcome';
+ this.toast(tr(key, null, {username: r.data.username}), 'success');
} else {
- if (r.error && (r.error.includes('not found') || r.error.includes('Invalid'))) {
+ if (!this._registerMode && r.error && (r.error.includes('not found') || r.error.includes('Invalid'))) {
var reg = await this.req('/register', { method: 'POST', body: JSON.stringify({ username, password }) });
if (reg.success) {
this.state.account = reg.data;
@@ -549,6 +583,45 @@ class ZernMCLauncher {
}
}
+ toggleMode() {
+ this._registerMode = !this._registerMode;
+ const isReg = this._registerMode;
+ const container = document.getElementById('login-container');
+ const title = document.getElementById('login-title');
+ const subtitle = document.getElementById('login-subtitle');
+ const btnLabel = document.getElementById('login-btn-label');
+ const footerHint = document.getElementById('footer-hint');
+ const footerAction = document.getElementById('footer-action');
+ const confirmField = document.getElementById('confirm-field');
+
+ container.style.transform = 'translateY(-4px)';
+ container.style.opacity = '0.6';
+
+ setTimeout(() => {
+ title.textContent = isReg ? t('login.registerTitle') : t('login.title');
+ subtitle.textContent = isReg ? t('login.registerSubtitle') : t('login.subtitle');
+ btnLabel.textContent = isReg ? t('login.register') : t('login.title');
+ footerHint.textContent = isReg ? t('login.hasAccount') : t('login.hint');
+ footerAction.textContent = isReg ? t('login.title') : t('login.register');
+
+ if (isReg) {
+ confirmField.classList.remove('hidden');
+ document.getElementById('confirm-password').required = true;
+ } else {
+ confirmField.classList.add('hidden');
+ document.getElementById('confirm-password').required = false;
+ document.getElementById('confirm-password').value = '';
+ }
+
+ document.getElementById('login-error').classList.add('hidden');
+
+ container.style.transform = '';
+ container.style.opacity = '';
+
+ if (this.setWaveMode) this.setWaveMode(isReg ? 'register' : 'login');
+ }, 150);
+ }
+
showLoginError(msg) {
const el = document.getElementById('login-error');
el.textContent = msg;
@@ -604,6 +677,7 @@ class ZernMCLauncher {
// ==================== NAV ====================
bindEvents() {
document.getElementById('login-form').addEventListener('submit', e => this.handleLogin(e));
+ document.getElementById('login-footer').addEventListener('click', () => this.toggleMode());
document.getElementById('logout-btn').addEventListener('click', () => this.logout());
document.getElementById('settings-btn').addEventListener('click', () => this.switchView('settings'));
diff --git a/launcher/launcher/src/resources/ui/style.css b/launcher/launcher/src/resources/ui/style.css
index e8caeab..4d9bf59 100644
--- a/launcher/launcher/src/resources/ui/style.css
+++ b/launcher/launcher/src/resources/ui/style.css
@@ -59,21 +59,26 @@ body {
/* ========== LOGIN ========== */
.login-container {
+ position: relative; z-index: 1;
background: var(--bg-elevated);
border-radius: var(--radius-md);
padding: 32px 32px 24px;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 40px rgba(0,0,0,0.5);
+ transition: transform 0.35s ease, opacity 0.35s ease;
}
-.login-header { text-align: center; margin-bottom: 28px; }
+.login-header { text-align: center; margin-bottom: 28px; overflow: hidden; }
.brand-icon { margin-bottom: 12px; }
.login-title {
font-size: 22px; font-weight: 700;
- color: var(--text);
+ color: var(--text); transition: transform 0.3s ease, opacity 0.3s ease;
+}
+.login-subtitle {
+ color: var(--text-muted); font-size: 13px; margin-top: 4px;
+ transition: transform 0.3s ease, opacity 0.3s ease;
}
-.login-subtitle { color: var(--text-muted); font-size: 13px; margin-top: 4px; }
.login-form { display: flex; flex-direction: column; gap: 16px; }
@@ -101,6 +106,11 @@ body {
font-family: var(--font); cursor: pointer; outline: none;
}
.field select:focus { border-color: var(--accent); }
+#confirm-field {
+ transition: opacity 0.3s ease, max-height 0.3s ease, margin 0.3s ease;
+ overflow: hidden; max-height: 60px; opacity: 1; margin: 0;
+}
+#confirm-field.hidden { max-height: 0; opacity: 0; margin: 0; display: block !important; padding: 0; overflow: hidden; pointer-events: none; }
.btn-primary {
width: 100%; padding: 14px; border: none; border-radius: 4px;