Unfolding an iPhone Duo kept the old scroll offset, leaving the selected
card misaligned. Re-snap to the current card on width change, and clear
the requested position once it settles so the lazy stack doesn't pull
the next swipe back halfway between cards.
Open Link gets a Load action that resolves one link and shows its video
details page, playlist or channel screen instead of only playing or
downloading it. Play and Download stay as shortcuts for video links.
Timed links start at their timestamp from the details page, the Show
Details link action opens the details page, and a short extraction cache
avoids re-extracting the same link for details and playback.
Error details were declared in the video info view and the expanded
player, and Open Link three times: at the app root for deep links and
for the File menu command, and on Home. All of them are now AppSheets;
the deep link and menu requests go through the global request, so a
deep link that arrives while the player is expanded no longer opens the
sheet behind it.
The four sign-in modals that were a full-screen cover on tvOS and a
sheet elsewhere, each written out twice, use a platformSheet helper.
Each video row carried its own sheets: tappableVideo added the queue
action and resume sheets, the context menu and swipe actions added
add-to-playlist and download options, so a row could declare up to six
sheets, owned by a row that a list may recycle.
Rows now present those through the nearest sheet host, as does the
video info view's resume prompt. The queue action sheet for a tapped
description link becomes a global request instead of being declared
at both the app root and the expanded player.
Sheets take the color scheme where their host sits, so the floating
details panel's dark override no longer reaches them and the
presentationColorScheme workaround is gone.
The add-to-playlist, download options and queue sheets were declared in
every view that could open them: the root view (twice, mirrored from
coordinator flags), the expanded player, its portrait and floating
panels, the player controls and the video info view.
AppSheet lists the shared sheets, and .appSheetHost() declares them
once. The app root and the expanded player each host them, and every
presented sheet hosts the next level. Views present through the
presentAppSheet environment action, which reaches the nearest host;
code without a view (and the expanded player itself, whose host sits
in its own body) uses PresentationRouter.request, taken by the
top-most host: the player while it is up, the root otherwise.
The mini player's queue and playlist sheets now go through the host,
replacing the isMiniPlayer*SheetPresented flags.
The content VStack was re-identified with currentVideoIndex, so every
page change recreated the thumbnail carousel and reset its offset to 0
while the index already pointed at the next video. On the iPhone Duo
exterior display the recreated scroll view could not restore its
position, leaving the first card dimmed at peek opacity. Only the
sections below the header are re-identified now, and the carousel pages
use an explicit width instead of containerRelativeFrame so paging stays
aligned when the scroll view extends under a horizontal safe area.
The toolbar layout on macOS now lists Play, Download, Share, Add to Playlist
and Bookmark as separate toolbar items, with a spacer setting Play apart from
the secondary actions. The primary action picker stays iOS-only, where the
pinned action plus the ⋯ menu layout is kept, and macOS gets its own settings
footer text.
The macOS items are written out explicitly rather than with ForEach: a ForEach
inside the toolbar builder is evaluated once and never re-renders on state
changes, which left the bookmark and download icons stale.
Layout & Navigation gains a Video Info section on iOS/macOS: the actions
(Play, Download, Share, Add to Playlist, Bookmark) can stay as inline
button rows or move to the navigation bar, where the chosen primary
action is pinned and the rest live in an ellipsis menu. The toolbar
layout with Download pinned is the new default, since the hero thumbnail
already plays the video on tap; a one-time TipKit popover on the
thumbnail points that out. tvOS keeps the existing button layout.
The thumbnail now spans the card width (capped at 640pt on wide
screens) instead of a fixed 240/280pt, and title, channel row, and
action buttons are leading-aligned into a single column. The title
always reserves two lines so carousel cards stay vertically stable
while swiping.
Thumbnails are sized explicitly from the container width because the
carousel's LazyHStack proposes its ideal height back to the cards;
that ideal follows the loaded bitmap's native size, so a low-quality
fallback thumbnail would otherwise shrink its card.
A failed details fetch for a video opened with list metadata set the
error state but never rendered it - the spinner cleared and the
description section silently disappeared. Show an inline error section
with a retry button and an error details sheet (copy/share, reusing the
player's ErrorDetailsSheet). Also fill in the empty video.error.title
localization.
tvOS full screen covers are transparent, so each one filled itself with
a hardcoded black — invisible black-on-black text under the light color
scheme (e.g. the View All Comments page). A shared
tvFullScreenCoverBackground() modifier now picks black or light gray
from the color scheme; applied to the comments cover, subscription
destination picker, instance login, edit source, iCloud progress, and
legacy accounts import covers.
The video title and TVScrollableDescription hardcoded white text, which
was unreadable on light backgrounds. Description colors now adapt to the
color scheme by default; the player details panel opts back into white
via overlaysDarkBackground since it always sits over a dark backdrop.
When enabled (the default), share links to YouTube videos point to the
active source's web watch page instead of youtube.com. Invidious serves
its frontend at the API URL; Piped uses its configured frontend URL.
Videos the active source can't serve (PeerTube, extracted sources) and
sources without a web frontend keep their original links.
The toggle lives in Settings > Layout & Navigation > Sharing on iOS and
macOS, and is shown only when an enabled source has a web watch page.
Opening a members-only video from a history or feed row got a 500 from
the videos API and left the detail view blank: the retry error view only
covered the videoID init mode, while the details fetch for a loaded
video failed silently.
Record the details-fetch error and route it into the same error view
when the displayed video is a bare placeholder (no author, no
thumbnails), show a spinner while that fetch is in flight, and make
retry re-run both loaders. Videos with real partial metadata keep
rendering as before.
tvOS UX campaign polish (TV1, TV3, TV5, TV6):
- Invidious sign-in asks for a Username, which is what it verifies;
the wire field is still called email, noted where it is sent
- Video counts pluralize ("1 video"), via catalog plural variations;
the two-argument count-and-duration key uses fully positional
substitutions, since a mixed format made CFRetain fault on the
integer argument (TV10)
- Character counters render their limits verbatim ("0/1000"), here
and in the preset-name and bookmark-note counters with the same bug
- Deleting a playlist, local or remote, names the playlist the way
removing an account already named the account
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.
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.
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.
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
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
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.
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.
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.
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.
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.
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().
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.
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