Commit graph

193 commits

Author SHA1 Message Date
npeter83
12339009b5 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
240b6fb611 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
cbf5037374 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
2e536afe21 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
639d761bef 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
bd085fc88f 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
2f66196816 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
86844b0bdd Merge fix/playlist-detail-invalidation: live playlist detail after popover add 2026-06-15 15:20:28 +02:00
npeter83
5400de3418 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
52d8ce14e7 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
26e72d5d00 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
8a82d56950 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
49cf2246b8 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
201ec4c3fb 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
81e28b083d 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
37804ee393 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
da9dcf93d5 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
2ec1887abf Merge chore/release-0.2.0: version bump + release notes 2026-06-15 13:58:22 +02:00
npeter83
b0b1800047 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
95d0e4ae33 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
npeter83
78182f3ba6 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
eef64ef811 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
282cc978f6 Merge feature/s3-dynamic-facets: dynamic faceted filter chips
S3a:
- feat(feed): /api/facets endpoint for contextual per-tag channel counts
- feat(filters): dynamic chips (live counts, hide non-matching, sort by count)
- feat(filters): prominent topic Any/All match toggle
- fix(feed): conjunctive facet counts in AND mode
2026-06-15 12:25:10 +02:00
npeter83
2866baa3e8 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
npeter83
c44721ed98 fix(feed): conjunctive facet counts when topic match mode is AND
In AND ("All") topic mode the facet endpoint still excluded the topic selections
when counting topic chips, so every topic kept its full count and none dropped
out as you narrowed — e.g. picking Comedy left Cooking visible even though no
channel has both. Count topics conjunctively in AND mode (keep the selected
topics applied) so each remaining chip reflects channels that ALSO have all
already-selected topics; non-co-occurring tags fall to zero and hide. OR mode
stays disjunctive. Verified: Comedy selected narrows topic chips 21 -> 6.
2026-06-15 12:20:08 +02:00
npeter83
6e42e010dd feat(filters): sort facet chips by count, then name
Order topic/language chips by their (contextual) count descending, name as the
tiebreaker, so the most-populated tags sit at the top and the smallest counts
fall to the bottom as you scan down.
2026-06-15 12:10:09 +02:00
npeter83
d910bca5cf feat(filters): dynamic faceted chips driven by /api/facets
Topic and language chips now show live channel counts for the current filter
context instead of the static global count, and chips that match nothing are
hidden (selected chips stay so they can be cleared). Selecting a channel (or any
filter) drops the now-irrelevant chips and updates the rest. Extract a shared
filterParams() so the feed and facets queries see identical filters; the facets
query is keyed on filters so it refetches as they change. Trilingual empty-state
string when a category has no matching tags.
2026-06-15 12:06:02 +02:00
npeter83
17e64156b8 feat(feed): /api/facets endpoint for contextual tag counts
Add a facets endpoint that returns per-tag channel counts for the current filter
context (scope, channel, date, content type, search, watch state, and the other
category's tags). Each category is counted with its own selections ignored —
standard drill-down faceting — by a new exclude_tag_category param threaded into
_filtered_query, so selecting one topic doesn't zero out the other topics. Count
is distinct channels with a matching video, keeping the channel-count chip
semantics. Reuses the feed's filter query so both stay in lockstep.
2026-06-15 12:05:53 +02:00
npeter83
cf2b49a179 Merge improvement/deep-queue-chip-clarity: clearer deep-backfill status
- improvement(channels): distinct "full history coming" label when another user
  queued a channel's full history (vs your own "full history queued")
- improvement(sync): header shows an active "fetching history" state during deep
  backfill instead of a misleading "all synced"
2026-06-15 04:31:56 +02:00
npeter83
99b963dbe6 improvement(sync): reflect deep backfill as active in the header status
The header status only considered recent-sync pending, so it read "all synced"
while the scheduler was still backfilling full history — directly contradicting
the adjacent "N without full history" notice. Add an active "fetching history"
state (spinner) shown when recent sync is done but deep backfill is still
pending, so "all synced" appears only when nothing is pending at all. The admin
pause button now also shows during deep backfill (it's pausable work). Trilingual.
2026-06-15 04:30:27 +02:00
npeter83
12fe21d693 improvement(channels): distinct label for full-history queued by another user
The channel manager already styled the two deep-backfill-pending states
differently (solid clickable chip when you requested full history vs a faint
outline when another subscriber did), but both used the same "full history
queued" label, so a channel queued by someone else looked identical to one you
queued yourself. Give the by-other case its own label ("full history coming")
so the distinction is legible at a glance, not just on hover. Trilingual.
2026-06-15 04:23:49 +02:00
npeter83
2604f55dac Merge feature/s2-shared-library: shared-library feed scope
S2 shared-library feed:
- feat(feed): scope=all query path to browse the whole shared catalog
- feat(feed): Mine/Library header toggle + read-scope-less browse path
2026-06-15 04:14:48 +02:00
npeter83
eb58f436b6 feat(feed): shared-library scope toggle in the header
Add a "Mine / Library" segmented toggle (shown on the feed page) that switches
FeedFilters.scope between the user's own subscriptions and the whole shared
catalog. A read-scope-less user (signed in but no YouTube grant) can now browse
and manage the shared library on their own account — the empty "my feed" state
offers a "browse the shared library" shortcut alongside the connect-YouTube CTA.
scope is preserved across "Clear all" (it's a mode, not a filter) and kept out
of the URL state. Trilingual strings (HU/EN/DE) for the toggle and the CTA.
2026-06-15 04:06:22 +02:00
npeter83
95a18dfcc1 feat(feed): add scope=all to browse the whole shared catalog
The feed query was always scoped to the user's own non-hidden subscriptions
via an INNER JOIN on subscriptions. Add a scope param: scope=my (default)
keeps that behaviour; scope=all LEFT-joins the subscription instead, so every
video in the shared catalog shows while per-channel priority still resolves
for channels the user is subscribed to. Per-user watch state stays private via
the VideoState outer join in both modes. The priority sort is made null-safe
(coalesce to 0) since unsubscribed channels have no subscription row in all-mode.
2026-06-15 04:06:14 +02:00
npeter83
e7e35578b8 Merge feature/s1-filter-quick-wins: reshuffle button + channel counts on chips
S1 filter quick wins:
- feat(filters): channel-count badge on topic/language chips
- feat(feed): reshuffle button for the shuffle ("surprise me") sort
2026-06-15 03:10:40 +02:00
npeter83
581faeaa34 feat(feed): reshuffle button for the "surprise me" sort
The backend shuffle sort already accepts a seed param. Add a circular
reshuffle button next to the sort control (shown only when shuffle is
active) that re-rolls the seed and re-queries the feed; selecting shuffle
also seeds a fresh order instead of the deterministic seed-0 one. The seed
lives in FeedFilters but is intentionally kept out of the URL state.
2026-06-15 03:08:10 +02:00
npeter83
ecea6b403c feat(filters): show channel counts on topic and language chips
The per-tag channel_count was already returned by GET /api/tags but only
surfaced in the chip tooltip. Render it as a small count badge on the chip
face so the relative weight of each topic/language is visible at a glance.
2026-06-15 03:08:03 +02:00
npeter83
1cb60066cd fix(version): VERSION-file as source of truth
Some checks failed
CI / frontend (push) Failing after 43s
CI / backend (push) Failing after 38s
2026-06-15 02:22:27 +02:00
npeter83
cf14c53b89 fix(version): read app version from the committed VERSION file
Make APP_VERSION come from the VERSION file (Dockerfile reads it for the SPA build
and ships it for the backend to read at runtime) instead of depending solely on a
deploy-time build-arg. This keeps the version correct even for a plain
`docker compose build` and removes the first-deploy bootstrap gap. git_sha/build_date
stay best-effort build-args.
2026-06-15 02:22:27 +02:00
npeter83
9c639c3a92 Release v0.1.0: watch progress, status bar, version/release-notes, i18n (HU/EN/DE)
Some checks failed
CI / frontend (push) Failing after 37s
CI / backend (push) Failing after 45s
2026-06-15 02:09:22 +02:00
npeter83
30a84cbcb9 docs(release): note multilingual UI in v0.1.0 release notes 2026-06-15 02:09:22 +02:00
npeter83
196eff6232 feat(channels): clearer error when a YouTube action lacks the scope
api: HttpError now carries the server's detail. Channels sync/backfill/unsubscribe
now detect a 403 (no YouTube grant) and show a 'connect your YouTube account'
message with a Connect action that opens the onboarding wizard, instead of a vague
'failed' toast. Translated HU/EN/DE.
2026-06-15 02:02:05 +02:00
npeter83
9c61bd898d feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
npeter83
b38ae92d8d feat(i18n): translate login and app chrome (HU/EN/DE)
Login screen (with a language picker), header, account menu, sync status, About and
Release Notes dialogs, and the version banner are now fully translated in Hungarian,
English and German.
2026-06-15 00:30:34 +02:00
npeter83
c165c3f274 feat(i18n): foundation — react-i18next, language switcher, server-persisted choice
Set up react-i18next with locale files auto-loaded per area (Vite glob), a compact
LanguageSwitcher, and language as a server-persisted preference (preferences.language)
mirrored to localStorage. On first login the default UI language is guessed from the
Google-reported locale (hu/en/de, else English). vite-env.d.ts types the build-time env.
2026-06-15 00:30:34 +02:00
npeter83
ffc8a87fe4 Merge: release notes, About dialog, and version stamping 2026-06-15 00:07:13 +02:00
npeter83
31591d8ff1 feat(ui): About dialog, Release Notes, and new-version banner
About (in the account menu) shows frontend/backend/database versions + build.
Release Notes renders per-version highlights with a commit-SHA reference; a
dismissible banner appears once after the running build's version changes and
links into the notes. Adds a reusable Modal shell and the release-notes data
(detailed v0.1.0).
2026-06-15 00:06:57 +02:00
npeter83
882429d6af feat(version): /api/version + build-time version/commit stamping
Add a VERSION file (0.1.0) and inject APP_VERSION/GIT_SHA/BUILD_DATE as Docker
build-args (both stages; Vite inlines them into the SPA). New public GET
/api/version returns app_version, git_sha, build_date and the Alembic head as the
database revision. deploy.sh and the localdev build pass the args.
2026-06-15 00:06:57 +02:00
npeter83
8a0da8df83 Merge: repo hygiene for public readiness
Some checks failed
CI / frontend (push) Failing after 38s
CI / backend (push) Failing after 33s
2026-06-14 23:20:02 +02:00
npeter83
c19f4d9d31 chore(repo): remove internal deployment plan, sanitize infra references
Drop docs/deployment-plan.md (internal planning doc, kept out of the repo) and
replace the personal lab DB IP and host names in the env/compose/deploy docs with
generic placeholders, so the repo is safe to make public later.
2026-06-14 23:20:02 +02:00