Commit graph

523 commits

Author SHA1 Message Date
npeter83
250c1da214 feat(nav): N2 — browser Back navigates in-app, not to OAuth
Push an in-app history entry on each page switch (page rides in history.state, URL stays
clean — filters still never go in the URL) and sync the page from history.state on
popstate. The initial entry is stamped after the strip-params effect (which nulls
history.state). Now the mouse/browser Back button steps through visited pages instead of
jumping straight to the Google consent/redirect. Second step of Epic N.
2026-06-16 01:27:49 +02:00
npeter83
36c7a649ab feat(ui): stronger ambient + more translucent glass in dark mode
In dark mode the faint accent pools left the docked frosted chrome (nav/header) looking
flat — no content behind it to refract, unlike dialogs over the feed. Boost the dark-mode
ambient pools and make .glass a bit more translucent there so the chrome reads as glass
near the accent pools. Light mode unchanged. The full app-wide dark effect still needs
Phase 2 (ambient content/mosaic), deferred.
2026-06-16 01:20:40 +02:00
npeter83
67e518f7ef Merge feature/glassmorphism-phase1: frosted glass on nav, header, dialogs 2026-06-16 01:11:50 +02:00
npeter83
d46be00d47 feat(ui): glassmorphism phase 1 — frost the app chrome
Apply the real frosted .glass surface (blur) to the few large chrome surfaces — the nav
sidebar, the header and all Modal-based dialogs (was glass-card / plain surfaces) — and
enrich the ambient backdrop a touch (three soft accent pools) so the glass has more to
refract app-wide. Popovers (notifications, language, account, add-to-playlist) were
already glass. Bulk feed cards stay glass-card (no per-card blur) for performance; the
existing perf-mode still disables blur. Phase 2 (ambient thumbnail mosaic / bg image +
toggle) deferred to end-of-project polish.
2026-06-16 01:11:50 +02:00
npeter83
2d54167afd Merge feature/n1b-settings-page: Settings as a left-nav page (Design B) 2026-06-16 01:05:05 +02:00
npeter83
d30dc7f760 feat(nav): Settings as a page module (Design B)
Move Settings out of the right-side overlay into a left-nav page (page='settings'),
so it opens where you're already looking — no cross-screen mouse travel. The Settings
rail item now sets the page (with active highlight) instead of opening a dialog; the
panel is refactored to an in-flow .glass card (keeps the frosted look over the ambient
backdrop), with the page title shown in the header. Removed the overlay + Esc/backdrop
close path.
2026-06-16 01:05:05 +02:00
npeter83
7b1fc02c68 Merge feature/n1-nav-shell: collapsible left navigation (Epic N, N1) 2026-06-16 00:42:23 +02:00
npeter83
4a80ea5aa5 feat(nav): N1 — collapsible left nav sidebar (Design C)
Move the modules (Feed/Channels/Playlists/Stats) out of the avatar dropdown into a
persistent left sidebar with icon+label entries; collapses to a thin icon rail
(persisted in localStorage). Account actions (About, Sign out, admin badge) move to a
popover at the sidebar bottom; Settings is a rail entry. Header is now a contextual top
bar (sync status, feed scope + search or page title, language, notifications) — logo and
account menu removed from it. New nav.json strings (HU/EN/DE). First step of Epic N.
2026-06-16 00:42:23 +02:00
npeter83
3143d1c1db chore(release): v0.3.0 — playlists + YouTube two-way sync
Bump VERSION to 0.3.0 and add the 0.3.0 release-notes entry (local playlists, watch
later as a playlist, YouTube mirror + two-way sync, sort + undo/redo, rail sorting).
2026-06-16 00:16:59 +02:00
npeter83
993e6ba7d5 fix(playlists): dirty lagged one edit (flush before fingerprinting)
The session has autoflush off, so recompute_dirty's query read the last-committed
state, not the current request's pending item/order edits — so the first reorder
after a sync compared against itself and missed dirty, and only the next edit showed
it. Flush pending changes before computing the current fingerprint. Verified: a swap
now flips dirty immediately, and reverting to the original clears it.
2026-06-16 00:11:58 +02:00
npeter83
7a547d40fe Merge feature/playlist-fingerprint-dirty: fingerprint-derived dirty state 2026-06-15 23:00:56 +02:00
npeter83
6330ac3184 feat(playlists): derive dirty from a synced-state fingerprint
Store a SHA-256 of each playlist's name + ordered video ids as last synced from /
pushed to YouTube (migration 0013, backfilled for already-clean linked playlists).
'dirty' is now recomputed on every edit by comparing the current fingerprint to that
baseline instead of being stickily set true. So manually restoring the original order
(or undo back to it) clears dirty on its own — the Reset to YouTube button disappears
and no YouTube read is needed. Baseline is set on read-sync, repull (reset), and a
clean push; a missing baseline counts as dirty (unknown YT state). Verified the
migration's fingerprint matches the app's runtime fingerprint exactly.
2026-06-15 23:00:56 +02:00
npeter83
a2ad555ef3 fix(playlists): show Reset/Unsynced right after a reorder
A reorder only invalidated the sidebar query, so detail.dirty stayed stale and the
Reset to YouTube button + Unsynced indicator appeared only after F5. Also invalidate
the detail query on reorder; the membership guard keeps the refetch from wiping the
undo history on a pure reorder.
2026-06-15 22:44:57 +02:00
npeter83
f6e662ff14 Merge improvement/playlist-header-polish: tidy playlist detail header 2026-06-15 22:40:21 +02:00
npeter83
17bbb05593 polish(playlists): compact header actions + open-on-YouTube link
The detail header overflowed the items column. Make the secondary actions icon-only
with tooltips (Sync/Export, Reset to YouTube, Delete) keeping only Play all labeled;
replace the long 'Unsynced changes' / 'Synced from YouTube' chips with a compact
accent 'Unsynced' dot (tooltip explains). Add an open-on-YouTube external-link button
next to the name + edit pencil (tooltip), shown for any YouTube-linked playlist.
2026-06-15 22:40:21 +02:00
npeter83
55d476c0b7 Merge feature/playlist-revert-youtube: reset playlist to YouTube state 2026-06-15 22:28:16 +02:00
npeter83
55833d8f72 feat(playlists): reset a playlist to its YouTube state (discard local edits)
The in-session undo/redo is lost when switching playlists, leaving no way to undo
local edits to a YouTube-linked list afterwards. Add a per-playlist 'Reset to YouTube'
action (shown when a linked playlist is dirty) that force-repulls that single playlist
from YouTube, replacing its items/order/name and clearing dirty — even though the bulk
read-sync skips dirty playlists. Backend: repull_playlist() + POST /api/playlists/
{id}/revert-youtube (read-scope gated). Confirm dialog (destructive). Trilingual.
2026-06-15 22:28:16 +02:00
npeter83
380ad39ad4 fix(playlists): don't drop the restored selection during initial load
The auto-select effect ran while the playlist query was still loading (empty list),
nulling the localStorage-restored selection so F5 fell back to the first playlist.
Wait for the first load (listQuery.data) before adjusting the selection.
2026-06-15 22:19:05 +02:00
npeter83
c1d43d8961 Merge improvement/playlist-rail-sort: rail sorting + persisted selection 2026-06-15 22:13:39 +02:00
npeter83
a00779a1c9 feat(playlists): rail sorting, consolidated item sort, persist selection
1) Fix: the selected playlist is now persisted (localStorage) and scrolled into view,
   so F5 keeps it instead of jumping to the first one.
