Commit graph

27 commits

Author SHA1 Message Date
npeter83
c84cb22e5a fix(player): freeze the launch queue so Loop All + Next/Previous use the original list
The player's queue came live from the feed query, which refetches under a
watch-state filter as each video is auto-marked watched — steadily shrinking
the list mid-session. Loop "All" could then never wrap back to the first
video (earlier items had dropped out), and prev/next drifted. Snapshot the
queue once at mount (useState lazy init) and step through that frozen list for
the whole session; the live prop is ignored thereafter. Supersedes the earlier
active-item pin with a list-level freeze that also fixes Loop All.
2026-07-05 20:50:05 +02:00
npeter83
0fff5ad35d fix(player): keep playing the current video when it leaves the live feed queue
Auto-marking a video watched near its end invalidates the feed query; the
refetched/reordered queue can no longer contain the playing item, so
findIndex returned -1 and the active item fell back to queue[0] — silently
jumping playback to an unrelated video and defeating Loop "One" (and
auto-advance:off). Pin the last resolved video in a ref and keep playing it
when it drops out of the queue instead of snapping to queue[0].
2026-07-05 19:49:39 +02:00
npeter83
d02c338465 feat(player): persistent auto-advance + loop playback modes
Two saved-to-account playback settings on the player's prev/next bar (any queued
player — feed or playlist): Auto-advance (Off/Next/Prev/Random — what plays when a
video ends) and Loop (Off / One = repeat the current video / All = wrap the list
at its ends; a single-item list repeats). Stored in users.preferences
(playerAutoAdvance/playerLoop), read from the cached me + written via savePrefs so
they apply everywhere and survive reloads.

The feed's player queue is now the live filtered feed with the watch-state filter
NOT applied — marking the current video watched keeps it in the sequence (no
reindex/reload mid-play), while a hidden video still drops out. Removes the old
frozen-queue workaround and the boolean autoAdvance prop. i18n en/hu/de.
2026-07-05 00:20:50 +02:00
npeter83
6c81419057 feat(player): full-height glassy nav strips flanking the modal
Replace the small centred prev/next arrow buttons with faint translucent glass
strips that span the modal's full height, hugging each side of the card (hidden
on narrow screens where there's no room). Same stepping behaviour + Shift+arrow
shortcut; just a larger, easier target that reads as part of the dialog.
2026-07-04 23:42:13 +02:00
npeter83
c81c4a351b feat(player): show the Shift+arrow shortcut in prev/next tooltips 2026-07-04 23:20:02 +02:00
npeter83
53eba7a57d feat(player): step through the feed, reachable native controls, hover-intent
Three in-app player refinements:
- Prev/next stepping: the modal now takes the feed's loaded order as a queue, with
  faint arrow zones flanking the card and Shift+Left/Right shortcuts (plain arrows
  seek ±5s). The queue is frozen at open so marking the current video watched can't
  drop it and reload a different one; auto-advance-on-end stays off for the feed
  (it isn't a playlist) — playlists keep theirs.
- Reachable native YouTube controls: the interaction overlay (wheel volume / click
  pause / keyboard focus) now covers only the centre band (top-[12%] bottom-[22%]),
  leaving the top-right cluster (volume/CC/settings) and the bottom bar (seek /
  More videos / fullscreen) clickable. Verified live: settings menu + fullscreen work.
- Hover-intent description: the title popover now waits ~400ms so a quick pass no
  longer flashes it.
2026-07-04 22:25:37 +02:00
npeter83
7d1ed24fea feat(downloads): M5 — Download Center frontend
- api.ts: download types + methods (profiles, enqueue, list/manage, file url, share,
  usage, index, admin storage/quota)
- format.ts: formatBytes / formatSpeed
- i18n: en/hu/de downloads.json (trilingual)
- Downloads nav module (Download icon, active-count badge, hidden for demo) placed after
  Messages; urlState page + App render + Header title
- DownloadButton: self-contained per-card/player affordance (demo-hidden, reflects the
  per-user download index: downloaded / in-queue), opens the preset dialog
- DownloadDialog: preset picker + optional display name -> enqueue + 'View' toast
- ProfileEditor: manage built-in + custom formats (full spec form incl. SponsorBlock)
- DownloadCenter: Queue / Library / Shared / (admin) System tabs, live progress via
  useLiveQuery, usage bar, pause/resume/cancel/delete, save-to-device, rename, share,
  admin storage dashboard + per-user quota editor
- wired DownloadButton into VideoCard + PlayerModal
- lib/nav.ts: decoupled navigator so the download toast can jump to the page

tsc + vite build clean. Verified in a headless authed browser: page + tabs render, Library
shows a real download with usage bar + actions, no console errors.
2026-07-03 01:52:58 +02:00
npeter83
419231bddb fix(player): open the channel page from the player without it bouncing back
Two bugs made clicking the channel name in the video modal just close the player:
- the second PlayerModal mount (main feed path) was missing onOpenChannel, so the handler
  hit its no-op early return;
- opening the channel synchronously pushed the _chan history entry, which the player's own
  useBackToClose teardown (history.back on unmount) then immediately popped. Now the open is
  deferred to a one-shot popstate listener that fires AFTER that teardown, so the channel
  entry lands at the feed level. Verified: player → channel name → channel page; Back → feed
  (player does not reappear).
2026-06-30 04:14:14 +02:00
npeter83
641fc393a7 feat(player): channel name opens the in-app channel page
The PlayerModal channel name now opens our channel page (closing the player first); a
small external-link icon beside it keeps the open-on-YouTube behaviour. Threaded
onOpenChannel from Feed into both PlayerModal mounts.
2026-06-30 03:12:44 +02:00
npeter83
d84d5a1819 feat(player): keyboard + scroll-wheel controls in the video modal
Adds player-modal shortcuts and keeps keyboard focus on the modal (not the
cross-origin player iframe) on open and on player-ready, so they work until you
click into YouTube's native controls:
- F: toggle fullscreen (the player stage element, so the volume flash stays
  visible; a :fullscreen CSS rule drops the 16:9 letterbox). Esc in fullscreen
  exits fullscreen only, without also closing the modal.
- Space: play/pause (ignored while a button/input is focused).
- Scroll wheel over the video: volume up/down with an auto-fading volume-bar
  overlay. A transparent interaction layer over the iframe catches the wheel
  (the cross-origin iframe otherwise swallows it) and maps click to play/pause;
  it leaves the bottom strip uncovered so YouTube's native control bar (seek /
  settings / captions / fullscreen) stays usable, and is hidden on an embed
  error so the 'Open on YouTube' CTA stays clickable.
