/* ── Burmalda Music — Renderer ──────────────────────────────────────────── */
'use strict';
const API = 'https://nodeshift.space/api';
const CDN = 'https://nodeshift.space';
/* ── State ──────────────────────────────────────────────────────────────── */
let token = null;
let me = null;
let queue = [];
let qi = -1;
let repeatMode = 0; // 0=off 1=all 2=one
let shuffleOn = false;
let likedSet = new Set();
let localTracks = [];
let searching = null;
const aud = document.getElementById('aud');
/* ── Titlebar ─────────────────────────────────────────────────────────────── */
document.getElementById('tb-min').addEventListener('click', () => window.electron.minimize());
document.getElementById('tb-max').addEventListener('click', () => window.electron.maximize());
document.getElementById('tb-close').addEventListener('click', () => window.electron.close());
window.electron.onWindowState(s => {
document.getElementById('tb-max').title = s === 'maximized' ? 'Восстановить' : 'Развернуть';
});
/* ── Auth helpers ─────────────────────────────────────────────────────────── */
function showForm(which) {
document.getElementById('form-login').style.display = which === 'login' ? '' : 'none';
document.getElementById('form-reg').style.display = which === 'reg' ? '' : 'none';
document.getElementById('login-err').textContent = '';
document.getElementById('reg-err').textContent = '';
}
async function initAuth() {
token = await window.electron.storeGet('token');
if (token) {
const ok = await fetchMe();
if (ok) { showApp(); return; }
token = null;
await window.electron.storeDelete('token');
}
document.getElementById('auth-screen').style.display = '';
document.getElementById('app').style.display = 'none';
}
async function fetchMe() {
try {
const r = await apiFetch('GET', '/music/profile/me');
if (!r.ok) return false;
me = await r.json();
return true;
} catch { return false; }
}
function showApp() {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('app').style.display = '';
afterLogin();
}
/* Login submit */
document.getElementById('login-btn').addEventListener('click', async () => {
const btn = document.getElementById('login-btn');
const err = document.getElementById('login-err');
btn.disabled = true; btn.textContent = 'Входим…'; err.textContent = '';
try {
const body = new URLSearchParams({
username: document.getElementById('login-email').value.trim(),
password: document.getElementById('login-pass').value,
});
const r = await fetch(`${API}/auth/token`, { method: 'POST', body });
if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || 'Неверный логин или пароль'); }
const d = await r.json();
token = d.access_token;
await window.electron.storeSet('token', token);
if (!await fetchMe()) throw new Error('Не удалось загрузить профиль');
showApp();
} catch(e) { err.textContent = e.message; }
finally { btn.disabled = false; btn.textContent = 'Войти'; }
});
/* Register submit */
document.getElementById('reg-btn').addEventListener('click', async () => {
const btn = document.getElementById('reg-btn');
const err = document.getElementById('reg-err');
btn.disabled = true; btn.textContent = 'Создаём…'; err.textContent = '';
try {
const email = document.getElementById('reg-email').value.trim();
const pass = document.getElementById('reg-pass').value;
const username = email.split('@')[0].replace(/[^a-z0-9_]/gi, '');
const r = await fetch(`${API}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, username, password: pass }),
});
if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || 'Ошибка регистрации'); }
/* auto-login */
const r2 = await fetch(`${API}/auth/token`, { method: 'POST', body: new URLSearchParams({ username: email, password: pass }) });
if (!r2.ok) throw new Error('Аккаунт создан — теперь войдите.');
const d2 = await r2.json();
token = d2.access_token;
await window.electron.storeSet('token', token);
await fetchMe();
showApp();
} catch(e) { err.textContent = e.message; }
finally { btn.disabled = false; btn.textContent = 'Создать аккаунт'; }
});
function logout() {
token = null; me = null;
window.electron.storeDelete('token');
document.getElementById('auth-screen').style.display = '';
document.getElementById('app').style.display = 'none';
showForm('login');
}
/* ── API fetch ────────────────────────────────────────────────────────────── */
function apiFetch(method, path, body) {
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
if (body) headers['Content-Type'] = 'application/json';
return fetch(API + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
}
/* ── After login ──────────────────────────────────────────────────────────── */
async function afterLogin() {
renderGreeting();
renderUserChip();
await loadLiked();
showPage('home', document.querySelector('.nav-item[data-page="home"]'));
}
/* ── Greeting ─────────────────────────────────────────────────────────────── */
function renderGreeting() {
const h = new Date().getHours();
const greet = h < 6 ? 'Доброй ночи' : h < 12 ? 'Доброе утро' : h < 18 ? 'Добрый день' : 'Добрый вечер';
const el = document.getElementById('greeting');
if (el) el.textContent = greet + (me?.username ? `, ${me.username}` : '') + '!';
loadHomeTracks();
}
/* ── User chip ────────────────────────────────────────────────────────────── */
function renderUserChip() {
if (!me) return;
const av = document.getElementById('user-av');
const nm = document.getElementById('user-name');
const pl = document.getElementById('user-plan');
if (av) {
if (me.avatar_url) {
av.innerHTML = `
`;
} else {
av.textContent = (me.username || me.email || '?')[0].toUpperCase();
}
}
if (nm) nm.textContent = me.username || me.email || '';
if (pl) pl.textContent = me.plan || 'Free';
}
/* ── Navigation ──────────────────────────────────────────────────────────── */
function showPage(page, el) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const p = document.getElementById('page-' + page);
if (p) p.classList.add('active');
if (el) el.classList.add('active');
if (page === 'home') loadHomeTracks();
if (page === 'library') loadLibrary();
if (page === 'liked') renderLiked();
if (page === 'search') document.getElementById('search-input').focus();
if (page === 'local') renderLocalList();
}
/* ── Home ────────────────────────────────────────────────────────────────── */
async function loadHomeTracks() {
const el = document.getElementById('list-home');
if (!el) return;
el.innerHTML = '
';
try {
const r = await apiFetch('GET', '/music/tracks?limit=40');
if (!r.ok) throw new Error();
const d = await r.json();
const tracks = d.tracks || d || [];
renderTrackList(el, tracks);
} catch { el.innerHTML = empty('Не удалось загрузить треки'); }
}
/* ── Library ─────────────────────────────────────────────────────────────── */
async function loadLibrary() {
const el = document.getElementById('list-lib');
if (!el) return;
el.innerHTML = '';
try {
const r = await apiFetch('GET', `/music/profile/${me?.username}/tracks`);
const d = r.ok ? await r.json() : { tracks: [] };
const tracks = d.tracks || [];
const cnt = document.getElementById('cnt-lib');
if (cnt) cnt.textContent = tracks.length ? `${tracks.length} треков` : '';
renderTrackList(el, tracks);
} catch { el.innerHTML = empty('Ошибка загрузки'); }
}
/* ── Liked ───────────────────────────────────────────────────────────────── */
async function loadLiked() {
try {
const r = await apiFetch('GET', '/music/liked');
if (!r.ok) return;
const d = await r.json();
likedSet = new Set((d.tracks || []).map(t => t.id));
} catch {}
}
async function renderLiked() {
const el = document.getElementById('list-liked');
if (!el) return;
el.innerHTML = '';
try {
const r = await apiFetch('GET', '/music/liked');
const d = r.ok ? await r.json() : { tracks: [] };
const tracks = d.tracks || [];
const cnt = document.getElementById('cnt-liked');
if (cnt) cnt.textContent = tracks.length ? `${tracks.length} треков` : '';
renderTrackList(el, tracks);
} catch { el.innerHTML = empty('Ошибка загрузки'); }
}
/* ── Search ──────────────────────────────────────────────────────────────── */
let searchT = null;
function onSearch(q) {
clearTimeout(searchT);
const el = document.getElementById('list-search');
const em = document.getElementById('search-empty');
if (!q.trim()) { if (el) el.innerHTML = ''; if (em) em.style.display = 'none'; return; }
searchT = setTimeout(() => doSearch(q.trim()), 300);
}
async function doSearch(q) {
const el = document.getElementById('list-search');
const em = document.getElementById('search-empty');
if (el) el.innerHTML = '';
if (em) em.style.display = 'none';
try {
const r = await apiFetch('GET', `/music/tracks?q=${encodeURIComponent(q)}&limit=50`);
if (!r.ok) throw new Error();
const d = await r.json();
const tracks = d.tracks || d || [];
if (el) renderTrackList(el, tracks);
if (em) em.style.display = !tracks.length ? 'block' : 'none';
} catch { if (el) el.innerHTML = empty('Ошибка поиска'); }
}
/* ── Local files ──────────────────────────────────────────────────────────── */
async function addLocalFiles() {
const files = await window.electron.openFiles();
ingestLocal(files);
}
async function addLocalFolder() {
const files = await window.electron.openFolder();
ingestLocal(files);
}
function ingestLocal(files) {
if (!files?.length) return;
const mapped = files.map(f => ({ ...f, title: f.name, artist: '', _local: true, id: null }));
localTracks = [...localTracks, ...mapped];
renderLocalList();
toast(`Добавлено ${files.length} файлов`);
}
function renderLocalList() {
const el = document.getElementById('list-local');
const dz = document.getElementById('drop-zone');
if (!el) return;
if (!localTracks.length) {
el.style.display = 'none';
if (dz) dz.style.display = '';
return;
}
if (dz) dz.style.display = 'none';
el.style.display = '';
renderTrackList(el, localTracks);
}
/* ── Drop zone ────────────────────────────────────────────────────────────── */
const dz = document.getElementById('drop-zone');
if (dz) {
dz.addEventListener('click', addLocalFiles);
document.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('over'); });
document.addEventListener('dragleave', e => { if (!e.relatedTarget) dz.classList.remove('over'); });
document.addEventListener('drop', async e => {
e.preventDefault(); dz.classList.remove('over');
const files = Array.from(e.dataTransfer.files)
.filter(f => /\.(mp3|flac|wav|aac|ogg|m4a|opus|wv|ape)$/i.test(f.name))
.map(f => ({ path: f.path, name: f.name.replace(/\.[^.]+$/, ''), ext: f.name.split('.').pop().toUpperCase(), size: f.size }));
ingestLocal(files);
});
}
/* ── Render track list ───────────────────────────────────────────────────── */
function renderTrackList(container, tracks) {
container.innerHTML = '';
if (!tracks.length) { container.innerHTML = empty('Нет треков'); return; }
tracks.forEach((t, i) => container.appendChild(makeRow(t, tracks, i)));
}
function makeRow(t, arr, idx) {
const row = document.createElement('div');
row.className = 'track-row' + (queue[qi]?.id === t.id && t.id ? ' active' : '');
if (t.id) row.dataset.id = t.id;
const isLocal = !!t._local;
const coverSrc = t.cover_url ? (isLocal ? t.cover_url : CDN + t.cover_url) : null;
const isPlaying = queue[qi] && (t.id ? t.id === queue[qi].id : t === queue[qi]);
row.innerHTML = `
${coverSrc
? `
`
: `${svgMusic(18)}
`}
${esc(t.title || t.name || 'Без названия')}
${t.artist
? `${esc(t.artist)}`
: 'Неизвестен'}
${isPlaying ? `
` : ''}
${!isLocal ? `
${fmtDur(t.duration)}` : `
${esc(t.ext||'FILE')}`}
${!isLocal && t.id ? `` : ''}
${!isLocal && t.audio_url ? `` : ''}
`;
row.addEventListener('click', e => {
const act = e.target.closest('[data-act]')?.dataset.act;
if (act === 'like') { toggleLikeTrack(t, row); return; }
if (act === 'dl') { downloadTrack(t); return; }
const al = e.target.closest('.alink');
if (al) { e.stopPropagation(); openArtist(al.dataset.artist); return; }
playFrom(arr, idx);
});
row.addEventListener('contextmenu', e => { e.preventDefault(); showCtx(e, t, arr, idx); });
return row;
}
/* ── Artist page ─────────────────────────────────────────────────────────── */
async function openArtist(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
let pg = document.getElementById('page-artist');
if (!pg) {
pg = document.createElement('section');
pg.className = 'page'; pg.id = 'page-artist';
pg.innerHTML = `
`;
document.querySelector('.main').appendChild(pg);
}
pg.classList.add('active');
document.getElementById('artist-pg-name').textContent = name;
const list = document.getElementById('artist-pg-list');
list.innerHTML = '';
try {
const r = await apiFetch('GET', `/music/tracks?artist=${encodeURIComponent(name)}&limit=100`);
const d = r.ok ? await r.json() : { tracks: [] };
const tracks = d.tracks || d || [];
renderTrackList(list, tracks);
} catch { list.innerHTML = empty('Ошибка загрузки'); }
}
function backFromArtist() {
document.getElementById('page-artist')?.classList.remove('active');
document.getElementById('page-home')?.classList.add('active');
document.querySelector('.nav-item[data-page="home"]')?.classList.add('active');
}
/* ── Playback ────────────────────────────────────────────────────────────── */
async function playFrom(arr, idx) {
queue = arr.slice();
qi = idx;
await playItem();
}
async function playItem() {
if (qi < 0 || qi >= queue.length) return;
const t = queue[qi];
let src;
if (t._local) {
/* local files — play directly, no caching */
src = await window.electron.readFileUrl(t.path);
} else {
if (!t.audio_url) { nextTrack(); return; }
/* CDN tracks — route through encrypted cache */
try {
src = await window.electron.cacheStream({
url: CDN + t.audio_url,
trackId: String(t.id || t.audio_url.replace(/\W+/g, '')),
userToken: token || '',
});
} catch {
/* fallback to direct streaming if cache fails */
src = CDN + t.audio_url;
}
}
aud.src = src;
aud.load();
try { await aud.play(); } catch {}
updatePlayerBar();
updateAllRows();
updateAmbiance();
/* register play stat */
if (t.id) apiFetch('POST', `/music/tracks/${t.id}/play`).catch(() => {});
}
function prevTrack() {
if (aud.currentTime > 3) { aud.currentTime = 0; return; }
if (shuffleOn) { qi = randIdx(); playItem(); return; }
if (qi > 0) { qi--; playItem(); }
else if (repeatMode > 0) { qi = queue.length - 1; playItem(); }
}
function nextTrack() {
if (shuffleOn) { qi = randIdx(); playItem(); return; }
if (qi < queue.length - 1) { qi++; playItem(); return; }
if (repeatMode === 1) { qi = 0; playItem(); return; }
/* stop */
updatePlayBtn();
}
function randIdx() {
if (queue.length <= 1) return 0;
let n; do { n = Math.floor(Math.random() * queue.length); } while (n === qi);
return n;
}
aud.addEventListener('ended', () => {
if (repeatMode === 2) { aud.currentTime = 0; aud.play(); return; }
nextTrack();
});
function togglePlay() {
if (!queue[qi]) return;
aud.paused ? aud.play() : aud.pause();
}
function toggleShuffle() {
shuffleOn = !shuffleOn;
document.getElementById('btn-shuf')?.classList.toggle('on', shuffleOn);
document.getElementById('fs-shuf')?.classList.toggle('on', shuffleOn);
}
function cycleRepeat() {
repeatMode = (repeatMode + 1) % 3;
const labels = ['Повтор выкл', 'Повторять всё', 'Повторять трек'];
document.getElementById('btn-rep')?.classList.toggle('on', repeatMode > 0);
document.getElementById('fs-rep')?.classList.toggle('on', repeatMode > 0);
if (document.getElementById('btn-rep')) document.getElementById('btn-rep').title = labels[repeatMode];
if (document.getElementById('fs-rep')) document.getElementById('fs-rep').title = labels[repeatMode];
}
aud.addEventListener('play', updatePlayBtn);
aud.addEventListener('pause', updatePlayBtn);
/* ── Player bar UI ────────────────────────────────────────────────────────── */
function updatePlayerBar() {
const t = queue[qi];
if (!t) return;
const isLocal = !!t._local;
const coverSrc = t.cover_url ? (isLocal ? t.cover_url : CDN + t.cover_url) : null;
const pc = document.getElementById('player-cover');
const fc = document.getElementById('fs-cover');
const fb = document.getElementById('fs-bg');
if (pc) pc.innerHTML = coverSrc
? `
`
: `${svgMusic(20)}
`;
if (fc) fc.innerHTML = coverSrc
? `
`
: `${svgMusic(40)}
`;
if (fb && coverSrc) fb.style.backgroundImage = `url(${coverSrc})`;
else if (fb) fb.style.backgroundImage = '';
document.getElementById('player-title').textContent = t.title || t.name || 'Без названия';
document.getElementById('player-artist').textContent = t.artist || '—';
document.getElementById('fs-title').textContent = t.title || t.name || '—';
document.getElementById('fs-artist').textContent = t.artist || '';
document.getElementById('fs-album')?.setAttribute('textContent', t.album || '');
updateLikeBtn();
updatePlayBtn();
/* vinyl spin */
const vinyl = document.getElementById('fs-vinyl');
if (vinyl) vinyl.classList.toggle('spinning', !aud.paused);
}
function updatePlayBtn() {
const playing = !aud.paused;
const vinyl = document.getElementById('fs-vinyl');
if (vinyl) vinyl.classList.toggle('spinning', playing);
[
{ play: document.getElementById('ico-play'), pause: document.getElementById('ico-pause') },
{ play: document.getElementById('fs-ico-play'), pause: document.getElementById('fs-ico-pause') },
].forEach(({ play, pause }) => {
if (!play || !pause) return;
play.style.display = playing ? 'none' : '';
pause.style.display = playing ? '' : 'none';
});
}
function updateLikeBtn() {
const t = queue[qi];
const isLiked = t?.id && likedSet.has(t.id);
document.getElementById('player-like')?.classList.toggle('liked', !!isLiked);
}
function updateAllRows() {
const cur = queue[qi];
document.querySelectorAll('.track-row').forEach(row => {
row.classList.toggle('active', !!cur && (cur.id ? row.dataset.id == cur.id : false));
});
}
function updateAmbiance() {
const t = queue[qi];
const ba1 = document.getElementById('ba1');
const ba2 = document.getElementById('ba2');
if (!ba1 || !ba2 || !t?.cover_url) { return; }
const src = t._local ? t.cover_url : CDN + t.cover_url;
ba1.style.backgroundImage = `url(${src})`;
ba2.style.backgroundImage = `url(${src})`;
ba1.style.opacity = '0.18';
ba2.style.opacity = '0.12';
}
/* ── Progress / time ─────────────────────────────────────────────────────── */
aud.addEventListener('timeupdate', () => {
if (!aud.duration) return;
const pct = aud.currentTime / aud.duration;
const v = Math.round(pct * 1000);
const pf = document.getElementById('prog-fill');
if (pf) pf.style.width = (pct * 100) + '%';
const ps = document.getElementById('prog-seek');
if (ps && !ps.matches(':active')) ps.value = v;
const ff = document.getElementById('fs-fill');
if (ff) ff.style.width = (pct * 100) + '%';
const fs = document.querySelector('.fs-seek');
if (fs && !fs.matches(':active')) fs.value = v;
document.getElementById('t-cur').textContent = fmtDur(aud.currentTime);
document.getElementById('fs-cur').textContent = fmtDur(aud.currentTime);
if (aud.duration) {
const rem = aud.duration - aud.currentTime;
document.getElementById('t-dur').textContent = fmtDur(aud.duration);
document.getElementById('fs-dur').textContent = fmtDur(rem);
}
});
aud.addEventListener('loadedmetadata', () => {
document.getElementById('t-dur').textContent = fmtDur(aud.duration);
document.getElementById('fs-dur').textContent = fmtDur(aud.duration);
});
/* buffer progress */
aud.addEventListener('progress', () => {
if (!aud.duration || !aud.buffered.length) return;
const pct = aud.buffered.end(aud.buffered.length - 1) / aud.duration;
const pb = document.getElementById('prog-buf');
if (pb) pb.style.width = (pct * 100) + '%';
const fb2 = document.getElementById('fs-buf');
if (fb2) fb2.style.width = (pct * 100) + '%';
});
function seekTo(val) {
if (!aud.duration) return;
aud.currentTime = (val / 1000) * aud.duration;
}
function setVolume(val) {
aud.volume = val / 100;
}
/* ── Fullscreen ───────────────────────────────────────────────────────────── */
function toggleFullscreen() {
document.getElementById('fullscreen').classList.toggle('open');
}
/* ── Like ──────────────────────────────────────────────────────────────────── */
async function toggleLikeTrack(t, row) {
if (!t.id) return;
const was = likedSet.has(t.id);
try {
if (was) { await apiFetch('DELETE', `/music/liked/${t.id}`); likedSet.delete(t.id); }
else { await apiFetch('POST', `/music/liked/${t.id}`); likedSet.add(t.id); }
const btn = row.querySelector('[data-act="like"]');
if (btn) btn.classList.toggle('liked', likedSet.has(t.id));
updateLikeBtn();
} catch {}
}
function toggleLike() {
const t = queue[qi];
if (!t) return;
const row = document.querySelector(`.track-row[data-id="${t.id}"]`) || { querySelector: () => null };
toggleLikeTrack(t, row);
}
/* ── Download ─────────────────────────────────────────────────────────────── */
async function downloadTrack(t) {
if (!t.audio_url) { toast('Нет аудиофайла'); return; }
toast(`Скачиваем ${t.title || 'трек'}…`);
try {
const ext = t.audio_url.split('.').pop().split('?')[0] || 'mp3';
const safe = s => String(s).replace(/[\\/:*?"<>|]/g, '');
const filename = `${safe(t.artist || 'Unknown')} - ${safe(t.title || 'track')}.${ext}`;
const dest = await window.electron.downloadTrack({ url: CDN + t.audio_url, filename });
toast(`Сохранено: ${dest.replace(/.*[\\/]/, '')}`);
} catch(e) { toast('Ошибка: ' + e); }
}
/* ── Queue panel ─────────────────────────────────────────────────────────── */
function toggleQueue() {
const qp = document.getElementById('queue-panel');
const open = qp.classList.toggle('open');
if (open) renderQueuePanel();
}
function renderQueuePanel() {
const list = document.getElementById('qp-list');
if (!list) return;
list.innerHTML = '';
queue.forEach((t, i) => {
const el = document.createElement('div');
el.className = 'track-row' + (i === qi ? ' active' : '');
el.style.fontSize = '12px';
const name = (t.title || t.name || 'Без названия');
el.innerHTML = `${svgMusic(14)}
${esc(name)}
${esc(t.artist||'')}
`;
el.addEventListener('click', () => { qi = i; playItem(); });
list.appendChild(el);
});
}
/* ── Context menu ─────────────────────────────────────────────────────────── */
let ctxEl = null;
function showCtx(e, t, arr, idx) {
if (ctxEl) ctxEl.remove();
const m = document.createElement('div');
m.className = 'ctx';
m.style.left = e.clientX + 'px';
m.style.top = e.clientY + 'px';
const items = [
{ label: 'Воспроизвести', fn: () => playFrom(arr, idx) },
{ label: 'Добавить в очередь', fn: () => { queue.push(t); toast('Добавлено в очередь'); } },
...(t.id ? [{ sep: true }, { label: likedSet.has(t.id) ? 'Убрать из любимых' : 'В любимые', fn: () => toggleLikeTrack(t, m) }] : []),
...(t.audio_url ? [{ label: 'Скачать', fn: () => downloadTrack(t) }] : []),
...(t.artist ? [{ sep: true }, { label: 'Все треки: ' + t.artist, fn: () => openArtist(t.artist) }] : []),
];
items.forEach(item => {
if (item.sep) { const s = document.createElement('div'); s.className = 'ctx-sep'; m.appendChild(s); return; }
const el = document.createElement('div');
el.className = 'ctx-item';
el.textContent = item.label;
el.addEventListener('click', () => { item.fn(); m.remove(); ctxEl = null; });
m.appendChild(el);
});
document.body.appendChild(m);
ctxEl = m;
/* keep in viewport */
const r = m.getBoundingClientRect();
if (r.right > window.innerWidth) m.style.left = (e.clientX - r.width) + 'px';
if (r.bottom > window.innerHeight) m.style.top = (e.clientY - r.height) + 'px';
setTimeout(() => document.addEventListener('click', () => { m.remove(); ctxEl = null; }, { once: true }), 0);
}
/* ── Media keys ───────────────────────────────────────────────────────────── */
window.electron.onMediaKey(key => {
if (key === 'playpause') togglePlay();
if (key === 'next') nextTrack();
if (key === 'prev') prevTrack();
});
/* ── Keyboard ─────────────────────────────────────────────────────────────── */
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT') return;
if (e.code === 'Space') { e.preventDefault(); togglePlay(); }
if (e.code === 'ArrowRight') { e.preventDefault(); aud.currentTime += 5; }
if (e.code === 'ArrowLeft') { e.preventDefault(); aud.currentTime -= 5; }
if (e.code === 'ArrowUp') { e.preventDefault(); aud.volume = Math.min(1, aud.volume + .05); syncVolSlider(); }
if (e.code === 'ArrowDown') { e.preventDefault(); aud.volume = Math.max(0, aud.volume - .05); syncVolSlider(); }
if (e.code === 'Escape') { document.getElementById('fullscreen')?.classList.remove('open'); }
});
function syncVolSlider() {
const vs = document.getElementById('vol-slider');
if (vs) vs.value = Math.round(aud.volume * 100);
}
/* ── Toast ────────────────────────────────────────────────────────────────── */
function toast(msg, dur = 3500) {
let wrap = document.getElementById('toast-wrap');
if (!wrap) {
wrap = document.createElement('div');
wrap.id = 'toast-wrap';
document.body.appendChild(wrap);
}
const el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
wrap.appendChild(el);
setTimeout(() => el.remove(), dur);
}
/* ── Helpers ──────────────────────────────────────────────────────────────── */
function fmtDur(s) {
if (!s || isNaN(s)) return '--:--';
const m = Math.floor(s / 60), sec = Math.floor(s % 60);
return m + ':' + String(sec).padStart(2, '0');
}
function esc(s) {
return String(s)
.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
function empty(txt) {
return `${svgMusic(40)}
${esc(txt)}
`;
}
/* ── SVG icons ────────────────────────────────────────────────────────────── */
function svgMusic(s=22){return``;}
function svgHeart() {return``;}
function svgDl() {return``;}
function svgChevronL() {return``;}
/* ── Init ──────────────────────────────────────────────────────────────────── */
initAuth();