2) Consolidate the in-detail sort: one key select (Title/Duration/Channel) + an
   asc/desc direction toggle (was separate A-Z / Z-A / shortest / longest options);
   add Channel as a sort key. Direction also orders the channel groups when grouping.
3) Left rail sorting: by name / item count / total length, asc/desc, plus an
   'unsynced first' toggle that floats playlists with unpushed edits to the top.
   Backend: list/summary now return total_duration_seconds (grouped sum). The rail
   also shows each playlist's total length. Sort prefs persist to localStorage.
2026-06-15 22:13:32 +02:00
npeter83
156e10235e Merge feature/s4e-playlist-sort-undo: playlist sort + reusable undo/redo 2026-06-15 21:52:35 +02:00
npeter83
40c0cbfc5a feat(playlists): sort + group-by-channel with reusable undo/redo
Add a sort toolbar to the Playlists page: Title A-Z/Z-A, shortest/longest first,
and a 'group by channel' toggle that groups items by channel (A-Z) and applies the
chosen sort within each group. Applying a sort reorders the items and persists via
the existing reorder API.

The item order is now an undoable value: drag, sort and grouping all go through it,
so every change is reversible via undo/redo buttons and Ctrl/Cmd+Z / Ctrl+Y
(Ctrl+Shift+Z). Built on two reusable pieces, not playlist-specific:
- useUndoable<T> — a generic past/present/future snapshot hook (set/undo/redo/reset
  + canUndo/canRedo), ref-backed so callbacks are stable and never see a stale snapshot;
  onApply runs the side effect (here: persist order).
- UndoToolbar — undo/redo buttons + keyboard shortcuts, plain props so any undo
  source can use it.

