refactor: split ui/ui.js into 5 focused modules
settings.js — settings modal, romaji overrides, sidebar, theme, nav mode modals.js — context menu, playlist-add modal, confirm dialog, edit-playlist modal metadata.js — song info/edit modal, MusicBrainz/iTunes autofill, saveMetadata, deleteSong notify.js — notification panel, bell badge, _addNotif, _notifNavigate downloads.js — download modal, Spotify download, jobs panel, startArtistSortFix ui.js — barrel re-export; all external imports unchanged player.js left unsplit: shared private state (_nativePlaying, _playGen, _queueGen, etc.) spans every section — splitting would require exporting internal state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a2e5ea495
commit
0663b1c923
6 files changed
+1125
-1137
No files matched your search
@@ -0,0 +1,236 @@
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
import { api, coverUrl, ndAuth } from '../api.js';
|
||||
// Circular — safe (only used inside function bodies)
|
||||
import { playQueue, dlSongLocal, nativeInsertAt, renderQueue } from '../player/player.js';
|
||||
import { navigate, loadPlaylists, loadPlaylistDetail } from '../views/views.js';
|
||||
|
||||
// ─── CONTEXT MENU ───
|
||||
export function showCtxMenu(e, song, list, idx) {
|
||||
const menu = document.getElementById('ctx-menu');
|
||||
menu.innerHTML = '';
|
||||
const items = [
|
||||
{ icon: '▶', label: 'Abspielen', fn: () => playQueue(list, idx) },
|
||||
{ icon: '+', label: 'Als Nächstes einfügen', fn: () => {
|
||||
const insertIdx = S.qIdx + 1;
|
||||
S.queue.splice(insertIdx, 0, {...song});
|
||||
nativeInsertAt(song, insertIdx);
|
||||
renderQueue(); toast('In Warteschlange eingefügt');
|
||||
}},
|
||||
{ icon: '⬇', label: 'Am Ende der Queue', fn: () => {
|
||||
S.queue.push({...song});
|
||||
nativeInsertAt(song, -1);
|
||||
renderQueue(); toast('Zur Queue hinzugefügt');
|
||||
}},
|
||||
{ sep: true },
|
||||
{ icon: '🎶', label: 'Zu Playlist hinzufügen →', submenu: true, fn: () => openPlModal([song]) },
|
||||
{ icon: '⭐', label: song._starred ? 'Aus Favoriten entfernen' : 'Zu Favoriten', fn: () => {
|
||||
api(song._starred ? 'unstar' : 'star', `&id=${song.id}`)
|
||||
.then(() => { song._starred = !song._starred; toast(song._starred ? '⭐ Favorit hinzugefügt' : 'Favorit entfernt'); })
|
||||
.catch(err => toast('Fehler: ' + err.message));
|
||||
}},
|
||||
{ sep: true },
|
||||
{ icon: '💿', label: 'Zum Album', fn: () => song.albumId && navigate('album-detail', { id: song.albumId, name: song.album||'', artist: song.artist||'', artistId: song.artistId||'', coverArt: song.coverArt||'' }) },
|
||||
{ icon: 'ℹ', label: 'Song-Info', fn: () => import('./metadata.js').then(m => m.showSongInfo(song)) },
|
||||
{ icon: '⬇', label: 'Herunterladen', fn: () => dlSongLocal(song) },
|
||||
];
|
||||
items.forEach(item => {
|
||||
if(item.sep) { const d=document.createElement('div'); d.className='ctx-separator'; menu.appendChild(d); return; }
|
||||
const el = document.createElement('div'); el.className = 'ctx-item';
|
||||
el.innerHTML = `<span style="font-size:14px;width:18px;text-align:center">${item.icon}</span><span>${item.label}</span>`;
|
||||
el.onclick = () => { hideCtxMenu(); item.fn(); };
|
||||
menu.appendChild(el);
|
||||
});
|
||||
const vw=window.innerWidth, vh=window.innerHeight;
|
||||
let x=e.clientX, y=e.clientY;
|
||||
menu.style.left='-9999px'; menu.style.top='-9999px'; menu.style.display='block';
|
||||
const w=menu.offsetWidth, h=menu.offsetHeight;
|
||||
if(x+w>vw-8) x=vw-w-8;
|
||||
if(y+h>vh-8) y=vh-h-8;
|
||||
menu.style.left=x+'px'; menu.style.top=y+'px';
|
||||
}
|
||||
export function hideCtxMenu() {
|
||||
const m = document.getElementById('ctx-menu');
|
||||
if (!m) return;
|
||||
m.style.display='none'; m.innerHTML='';
|
||||
}
|
||||
document.addEventListener('click', e => {
|
||||
const m = document.getElementById('ctx-menu');
|
||||
if (m && !m.contains(e.target)) hideCtxMenu();
|
||||
});
|
||||
document.addEventListener('scroll', () => {
|
||||
const m = document.getElementById('ctx-menu');
|
||||
if (m && m.style.display !== 'none') hideCtxMenu();
|
||||
}, { passive: true, capture: true });
|
||||
document.addEventListener('touchmove', () => {
|
||||
const m = document.getElementById('ctx-menu');
|
||||
if (m && m.style.display !== 'none') hideCtxMenu();
|
||||
}, { passive: true, capture: true });
|
||||
document.addEventListener('keydown', e => { if(e.key==='Escape') hideCtxMenu(); });
|
||||
|
||||
// ─── PLAYLIST MODAL (Add to / Create) ───
|
||||
export async function openPlModal(songs) {
|
||||
S.plModalSongs = songs;
|
||||
document.getElementById('pl-modal-title').textContent =
|
||||
songs.length===1 ? `"${songs[0].title}" zu Playlist` : `${songs.length} Songs zu Playlist`;
|
||||
document.getElementById('pl-new-name').value = '';
|
||||
const list = document.getElementById('pl-list');
|
||||
list.innerHTML = '<div style="font-size:12px;color:var(--text3);padding:8px">Lädt…</div>';
|
||||
document.getElementById('pl-modal').classList.add('open');
|
||||
try {
|
||||
const r = await api('getPlaylists');
|
||||
S.cachedPlaylists = r.playlists?.playlist || [];
|
||||
list.innerHTML = '';
|
||||
if(!S.cachedPlaylists.length) {
|
||||
list.innerHTML = '<div style="font-size:12px;color:var(--text3);padding:8px">Noch keine Playlists vorhanden</div>';
|
||||
return;
|
||||
}
|
||||
S.cachedPlaylists.forEach(pl => {
|
||||
const row = document.createElement('div'); row.className = 'pl-row';
|
||||
row.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/></svg>
|
||||
<span>${esc(pl.name)}</span><span style="margin-left:auto;font-size:11px;color:var(--text3)">${pl.songCount} Titel</span>`;
|
||||
row.onclick = () => addSongsToPlaylist(pl.id, pl.name);
|
||||
list.appendChild(row);
|
||||
});
|
||||
} catch(e) { list.innerHTML = `<div style="color:var(--danger);font-size:12px;padding:8px">${e.message}</div>`; }
|
||||
}
|
||||
export function closePlModal() {
|
||||
document.getElementById('pl-modal').classList.remove('open');
|
||||
S.plModalSongs = [];
|
||||
}
|
||||
export async function addSongsToPlaylist(plId, plName) {
|
||||
const songs = [...S.plModalSongs];
|
||||
closePlModal();
|
||||
try {
|
||||
const ids = songs.map(s => `&songIdToAdd=${s.id}`).join('');
|
||||
await api('updatePlaylist', `&playlistId=${plId}${ids}`);
|
||||
toast(`✅ ${songs.length===1 ? songs[0].title : songs.length+' Songs'} → ${plName}`);
|
||||
} catch(e) { toast('Fehler: ' + e.message); }
|
||||
}
|
||||
export async function createAndAddPlaylist() {
|
||||
const name = document.getElementById('pl-new-name').value.trim();
|
||||
if(!name) { document.getElementById('pl-new-name').focus(); return; }
|
||||
closePlModal();
|
||||
try {
|
||||
const ids = S.plModalSongs.map(s => `&songId=${s.id}`).join('');
|
||||
await api('createPlaylist', `&name=${encodeURIComponent(name)}${ids}`);
|
||||
toast(`✅ Playlist "${name}" erstellt`);
|
||||
if(document.getElementById('page-playlists').classList.contains('active')) loadPlaylists();
|
||||
} catch(e) { toast('Fehler: ' + e.message); }
|
||||
}
|
||||
export function createPlaylistPrompt() {
|
||||
document.getElementById('pl-new-name').value = '';
|
||||
S.plModalSongs = [];
|
||||
document.getElementById('pl-modal-title').textContent = 'Neue Playlist erstellen';
|
||||
document.getElementById('pl-list').innerHTML = '<div style="font-size:12px;color:var(--text3);padding:8px 0">Füge Songs später per Rechtsklick hinzu.</div>';
|
||||
document.getElementById('pl-modal').classList.add('open');
|
||||
}
|
||||
document.addEventListener('click', e => {
|
||||
const m = document.getElementById('pl-modal');
|
||||
if(m.classList.contains('open') && e.target===m) closePlModal();
|
||||
});
|
||||
|
||||
// ─── CONFIRM DIALOG ───
|
||||
let _confirmResolve = null;
|
||||
export function _confirmOk() { document.getElementById('confirm-overlay').classList.remove('open'); _confirmResolve?.(true); _confirmResolve = null; }
|
||||
export function _confirmCancel() { document.getElementById('confirm-overlay').classList.remove('open'); _confirmResolve?.(false); _confirmResolve = null; }
|
||||
|
||||
export function showConfirm(title, msg, okLabel = 'Bestätigen') {
|
||||
document.getElementById('confirm-title').textContent = title;
|
||||
document.getElementById('confirm-msg').textContent = msg;
|
||||
document.getElementById('confirm-ok-btn').textContent = okLabel;
|
||||
document.getElementById('confirm-overlay').classList.add('open');
|
||||
return new Promise(resolve => { _confirmResolve = resolve; });
|
||||
}
|
||||
|
||||
// ─── EDIT PLAYLIST MODAL ───
|
||||
export function openEditPlModal(pl) {
|
||||
const overlay = document.getElementById('edit-pl-overlay');
|
||||
document.getElementById('edit-pl-name').value = pl.name || '';
|
||||
document.getElementById('edit-pl-desc').value = pl.comment || '';
|
||||
const tog = document.getElementById('edit-pl-public-toggle');
|
||||
tog.classList.toggle('on', !!pl.public);
|
||||
const cv = coverUrl(pl.coverArt, 300);
|
||||
document.getElementById('edit-pl-cover').innerHTML = cv
|
||||
? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">`
|
||||
: '<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px">🎶</div>';
|
||||
overlay.classList.add('open');
|
||||
}
|
||||
export function closeEditPlModal() { document.getElementById('edit-pl-overlay').classList.remove('open'); }
|
||||
export async function saveEditPlaylist() {
|
||||
const pl = S._currentPlaylist; if(!pl) return;
|
||||
const name = document.getElementById('edit-pl-name').value.trim();
|
||||
if(!name) { toast('Name darf nicht leer sein'); return; }
|
||||
const comment = document.getElementById('edit-pl-desc').value.trim();
|
||||
const pub = document.getElementById('edit-pl-public-toggle').classList.contains('on');
|
||||
const btn = document.querySelector('.edit-pl-btn-save');
|
||||
btn.disabled=true; btn.textContent='Speichern…';
|
||||
try {
|
||||
await api('updatePlaylist', `&playlistId=${pl.id}&name=${encodeURIComponent(name)}&comment=${encodeURIComponent(comment)}&public=${pub}`);
|
||||
S._currentPlaylist = {...S._currentPlaylist, name, comment, public:pub};
|
||||
document.getElementById('playlist-detail-name').textContent = name;
|
||||
const descEl = document.getElementById('playlist-detail-desc');
|
||||
if(descEl) { descEl.textContent=comment; descEl.style.display=comment?'':'none'; }
|
||||
closeEditPlModal();
|
||||
toast('Playlist aktualisiert');
|
||||
} catch(e) { toast('Fehler: '+e.message); }
|
||||
finally { btn.disabled=false; btn.textContent='Speichern'; }
|
||||
}
|
||||
export function editPlUploadCover() { document.getElementById('edit-pl-file').click(); }
|
||||
export async function editPlCoverSelected(input) {
|
||||
const file = input.files?.[0]; if(!file || !S._currentPlaylist) return;
|
||||
input.value = '';
|
||||
const preview = URL.createObjectURL(file);
|
||||
document.getElementById('edit-pl-cover').innerHTML = `<img src="${preview}" style="width:100%;height:100%;object-fit:cover">`;
|
||||
try {
|
||||
const token = await ndAuth();
|
||||
const form = new FormData(); form.append('image', file, file.name);
|
||||
const res = await fetch(`${S.cfg.url}/api/playlist/${S._currentPlaylist.id}/image`, {
|
||||
method: 'POST',
|
||||
headers: {'X-Nd-Authorization': 'Bearer '+token},
|
||||
body: form,
|
||||
});
|
||||
if(!res.ok) throw new Error(res.status);
|
||||
toast('Cover aktualisiert');
|
||||
setTimeout(() => {
|
||||
const bust = coverUrl(S._currentPlaylist.coverArt, 300) + '&_cb=' + Date.now();
|
||||
const detailImg = document.getElementById('playlist-cover');
|
||||
if(detailImg) detailImg.innerHTML = `<img src="${bust}" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>🎶</div>'">`;
|
||||
document.getElementById('edit-pl-cover').innerHTML = `<img src="${bust}" style="width:100%;height:100%;object-fit:cover">`;
|
||||
}, 800);
|
||||
} catch(e) {
|
||||
S._ndToken = null;
|
||||
toast('Cover-Upload fehlgeschlagen: '+e.message);
|
||||
const cv = coverUrl(S._currentPlaylist.coverArt, 300);
|
||||
document.getElementById('edit-pl-cover').innerHTML = cv
|
||||
? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">`
|
||||
: '<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px">🎶</div>';
|
||||
}
|
||||
}
|
||||
export async function editPlRemoveCover() {
|
||||
if(!S._currentPlaylist) return;
|
||||
try {
|
||||
const token = await ndAuth();
|
||||
const res = await fetch(`${S.cfg.url}/api/playlist/${S._currentPlaylist.id}/image`, {
|
||||
method: 'DELETE', headers: {'X-Nd-Authorization': 'Bearer '+token},
|
||||
});
|
||||
if(!res.ok) throw new Error(res.status);
|
||||
toast('Cover entfernt');
|
||||
S._currentPlaylist.coverArt = null;
|
||||
document.getElementById('edit-pl-cover').innerHTML = '<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px">🎶</div>';
|
||||
document.getElementById('playlist-cover').innerHTML = '<div class="no-art">🎶</div>';
|
||||
} catch(e) { S._ndToken = null; toast('Fehler: '+e.message); }
|
||||
}
|
||||
|
||||
export async function deletePlaylist() {
|
||||
const pl = S._currentPlaylist;
|
||||
if (!pl) return;
|
||||
const ok = await showConfirm('Playlist löschen', `"${pl.name}" wird unwiderruflich gelöscht.`, 'Löschen');
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api('deletePlaylist', `&id=${pl.id}`);
|
||||
closeEditPlModal();
|
||||
toast(`🗑 "${pl.name}" gelöscht`);
|
||||
navigate('playlists');
|
||||
} catch(e) { toast('Fehler beim Löschen: ' + e.message); }
|
||||
}
|
||||
Reference in new issue
Block a user