i18n shortcutsHint EN/HU/DE.
2026-06-29 23:37:18 +02:00
npeter83
83540b01eb refactor(player): extract description link-rendering into lib/descriptionLinks
Moves the ~140 lines of pure URL/timestamp/hashtag parsing + renderDescription out
of the 659-line PlayerModal into a standalone, testable module. No behavior change.
2026-06-29 00:24:49 +02:00
npeter83
71f79fc73d fix(nav): Back steps through sub-views and closes modals before leaving a page
In-app history only tracked the top-level page, so Back from a module sub-view
(e.g. a Messages thread) or with a modal open jumped straight to the previous
module. Add two history primitives: useHistorySubview (a module's sub-view rides
in history.state, so Back returns to its root first) and useBackToClose (a
mounted overlay occupies one history entry; Back closes the topmost, nesting-safe
so a button-close doesn't trip the modals underneath). Apply to the Messages page
views and to PlayerModal + the shared Modal. setPage now pushes a clean entry so
each page starts at its root.
2026-06-26 01:37:43 +02:00
npeter83
5461cada84 refactor(channels): share one ChannelLink + channelYouTubeUrl helper
The "avatar + name + open-on-YouTube" cell and the @handle-or-/channel/<id>
URL were copy-pasted across the channel manager, the discovery tab, the
subscribe notice and the player. Extract a single ChannelLink component
(optional in-app onView, middle-click opens YouTube) and a channelYouTubeUrl
helper, and route all four through them. Removes the NameCell / DiscoveryNameCell
duplication (the latter introduced with the discovery tab).
2026-06-19 03:22:10 +02:00
npeter83
a09e0dda0c fix(player): persist watched at end-of-playback (atomic upsert)
The in-app player marks a video watched when it reaches the end, firing
POST /videos/{id}/state at the same instant as a progress checkpoint. The
progress endpoint deletes the 'new' video_states row near the end, so the
concurrent state UPDATE matched 0 rows and raised StaleDataError -> 500. The
watched status was never persisted (and Feed swallowed the error), so the
video stayed in the unwatched feed even after a refresh.

- set_video_state: atomic INSERT ... ON CONFLICT (uq_user_video) DO UPDATE for
  watched/hidden, immune to a concurrent delete; tolerate StaleDataError on the
  'new' branch and in the progress endpoint.
- PlayerModal: skip the redundant near-end progress checkpoint when we just
  auto-marked watched, removing the self-inflicted race.
- Feed: drop the optimistic override if the server rejects the change, so a
  failed request can't leave a card phantom-hidden.
2026-06-17 13:38:47 +02:00
npeter83
de4d657d42 fix(player): make the unavailable overlay fully opaque
The fallback used a translucent background and left the iframe mounted, so
YouTube's own 'Video unavailable' screen bled through and overlapped our
message. Use a solid background and hide the iframe while the error shows.
2026-06-16 11:58:57 +02:00
npeter83
c2d9e7b5a9 fix(player): friendly fallback when a video can't be embedded
The in-app IFrame player showed YouTube's bare 'Video unavailable' screen
with no way out when a video couldn't be embedded (auto-generated Topic
art-tracks disable embedding -> error 101/150; removed/private -> 100).
Catch the IFrame onError and overlay a clear message + 'Open on YouTube'
button, with a tailored note for embedding-disabled videos.
2026-06-16 10:45:57 +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
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
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
ea317c0009 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
04c971f623 feat(feed): resume progress bar, play/continue/restart, in-progress filter
Video cards show a resume progress bar for started-but-unfinished videos and a
hover overlay: Play on every card, Continue + Restart on in-progress ones. The
in-app player now resumes from (and checkpoints to) the server position instead
of localStorage, accepts an explicit startAt (Restart -> 0), and refreshes the
feed on close so the card bar reflects the session. Sidebar gains an
'In progress' show filter.
2026-06-14 18:40:12 +02:00
npeter83
a640b181ee fix(ui): robust avatars — no-referrer + graceful fallback
Channel/account avatars come from Google's image CDNs (yt3.ggpht.com,
lh3.googleusercontent.com). On a feed page dozens load at once; the CDN
rate-limits the referrer-bearing burst (429), so a random subset rendered the
browser's broken-image icon (the URLs themselves are valid — verified 200).

