Commit Graph

31 Commits

Author SHA1 Message Date
Arkadiusz Fal
b4855f5be9 Read a playlist back before writing over it
Ten review findings in the remote-playlist work, all of them the same
shape: something acted on what it remembered instead of what the server
says now.

- A confirmed-gone playlist is forgotten whole — snapshot, list entry,
  and both ordering fences — so a refresh that started before the delete
  can no longer republish it, and a list refetch that fails offline no
  longer leaves it on screen.
- `.accountsDidChange` discards account-scoped content when the account
  it belonged to is no longer the effective one, as the status
  notification already did.
- A tab that stops being offered no longer stays selected: the browse
  view moves to the first available one, which reloads by itself.
- A Handoff account id is accepted only when that account is on the
  instance the playlist lives on.
- Composite writes moved up into `AccountService`, over a transport whose
  every method is exactly one request: create-with-privacy, rename, and
  set-description. The credential is rechecked between each pair, so a
  sign-out between the two halves of a create or an edit can no longer
  send the second half under the previous user's secret.
- Editing a playlist no longer forces it private. Invidious serialises
  visibility as `isListed` — `privacy.public?`, so unlisted and private
  are indistinguishable — which the snapshot now carries; the picker
  starts there, and sends nothing at all unless it was moved. Invidious
  keeps the stored visibility when the PATCH omits it (measured), so an
  untouched picker leaves it exactly as it was.
- Tapping Edit on a server-side playlist awaits an authenticated refetch
  first and populates the form from it; a failed refetch shows an error
  and no sheet.
- Playlist rows are keyed by row rather than by video id, which two rows
  of the same video share.
- The add-to-playlist sheet offers only accounts whose instance is
  enabled and that hold a credential, alongside the write capability.
2026-09-02 23:59:31 +02:00
Arkadiusz Fal
f0ebe9b612 Reach a playlist through the account that owns it
The app could show a server-side playlist and nothing else. Editing was
never the missing part on its own: nothing on screen knew whose playlist
it was looking at, and without an owner there is no credential to fetch it
with, no row identity to remove by, and nobody to send a change to.

So the destination carries the account. Only Invidious ids hint at
ownership — the IVPL prefix — and that heuristic stays for links that
arrive from elsewhere; a Piped playlist looks exactly like any other, so
the places that already know whose list a row came out of now say so. An
owned playlist is then fetched authenticated, which is both the only way
to see a private one and the only way to learn what the server calls its
rows. A playlist read from the public endpoint has no row identities at
all, and that is precisely why nothing about it may be changed: the edit
affordances appear over the authenticated snapshot or not at all.

Rows are paired with entries by walking the two lists together rather
than by index. Entries are a subsequence — a row the server declined to
name is still displayed, it just can never be a target — and matching on
the video alone would be wrong too, since a playlist may hold the same
video twice and each copy is its own row.

Every change goes through the cache, so the refetch that reconciles it
cannot be skipped, and what the view shows afterwards is only ever what
came back. That includes the failures: a removal aimed at a row that has
since moved says so specifically, over a list that has already been
refreshed.

Adding a video now offers every account that can be written to, on every
instance rather than the selected one alone — this is not a browsing
decision. Those rows never claim the video is already there: the list
fetch does not carry a playlist's videos, so membership is unknowable
from here, and a duplicate is the server's business. Capabilities decide
throughout, never the instance type, which is what finally lets a Piped
login list and edit its playlists in the browse tab beside an Invidious
one — and what keeps the visibility picker away from a backend that has
no such setting.

Handoff leaves the account out of its payload deliberately. It assembles
its userInfo key by key, so the added value changes nothing about the
coding either way, but an account is only usable on a device holding its
credential — the receiving device resolves ownership for itself.
2026-09-02 23:44:07 +02:00
Arkadiusz Fal
2c0c2e853a Apply thumbnail fallback everywhere a single URL was rendered
The previous fix rebuilt the quality chain when converting persisted
models back to videos, but many consumers discarded it again by
rendering bestThumbnail (usually a maxresdefault.jpg that 404s for
older videos) as a single URL with no fallback.

Extract the retry logic from VideoThumbnailView into FallbackLazyImage
and use it at every in-app site that can iterate: player thumbnails
(mini bar, expanded sheet loaders, autoplay previews, tvOS audio-mode
artwork - previously a silent black screen), video info card and tvOS
header, and the tvOS playlist cover.

Sites that fetch or send exactly one URL are rewritten to the
always-available hqdefault variant via Thumbnail.reliableURL (exposed
as Video.reliableThumbnailURL): Now Playing artwork, Top Shelf
snapshots, remote control state, the frozen transition thumbnail,
blurred info background, navigation covers, and playlist covers
derived from a video's first thumbnail in Invidious/Yattee Server
responses.

