Playback menu items toggle the info panel below the video (⌥⌘I), the
side column (⌥⌘S) and both at once (⌥⌘P) while the corner layout is
active. Showing one region from the hidden state brings back only that
region; hiding the last visible one hides the whole panel.
Every layout pass with a new size recreated the framebuffer and rendered
synchronously on the render queue, blocking the main thread behind the
in-flight render. Animated layout changes (corner layout panel toggles,
rotation) dropped to a few frames per second. Size changes arriving in
quick succession now let the layer stretch the last frame and recreate
the framebuffer once the size settles; one-off resizes still recreate
immediately.
The window-level pinch was blocked whenever the landscape panel was
pinned and visible (the corner layout always counts as pinned) or a
stale portrait-panel flag was set. In the corner layout the pinch now
starts when it begins inside the video region, and the panscan
control button is enabled there too.
Newer Invidious returns a single authorThumbnail URL on comments instead
of the authorThumbnails array, so every comment fell back to the
placeholder avatar. Decode authorThumbnail as a fallback.
Subscribing or unsubscribing, locally or via iCloud, only posted a
notification, so the cached feed stayed valid and Yattee Server never
received the new channel list until the cache expired. Invalidate the
feed cache on every subscription add and removal, and skip the disk
write when it is already stale.
Toggling cellular downloads recreated the background session with the
same identifier before the old one finished invalidating, and nothing
ever replaced a dead session, so every download threw "Task created in
a session that has been invalidated" until relaunch. Wait for the old
session's invalidation before creating the new one, rebuild the session
when the delegate reports the live one invalid, and recreate it once
when task creation hits the invalidated exception.
refreshSync() discarded the persisted engine state and the pending
buffer, and the engine state is where queued saves and deletions live,
so tapping Sync Now within the debounce window dropped history,
progress and bookmark changes. Carry the old engine's pending changes
into the buffer that the new engine adopts, then schedule a send.
NowPlayingService set the media-library key MPMediaItemPropertyMediaType,
which MediaRemote ignores, so external clients classified playback as
audio. Use MPNowPlayingInfoPropertyMediaType instead.
yattee://downloads still reaches the downloads destination on tvOS,
where it renders a bare ContentUnavailableView with nothing focusable,
leaving the remote stuck.
One build 270 crash log aborts in talloc (talloc_abort_unknown_value)
inside libsmbclient name resolution while a media-source download opens
a file through SMBBridge, with FFmpeg reading an smb:// file on another
thread at the same time.
libsmbclient keeps its talloc stackframe behind a table of thread
functions, and none was installed (this build does not export
smbc_thread_posix()), so every thread shared one stackframe and the two
users freed each other's allocations. Install a pthread implementation
of smb_thread_functions at load time, before any libsmbclient use,
including FFmpeg's. The struct layout matches what talloc_stack and
smb_threads in the bundled library read (get_tls at +0x30, lock_mutex
at +0x10).
Four build 270 crash logs abort inside the GL driver
(GLDContextRec::flushContextInternal) from presentRenderbuffer, reached
from the immediate render queued by appDidBecomeActive/resumeRendering.
Only displayLinkFired checked isInBackground; renders dispatched
directly to the render queue could run, or present, after the app was
backgrounded again, where GPU work is not permitted.
Check isInBackground at the top of performRender and again before
presenting. PiP rendering is unaffected.
17 of the build 270 crash logs are 0xdead10cc kills on the main thread
inside DataManager.save(), every one reached from the 5-second
PlayerService progress-save timer. During background audio the timer
keeps writing; when playback stops the app can be suspended mid-save,
still holding the SQLite lock in the shared app-group container.
Wrap the periodic save in a background-task assertion when the app is
not active, and skip the write when the position has not moved since
the last save, so a paused player stops writing every five seconds.
The remaining six 0xdead10cc logs die in DataManager.init recovering
the WAL during a background launch. They are most likely the aftermath
of these dirty kills, so store opening is left as is.
The carousel's lazy stack kept the old widths of cards it had laid out
off-screen, so after a resize (tab bar to split view sidebar on iPad,
rotation) every later card sat at a stale offset and the current card
landed beside the viewport. The stack now takes a new identity per
card width, and the existing re-snap aligns the current card.
Swapping between the tab bar and the sidebar also rebuilds the page,
which reset the carousel to the video it was opened on. The page's
position and continuation-loaded videos now live on its queue context,
fresh for every push, so a rebuilt page resumes where it was.
The detail column absorbs its NavigationStack, so the mini player inset
on the stack only reached the root page and disappeared on push. Float
it over the split view, past the sidebar, and have every page reserve
its height as bottom safe area.
The corner preset's landscape fallback opened the panel whenever the
device could host the corner layout. On the iPhone Duo exterior display
the player falls back to the widescreen layout, so the floating panel
opened on every rotation into landscape. Resolve the fallback from
whether the corner layout fits the current size instead.
iPad and iPhone Duo users can pick Tab Bar instead of Automatic to opt
out of the sidebar layout. Switching rebuilds the navigation Settings
lives in, so the change asks for confirmation and lands on Home first.
Videos carry isShort from Yattee Server and Piped, and Invidious feed
entries are taken as Shorts when they arrive with no length and aren't
live or upcoming: the feed comes from RSS, which lists Shorts, while
lengths come from the Videos tab, which doesn't. Subscriptions, search
and instance browse hide videos known to be Shorts when the toggle is
on; videos whose source can't tell are always shown.
The window-level dismiss pan recognizes before SwiftUI's slider
DragGesture sets isAdjustingPlayerSliders, so dragging volume or
brightness often dismissed the player instead. Side sliders now publish
their frames and the pan refuses to begin from touches inside them.
mpv's volume/mute properties are software volume applied before samples
reach the AO, and AVSampleBufferAudioRenderer queues ~2s ahead, so slider
and mute changes were heard only after the queue drained. With the
avfoundation AO active, drive ao-volume/ao-mute (renderer volume, instant)
and keep software volume neutral; other AOs keep software volume. Re-apply
on every audio reconfig since a new renderer starts at full volume.
Related and queue rows in the player use the default menu context, so the
pushed screen landed underneath the still-expanded player. Collapse on
player state instead of the .player context. Recommendations without an
authorId no longer offer Go to Channel, which opened an empty channel.
Wide videos showed letterbox bars in the tiny iOS mini player preview right
after the dismiss morph, which lands the video filling the preview. The
container that takes the shared player view now sets mpv's keepaspect, so
the mini player fills its frame and every other surface keeps the aspect
ratio.
Releasing the dismiss drag now shrinks the player window onto its video and
flies it into the mini player's preview instead of sliding it off screen and
showing the mini player afterwards. The mini player stays mounted under the
expanded player and reports its preview frame, and the window is masked down
to the video while its root view is transformed onto that frame (with the
safe area the transform drops added back, so the content doesn't jump).
The handoff of the shared MPV view is made seamless: a freshly made player
container waits for its first layout before taking a view that is on screen
elsewhere, sizes and redraws it there, and the window hides in that same
frame. Resizing the render view draws the current frame into the recreated
framebuffer right away instead of leaving it blank until the next tick.
Falls back to the slide when no preview target is available (rotated
session, video preview hidden, different window size).
Delete declarations that Periphery reports unused on every platform that
compiles them (iOS, macOS, tvOS), including nine views used only by their
own previews. Platform-gated code still used on another platform, device-
only MPV render paths, migration models, CloudKit record names, App
Intents metadata and unused parameters are kept.
Route YouTube search results links to search and nocookie embeds to the
video, match YouTube hosts exactly, and report YouTube pages without a
video as unsupported instead of sending them to yt-dlp. A watch link
inside a playlist shows both the video and the playlist when loaded, and
plays the video with the playlist queued around it. URL scheme play,
queue and bookmark actions accept playlist links.
Load All in Open Link opens an Opened Links page that resolves each link
into a video, playlist or channel row like search results, in input
order. Video rows keep the usual context menu and swipe actions, and the
page can play, queue, bookmark or download all resolved videos.
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.
On tvOS SwiftData keeps the store in the app group's Library/Caches,
which the system may purge. The CKSyncEngine tokens live in UserDefaults
and survive, so the next launch opened an empty store and fetched only
records changed since the old tokens; subscriptions and bookmarks that
had not changed since never came back.
DataManager now records whether the store file existed when the
container opened, and the sync engine clears its persisted state once
per launch when it did not, so the first sync after a loss is a full
fetch. Also log the store path and a persistent-store failure that
previously fell back to in-memory storage silently.
A tvOS sheet fills most of the screen, far too much for a two-button
question. The shared sheet host now presents link action confirmations
as an alert there, answering cancel when it goes away without a choice.
A toast shown while a shared sheet was up, such as a failed link action
over Open Link, appeared behind the sheet. Sheets from .appSheetHost now
carry their own toast overlay, and only the top-most overlay of a scope
shows its toasts.
Piped's /channel/{id} rejects handles, so channel pages, tabs and
subscribe links for youtube.com/@name failed. Resolve the handle
through /c/@handle once per instance and use the channel ID.
Invidious' channel endpoints take only UC… IDs and return 500 for a
youtube.com/@handle, so channels opened from handle links (and the
subscribe link action) failed with "Could not get channel info". Resolve
the handle through /api/v1/resolveurl first, cached per instance.
Play Video, Add Video to Queue, Download Video, Bookmark Video, Subscribe
to / Unsubscribe from Channel, Control Player, Seek, Set Playback Speed,
Get Now Playing, Open Section and Search Videos. Each runs the same
LinkAction as the matching yattee:// link through LinkActionPerformer,
without the confirmation sheet (the person built the shortcut). Failures
surface their message in Shortcuts.
App Shortcuts: control player, now playing, open section, search.
yattee://x-callback-url/{verb}?… runs the same action as yattee://{verb}?…
and then opens x-success with the result's values appended, x-error with
errorCode/errorMessage, or x-cancel when a confirmation is declined (x-error
with errorCode=cancelled when no x-cancel is given). Unknown verbs report
unsupportedAction. Callbacks to file:, javascript: and data: URLs are ignored.
Adds yattee://player/state, which reports what is playing (title, videoID,
channel, playing, position, duration, rate) without changing anything.
yattee://play|play-next|queue|download|bookmark|subscribe|unsubscribe?url={link}
and yattee://player/play|pause|toggle|next|previous|close|seek|rate run through
one LinkActionPerformer that reports a LinkActionResult, ready for
x-callback-url. An unencoded url= keeps its own query; only the action's own
parameters end it.
Actions that change saved state ask first through a confirmation sheet
presented by the top-most sheet host, unless the new 'Run Link Actions
Without Asking' setting is on. Download skips the question when the quality
picker will show anyway. Player commands leave sheets and the player alone.
SettingsView owns its navigation stack, so pushing it onto a section's
stack rendered nothing. Select the Settings section where the layout has
one, otherwise present Home's settings sheet; open the Settings window on
macOS.
- Route legacy yattee://watch?v=, yattee://youtube.com/..., yattee://https://...
links like their web equivalents (only natively handled destinations)
- Unwrap yattee://open?url= inside route() so every caller keeps the inner
URL's own query items
- Never route file:// URLs as direct media
- Support source/instance on yattee://playlist and PeerTube /w/p/ and
/video-playlists/ URLs
- Leave open sheets alone and show a toast for links that go nowhere
Both were DEBUG-only Developer settings entries used while building
the accounts revamp and are no longer needed. The simulator's
account-sync suspension barrier in AccountSyncCoordinator existed only
to keep invented scopes away from the real zone, so it goes with it.
Tapping a new-videos notification while Home's Settings sheet was up
switched to the Subscriptions feed underneath the sheet, so the app
came back still showing Settings. The existing dismissSettings() hook
was never called.
NavigationCoordinator.dismissPresentations() bumps a generation that
the main window's root sheet host and screen sheets (Home's Settings,
Remote Devices, Customize Home; Subscriptions' view options and data)
observe via .dismissesOnExternalNavigation(_:). The notification,
deep links and Handoff call it; in-app navigation and layout swaps do
not. The expanded player keeps its own sheets.
The notification also collapses the expanded player so the feed it
opens is visible.
In the split-view sidebar layout (macOS, and now iPad and the iPhone
Duo), the detail NavigationStack that a selection change replaces
writes an empty path through its binding on the way out, so going to
another section and back always landed on the section's root. macOS
already behaved this way before the router.
Only the selected section's stack is on screen and can pop, so path
writes for any other section are ignored.
The swipe-action share sheet, the expanded player's safe area and the
Dynamic Island side check took connectedScenes.first, which ignores
which scene is active (on the iPhone Duo it can be the other panel's)
and presented the share sheet from the main window, behind an expanded
player.
UIApplication.foregroundWindowScene/foregroundWindow resolve the
foreground-active scene (else foreground-inactive, else any) and its
key window, the order the widescreen layout already used. The share
sheet now presents from the key window, which is the player's while it
is up. The tvOS display mode manager uses the same lookup.
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 sidebar layout was gated on iPad idiom. It now appears wherever there
is room for it: regular width and regular height, or an iPad with
regular width (a short Stage Manager window keeps it). That covers the
Duo inner screen in both orientations, while large iPhones in landscape
and the Duo outer screen keep the tab bar. The subscriptions feed's
channel sidebar follows the same rule, and Sidebar settings are offered
on devices with a hinge.
SwiftUI's sidebarAdaptable TabView never shows a sidebar on iPhone
idiom, so the iOS sidebar layout now uses the NavigationSplitView with a
list sidebar that macOS already used, on iPad as well. The sidebar
layout shows the floating mini player instead of the tab bar accessory.
The tab bar and sidebar layouts each kept their own navigation paths and
selection, so swapping between them (resizing an iPad Stage Manager
window across the size-class breakpoint) lost every pushed screen and
re-applied the startup tab.
NavigationRouter now holds the selected section, a typed stack per
section keyed by SidebarItem.id, the search text and the sidebar
manager. Both layouts select with SidebarItem and render the router's
state; navigate(to:) pushes onto it directly instead of going through
an observed pendingNavigation, so a request made before a layout
appears is not lost, and a destination pushed before startup keeps its
section. When a layout can't show the selection (a sidebar channel in
the tab bar, a section hidden by customization) it lands on the section
that can host it with that screen pushed first.
This also fixes keyboard section commands in the tab bar layout and the
coordinator's stale tab on iPad. AppTab, pendingNavigation,
selectedSidebarItem and the unused SheetDestination/path API are gone,
and the iOS 18 and 26.1 root views are merged into AdaptiveTabView.