diff --git a/client/css/app.css b/client/css/app.css index ab24f12..ab469f6 100644 --- a/client/css/app.css +++ b/client/css/app.css @@ -277,6 +277,11 @@ button.linklike:hover { background: none; color: var(--accent-bright); } display: flex; flex-direction: column; gap: 6px; + /* Without this, a long title (or an image's intrinsic size before it + loads) can force this flex/grid item wider than its declared size - + the classic "min-width: auto" flexbox/grid pitfall. Every tile must + stay exactly the track/flex-basis size regardless of content. */ + min-width: 0; } .tile img { @@ -437,19 +442,38 @@ button.linklike:hover { background: none; color: var(--accent-bright); } .overlay .itemTitle { font-weight: 600; } .overlay .members { color: var(--muted); flex: 1; } -.hostControls { - position: absolute; - top: 64px; - right: 16px; - display: flex; - gap: 10px; - background: rgba(0, 0, 0, .6); - padding: 10px; - border-radius: 10px; - font-size: 12px; +/* Settings: a small gear icon in the action bar, not a permanently-visible + block of dropdowns - opens a compact popup on click. */ +.settingsWrap { position: relative; } + +.iconBtn { + background: none; + padding: 4px 8px; + font-size: 16px; + line-height: 1; + border-radius: 6px; } -.hostControls label { +.iconBtn:hover { background: rgba(255, 255, 255, .12); } + +.settingsPanel { + position: absolute; + top: 100%; + right: 0; + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 10px; + background: var(--bg-header); + padding: 12px; + border-radius: 10px; + font-size: 12px; + min-width: 180px; + box-shadow: 0 8px 24px rgba(0, 0, 0, .4); + z-index: 5; +} + +.settingsPanel label { color: var(--muted); margin-bottom: 0; display: flex; @@ -457,7 +481,7 @@ button.linklike:hover { background: none; color: var(--accent-bright); } gap: 4px; } -.hostControls select { +.settingsPanel select { background: var(--panel); color: var(--text); border: 1px solid #3a3a3a; @@ -466,6 +490,30 @@ button.linklike:hover { background: none; color: var(--accent-bright); } font-size: 12px; } +.loadingNotice { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--muted); + font-size: 14px; + margin: 0; +} + +.playerError { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 360px; + text-align: center; + background: rgba(0, 0, 0, .75); + padding: 20px; + border-radius: 10px; +} + +.playerError p { color: var(--danger); font-size: 14px; margin: 0 0 10px; } + .followerNotice { position: absolute; bottom: 24px; diff --git a/client/js/shell.js b/client/js/shell.js index 9eba509..7d5361b 100644 --- a/client/js/shell.js +++ b/client/js/shell.js @@ -3,8 +3,6 @@ // inside #mainContent - see app.js's boot(). import { auth, logout, getLibraries } from './embyApi.js'; -const COLLECTION_ICONS = { movies: '🎬', tvshows: '📺', music: '🎵' }; - export async function renderShell(appEl) { appEl.innerHTML = `
@@ -37,7 +35,7 @@ export async function renderShell(appEl) { const a = document.createElement('a'); a.href = `#/library/${lib.id}`; a.className = 'navItem'; - a.textContent = `${COLLECTION_ICONS[lib.collectionType] || '📁'} ${lib.name}`; + a.textContent = lib.name; nav.appendChild(a); }); } catch (err) { diff --git a/client/js/views/player.js b/client/js/views/player.js index 81b5c80..4b6d222 100644 --- a/client/js/views/player.js +++ b/client/js/views/player.js @@ -22,34 +22,60 @@ export function renderPartyPlayer(root, params) { root.innerHTML = `
+ +

Chargement du flux...

+
+

+ +
+
Code: + +
+ +
+ + + +
+
+
-
- - - -
+

L'hôte contrôle la lecture.

`; const video = root.querySelector('#video'); + const loadingNotice = root.querySelector('#loadingNotice'); + const errorBox = root.querySelector('#playerError'); + const errorText = root.querySelector('#playerErrorText'); const itemTitleEl = root.querySelector('#itemTitle'); const codeEl = root.querySelector('#code'); const membersEl = root.querySelector('#members'); const nextBtn = root.querySelector('#nextBtn'); const followerNotice = root.querySelector('#followerNotice'); - const hostControls = root.querySelector('#hostControls'); + const settingsWrap = root.querySelector('#settingsWrap'); + const settingsBtn = root.querySelector('#settingsBtn'); + const settingsPanel = root.querySelector('#settingsPanel'); const audioSelect = root.querySelector('#audioSelect'); const subtitleSelect = root.querySelector('#subtitleSelect'); const qualitySelect = root.querySelector('#qualitySelect'); qualitySelect.innerHTML = QUALITY_PRESETS.map((q) => ``).join(''); + settingsBtn.addEventListener('click', (e) => { + e.stopPropagation(); + settingsPanel.classList.toggle('hide'); + }); + document.addEventListener('click', (e) => { + if (!settingsWrap.contains(e.target)) settingsPanel.classList.add('hide'); + }); + let isHost = false; let ws = null; let hls = null; @@ -58,6 +84,17 @@ export function renderPartyPlayer(root, params) { let heartbeat = null; let hasNextQueued = false; + function showError(message, detail) { + loadingNotice.classList.add('hide'); + errorText.textContent = message; + errorBox.classList.remove('hide'); + console.error('[watchparty player]', message, detail || ''); + } + + function clearError() { + errorBox.classList.add('hide'); + } + function applyRemoteState(msg) { suppressLocalEvents = true; const targetSeconds = msg.positionTicks / TICKS_PER_SECOND; @@ -83,12 +120,16 @@ export function renderPartyPlayer(root, params) { subtitleSelect.innerHTML = '' + options.subtitleTracks.map((t) => ``).join(''); } catch (err) { - // Non-fatal: playback still works with Emby's own defaults, just without a picker. + console.error('[watchparty player] playback-options failed', err); + // Non-fatal: playback can still work with Emby's own defaults, just without a picker. } } function loadVideo(itemId, resumeSeconds, wasPlaying) { currentItemId = itemId; + clearError(); + loadingNotice.classList.remove('hide'); + if (hls) { hls.destroy(); hls = null; @@ -101,18 +142,33 @@ export function renderPartyPlayer(root, params) { }); const resume = () => { + loadingNotice.classList.add('hide'); if (resumeSeconds) video.currentTime = resumeSeconds; if (wasPlaying) video.play().catch(() => {}); }; if (window.Hls && window.Hls.isSupported()) { hls = new window.Hls(); + hls.on(window.Hls.Events.MANIFEST_PARSED, resume); + hls.on(window.Hls.Events.ERROR, (event, data) => { + if (!data.fatal) return; + showError( + "Erreur de lecture (HLS) - le flux n'a pas pu être chargé depuis Emby.", + data + ); + }); hls.loadSource(src); hls.attachMedia(video); - hls.once(window.Hls.Events.MANIFEST_PARSED, resume); - } else { + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + // Safari plays HLS natively - no hls.js involved, so wire the plain + //