Undo history is reset only when the item set actually changes (different playlist or
add/remove), so a refetch confirming our own reorder doesn't wipe it; membership
changes (remove) clear history since they aren't reorder-undoable. Trilingual.
2026-06-15 21:52:28 +02:00
npeter83
1ddeac57cd Merge improvement/s4d-editable-mirrors: editable YouTube playlist mirrors 2026-06-15 21:40:20 +02:00
npeter83
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
npeter83
49d226126c Merge feature/s4d-youtube-playlist-push: YouTube playlist write-back 2026-06-15 21:23:31 +02:00
npeter83
8911e9dc89 feat(playlists): Sync to YouTube UI + delete-on-YouTube choice
Editable local playlists get an Export/Sync to YouTube button (write-scope gated):
it fetches a dry-run plan, shows a confirm with the change counts, quota estimate
and divergence warning, then pushes. An 'unsynced changes' badge and an accented
YouTube icon mark linked playlists with local edits. Deleting a linked playlist
offers 'delete on YouTube too' vs 'here only'. Trilingual strings (HU/EN/DE).
2026-06-15 21:23:13 +02:00
npeter83
b150337c87 feat(playlists): push local playlists to YouTube (export + diff sync)
Local 'user' playlists (not Watch later, not YouTube mirrors) can now be pushed
to YouTube: create a new playlist on first push, else reconcile membership + order
so YouTube matches local (local wins). plan_push computes a dry-run estimate
(inserts/deletes/reorders + quota units + divergence) read from the live YouTube
state; push refuses if the estimate exceeds the remaining daily quota so a big
playlist can't strand half-pushed. Reorder uses an insertion-sort move count that
matches what executes. Edits to a linked playlist set dirty; a successful push
clears it. DELETE accepts on_youtube to also delete the playlist on YouTube. The
read-sync now skips YouTube playlists already owned by a local export, avoiding
duplicate read-only mirrors.
2026-06-15 21:23:02 +02:00
npeter83
b7bf6cc6f9 feat(playlists): YouTube write API client methods
Add OAuth-only write helpers to YouTubeClient (each 50 quota units):
create_playlist, add_playlist_item, move_playlist_item, delete_playlist_item,
delete_playlist, plus iter_playlist_items_with_ids for diffing. A shared _write
helper records quota and raises YouTubeError on non-2xx.
2026-06-15 21:22:51 +02:00
npeter83
35831dc3ce Merge feature/s4c-youtube-playlist-pull: YouTube playlist read-sync
S4c (read direction, YT -> local):
- feat(playlists): mirror users' YouTube playlists into source=youtube playlists,
  ingesting catalog-missing videos; manual /api/playlists/sync-youtube + scheduler job
- feat(playlists): YouTube-mirrored playlists shown read-only with a Sync button
- fix(notifications): success level for success toasts (green check, not accent)
2026-06-15 21:02:49 +02:00
npeter83
29696c549c fix(notifications): use the success level for success toasts
The 'Synced N playlists' and 'View link copied' toasts used the default info level,
which renders in the accent colour and read as an error/alert. Mark them level:success
so they show the green check — clearly positive and theme-independent.
2026-06-15 19:46:37 +02:00
npeter83
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
npeter83
a661d079ee feat(playlists): YouTube playlist read-sync (backend)
Mirror each user's own YouTube playlists into local source='youtube' playlists,
one-way (YT -> local). New client methods iter_my_playlists / iter_my_playlist_video_ids
(OAuth, so private playlists work); sync/playlists.py reconciles the mirror (matching YT
order) and ingests any playlist videos not in the shared catalog yet (with stub channels).
POST /api/playlists/sync-youtube for manual sync (read-scope gated, per-user quota) plus a
scheduler job (playlist_sync_minutes, default 6h) that syncs all read-scope users. YouTube's
Watch Later / History are not API-accessible and are never synced.
2026-06-15 19:37:03 +02:00
npeter83
9c28a75787 Merge feature/s4b-watch-later-queue: Watch later unification + queue playback
S4b:
- feat(playlists): unify Saved into a built-in Watch later playlist (migration 0012)
- feat(playlists): queue playback in the player (auto-advance + prev/next)
- feat(playlists): live cross-tab queue (track by id, refetch on focus)
- fix(header): keep the sync-status count live (focus refetch + background poll)
2026-06-15 17:04:36 +02:00
npeter83
4ff0b9705c fix(header): keep the sync-status count live (focus refetch + background poll)
The header 'N without full history' (and sync state) polled every 30s but only while
the tab was focused, and the global refetchOnWindowFocus is off — so after switching
tabs the count looked frozen until a manual reload. Poll in the background too and
refetch on focus so it tracks the scheduler's progress without F5.
2026-06-15 17:00:06 +02:00
npeter83
ddb1890e84 feat(playlists): keep the player queue live across tabs
Track the playing item by id instead of a frozen index, deriving the index from the
live queue so the N / M counter and prev/next neighbours stay correct when the playlist
changes underneath (e.g. an item removed in another tab) without disrupting playback of
the current video. Refetch the playlist list/detail on window focus so such changes flow
in, updating the player's queue length in near-real-time.
2026-06-15 16:27:28 +02:00
npeter83
4689d2cbbd feat(playlists): queue playback in the player (auto-advance + prev/next)
PlayerModal now accepts an optional queue + startIndex. The active item drives the
player; it recreates per item (reusing the single-video resume / auto-watch / progress
logic), auto-advances to the next item when one ends, and shows Previous / Next controls
with an N / M indicator. The Playlists page passes the playlist as the queue from Play all
or a clicked row. Trilingual previous/next strings.
2026-06-15 16:11:37 +02:00
npeter83
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
npeter83
47bad6d9ce Merge fix/playlist-detail-invalidation: live playlist detail after popover add 2026-06-15 15:20:28 +02:00
npeter83
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
npeter83
a6a0c429d5 Merge feature/s4a-playlists-foundation: local playlists foundation
S4a (local only; YouTube sync in later phases):
- feat(playlists): backend model + migration 0011 + CRUD API
- feat(playlists): Playlists page (rail + detail, drag-reorder, rename, delete,
  remove item, play) + AddToPlaylist popover (cards + player) + nav
