diff --git a/webui/static/script.js b/webui/static/script.js index a08b209d..bc59183e 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1251,21 +1251,71 @@ async function openPersonalSettings() { const lbData = await lbRes.json(); const spotifyData = await spotifyRes.json(); - // Build sections body.innerHTML = ''; - // Spotify + Tidal + server library per-profile only shown for non-admin profiles - if (currentProfile && !currentProfile.is_admin) { - renderPersonalSettingsSpotify(body, spotifyData); - renderPersonalSettingsTidal(body); - try { - const libRes = await fetch('/api/profiles/me/server-library'); - const libData = await libRes.json(); - renderPersonalSettingsServerLibrary(body, libData); - } catch (e) { - console.debug('Failed to load server library settings:', e); - } + const isNonAdmin = currentProfile && !currentProfile.is_admin; + + if (isNonAdmin) { + // Tabbed layout for non-admin with multiple sections + const tabs = [ + { id: 'music', label: 'Music Services' }, + { id: 'server', label: 'Server' }, + { id: 'scrobble', label: 'Scrobbling' }, + ]; + const tabBar = document.createElement('div'); + tabBar.className = 'ps-tabbar'; + tabs.forEach((t, i) => { + const btn = document.createElement('button'); + btn.className = 'ps-tab' + (i === 0 ? ' active' : ''); + btn.textContent = t.label; + btn.onclick = () => { + tabBar.querySelectorAll('.ps-tab').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + body.querySelectorAll('.ps-tab-content').forEach(c => c.classList.remove('active')); + const target = document.getElementById(`ps-tab-${t.id}`); + if (target) target.classList.add('active'); + }; + tabBar.appendChild(btn); + }); + body.appendChild(tabBar); + + // Music Services tab + const musicTab = document.createElement('div'); + musicTab.id = 'ps-tab-music'; + musicTab.className = 'ps-tab-content active'; + renderPersonalSettingsSpotify(musicTab, spotifyData); + renderPersonalSettingsTidal(musicTab); + body.appendChild(musicTab); + + // Server tab + const serverTab = document.createElement('div'); + serverTab.id = 'ps-tab-server'; + serverTab.className = 'ps-tab-content'; + serverTab.innerHTML = '
Loading libraries...
'; + body.appendChild(serverTab); + // Load server libraries async (don't block modal) + fetch('/api/profiles/me/server-library').then(r => r.json()).then(libData => { + serverTab.innerHTML = ''; + renderPersonalSettingsServerLibrary(serverTab, libData); + }).catch(() => { + serverTab.innerHTML = ''; + renderPersonalSettingsServerLibrary(serverTab, {}); + }); + + // Scrobbling tab + const scrobbleTab = document.createElement('div'); + scrobbleTab.id = 'ps-tab-scrobble'; + scrobbleTab.className = 'ps-tab-content'; + body.appendChild(scrobbleTab); + // Render LB into the scrobble tab + const origBody = body; + renderPersonalSettingsLB(lbData, scrobbleTab); + } else { + // Admin: just ListenBrainz, no tabs + const content = document.createElement('div'); + content.style.padding = '18px 22px 22px'; + body.appendChild(content); + renderPersonalSettingsLB(lbData, content); } - renderPersonalSettingsLB(lbData); } catch (e) { body.innerHTML = '
Failed to load settings
'; } @@ -1402,98 +1452,153 @@ function authenticatePersonalTidal() { window.open('/auth/tidal?profile_id=' + (currentProfile?.id || ''), '_blank'); } -function renderPersonalSettingsServerLibrary(body, data) { - const plexLib = data.plex_library_id || ''; - const jellyfinUser = data.jellyfin_user_id || ''; - const jellyfinLib = data.jellyfin_library_id || ''; - const navidromeLib = data.navidrome_library_id || ''; - const hasAny = plexLib || jellyfinUser || jellyfinLib || navidromeLib; - +async function renderPersonalSettingsServerLibrary(container, profileData) { const section = document.createElement('div'); section.id = 'ps-server-library-section'; - section.innerHTML = ` -
-
-

Media Server Library

- - - ${hasAny ? 'Customized' : 'Using default'} - -
-
- Choose which library playlists sync to. Leave empty to use the admin's default. -
-
- - -
-
- - -
-
- - + + // Detect which server is active + let serverType = 'none'; + let libraries = []; + let users = []; + const currentLib = profileData || {}; + + try { + // Try each server type to find the active one + const plexRes = await fetch('/api/plex/music-libraries'); + if (plexRes.ok) { + const plexData = await plexRes.json(); + if (plexData.libraries && plexData.libraries.length > 0) { + serverType = 'plex'; + libraries = plexData.libraries; + } + } + } catch (e) {} + + if (serverType === 'none') { + try { + const jellyRes = await fetch('/api/jellyfin/music-libraries'); + if (jellyRes.ok) { + const jellyData = await jellyRes.json(); + if (jellyData.libraries && jellyData.libraries.length > 0) { + serverType = 'jellyfin'; + libraries = jellyData.libraries; + users = jellyData.users || []; + } + } + } catch (e) {} + } + + if (serverType === 'none') { + section.innerHTML = ` +
+
+

Media Server

+
+
No media server connected. Ask your admin to configure Plex, Jellyfin, or Navidrome in Settings.
-
- - + `; + } else if (serverType === 'plex') { + const selectedLib = currentLib.plex_library_id || ''; + const optionsHtml = libraries.map(lib => { + const name = lib.name || lib.title || lib; + const val = typeof lib === 'string' ? lib : (lib.name || lib.title); + return ``; + }).join(''); + + section.innerHTML = ` +
+
+

Plex Library

