Drop nested NavigationStack, .searchable, and close button from the
PeerTube explore view on tvOS; use an inline search field plus Filters
button so focus separates cleanly and the sidebar title is no longer
overlapped. Keep the header visible across empty/error states so the
query can be cleared. Hide the filters and scan-network sheet toolbars
on tvOS, apply filter changes immediately, and add padding plus
scrollClipDisabled so focused rows aren't clipped.
Give TVSidebarDetailContainer an optional bottom action slot and use it to
show the Add Source button beside the sources list on tvOS. Switch the
Settings > Sources list from a focus-capturing List to the same
ScrollView+LazyVStack layout MediaSourcesView already uses, drop
.buttonStyle(.card) so row icons no longer clip, and bump the row
icon-to-title spacing to 24pt. Replace the sheet-based Add/Edit flow in
MediaSourcesView with navigationDestinations wrapped in the sidebar
container, and decorate each Add Source form (WebDAV, SMB, remote server,
PeerTube browse) with its own sidebar icon and title.
The shared AppIconPreview asset maxed out at 180px, causing the icon in
the tvOS Settings sidebar (rendered at 200pt) to appear pixelated.
Regenerate all three scales from the 1024px master: 400/600/900px.
Match the info/comments and queue panels by replacing the black dim and
inner rounded card with a full-screen ultraThinMaterial backdrop and a
transparent list background on tvOS.
Introduce PlatformMenuPicker that wraps short-option pickers in
LabeledContent + .pickerStyle(.menu) on tvOS so they render as a
compact dropdown instead of pushing a full-screen option list. On
iOS/macOS it falls through to a plain Picker, leaving rendering
unchanged.
Applied across Playback, Subtitles, Sidebar, Privacy, and Advanced
settings. Long language lists in PlaybackSettingsView are left as
push-style.
Replace the tvOS Subscriptions header (All Channels link + View Options
button) with a left-column channels sidebar that filters the feed in
place, and move view options into a button at the top of the sidebar.
Drops the channel strip size picker from the tvOS options sheet since
the strip does not apply there, and mirrors the ContinueWatchingView
focus pattern so initial focus and post-filter focus land on the first
video row.
Move the channel-link button and View Options into a top safe-area inset
on tvOS so they are reachable with the remote, mirroring the Continue
Watching pattern. Wrap chrome and content in focus sections with a
default-focus namespace so initial focus lands on the first video. Hide
the duplicate in-content section header on tvOS, and add
scrollClipDisabled to VideoListContainer so focus scaling on rows is
not clipped at the scroll edges.
Use @FocusState with programmatic assignment on appear instead of
prefersDefaultFocus, which is broken when the target sits inside a
ScrollView/LazyVGrid on tvOS. A 0.15s delay gives the grid time to
materialize cells before the focus write.
Replace the toolbar-based controls with an inline header row on tvOS so
the View Options and clear buttons are reachable with the remote. Drop
the navigation title, add an inline title, and disable ScrollView
clipping so the focus scale effect isn't clipped.
The container is now used beyond settings (Open URL and Remote Control
tabs), so the name is broadened to reflect its general role as a
tvOS sidebar-decorated detail container.
Wrap OpenLinkView and RemoteControlContentView in TVSettingsContainer on
tvOS so they get the same left sidebar with large SF Symbol icon and
title as settings detail screens, for visual consistency.
yattee-server returns direct YouTube CDN URLs in the storyboard `url`
and `templateUrl` fields instead of an Invidious-style VTT proxy path.
Two resulting issues:
- `Storyboard.directSheetURL` was replacing the whole `M$M` token with
just the index, producing `.../0.jpg` (404) instead of `.../M0.jpg`.
Replace `M$M` with `M\(index)` to preserve the literal `M` prefix;
matching the full token also avoids clobbering `$M` sequences that
may appear in `sigh=rs$...` query params.
- The download code fetched `proxyUrl` as if it were a WebVTT file;
with yattee-server that downloads a JPEG that fails UTF-8 parsing.
Skip the VTT round-trip when `proxyUrl` obviously points at an image.
Also align the on-disk filename with the local-playback template
(`sb_M$M.jpg` → `sb_M{N}.jpg`) so offline seek-bar previews resolve,
and add [Storyboard] debug logs at each decision point so future
failures can be diagnosed without guessing.
tvOS cannot open URLs in a browser, so the Community section
(GitHub/Discord) is omitted and Acknowledgements dependencies
render as plain text rather than tappable buttons.
Generated from the existing 1024x1024 iOS icon — gradient extended
horizontally to fill the 5:3 tvOS canvas without stretching the symbol.
Top shelf images are pure blue gradients sampled from the icon.
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.
Introduces a "Display sections as" picker in Home settings with List and
Grid modes. Grid renders each section as a horizontal shelf of video
cards, defaulting to Grid on tvOS and List on iOS/macOS. Per-platform
defaults are preserved via a platform-specific settings key.
On tvOS the shelf is a focus section so swiping up/down between rows of
different lengths works without getting stuck at the end of a row.
After disabling home shortcuts on tvOS, Open URL and Remote Control had
no entry point. Add them as configurable sidebar main items. Remote
Control defaults to visible on tvOS; Open URL defaults to hidden on all
platforms.
The "View Channel" button in the tvOS video details panel only dismissed
the player; the channel was never pushed. Delegate to
NavigationCoordinator.navigateToChannel(for:collapsePlayer:) so
UnifiedTabView's pendingNavigation observer appends the destination,
matching iOS behavior and handling extracted/media-source cases.
Matches the iOS controls by showing the channel avatar next to the
video title and channel name, reusing ChannelAvatarView and the
Yattee Server fallback.
Close button is now the rightmost action (after Queue), matching the
user-requested layout. A Previous button sits before Next and appears
whenever a queue is present, disabled until history accumulates.
Turn the tvOS bottom-row queue count indicator into a focusable button
that opens QueueManagementSheet in a fullScreenCover with an
ultraThinMaterial backdrop, matching the Settings sheet pattern. Hide
the sheet's close toolbar button on tvOS (Menu button dismisses) and
replace the unusable Menu-based queue mode picker with an icon-only
tap-to-cycle button.
Pressing Menu while scrubbing now discards the pending scrub and leaves
playback time unchanged, instead of committing the seek via the
focus-loss path.
tvOS rasterizes Siri Remote touchpad swipes into discrete onMoveCommand
events at ~300-400ms, so a fast swipe and a single tap delivered the
same fixed step. Track gap between events: rapid same-direction events
(under 500ms) build a streak that multiplies the step via a power curve,
while deliberate taps still land on the base step.
Pressing left/right on the focused progress bar now triggers the same
accumulating 10s seek as the hidden-controls flow, but updates the
visible scrubber in place with no overlay. Both tvOS arrow-seek paths
accumulate a signed net offset so a reverse press subtracts from the
pending amount instead of restarting from the current playback time.