Also invert RecentPlaylist's upgrade helper, which rewrote covers *to*
maxresdefault, walk the quality chain when caching download thumbnails
for offline artwork instead of giving up after one 404, and expand the
remaining single-thumbnail Piped conversions into full chains.
2026-08-03 23:34:06 +02:00
Arkadiusz Fal
aa6e7fbce9 Stop tracking resume progress for live streams
With "Partially Watched Videos" set to Ask, opening a live stream showed
the resume sheet with a playback timestamp. A live position is relative
to the live edge, not a fixed timeline, so it is meaningless as a
playhead.

Live views are still recorded in history, but carry no resume position:

- WatchEntry gains a persisted isLive flag plus recordLiveWatch(), which
  stores no position and clears state a drifting HLS duration may have
  left behind. progress is always 0 and toVideo() propagates the flag so
  history rows render the LIVE badge.
- from(video:) clamps duration, keeping Piped's -1 live sentinel out of
  storage.
- DataManager routes live saves through recordLiveWatch() and clears the
  flag on the non-live path (before updateProgress, so the auto-finish
  check is not suppressed by the hard-0 live progress), letting an entry
  heal once the stream becomes a VOD.
- PlayerService skips completion saves, always starts at the live edge,
  and drops any startTime passed for a live video.
- The resume prompt, the "Continue at" button label, thumbnail progress
  bars, "X remaining", Continue Watching and tvOS Top Shelf all exclude
  live entries.
- CloudKit syncs isLive with the rest of the watch state: the conflict
  resolver carries it with watchedSeconds/isFinished and the sync engine
  applies it when merging into an existing entry. It is read leniently,
  so the schema version stays at 2 and older clients keep parsing these
  records.
2026-08-03 23:34:06 +02:00
Arkadiusz Fal
74442f2764 Force soft top scroll edge effect for banner and video info views
iOS 27 changes the default scroll edge effect style to hard, which
draws a sharp cutoff line over the channel banner and video info
gradient extending under the toolbar.
2026-07-18 10:40:31 +02:00
Arkadiusz Fal
8d4f472348 Unify macOS content view backgrounds with opaque window background
Views without an explicit background sat on the translucent window
material, which picks up a wallpaper tint (bluish in dark mode) and
clashed with views drawing windowBackgroundColor, making the video
info header gradient fade visibly mismatch. Add opaqueWindowBackground()
and apply it centrally in NavigationDestinationHandlerModifier to both
stack roots and pushed destinations, plus VideoInfoView for
presentations outside navigation stacks.

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
2026-07-06 09:36:49 +02:00
Arkadiusz Fal
b47888fe04 Add custom accent color with system color picker
Add a Custom swatch to the appearance settings accent color grid,
backed by a hex value stored in settings (synced via iCloud like the
rest). iOS uses the native ColorPicker wheel; macOS uses a circular
swatch that opens NSColorPanel, since the SwiftUI color well looks out
of place among the circles. All accent consumers now read the resolved
color through SettingsManager.resolvedAccentColor.

The indigo preset is retired from the grid but kept in the enum, so
users who selected it keep their color until they pick another one.

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
2026-07-04 23:57:59 +02:00
Arkadiusz Fal
9568149c21 Use native text Close button for sheet toolbars on macOS
Sheet dismiss buttons rendered as an iOS-style circular glass "X" on
macOS 26, which looks foreign on the desktop. Add a shared
sheetCloseToolbarItem helper that renders a native text "Close" button
on macOS while keeping the compact icon-only xmark on iOS/tvOS, and
replace the ~14 duplicated inline close buttons across sheets with it.
2026-05-24 12:34:53 +02:00
Arkadiusz Fal
64171a9cfb Add explicit sizing to sheets on macOS
Give player, download, source, and login sheets minimum frame dimensions on macOS so they open at a usable size instead of collapsing.
2026-05-21 18:19:19 +02:00
Arkadiusz Fal
7cb068c800 Fix Add to Playlist sheet layout on macOS
The sheet collapsed to an empty strip because a List inside a sheet
has no intrinsic height on macOS. Add an explicit minimum frame like
other sheets, gate presentationDetents to iOS, and replace the
prominent xmark toolbar button with a native Close button on macOS.
2026-05-20 09:15:27 +02:00
Arkadiusz Fal
fd0eab7784 Prefetch fresh video thumbnail before swapping it into info view 2026-04-23 18:37:19 +02:00
Arkadiusz Fal
664eeadba2 Stabilize Nuke cache key across rotating thumbnail URL tokens 2026-04-23 18:22:03 +02:00
Arkadiusz Fal
20b88a811e Use user-selected accent color for Play button tint on macOS 2026-04-23 18:07:58 +02:00
Arkadiusz Fal
a32582e171 Replace macOS video nav arrows with left/right keyboard shortcuts 2026-04-23 18:06:47 +02:00
Arkadiusz Fal
b23dfde602 Use macOS-native styling and larger text in video info view 2026-04-23 07:58:01 +02:00
Arkadiusz Fal
2efa0708c8 Refresh expired thumbnail URLs for downloads and video info
Proxied thumbnail URLs from Invidious/Piped/Yattee server expire over
time. Two paths were left holding stale URLs: the Video Info carousel
kept the original list copy even after fresh details arrived, and
downloaded videos rendered from the remote URL snapshot taken at
download time while the local thumbnail on disk was ignored.

