Name the three party kinds: watch, listen and lounge party
- UI wording: "Watch party" = classic video, "Listen party" = classic audio, "Lounge party" = static party (tab, page, create buttons, toasts/confirms, server error messages); code names are unchanged - home: "Parties en cours" with a Watch party / Listen party badge on each card - music launch buttons say "Lancer une listen party"; a failed tile launch now restores its own label instead of always "watch party" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e437e4a91
commit
58d51f5f74
18 changed files with 79 additions and 45 deletions
|
|
@ -3,7 +3,7 @@
|
|||
<div class="sidebarBrand">Watch Party</div>
|
||||
<nav class="sidebarNav" id="sidebarNav">
|
||||
<a href="#/home" class="navItem">Accueil</a>
|
||||
<a href="#/static" class="navItem">Parties permanentes</a>
|
||||
<a href="#/static" class="navItem">Lounge parties</a>
|
||||
<div class="navSectionLabel">Bibliothèques</div>
|
||||
<p class="hint" id="libStatus"><span class="spinner"></span>Chargement...</p>
|
||||
</nav>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
plain scrim tap-only close used to leave unsolved). -->
|
||||
<div class="sheetGrabberHandle" id="sheetGrabberHandle"><div class="sheetGrabber"></div></div>
|
||||
<a href="#/home" class="navItem sheetPrimary"><span class="navIcon" data-icon="home"></span>Accueil</a>
|
||||
<a href="#/static" class="navItem sheetPrimary"><span class="navIcon" data-icon="static"></span>Parties permanentes</a>
|
||||
<a href="#/static" class="navItem sheetPrimary"><span class="navIcon" data-icon="static"></span>Lounge parties</a>
|
||||
<div class="navSectionLabel">Bibliothèques</div>
|
||||
<div class="sheetGrid" id="mobileLibGrid"></div>
|
||||
<div id="mobilePlaylistsSlot"></div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const SHOWS_ICON_SVG = '<svg viewBox="0 0 24 24"><rect x="3" y="5.5" width="18"
|
|||
const MUSIC_ICON_SVG = '<svg viewBox="0 0 24 24"><circle cx="8" cy="17" r="2.6" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M10.5 17V5.5l7-1.5v10" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="17.5" cy="14" r="2.6" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>';
|
||||
const MUSICVIDEOS_ICON_SVG = '<svg viewBox="0 0 24 24"><rect x="3" y="5.5" width="18" height="13" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M10 9.5 15.5 12 10 14.5Z" fill="currentColor"/></svg>';
|
||||
const PLAYLISTS_ICON_SVG = '<svg viewBox="0 0 24 24"><circle cx="4.5" cy="7" r="1.3" fill="currentColor"/><circle cx="4.5" cy="12" r="1.3" fill="currentColor"/><circle cx="4.5" cy="17" r="1.3" fill="currentColor"/><line x1="9" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="9" y1="12" x2="20" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="9" y1="17" x2="20" y2="17" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>';
|
||||
// The "Watch parties permanentes" tab - a circular "resume" arrow.
|
||||
// The "Lounge parties" tab (static watch parties) - a circular "resume" arrow.
|
||||
const STATIC_PARTIES_ICON_SVG = '<svg viewBox="0 0 24 24"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M19.5 4v4h-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 9l5 3-5 3Z" fill="currentColor"/></svg>';
|
||||
// Emby could in principle add a CollectionType this app doesn't map yet
|
||||
// (server/src/services/CatalogService.ts's VISIBLE_COLLECTION_TYPES
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ const DRILL_DOWN: Record<string, { route: string; label: string }> = {
|
|||
function posterTileHtml(item: ItemDto): string {
|
||||
const drill = item.type ? DRILL_DOWN[item.type] : undefined;
|
||||
const drillHref = drill ? `#/${drill.route}/${item.id}` : null;
|
||||
const partyLabel = item.type === 'Audio' ? 'Écouter ensemble' : 'Lancer une watch party';
|
||||
// Video -> "watch party", audio -> "listen party" - see the party kinds
|
||||
// in docs/architecture.md (the third, "lounge party", is the static one).
|
||||
const partyLabel = item.type === 'Audio' ? 'Lancer une listen party' : 'Lancer une watch party';
|
||||
return `
|
||||
<div class="tile">
|
||||
${drillHref ? `<a href="${drillHref}" class="tileLink" aria-label="${escapeHtml(item.name)}"></a>` : ''}
|
||||
|
|
@ -116,7 +118,7 @@ export function tileHtml(item: ItemDto): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wires up every "Lancer une watch party" button inside `container`, now
|
||||
* Wires up every "Lancer une watch/listen party" button inside `container`, now
|
||||
* and for any tile added to it later. `onStart(itemId)` does the actual
|
||||
* work (create the party + navigate) - kept as a caller-supplied callback
|
||||
* so this module doesn't need to depend on api/party.ts/router.ts itself.
|
||||
|
|
@ -132,6 +134,7 @@ export function wireTileButtons(container: ParentNode, onStart: (itemId: string)
|
|||
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('.startPartyBtn');
|
||||
if (!btn || btn.disabled) return;
|
||||
e.preventDefault();
|
||||
const idleLabel = btn.textContent; // "watch party" or "listen party" - put back as-is on failure
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Démarrage...';
|
||||
try {
|
||||
|
|
@ -139,7 +142,7 @@ export function wireTileButtons(container: ParentNode, onStart: (itemId: string)
|
|||
} catch (err) {
|
||||
showToast((err as Error).message, { variant: 'error' });
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Lancer une watch party';
|
||||
btn.textContent = idleLabel;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export async function renderAlbum(root: HTMLElement, params: RouteParams): Promi
|
|||
// states) - the label lives in aria-label/title for accessibility
|
||||
// rather than as visible text, now that there's one per track instead
|
||||
// of one per page (see docs/architecture.md's "Music UI rework").
|
||||
const startLabel = 'Écouter ensemble';
|
||||
const startLabel = 'Lancer une listen party';
|
||||
const addLabel = 'Ajouter à la file';
|
||||
const startAlbumLabel = 'Écouter cet album';
|
||||
const addAlbumLabel = "Ajouter l'album à la file";
|
||||
|
|
|
|||
|
|
@ -60,3 +60,18 @@
|
|||
color: var(--accent-bright);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Which kind of party a card is - "Watch party" (video) or "Listen party"
|
||||
(audio). Lounge parties (static) have their own tab, never this list. */
|
||||
.partyKind {
|
||||
align-self: flex-start;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.partyKind--video { color: var(--accent-bright); background: rgba(82, 181, 75, .15); }
|
||||
.partyKind--music { color: #7fb4ff; background: rgba(127, 180, 255, .15); }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
</div>
|
||||
|
||||
<section class="section" id="partiesSection" hidden>
|
||||
<h2>Watch parties en cours</h2>
|
||||
<h2>Parties en cours</h2>
|
||||
<div id="partiesCarousel" class="carousel"></div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ function renderParties(root: HTMLElement, parties: LobbyPartyDto[]): void {
|
|||
<div class="partyCard">
|
||||
<img class="${p.mediaType === 'music' ? 'partyCardArt--square' : ''}" src="${p.itemImageUrl || ''}" alt="" onerror="this.classList.add('noImage')" />
|
||||
<div class="partyCardInfo">
|
||||
<span class="partyKind partyKind--${p.mediaType}">${p.mediaType === 'music' ? 'Listen party' : 'Watch party'}</span>
|
||||
<strong>${escapeHtml(p.itemName || '')}</strong>
|
||||
<span class="hint">
|
||||
${escapeHtml(p.hostUserName || '')} · ${p.memberCount} participant${p.memberCount > 1 ? 's' : ''}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<p class="hint" id="playlistMeta"></p>
|
||||
<div class="playlistActions">
|
||||
<button id="launchBtn">Lancer cette playlist</button>
|
||||
<button class="hide" id="createStaticBtn">Créer une watch party permanente</button>
|
||||
<button class="hide" id="createStaticBtn">Créer une lounge party</button>
|
||||
<a class="linklike" id="embyLink" target="_blank" rel="noopener">Voir sur Emby</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<h1 id="seriesTitle"></h1>
|
||||
<p class="seriesOverview hide" id="seriesOverview"></p>
|
||||
<div class="seriesActions">
|
||||
<button id="createStaticBtn">Créer une watch party permanente</button>
|
||||
<button id="createStaticBtn">Créer une lounge party</button>
|
||||
<a class="linklike" id="embyLink" target="_blank" rel="noopener">Voir sur Emby</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* ---- "Watch parties permanentes" tab: one card per static party ---- */
|
||||
/* ---- "Lounge parties" tab: one card per static party ---- */
|
||||
|
||||
.staticGrid {
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<div class="pageHeader">
|
||||
<h1>Watch parties permanentes</h1>
|
||||
<h1>Lounge parties</h1>
|
||||
</div>
|
||||
<p class="hint">
|
||||
Une série ou une playlist regardée en groupe sur plusieurs séances : la progression du groupe
|
||||
et celle de chacun sont gardées en mémoire. Pour en créer une, ouvre une série ou une playlist
|
||||
vidéo et choisis « Créer une watch party permanente ».
|
||||
vidéo et choisis « Créer une lounge party ».
|
||||
</p>
|
||||
<div class="staticGrid" id="staticGrid"></div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { LOADING_HINT } from '../../loading';
|
|||
import type { StaticPartySummaryDto } from '../../types';
|
||||
|
||||
/**
|
||||
* Route `/static` - the "Watch parties permanentes" tab: every static
|
||||
* Route `/static` - the "Lounge parties" tab: every static
|
||||
* watch party, kept apart from the home page's classic live parties
|
||||
* (whose lobby list leaves static sessions out - see Home.ts).
|
||||
*
|
||||
|
|
@ -54,7 +54,7 @@ function load(grid: HTMLElement): void {
|
|||
* count, and whether a session is running right now. */
|
||||
function render(grid: HTMLElement, parties: StaticPartySummaryDto[]): void {
|
||||
if (!parties.length) {
|
||||
grid.innerHTML = '<p class="hint">Aucune watch party permanente pour le moment.</p>';
|
||||
grid.innerHTML = '<p class="hint">Aucune lounge party pour le moment.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = parties.map((p) => {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
<div class="staticHeader">
|
||||
<img class="staticCover" id="staticCover" alt="" loading="lazy" onerror="this.classList.add('noImage')" />
|
||||
<div class="seriesInfo">
|
||||
<span class="staticBadge">Watch party permanente</span>
|
||||
<span class="staticBadge">Lounge party</span>
|
||||
<h1 id="staticTitle"></h1>
|
||||
<p class="hint staticMeta" id="staticMeta"></p>
|
||||
<div class="staticProgress" aria-hidden="true"><div class="staticProgressFill" id="staticProgressFill"></div></div>
|
||||
<p class="staticNext" id="staticNext"></p>
|
||||
<div class="staticActions">
|
||||
<button id="sessionBtn">Lancer la séance</button>
|
||||
<button class="secondaryBtn hide" id="joinBtn">Rejoindre cette watch party</button>
|
||||
<button class="secondaryBtn hide" id="joinBtn">Rejoindre cette lounge party</button>
|
||||
<button class="linklike hide" id="leaveBtn">Quitter</button>
|
||||
<button class="linklike hide" id="deleteBtn">Supprimer</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export async function renderStaticParty(root: HTMLElement, params: RouteParams):
|
|||
detail = await getStaticParty(staticId);
|
||||
} catch (err) {
|
||||
console.error('[staticParty] load failed', staticId, err);
|
||||
root.innerHTML = `<p class="error">${escapeHtml((err as Error).message || 'Impossible de charger cette watch party.')}</p>`;
|
||||
root.innerHTML = `<p class="error">${escapeHtml((err as Error).message || 'Impossible de charger cette lounge party.')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -144,14 +144,14 @@ function wireActions(root: HTMLElement, staticId: string): void {
|
|||
joinBtn.addEventListener('click', () => {
|
||||
runBusy(joinBtn, async () => {
|
||||
await joinStaticParty(staticId);
|
||||
showToast('Tu fais maintenant partie de cette watch party.');
|
||||
showToast('Tu fais maintenant partie de cette lounge party.');
|
||||
await reload(root, staticId);
|
||||
});
|
||||
});
|
||||
|
||||
const leaveBtn = root.querySelector('#leaveBtn') as HTMLButtonElement;
|
||||
leaveBtn.addEventListener('click', () => {
|
||||
if (!confirm('Quitter cette watch party ? Ta progression y sera oubliée.')) return;
|
||||
if (!confirm('Quitter cette lounge party ? Ta progression y sera oubliée.')) return;
|
||||
runBusy(leaveBtn, async () => {
|
||||
await leaveStaticParty(staticId);
|
||||
await reload(root, staticId);
|
||||
|
|
@ -160,10 +160,10 @@ function wireActions(root: HTMLElement, staticId: string): void {
|
|||
|
||||
const deleteBtn = root.querySelector('#deleteBtn') as HTMLButtonElement;
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
if (!confirm('Supprimer cette watch party pour tout le monde ? La progression de chacun sera perdue.')) return;
|
||||
if (!confirm('Supprimer cette lounge party pour tout le monde ? La progression de chacun sera perdue.')) return;
|
||||
runBusy(deleteBtn, async () => {
|
||||
await deleteStaticParty(staticId);
|
||||
showToast('Watch party supprimée.');
|
||||
showToast('Lounge party supprimée.');
|
||||
navigate('/static');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { navigate } from '../../router';
|
|||
import { showToast } from '../../toast';
|
||||
|
||||
/**
|
||||
* "Créer une watch party permanente" - shared by views/series and
|
||||
* "Créer une lounge party" - shared by views/series and
|
||||
* views/playlist: creates a static watch party over `sourceId` (a series
|
||||
* or a video playlist - the server decides which, and refuses anything
|
||||
* else with a message) and opens its page.
|
||||
|
|
|
|||
|
|
@ -4583,9 +4583,12 @@ short version:
|
|||
touches the party that changed and a corrupt file costs only that
|
||||
party. The old single file is migrated once, on first load. "Party
|
||||
lifecycle" above still holds for ordinary parties.
|
||||
- **Its own tab.** "Parties permanentes" (`/static`) lists them. The
|
||||
home page's live "Watch parties en cours" list leaves their sessions
|
||||
out, so classic and permanent parties never share a list.
|
||||
- **Its own tab and its own name.** The UI calls these **Lounge
|
||||
parties**, next to **Watch party** (classic video) and **Listen
|
||||
party** (classic audio). The code keeps "static party". The "Lounge
|
||||
parties" tab (`/static`) lists them. The home page's live "Parties en
|
||||
cours" list (each card badged Watch/Listen party) leaves their
|
||||
sessions out, so classic and lounge parties never share a list.
|
||||
- **"Completed" gained a second trigger.** `PartyService
|
||||
.completeCurrentItem` is now the one place an item counts as done. It
|
||||
handles Emby Played, as before, plus static progress. It runs from
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ app.get('/api/catalog/party-stream/:partyId/master.m3u8', allowCastCors, asyncRo
|
|||
if (!session) { res.status(401).end(); return; }
|
||||
const party = PartyService.getById(partyId);
|
||||
if (!party || !party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
await PartyStreamService.serveMasterPlaylist(req, res, party, session);
|
||||
|
|
@ -272,7 +272,7 @@ app.get('/api/catalog/party-stream/:partyId/raw', allowCastCors, asyncRoute(asyn
|
|||
if (!session) { res.status(401).end(); return; }
|
||||
const party = PartyService.getById(partyId);
|
||||
if (!party || !party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
await PartyStreamService.serveRawAsset(req, res, party, session);
|
||||
|
|
@ -302,7 +302,7 @@ app.get('/api/catalog/burn/:partyId/master.m3u8', allowCastCors, asyncRoute(asyn
|
|||
if (!session) { res.status(401).end(); return; }
|
||||
const party = PartyService.getById(partyId);
|
||||
if (!party || !party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
if (!party.burnInSubtitleIndex) {
|
||||
|
|
@ -318,7 +318,7 @@ app.get('/api/catalog/burn/:partyId/:segment', allowCastCors, asyncRoute(async (
|
|||
if (!session) { res.status(401).end(); return; }
|
||||
const party = PartyService.getById(partyId);
|
||||
if (!party || !party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
await TranscodeService.serveSegment(req, res, party, param(req, 'segment'));
|
||||
|
|
@ -556,12 +556,12 @@ function createMusicPartyFromTracks(res: Response, session: EmbySession, tracks:
|
|||
app.post('/api/parties/:partyId/tracks', requireSession, (req, res) => {
|
||||
const party = PartyService.getById(param(req, 'partyId'));
|
||||
if (!party || party.mediaType !== 'music') {
|
||||
res.status(404).json({ error: 'Watch party musique introuvable.' });
|
||||
res.status(404).json({ error: 'Listen party introuvable.' });
|
||||
return;
|
||||
}
|
||||
const session = req.embySession!;
|
||||
if (!party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
const track = req.body || {};
|
||||
|
|
@ -587,12 +587,12 @@ app.post('/api/parties/:partyId/tracks', requireSession, (req, res) => {
|
|||
app.post('/api/parties/:partyId/queue', requireSession, asyncRoute(async (req, res) => {
|
||||
const party = PartyService.getById(param(req, 'partyId'));
|
||||
if (!party || party.mediaType !== 'music') {
|
||||
res.status(404).json({ error: 'Watch party musique introuvable.' });
|
||||
res.status(404).json({ error: 'Listen party introuvable.' });
|
||||
return;
|
||||
}
|
||||
const session = req.embySession!;
|
||||
if (!party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
const itemId = req.body?.itemId;
|
||||
|
|
@ -662,7 +662,7 @@ app.post('/api/parties/:partyId/cast-token', requireSession, (req, res) => {
|
|||
const party = PartyService.getById(param(req, 'partyId'));
|
||||
const session = req.embySession!;
|
||||
if (!party || !party.members.has(session.userId)) {
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette watch party." });
|
||||
res.status(403).json({ error: "Tu n'es pas membre de cette party." });
|
||||
return;
|
||||
}
|
||||
res.json({ token: CastTokenService.issue(party.id, session) });
|
||||
|
|
@ -721,7 +721,7 @@ function liveGroupPartyId(sp: StaticParty): string | null {
|
|||
/** Finds `:staticId` or answers 404 - null means the response is sent. */
|
||||
function staticPartyOr404(req: Request, res: Response): StaticParty | null {
|
||||
const sp = StaticPartyService.getById(param(req, 'staticId'));
|
||||
if (!sp) res.status(404).json({ error: 'Watch party permanente introuvable.' });
|
||||
if (!sp) res.status(404).json({ error: 'Lounge party introuvable.' });
|
||||
return sp;
|
||||
}
|
||||
|
||||
|
|
@ -755,7 +755,7 @@ app.post('/api/static-parties', requireSession, asyncRoute(async (req, res) => {
|
|||
} else if (item.Type === 'Playlist') {
|
||||
source = { type: 'playlist', sourceId: item.Id };
|
||||
} else {
|
||||
res.status(400).json({ error: 'Une watch party permanente se crée depuis une série ou une playlist vidéo.' });
|
||||
res.status(400).json({ error: 'Une lounge party se crée depuis une série ou une playlist vidéo.' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -800,7 +800,7 @@ app.delete('/api/static-parties/:staticId', requireSession, (req, res) => {
|
|||
const sp = staticPartyOr404(req, res);
|
||||
if (!sp) return;
|
||||
if (!StaticPartyService.delete(sp, req.embySession!.userId)) {
|
||||
res.status(403).json({ error: 'Seule la personne qui a créé cette watch party peut la supprimer.' });
|
||||
res.status(403).json({ error: 'Seule la personne qui a créé cette lounge party peut la supprimer.' });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
|
|
@ -811,7 +811,7 @@ app.post('/api/static-parties/:staticId/items/:itemId/watched', requireSession,
|
|||
if (!sp) return;
|
||||
const session = req.embySession!;
|
||||
if (!StaticPartyService.markWatched(sp, { userId: session.userId, userName: session.userName }, param(req, 'itemId'))) {
|
||||
res.status(404).json({ error: 'Cet élément ne fait pas partie de cette watch party.' });
|
||||
res.status(404).json({ error: 'Cet élément ne fait pas partie de cette lounge party.' });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
|
|
@ -844,7 +844,7 @@ app.post('/api/static-parties/:staticId/session', requireSession, asyncRoute(asy
|
|||
|
||||
const queue = StaticPartyService.groupQueue(sp);
|
||||
if (!queue.length) {
|
||||
res.status(409).json({ error: 'Le groupe a déjà tout vu dans cette watch party.' });
|
||||
res.status(409).json({ error: 'Le groupe a déjà tout vu dans cette lounge party.' });
|
||||
return;
|
||||
}
|
||||
const first = queue[0]!;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
# Watch party statique (permanente)
|
||||
# Lounge party (watch party statique)
|
||||
|
||||
## Les trois types de party
|
||||
|
||||
| Nom dans l'interface | Quoi | Où |
|
||||
|---|---|---|
|
||||
| **Watch party** | party vidéo classique, éphémère | accueil, « Parties en cours » |
|
||||
| **Listen party** | party audio classique, éphémère (barre musique) | accueil, « Parties en cours » |
|
||||
| **Lounge party** | party statique : une série ou une playlist vidéo suivie sur plusieurs séances | onglet « Lounge parties » |
|
||||
|
||||
Côté code, la lounge party reste nommée « static party » (`StaticPartyService`,
|
||||
route `/static`, `staticLink`...). Seuls les libellés visibles ont changé. Sur
|
||||
l'accueil, chaque carte affiche un badge « Watch party » ou « Listen party ».
|
||||
|
||||
## Objectif
|
||||
|
||||
|
|
@ -119,14 +131,14 @@ des watch parties en cours.
|
|||
solo, ou lancer une watch party de rattrapage ;
|
||||
- les membres ;
|
||||
- la liste complète des items.
|
||||
- Bouton « Créer une watch party permanente » sur `views/series` et sur
|
||||
- Bouton « Créer une lounge party » sur `views/series` et sur
|
||||
`views/playlist` (vidéo uniquement).
|
||||
- Onglet dédié « Parties permanentes » (sidebar et menu mobile), route
|
||||
- Onglet dédié « Lounge parties » (sidebar et menu mobile), route
|
||||
`/static` (`views/staticParties/`) : une carte par watch party avec la
|
||||
progression, le prochain élément, un badge « N manqués » et une pastille
|
||||
« séance en cours ». L'onglet reste actif sur la page d'une party.
|
||||
- Séparation avec les watch parties classiques : la liste « Watch parties en
|
||||
cours » de l'accueil exclut les séances de watch party permanente
|
||||
- Séparation avec les watch/listen parties classiques : la liste « Parties
|
||||
en cours » de l'accueil exclut les séances de lounge party
|
||||
(`staticPartyId` non nul) ; elles ne s'affichent que dans leur onglet.
|
||||
|
||||
## Limites connues
|
||||
|
|
|
|||
Loading…
Reference in a new issue