- feat(ui): reusable app-styled confirm dialog (ConfirmProvider/useConfirm),
  replacing window.confirm
- fixes: popover overflow flip, post-delete refresh/reselect, page persistence
  across reloads, header title on the Playlists page
2026-06-15 15:17:35 +02:00
npeter83
4ed52cd244 fix(playlists): correctly clear/reselect after deleting a playlist
The post-delete auto-select effect re-picked the just-deleted id from the still-stale
playlists cache, so its detail lingered. Select the next remaining playlist directly on
delete (or null), drop the deleted detail from the cache, and make the effect re-validate
the selection against the current list (clear when empty, reselect when the current id is
gone). Also fix the header title showing "Channel manager" on the Playlists page.
2026-06-15 15:15:56 +02:00
npeter83
48cb11434d feat(ui): reusable app-styled confirm dialog, replacing window.confirm
Add a promise-based ConfirmProvider + useConfirm() hook (built on the Modal shell,
liquid-glass styling, danger variant, Esc/backdrop = cancel, Enter = confirm) so
confirmations match the app instead of the native browser dialog. Wire it in at the
app root and replace both window.confirm call sites (playlist delete, channel
unsubscribe). Trilingual common.confirm/confirmTitle strings.
2026-06-15 15:06:47 +02:00
npeter83
50bc35d78f fix(nav): persist the current page across reloads
Since filters/page are no longer mirrored to the URL (de-URL refactor), pressing F5
on the Channels/Playlists/Stats page dropped back to the feed because the initial
page was read only from the (now-absent) ?page= param. Persist page to localStorage
and restore it on load; a share link's ?page= still takes precedence.
2026-06-15 14:59:24 +02:00
npeter83
d7fc237d66 fix(playlists): clear the detail panel after deleting the selected playlist
The detail pane rendered from react-query's cached data, so deleting the selected
playlist emptied the rail but left its stale contents on the right. Gate the detail
view on the current selection so it falls back to the empty state when nothing (or a
just-deleted playlist) is selected.
2026-06-15 14:56:39 +02:00
npeter83
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
npeter83
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
npeter83
bf769379cb feat(playlists): backend foundation — model, migration, CRUD API
Add per-user local playlists: Playlist + PlaylistItem models (with forward-looking
kind/source/yt_playlist_id/dirty columns for the later YouTube-sync phases) and
migration 0011. New /api/playlists routes: list (with optional contains=<video_id>
membership flags for the add-to-playlist popover), create, get detail (items
serialized via the feed serializer, with per-user watch state), rename, delete,
add/remove item, and reorder. Watch later (built-in) is protected from deletion.
2026-06-15 14:37:09 +02:00
npeter83
dcb1605cc7 Merge chore/release-0.2.0: version bump + release notes 2026-06-15 13:58:22 +02:00
npeter83
34c41f7a41 chore(release): 0.2.0 — shared library, dynamic facets, share view
Bump VERSION to 0.2.0 and add release notes covering the Mine/Library shared-feed
scope, dynamic faceted filter chips (live counts, hide non-matching, Any/All topic
match), surprise-me reshuffle, share-view links, and the deep-backfill status
clarity fixes.
2026-06-15 13:58:22 +02:00
npeter83
c69d02d01a Merge feature/s3b-deurl-filter-state: de-URL filter state + share-view link
S3b:
- refactor(filters): stop mirroring filters to the URL; localStorage canonical
- feat(filters): opt-in "Share view" link button (filters/sort/scope)
2026-06-15 12:30:17 +02:00