Commit graph

309 commits

Author SHA1 Message Date
npeter83
a76bbb5825 chore(release): v0.5.0 — demo account, clearer live videos 2026-06-16 10:23:29 +02:00
npeter83
848fdd386f improvement(demo): permanent banner instead of a re-popping toast
The shared-account warning was a requiresInteraction toast that re-fired
on every reload. Replace it with a permanent, non-dismissible banner (like
the version banner) shown above the content while in the demo account.
2026-06-16 10:12:10 +02:00
npeter83
d42a038a3f feat(card): show LIVE/upcoming badge where duration is absent
Live streams and upcoming/premiere videos have no fixed duration, so the
playlist row and video card left a blank where the runtime usually sits.
Show a LIVE (red) or upcoming badge instead, so it's clear the missing
time is by design, not a sync gap.
2026-06-16 09:36:51 +02:00
npeter83
484c6364e6 fix(demo): graceful UX when YouTube actions are unavailable
The shared demo account no longer hits YouTube affordances: the onboarding
wizard never opens (or renders) for it, the empty-feed prompt nudges into
the shared library instead of the connect wizard, and the browser-facing
/auth/upgrade redirects the demo home instead of returning a raw 403 JSON.
2026-06-16 09:27:34 +02:00
npeter83
6405350104 feat(demo): login trigger, feature gating + admin UI (HU/EN/DE)
Login page quietly probes /auth/demo (debounced) as a valid email is
typed/pasted and reloads into the app on a match — no visible button.
Demo sessions default to the whole library, get a one-time shared-account
warning, never see the YouTube-connect onboarding/access UI or sync
actions, and admins get a demo whitelist + reset panel in Settings.
2026-06-16 09:17:34 +02:00
npeter83
af84cc0404 chore(release): v0.4.1 — feed toolbar + clearer counts/dates
Bump VERSION to 0.4.1 and add the 0.4.1 release-notes entry (feed view/content-type chips
above the videos, key+direction sort, exact view count + upload date on cards/player).
2026-06-16 03:18:58 +02:00
npeter83
5a2928b810 feat(player): exact upload date inline in the player meta too
Mirror the card change in PlayerModal: append the precise locale-aware date after the
relative time, for both the active video and a linked (navigated) video's stats.
2026-06-16 03:09:28 +02:00
npeter83
71d7440a8b feat(card): show the exact upload date inline after the relative time
There's room on the meta line, so append the precise date (locale-aware, e.g. '12 Jan
2017') after '9 yr ago' instead of hiding it in a tooltip.
2026-06-16 03:04:50 +02:00
npeter83
48e7f8d11e feat(card): exact view count on hover (disambiguate 9B vs 98)
The abbreviated count (e.g. '9B' for ~9 billion) can read like '98' since the B glyph
resembles an 8. Add a title tooltip with the full localized count on the views label.
2026-06-16 02:57:53 +02:00
npeter83
4921a95d09 feat(feed): Show chips in the toolbar + key+direction sort
1) Move the Show view filter (Unwatched/In progress/All/Watched/Hidden) up into the
   toolbar chip row as its own single-select group, divided from the content-type chips;
   removed the 'show' sidebar widget (sidebar now = Upload date / Language / Topic).
2) Rework ordering like the Playlists page: a sort-key dropdown (Date, Popular, Duration,
   Name, Channel subscribers, Channel priority, Surprise me) + a single asc/desc arrow
   toggle, instead of separate directional entries. 'Most viewed' is now 'Popular'.
   Backend gains the missing directions (views_asc, title_desc, subscribers_asc,
   priority_asc); the frontend maps (key, dir) -> the backend sort string, so FeedFilters
   is unchanged. Trilingual (feed.sortKey.*, feed.dirAsc/dirDesc). Feed.tsx normalized to LF.
