Commit graph

4 commits

Author SHA1 Message Date
ff664f2c2b Replace every blocking alert() with a light toast, fix a mobile grid bug
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
Every message this app ever showed the client - the host-handoff
notice, every "couldn't start/join a party" error - went through a
blocking alert(), freezing the tab for what's usually just a heads-up.
New toast.ts is a self-mounting singleton (no per-party state, so it
doesn't need Shell.ts wiring like musicBar/MusicBar does) styled after
Player.css's own .requestBanner, with an 'error' variant that's still
just a plain left accent, not a red banner - even a failed click
doesn't need this app to suddenly shout. All ten alert() call sites
across Player/MusicBar/tiles/Home/Series/Playlist/Artist/Album now go
through it.

Also fixes a real reported bug while in the area: the library/artist
grid's minmax(160px, 1fr) floor left no room for a second column once
a real phone's padding was subtracted, unlike everything else mobile
in this app (the sidebar, the music bar) which already got a
narrow-screen treatment. Switched to a fluid clamp(100px, 28vw, 160px)
floor instead of a fixed breakpoint, confirmed at seven real widths via
an iframe (its own independent viewport for vw units, sidestepping
this sandbox's own unreliable viewport-resize emulation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 17:34:10 +02:00
cf82ea5c2d Add "Lire à partir d'ici" to playlists
Asked for directly: alongside "Lancer cette playlist" (always from the
first item), a way to start a playlist party from any chosen item.
views/playlist/Playlist.ts's own top comment used to rule this out,
written back when the page was deliberately read-only - the stated
reason was that a playlist item carries no back-reference to whichever
playlist it came from, unlike an Episode's own SeriesId, so "start from
item N, queue the rest" wasn't reconstructable the way views/series
does it for episodes.

That reasoning was correct for a bare item id on its own, but doesn't
actually block this exact page: renderPlaylist already fetches the
entire, ordered item list just to render it, so "item N of playlist P"
only ever needed playlist P's own already-in-hand list, never a
back-reference on the item itself. views/library's tiles and
views/album's per-track rows still can't do this - neither has that
ordered list around - which is why the feature lives only here.

createParty(itemId, startAtItemId?) gained the optional second
argument, threaded through POST /api/parties into
createPartyFromPlaylist (index.ts) - meaningless for every other item
type, so every other call site keeps working with just one argument.
createPartyFromPlaylist resolves it against the playlist's own
(post-splitPlaylistByMediaType) item list and slices from there, the
same "slice, don't wrap" shape the single-item path already uses for
SeriesId-based episode starts. Everything before the chosen item is
simply left out of the party, not queued for later - true for both
video and music.

Each row in the playlist page gets its own "Lire à partir d'ici"
button now, mirroring views/series' own per-episode button (reuses
.episodeRow's existing styling/responsive wrap, no new CSS). Both it
and the existing "Lancer cette playlist" button funnel through one
shared launch() helper.

Verified: full typecheck/test/build on both projects (118/118 server
tests - no dedicated test added for createPartyFromPlaylist itself,
consistent with this project's existing boundary that index.ts's route
glue isn't unit-tested, only the services it calls into are). Confirmed
in a real browser against the compiled bundle that the new per-row
button renders correctly at both desktop and phone width.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 20:17:08 +02:00
939beb6fa9 Add a spinner to every loading state
All checks were successful
Publier l'image Docker / test (push) Successful in 21s
Publier l'image Docker / publish (push) Successful in 7s
Every "Chargement..." spot across the app was plain text with no
visual motion - easy to mistake a still-fetching page for a stuck one.

New .spinner in styles/base.css - a plain CSS ring (border +
border-top-color, no image/icon-font asset), sized and vertically
aligned to drop into an existing text node inline instead of needing
a flex-row restructure. prefers-reduced-motion turns the spin off.

New client/src/loading.ts exports one shared LOADING_HINT constant so
every dynamic call site (Album/Artist/Library/Playlist/Series's own
route entry, Home's per-library carousels, a season switch reloading
its episode list, the player's route-level load) renders identical
markup. Shell.html's sidebar library list and Library.html's #grid
placeholder (both static templates, no JS import) got the same
markup pasted directly. Player.html's #loadingNotice (video
buffering) got the spinner too.

Left alone: a couple of static templates' own inert copies of this
same placeholder text (Playlist.html, Series.html) that are always
synchronously overwritten before ever painting - nothing a viewer
could ever actually see differently. Also left alone: the brief
button-label loading states elsewhere ("Ajout...", "Démarrage...") -
a different, smaller UI element, typically resolved within one REST
round trip.

Verified: full typecheck/build on the client (no server changes at
all). Visually confirmed in a real browser that the spinner renders
correctly inline with text and is genuinely animating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:30:25 +02:00
b45a243e59 Browse Emby playlists and launch one as a pre-queued party
All checks were successful
Publier l'image Docker / test (push) Successful in 23s
Publier l'image Docker / publish (push) Successful in 16s
Queued right after the ffmpeg burn-in work, asked for directly: see
what's inside a playlist, and launch it - pre-queued with the whole
thing, the same "one click, the rest queues itself" shape starting from
an episode already gets for its series.

- server/src/services/CatalogService.ts: VISIBLE_COLLECTION_TYPES gains
  'playlists' - Emby's /Views already returns that CollectionType once
  the user has any, so it appears in the sidebar through the exact same
  code every other library already uses, nothing else needed there. New
  getPlaylistItemsRaw/getPlaylistItems (Emby's own /Playlists/{id}/Items,
  mirroring getSeriesEpisodes/getAlbumTracks). New exported
  splitPlaylistByMediaType: a playlist can mix movies/episodes and songs,
  this app's party model can't - decides video vs music from the first
  item's type and drops anything that doesn't match (logged when it
  does) - pure, unit-tested on its own.
- server/src/index.ts: new GET /api/catalog/playlists/:playlistId/items
  route. POST /api/parties gains a sibling early branch for
  item.Type === 'Playlist' (a new createPartyFromPlaylist helper,
  deliberately NOT merged into the existing single-item path, which
  didn't need touching) - builds a video QueueItem[] exactly like the
  episode-queue path already does, or seeds MusicQueueService.init +
  addTrack per track for a music result, the same mechanism
  POST /api/parties/:id/tracks already uses.
- client/src/tiles.ts: one more DRILL_DOWN entry (Playlist -> its own
  page) - a playlist was never directly streamable, same as
  Series/MusicArtist/MusicAlbum before it.
- client/src/api/embyApi.ts: getPlaylistItems(). No new API for
  *launching* one - createParty(playlistId) already does everything,
  same as clicking any other item; the server decides what it means.
- client/src/views/playlist/Playlist.{ts,html,css} (new): a playlist's
  contents, read-only past the one "Lancer cette playlist" button -
  deliberately no per-item start/add the way album tracks or episode
  rows get (an Emby item carries no back-reference to "which playlist
  contains me" the way an Episode's SeriesId does, so "start from item
  N, queue the rest of the playlist" isn't cheaply reconstructable the
  same way - not asked for here anyway). The launch button branches on
  the created party's mediaType, same check Player.ts's own dispatcher
  already makes.
- client/src/app.ts: registers /playlist/:id.
- server/test/catalogService.test.ts: the new methods' target
  URL/mapping, plus splitPlaylistByMediaType's own tests (homogeneous
  video, homogeneous music, mixed, empty).
- docs/architecture.md, README.md: design + the scope decision (no
  per-item actions) spelled out.

Verified: full typecheck+test+build on both projects (62/62 server
tests). The new page driven through the same mocked-fetch + real-bundle
harness used throughout this session, in a real browser: header/meta/
item list render correctly from mocked catalog data (including the
sidebar showing "Playlists" once a mocked library declares that
CollectionType), and clicking "Lancer cette playlist" reaches
POST /api/parties with the right id and correctly routes into the video
player for a video result. Actual playback past that point still needs
a real Emby server - same standing caveat as the rest of this app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 19:04:31 +02:00