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
+ //