Evict stale URLs from the Nuke cache when fresh video details load,
pass the fresh details through to the videoCard thumbnail, and resolve
downloads' thumbnails from the local file when localThumbnailPath is
set.
2026-04-18 20:38:02 +02:00
Arkadiusz Fal
abd432fd0e Show video title placeholder while thumbnails load 2026-04-18 20:38:02 +02:00
Arkadiusz Fal
b479d63295 Respect video tap action settings in media browser
Playable files in the media source browser now honor tvOSVideoTapAction
on tvOS and thumbnailTapAction/textAreaTapAction on iOS/macOS, matching
other video lists. When openInfo navigates to VideoInfoView, playback
routes through QueueManager.playFromMediaBrowser so stream and caption
resolution keep working for Samba/WebDAV files.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
cea8fcfe64 Fix tvOS bookmark details disappearing when reopening video info 2026-04-18 20:38:01 +02:00
Arkadiusz Fal
87965d654d Polish tvOS playlist sheets for focus and narrow layout 2026-04-18 20:38:01 +02:00
Arkadiusz Fal
f49dfd6246 Hide description header in tvOS video info right column
The right column is already dedicated to video info so the "Description"
label is redundant. Add an opt-out `showsHeader` parameter to
`TVScrollableDescription` (default true) and pass false from
`VideoInfoView`; the player overlay and channel view keep the header.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
0aac9168cb Use two-column layout for tvOS video info view
Reworks VideoInfoView on tvOS into a persistent 30% left sidebar
(thumbnail, title, channel, Play / Add to Playlist / Bookmark) with a
scrollable right pane for description, stats, comments, related, and
watch history. Reuses the player's TVScrollableDescription (refactored
to self-manage focus) so the description supports click-to-lock
scrolling, and the outer ScrollView is disabled while locked. Comments
full-screen on tvOS, with commenter avatars no longer tappable and
accent-colored link text replaced with the default foreground.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
d6d15df105 Deduplicate time formatting and clean up unused code
Extract shared TimeInterval.formattedAsTimestamp replacing 8 identical
formatTime/formattedTime implementations across player views. Remove
unused currentTime parameter from GestureSeekPreviewView. Consolidate
duplicated geometry math in MacOSControlBar into seekPreviewPosition().
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
f8da242968 Fix ContentUnavailableView centering on Apple TV
On tvOS, ContentUnavailableView inside a Group doesn't expand to fill
available space — it sizes to content and aligns top-leading. Add
.frame(maxWidth: .infinity, maxHeight: .infinity) to all instances
so they center correctly in their parent containers.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
2faae65e8b Add context menu and swipe actions to related videos in VideoInfoView 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
fa0536549a Fix subscriber count layout shift in VideoInfoView channel row 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
ef3cddefeb Persist author cache to disk for instant channel info across restarts
Back the in-memory authorCache with a JSON file in ~/Library/Caches/AuthorCache/.
Disk is lazy-loaded on first lookup and saved asynchronously on each cache update.
Capped at 500 entries to prevent unbounded growth.

- Cache author data from video detail API responses (PlayerService, VideoInfoView)
- Replace ChannelView's private CachedChannelHeader with shared CachedChannelData
- Enrich author with cached avatar/subscriber count in VideoChannelRow, TVDetailsPanel, VideoInfoView
2026-04-18 20:37:25 +02:00
Arkadiusz Fal
02de3d0bd5 Fix blurred background gradient not using DeArrow thumbnail 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
11a8c79e21 Refactor views 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
425a2c590d Fix locales 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
100df744d9 Yattee v2 rewrite 2026-04-18 20:37:24 +02:00