Add a shared <Avatar> that sets referrerPolicy="no-referrer" (which the CDNs
serve without throttling) and falls back to a neutral initial placeholder on
error instead of the broken-image icon. Use it for video-card, player, channel
manager, header and settings avatars.
2026-06-12 18:01:43 +02:00
npeter83
1bdde051c1 feat(player): linkify descriptions and play YouTube links inline
- Narrow the title hover target to the actual text, not the whole row.
- Linkify descriptions: timestamps (mm:ss / hh:mm:ss) seek the player; emails
  become mailto:; hashtags link to YouTube's hashtag feed; other URLs open in a
  new tab. Blank lines are stripped so the popover isn't mostly whitespace.
- YouTube links play in the inline player: a link to the current video seeks
  (honoring t=), a link to another video navigates the player to it, with a Back
  button to the original. While on a linked video the title/author come from the
  player and its views/date/duration + a clickable channel come from the detail
  endpoint, which falls back to the YouTube API (videos.list, attributed to the
  user) for videos not in our DB.
2026-06-12 17:39:20 +02:00
npeter83
195044042f feat(player): polish the description popover (opaque, portaled, opens upward)
The popover bled through (it used the translucent glass surface), was clipped by
the modal card's overflow, and ran off the bottom of the viewport. Make it an
opaque surface, render it through a portal to <body> with fixed positioning so
nothing clips it, and anchor it above the title so it grows upward where there's
room. Add a small hover grace so the pointer can travel title → popover.
2026-06-12 17:39:08 +02:00
npeter83
fcb77ac2e1 feat(player): watched controls, compact layout, description popover
- Watched: an explicit toggle in the modal (Mark watched / Watched→unmark) plus
  auto-mark when playback reaches the end (within 10s, or on the ended event).
- Compact layout: drop the header bar and the redundant 'Open on YouTube' button
  (the embed's own YouTube logo already jumps out); Close moves to the title row,
  channel + meta share one line — fits without a scrollbar at higher zoom.
- Card actions reflect status: watched shows a double-check, saved a filled
  bookmark, with matching tooltips.
- Description: new GET /api/videos/{id} exposes the already-stored description,
  shown in a popover when hovering the modal title (fetched lazily).
2026-06-12 17:39:01 +02:00
npeter83
99dfa7691c feat(player): in-app modal YouTube player with resume
Left-clicking a feed card (Ctrl/Cmd/middle still open youtube.com in a new tab)
opens a modal that plays the video in-app via the YouTube IFrame Player API
instead of leaving the app. Using the JS API (not a bare embed) lets us read the
playback position: it's checkpointed per-video in localStorage and on close, and
restored via the 'start' param when the video is reopened. The modal closes via
a header button, the backdrop, or ESC (ESC only while focus is on our page — a
cross-origin iframe owns its own key events).
2026-06-12 17:38:45 +02:00