The hidden video position was derived from the tabletop layout height,
which only applies while the panel is hidden. Dragging the visible panel
down therefore interpolated the video towards the full-screen center and
it snapped back to the upper half once the hidden state flipped. Derive
the hidden position from the hinge state directly so every drag, dismiss
and reveal targets the upper-half center.
Observe the foldable hinge with `onHingeChange` (iOS 27.1): while it is
partially open and the portrait panel is hidden, the video is fitted to
the upper half of the screen and every control (title bar, center
controls, side sliders, bottom bar) moves to the lower half. Pinch to
fill is disabled in that state. `YATTEE_FORCE_TABLETOP=1` in the launch
environment forces the layout on simulators without a hinge control.
The exterior display has a vertical status bar with a trailing safe
area inset, which squeezed the fullscreen portrait player into the
content width. The video and controls layers now span the full window
width (shifted under the leading inset, like the existing vertical
handling) and the controls pad their edges by the sheet's actual
asymmetric horizontal insets instead of the symmetric max.
Requires the iOS 27.1 SDK (Xcode 27.1).
Exiting native fullscreen from the corner layout restores the panels, and
the controls view read that as a floating details panel opening: it set a
hide override and ignored mouse movement for as long as the panel stayed
visible, so hovering the video no longer showed the controls. Only a
floating (unpinned) panel covers the video, so the flag now excludes
pinned panels, matching the existing widescreen rule. Corner panels are
pinned, so the fullscreen restore and the title-tap toggle keep the
controls responsive to hover.
Pages list
- Info is pinned first and always enabled in every layout: normalized
pages force it to the front and enabled (older presets self-correct),
disabling it is a no-op, and any move that would displace it snaps
back. The iOS/macOS list renders it outside the reorderable ForEach
(no drag handle, nothing drops above it) and the editors hide its
enabled toggle; tvOS also hides its move buttons. The "at least one
page must stay enabled" guard and its strings are gone since Info
always counts.
- iPhone never renders queue or comments as pages, so its Sections list
hides them (PanelSettings.hiddenSettingsPageTypes). Reordering goes
through moveVisiblePages, which keeps hidden entries in their stored
slots. Comments get their own group on the Details Panel screen
(pill mode + avatars) and the overlay hosts pass the avatars flag.
Default visibility
- "When Player Opens" is now configured per orientation
(portraitDefaultVisibility / landscapeDefaultVisibility). The legacy
single value is still decoded and seeds both until set individually;
presets that never set it default to portrait Visible, landscape
Hidden. macOS shows one picker (no portrait layout).
- "Remember Last" no longer leaks across orientations: separate
remembered flags, written by the portrait toggles and by the wide and
corner layouts respectively, with the old shared flag read once as a
fallback. The corner layout seeded from the portrait resolution
before; it now uses landscape.
Section options (iOS inline-sections hosts)
- Chapters and Related editors gain "Expanded by Default" (on by
default). The Info editor gains a Description group with "Expanded by
Default" (off) and a "Collapsed Lines" stepper (1–10, default 3).
Applied when the panel appears and on each video change.
- Expanding a collapsed section scrolls its header to the top of the
panel: PanelPageScrollView can hand its scroll proxy to page content
and each inline section is wrapped in one anchored container.
- The portrait panel's bottom fade sized its touch blocker from a stale
"expanded comments pill" answer that ignored collapse on scroll, so a
collapsed section header (the last item) sat under it and could not
be tapped. The predicate now tracks the pill's actual collapse state.
Overdragging the side column's resize grabber past its minimum width now
slides the column off the trailing edge (the video grows with it), and a
swipe in from the trailing screen edge brings it back following the
finger. The seek pan skips that edge strip while the column is
collapsed. Showing the panel again restores every region hidden by its
own drag handle.
Swiping the below-video panel back up right after hiding it showed the
video clipped to a short centered box that sprang back to full size.
The hide flipped to the hidden layout in a `.logicallyComplete`
completion while the slide spring was still registered on the SwiftUI
host of the mpv render view; a reveal drag interrupting it made the
release spring start from a bogus shrunken rect. All slide-off
completions now wait for `.removed` and bail if a new drag took over.
The visibility-keyed `.animation` modifiers key on a toggle generation
bumped only by the animated show/hide paths, so drag flips never start
the toggle easing. The partnerless `matchedGeometryEffect` on the
widescreen video frame is dropped.
The pill mode now lives on CommentsPageSettings and is edited in the
comments page editor instead of a standalone settings section. The
legacy value on PlayerPillSettings is kept for decoding old presets
and migrated by effectivePanelSettings. Comments are loaded when
either the comments page is enabled or the pill is not disabled.
The corner layout now always renders the info page as a portrait-style
panel below the video (collapsible description, drag to hide/expand),
while the side column holds tabs for the remaining pages. Key pieces:
- Below-video panel reuses PortraitDetailsPanel (no comments pill,
square corners, chapters/related stay in the side column tabs)
- Drag down hides the panel with the video recentering in lockstep;
swipe up on the video reveals it following the finger
- Fullscreen button toggles the panels in corner layout (controls hide
first so their resize never plays visibly); status bar and home
indicator hide only while the panels are hidden
- Panel visibility and column width are seeded at init from a warmed
PlayerControlsLayout cache, so the first frame resolves corner mode
without flashing the default layout or animating the panels in
- Dismiss pan is blocked inside the below-video panel frame
(cornerBelowPanelFrame in NavigationCoordinator)
- Resize grabber slimmed to match the panel drag handle look
Two causes: the drag/dismiss animation targeted the content-area center
(screenHeight/2) while the resting position once the panel flips hidden
is fullScreenHeight/2 - top inset - the player window adds extra bottom
safe area, so the two centers differ and the flip snapped the video by
half that difference. And the hidden-state flip ran on a fixed 0.25s
timer that cut the 0.3-response spring short, snapping the video the
rest of the way. A shared hiddenVideoY is now the target everywhere
(drag interpolation, dismiss animation, swipe-up reveal), and the flip
runs in the spring's completion callback.
Also mark DEBUG os_log output public: without it the console shows
<private> for processes not launched by Xcode (e.g. USB syslog capture).
The preset's When Player Opens setting only drove the landscape/floating
panel state, so on iPhone in portrait the details panel reappeared on
every player expansion regardless of the setting. Mirror the resolved
visibility into the portrait panel state, and seed it into the sheet's
initial state at all creation sites so the first frame already renders
the correct layout - applying it in onAppear animated a visible
top-to-center video jump during expansion. Portrait panel toggles now
also persist the last visibility so Remember Last works there too.
Preset-selected two-region layout: video in the top-left, a resizable
side column, and an optional region below the video. Pages are assigned
to regions per preset (Location picker in the page editor); the layout
falls back to the stacked widescreen layout below 900pt width, so Split
View and narrow windows degrade automatically.
- CornerPlayerLayout hosts the shared playerAreaForWidescreen builder,
so the MPV hosting path is identical to the stacked layout
- Layout resolver in ExpandedPlayerSheet picks corner per layout pass;
NavigationCoordinator.isCornerLayoutActive feeds the macOS window
manager, which drops the aspect-ratio lock and auto-fit while corner
is active (and restores them on exit, incl. fullscreen replay and
window re-mounts)
- Side column width persisted via new cornerPanelColumnWidth setting
(280-560pt, video column kept >= 480pt)
- Settings: layout mode picker (iPad/macOS) with automatic-fallback
footer, per-page region picker when corner is on, region shown in
the pages list
Phase 1 of the player panels redesign:
- New PanelSettings model (pages, regions, layout mode, default
visibility) stored as an optional field on PlayerControlsLayout with
an effectivePanelSettings accessor; lenient Codable so future page
types and settings payloads never corrupt saved presets.
- New Views/Player/Panels content engine: PlayerPanelContentView
renders a page bar plus the selected page (info, comments, related,
chapters, queue, tracks), with runtime hiding of empty pages.
- PortraitDetailsPanel and FloatingDetailsPanel now host the shared
content instead of their duplicated info bodies; drag machinery,
pills, and fades are untouched.
- Comments list extracted into CommentsListContent shared by the
overlay and the comments page; comments pill routes to the comments
page (expanding the portrait panel) when the page is enabled, and
falls back to the overlay when it is not.
- Tracks page embeds QualitySelectorView via a new isEmbedded mode.
- Deleted the unused video info bodies in ExpandedPlayerSheet+VideoInfo
and the dead PlayerInfoTab model.
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.
Streams built for local folder files carry no resolution/codec/fps
metadata (unknowable before demux), so the quality selector labeled
them "Unknown". Use the video track mpv reports (demux-w/h/fps, codec)
to fill those fields into a display-only copy of the stream — the
Video row and summary now show e.g. "720p · 30fps" with a codec badge.
Selection and tap handling keep using the original stream.
Files with multiple embedded tracks (e.g. MKV from media sources or
downloads) previously exposed none of them: mpv's track-list was never
read, aid was never set, and sid was only ever no/auto. The quality
selector showed no Audio or Subtitles tab for such files.
MPVClient now observes track-list/aid/sid and delivers a coalesced,
leniently decoded [MPVTrack] snapshot; selection state is derived from
the reported "selected" flags. Embedded tracks are switched live via
aid/sid (no reload), surfaced in the existing Audio/Subtitles sections
alongside external captions on iOS, tvOS and macOS. External sub-add/
audio-add tracks are filtered out to avoid double-listing. User picks
are sticky across same-video reloads (quality switch, audio mode,
retries) and preferred audio/subtitle languages auto-select a matching
embedded track once per load, with external captions taking precedence.
When "Play in a separate window" is disabled, the expanded player was a
native sheet attached to the main window. AppKit refuses fullscreen with
an attached sheet, so pressing F swapped the sheet for an in-window
overlay before fullscreening — a visible two-step transition.
Present the inline player as the full-window overlay from the start, so
fullscreen is just the window's native toggle. This removes the whole
sheet-swap machinery (toggleSheetFullScreen with its detach timing,
SheetWindowResizer, sheet sizing helpers).
- beginInlineOverlay/endInlineOverlay hide/restore the main window
toolbar and mirror the window's fullscreen state into the renamed
isMacInlinePlayerFullScreen flag via NSWindow notifications, so exits
through Esc/green button/Mission Control stay in sync
- Collapsing exits fullscreen only if it was entered for the video
- Esc in the windowed overlay collapses to the mini bar via a new
onCollapse callback (replacing the sheet's default Esc-dismiss)
- ContentView registers its hosting window (MainContentWindowReader) so
the overlay targets the right window even when the setting is toggled
while the Settings window has focus
The controls overlay was gated on the player backend existing, but the
backend is created only after the video details fetch completes, so the
loading phase showed just the thumbnail and spinner with no controls.
Drop the backend gate (MPV is the only backend type) and resolve the
PiP backend dynamically in the toggle callback. Also make ended/failed
states hide controls directly in shouldShowControls instead of relying
on catching the state transition, and reset the manual visibility
override when a new load starts so controls reappear for the next video.
The fullscreen button did nothing in sheet mode: AppKit ignores
toggleFullScreen on a window attached as a sheet, and the old fallback
targeted the key window — the sheet itself. Two other strategies were
ruled out first: fullscreening the sheet's parent is silently refused
while any sheet is attached, and resizing the sheet's backing window to
screen size NaN-asserts inside AppKit's sheet positioning even without
animation.
Working approach: swap the sheet for a plain overlay in the main window
and native-fullscreen the now sheet-free main window.
- isMacPlayerFullScreenOverlay on NavigationCoordinator dismisses the
sheet (presentation binding excludes it; the setter ignores the
dismissal write-back) and mounts ExpandedPlayerSheet as a black-backed
overlay in ContentView
- ExpandedPlayerWindowManager fullscreens the parent after the sheet
detaches, verifies AppKit accepted it (rolls back if refused), hides
the window toolbar (NSWindow keeps it as a strip above the overlay
otherwise) and restores it on exit
- a didExitFullScreen observer restores the sheet on any exit path
(button, Esc, green button, Window menu); closing the player while
fullscreen exits the main window's fullscreen too
- scheduleSharedViewAdoptionRetry on entry re-homes the shared render
view once the dismissing sheet's window loses visibility — its steal
is declined mid-dismissal and nothing else retried, leaving the
overlay black until the next video load
A pinned window carries .fullScreenAuxiliary, which AppKit refuses to
make a primary fullscreen window, so toggleFullScreen was a silent
no-op. Drop the floating config before entering fullscreen and restore
it in windowDidExitFullScreen, keeping the pin setting intact.
The mini player buttons set NavigationCoordinator flags, but only the
iOS tab views hosted the corresponding sheets. Host QueueManagementSheet
and PlaylistSelectorSheet on the macOS root view, bound directly to the
coordinator flags.
Also wire the add-to-playlist button in the full macOS player controls:
add the missing .addToPlaylist case to MacOSControlsSectionRenderer and
thread onShowPlaylistSelector from the expanded sheet layouts through
MacOSPlayerControlsView to the existing playlist sheet host.
Render the macOS control bar button row and top bar from the active
preset's sections via a new MacOSControlsSectionRenderer, replacing the
hardcoded QuickTime-style rows in MacOSControlBar. Add a keepOnTop
button type and macOS-specific button availability lists, and adapt the
player controls settings editors (preset editor, section editor, button
configuration) for macOS.
Also apply the preset font style to the control bar time labels, and
update the built-in macOS Default preset order (queue before transport,
fullscreen at the end), bumping builtInPresetsVersion to 7.
Show a top row in the macOS player controls (like tvOS) with the channel
avatar, video title, author name, and a close button whenever controls are
visible.
- Offset the top row below the traffic lights in the floating window by
measuring the window buttons against the top bar (no effect in fullscreen,
the inline sheet, or the side panel where there is no overlap).
- Tapping the avatar/title/author toggles the floating details panel, mirroring
the iOS title/author control.
- Hide the controls immediately when the details panel opens instead of waiting
for the auto-hide timer.
Add Queue (list.bullet), More (VideoContextMenuView ellipsis), and Play
Previous buttons to the macOS control bar. Play Previous is shown always and
disabled when the queue has no previous item. Remove the skip-back/forward 10s
buttons and the fullscreen button from the bar (arrow-key seek and F-key
fullscreen shortcuts are retained).
Queue reuses the existing cross-platform queue sheet; More reuses the shared
VideoContextMenuView (already !os(tvOS)), styled borderless to match the flat
bar buttons.
Reset isCommentsExpanded and commentsFrame on the NavigationCoordinator
directly when the portrait panel is dismissed, since PortraitDetailsPanel
owns its own @State that doesn't sync back through .onChange during dismiss.
Also track comments overlay frame via GeometryReader so the dismiss gesture
can allow swipes outside the comments area instead of blanket-blocking.