Commit Graph
11 Commits
Author SHA1 Message Date
peter 83e673718f feat(playlists): hybrid table over the drag list, and guard its destructive paths
The column sort and the filters are a VIEW; the playlist's stored order only
changes when the user presses "Save this order", which still goes through the
undoable path. Picking a sort used to write straight to the server, so merely
looking at the list by title rewrote the playlist and marked a YouTube-linked
mirror dirty.

The table:
- playlistColumns.tsx holds the column defs; the body renders them as table rows
  and fills the grip cell itself, since the handle needs the row's sortable
  listeners that a render closure can't reach.
- The # column shows the STORED position, so a view-sort never renumbers the
  playlist under the reader.
- Saving is withheld while a filter is active (it would drop the hidden items),
  and drag is disabled under any view, with the reason spelled out rather than
  silently doing nothing.
- applyView/sameOrder live in lib/playlistView.ts with tests: the sort and the
  channel grouping compose, and getting that wrong produces a plausible order
  that the save button would then persist.
- dnd-kit KeyboardSensor: the grip was focusable and called itself "reorder",
  but only a pointer could move it.

The guards:
- Delete is ONE dialog with every outcome. It was two chained confirms where the
  second one's Cancel/Escape/backdrop meant "delete here only" — dismissing the
  dialog deleted the playlist. ConfirmProvider gained useChoice() for it.
- ConfirmProvider focuses Cancel when an offered action is destructive; the
  confirm button took autoFocus unconditionally, so a stray Enter on an open
  dialog ran "Delete user" or "Clear audit log".
- Removing an item asks first, and rebases the undo history instead of throwing
  it away: useUndoable.rebase maps every snapshot, so the reorder history stays
  valid for the remaining items.
- A failed detail query showed "Loading…" forever; it now says so and offers a
  retry. Rename and reorder failures are surfaced instead of swallowed (reorder
  was a bare .then(), which also produced an unhandled rejection).
- Local reorder/remove no longer require YouTube write scope.
- playlistName() replaces the watch-later localizer copied into three files.
2026-07-20 23:30:49 +02:00
peter c91caffa5e refactor(z-index): name the stacking layers as a token scale
Replace ~48 magic z-index values (z-10..z-[9999] + 2 inline zIndex) with named
tailwind zIndex tokens (base/menu/chrome/paneltab/panel/rail/overlay/popover/
tooltip/tuner). Values are UNCHANGED — a rename, not a renumber — so layering is
identical; the names give one source of truth for the stack.
2026-07-18 19:11:25 +02:00
peter 0ed58e3cb1 improvement(scroll): fade every capped list's clipped edges
The sweep's own three (the thread, the Plex collections filter, the tag manager)
plus the seven the spec missed — the playlist picker, the row tag menu, the Plex
playlist/collection dialogs, the player's track menu, the video description and
the table's multi-select filter. Same idiom throughout: a height-capped list that
silently clipped its content now says so, and hides its scrollbar like the rail
and the panels already do.

The table's option list is its own module-level component because it owns a hook:
FilterPopover is redeclared per DataTable render, so React remounts it.
2026-07-17 03:14:48 +02:00
peter a6c2357930 refactor(ui): useDismiss hook for outside-click/Escape close
lib/useDismiss.ts replaces the identical mousedown-outside + Escape effect that
DataTable (filter popover), Channels (tag picker) and AddToPlaylist each hand-rolled.
Positioning stays with each caller (it genuinely varies); AddToPlaylist keeps its
own resize/scroll reposition effect.
2026-06-29 00:11:44 +02:00
peter 74491991d7 fix(ui): opaque .glass-menu for floating popovers (readability)
Floating menus hover over undimmed content (no backdrop scrim like dialogs), so the
frosted .glass (78%) let the content bleed through and hurt readability. Add a near-opaque
.glass-menu (surface 92%, keeps blur) and use it for the account, language, notification
and add-to-playlist popovers. Dialogs/chrome keep the frostier .glass (they sit over a dark
scrim / the ambient bg). perf-mode disables its blur too.
2026-06-16 01:39:56 +02:00
peter 9acea11010 feat(playlists): make YouTube-mirrored playlists editable + syncable
Mirrors (source='youtube') were read-only; per the original two-way design they can
now be edited locally (add/remove/reorder/rename) and pushed back. The read-sync
skips a dirty mirror so it won't clobber unpushed edits; a successful push clears
dirty and lets the mirror refresh. _mark_dirty now covers any YouTube-linked list
(exported local or mirror), and rename marks dirty too. push/delete no longer reject
mirrors. Push reconciles the title as well (cheap playlists.list compare, then
playlists.update only if changed) so a local rename doesn't revert on the next pull.
Frontend: editable = not-built-in (was also excluding mirrors); rows/reorder enabled
for all owned lists; AddToPlaylist popover lists mirrors too (with a YT marker); the
origin chip now reads 'edits sync back'. Trilingual.
2026-06-15 21:40:13 +02:00
peter 84b56de21e feat(playlists): YouTube-mirrored playlists in the UI (read-only) + sync button
Show YouTube-sourced playlists with a YouTube icon; they're read-only for now (no
rename/delete/reorder/remove, excluded from the add-to-playlist popover) with a
'synced from YouTube' note — editing comes with the write-back phase. Add a 'Sync from
YouTube' button in the Playlists rail (api.syncYoutubePlaylists) with a result toast.
Trilingual strings.
2026-06-15 19:37:17 +02:00
peter dea740b728 feat(playlists): unify Saved into a built-in Watch later playlist
The old per-video status='saved' becomes membership in a built-in, undeletable
'watch_later' playlist. Migration 0012 moves existing saved videos into each user's
Watch later list and demotes the states to 'new'. The feed/playlist serializers now
expose a 'saved' boolean (EXISTS in watch_later); the card bookmark toggles watch_later
via new /api/playlists/watch-later add/remove endpoints (create-on-demand) instead of
setting a status. Removed the 'saved' show-filter and status. Watch later shows a
localized name and hides rename/delete in the Playlists page and add-to-playlist popover.
2026-06-15 15:33:53 +02:00
peter ef11c6c605 fix(playlists): refresh playlist detail after add/remove from the feed popover
Adding a video to a playlist via the AddToPlaylist popover invalidated the membership
and the playlist list, but not the playlist *detail* query, so the Playlists page
served stale cached items (the new video only appeared after F5). Invalidate the
["playlist"] detail queries too.
2026-06-15 15:20:28 +02:00
peter fb5a2f1ed2 fix(playlists): flip the add-to-playlist popover up when it would overflow
The popover was always positioned below its trigger, so on cards near the bottom
of the viewport it ran off-screen. Measure the panel's real height and open it
upward (clamped to the viewport) when there isn't room below; also clamp the
horizontal position.
2026-06-15 14:52:57 +02:00
peter 40a44cdde2 feat(playlists): frontend foundation — page, add-to-playlist, nav
Add the Playlists page (left rail of playlists + detail with drag&drop reorder,
inline rename, one-step delete, remove item, Play all / row-click playback via the
existing PlayerModal) and an AddToPlaylist popover (portaled, multi-toggle +
inline new-playlist) wired into the VideoCard hover overlay and the PlayerModal.
New api client methods + Playlist types, a 'playlists' page route + account-menu
entry, and trilingual strings. Local only — YouTube sync comes in later phases.
2026-06-15 14:45:18 +02:00