2026-06-16 02:46:26 +02:00
npeter83
d96b47ae5d feat(feed): 1D — content-type chips + sort in a toolbar above the cards
Move content type and ordering out of the filter sidebar into a feed toolbar (approved
proposal 2): a prominent content-type chip row (Normal/Shorts/Live toggle chips) with the
sort control + count + reshuffle in a quieter row beneath it. The toolbar renders even on
'no matches' so content-type can be turned back on after it filtered everything out. The
sidebar keeps Show / Upload date / Language / Topic; 'sort' and 'content' widgets removed
from the layout (normalizeLayout drops them from any saved layout). New feed.sortLabel
string (HU/EN/DE).
2026-06-16 02:34:13 +02:00
npeter83
f6efe44e37 chore(release): v0.4.0 — navigation overhaul, glass, account switch
Bump VERSION to 0.4.0 and add the 0.4.0 release-notes entry (Epic N: left nav sidebar,
multi-account switch, Settings page, in-app back, frosted-glass refresh).
2026-06-16 02:16:21 +02:00
npeter83
583e003c23 feat(auth): N3 — server-side multi-session account switch
Track every account that completes OAuth in a browser session (session 'account_ids',
~14-day cookie), so the user can switch between them without another Google round-trip.
New GET /api/me/accounts (switchable accounts, active flagged) and POST /api/me/switch
(guarded: target must be in the session list — proof it signed in here — and re-checked
against is_allowed in case the invite was revoked). Logout now signs out the active
account and falls back to the most recent remaining one, else clears the session. UI: the
account popover lists the other signed-in accounts (click to switch, reloads) plus an
'Add another account' action (-> /auth/login, which uses prompt=select_account).
Trilingual. Third step of Epic N.
2026-06-16 02:05:38 +02:00
npeter83
8c076f7a75 feat(ui): brighten dark-mode glass so the frost is visible over dark UI
Dark glass looked nearly flat because the blurred backdrop is itself dark. Add a
brightness lift to backdrop-filter in dark mode for .glass/.glass-menu so the frosted
sheen reads even over dark surfaces (over colourful content it just looks richer).
Perf-mode still disables the blur (later rule wins).
2026-06-16 01:57:41 +02:00
npeter83
ed2d76f92d fix(nav): remove backdrop div so the account popover frosts
The full-screen transparent dismiss backdrop sat directly behind the popover and acted as
a compositing layer, so the popover's backdrop-filter sampled it (empty) instead of the
page — making the glass look fully solid. Drop the backdrop and dismiss via document
mousedown/Escape listeners (like the other menus). Now backdrop-filter samples real
content and the frost shows.
2026-06-16 01:54:01 +02:00
npeter83
a18924bf97 fix(nav): restore frosted glass on the portaled account popover
Now that the popover is portaled to body its backdrop-filter actually samples the content
behind it, so the near-opaque glass-menu (needed only as a workaround while blur was
trapped) made it look solid. Switch it back to the frosty .glass (78%); the working blur
softens the background so text stays readable without the bleed-through. Trapped header
menus (language/notifications) keep glass-menu.
2026-06-16 01:50:01 +02:00
npeter83
5bb85b039a fix(nav): portal account popover to body (stacking + click-through)
The nav's .glass backdrop-filter makes it a containing block for fixed descendants and a
stacking context, so the account popover's fixed inset-0 backdrop only covered the nav (not
the viewport) and the popover sat below the main content — controls behind it stayed
clickable. Portal the popover + its dismiss backdrop to <body> with fixed coords computed
from the trigger rect, so it's truly top-most (z-50) and blocks clicks underneath.
2026-06-16 01:46:32 +02:00
npeter83
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
npeter83
0adc1104a3 fix(ui): restore glass opacity in dark mode (account popover readability)
The dark-mode experiment made .glass globally more translucent (surface 66%), which
also hit content-overlapping surfaces like the account popover — text became hard to
read over the feed behind it. Drop that override so .glass is back to the readable 78%
everywhere; keep the richer dark ambient (harmless to readability). Docked chrome loses
the marginal extra frost (which was barely noticeable anyway).
2026-06-16 01:34:25 +02:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
e18be73d46 feat(filters): "Share view" link button in the sidebar
Add a share button next to Clear all that copies a link reproducing the current
filter view (filters, sort, scope) to the clipboard, with a confirmation toast.
This is the opt-in replacement for the old always-on URL mirroring. Trilingual.
2026-06-15 12:29:51 +02:00
npeter83
3e90fe2e5b refactor(filters): stop mirroring filters into the URL; localStorage is canonical
Filters/sort/search/scope were written to the address bar on every change (a leftover
from sharing reproducible examples), giving two sources of truth. Make localStorage the
single source: drop the automatic syncUrl from setFilters/setPage. A "Share view" link
still hydrates filters on first load, after which the query is stripped from the URL
(stripUrlParams) so it stays clean. syncUrl is replaced by shareUrl (builds the link on
demand); the serializer now also round-trips scope.
2026-06-15 12:29:43 +02:00
npeter83
aa4b069567 feat(filters): make the topic Any/All match toggle prominent
The AND/OR ("Any"/"All") control for topic chips was a single faint corner link
that was easy to miss. Replace it with a labelled segmented control ("Match:
[Any][All]") so the AND option is discoverable. New trilingual 'match' label.
2026-06-15 12:20:17 +02:00