+ + + ${selectedLib ? 'Custom' : 'Default'} + +
+
Choose which Plex music library your playlists sync to.
+
+ + +
+
+ +
-
-
- - ${hasAny ? '' : ''} + `; + } else if (serverType === 'jellyfin') { + const selectedUser = currentLib.jellyfin_user_id || ''; + const selectedLib = currentLib.jellyfin_library_id || ''; + + const userOpts = users.map(u => { + const uid = u.id || u.Id; + const uname = u.name || u.Name; + return ``; + }).join(''); + + const libOpts = libraries.map(lib => { + const lid = lib.key || lib.id || lib.Id; + const lname = lib.name || lib.Name || lib.title; + return ``; + }).join(''); + + section.innerHTML = ` +
+
+

Jellyfin

+ + + ${selectedUser || selectedLib ? 'Custom' : 'Default'} + +
+
Choose which Jellyfin user and library your playlists sync to.
+ ${users.length ? `
` : ''} +
+ + +
+
+ +
-
- `; + `; + } const existing = document.getElementById('ps-server-library-section'); if (existing) existing.replaceWith(section); - else body.appendChild(section); + else container.appendChild(section); } async function savePersonalServerLibrary() { - const resultEl = document.getElementById('ps-server-result'); - try { - // Save each server type that has a value - const plex = document.getElementById('ps-plex-library-id')?.value?.trim(); - const jellyfinUser = document.getElementById('ps-jellyfin-user-id')?.value?.trim(); - const jellyfinLib = document.getElementById('ps-jellyfin-library-id')?.value?.trim(); - const navidrome = document.getElementById('ps-navidrome-library-id')?.value?.trim(); - - const saves = []; - if (plex !== undefined) saves.push(fetch('/api/profiles/me/server-library', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ server_type: 'plex', library_id: plex || null }) - })); - if (jellyfinUser !== undefined || jellyfinLib !== undefined) saves.push(fetch('/api/profiles/me/server-library', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ server_type: 'jellyfin', user_id: jellyfinUser || null, library_id: jellyfinLib || null }) - })); - if (navidrome !== undefined) saves.push(fetch('/api/profiles/me/server-library', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ server_type: 'navidrome', library_id: navidrome || null }) - })); + try { + const plexSelect = document.getElementById('ps-plex-library-select'); + const jellyUserSelect = document.getElementById('ps-jellyfin-user-select'); + const jellyLibSelect = document.getElementById('ps-jellyfin-library-select'); - await Promise.all(saves); - showToast('Server library settings saved', 'success'); - openPersonalSettings(); - } catch (e) { - if (resultEl) resultEl.innerHTML = '
Failed to save
'; - } -} + if (plexSelect) { + await fetch('/api/profiles/me/server-library', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ server_type: 'plex', library_id: plexSelect.value || null }) + }); + } + if (jellyUserSelect || jellyLibSelect) { + await fetch('/api/profiles/me/server-library', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + server_type: 'jellyfin', + user_id: jellyUserSelect?.value || null, + library_id: jellyLibSelect?.value || null + }) + }); + } -async function clearPersonalServerLibrary() { - try { - await Promise.all([ - fetch('/api/profiles/me/server-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server_type: 'plex', library_id: null }) }), - fetch('/api/profiles/me/server-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server_type: 'jellyfin', user_id: null, library_id: null }) }), - fetch('/api/profiles/me/server-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server_type: 'navidrome', library_id: null }) }), - ]); - showToast('Server library settings reset to default', 'info'); - openPersonalSettings(); + showToast('Server library settings saved', 'success'); } catch (e) { - showToast('Error resetting settings', 'error'); + showToast('Error saving settings', 'error'); } } @@ -1510,8 +1615,8 @@ async function disconnectPersonalSpotify() { } } -function renderPersonalSettingsLB(data) { - const body = document.getElementById('personal-settings-body'); +function renderPersonalSettingsLB(data, container) { + const body = container || document.getElementById('personal-settings-body'); const connected = data.connected; const username = data.username || ''; const baseUrl = data.base_url || ''; diff --git a/webui/static/style.css b/webui/static/style.css index d4431373..5d983f8a 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -34001,10 +34001,13 @@ body.downloads-disabled [onclick*="DownloadMissing"]:not([onclick*="close"]) { border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; width: 90%; - max-width: 460px; + max-width: 500px; + max-height: 85vh; box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5); animation: ps-slide-up 0.25s ease; overflow: hidden; + display: flex; + flex-direction: column; } @keyframes ps-slide-up { from { opacity: 0; transform: translateY(20px) scale(0.97); } @@ -34043,16 +34046,57 @@ body.downloads-disabled [onclick*="DownloadMissing"]:not([onclick*="close"]) { color: rgba(255, 255, 255, 0.9); } .personal-settings-body { - padding: 20px 22px 24px; + padding: 0; + overflow-y: auto; + flex: 1; } -/* LB Section within Personal Settings */ +/* Tab bar within personal settings */ +.ps-tabbar { + display: flex; + gap: 2px; + padding: 12px 22px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + background: rgba(0, 0, 0, 0.15); +} +.ps-tab { + padding: 8px 16px 10px; + border: none; + background: transparent; + color: rgba(255, 255, 255, 0.4); + font-size: 0.82em; + font-weight: 500; + font-family: inherit; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; + white-space: nowrap; +} +.ps-tab:hover { + color: rgba(255, 255, 255, 0.7); +} +.ps-tab.active { + color: #fff; + border-bottom-color: var(--accent-color, #1db954); + font-weight: 600; +} +.ps-tab-content { + display: none; + padding: 18px 22px 22px; +} +.ps-tab-content.active { + display: block; +} + +/* Section cards within Personal Settings */ .ps-section { background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 16px 18px; + margin-bottom: 12px; } +.ps-section:last-child { margin-bottom: 0; } .ps-section-header { display: flex; align-items: center;