Hide the toggle on macOS, where apps are never suspended and playback
always continues regardless of the setting. Pause playback on iOS when
backgrounding with the setting disabled — the audio background mode
previously kept mpv playing, making the off state a no-op.
Move the continue-playing policy into PlayerService and make the
backend's background render pause unconditional (PiP-gated only):
it is a GPU optimization that should not depend on the setting.
Also ungate the player sheet visibility handlers, which manage
foreground mini player rendering unrelated to background playback.
With auto-resize disabled the separate player window opened at a
computed 16:9 default and was never corrected, so non-16:9 videos sat
letterboxed until a manual resize. Persist the window frame via
NSWindow frame autosave and restore it on open when auto-resize is
off, so the window reopens exactly as the user last left it.
The navigationBarDrawer search field on iPad added a resting scroll
position above the banner on iOS 27, letting the view scroll past the
banner top with the mirrored banner filling the gap. iPhone keeps the
drawer placement since its header reserves space for the search bar.
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.
Audio-only streams never report a video size, so the aspect ratio of
the last played video stuck to the player area indefinitely once audio
mode was enabled. Reset it in play() when the selected stream is
audio-only, and let the macOS player window fall back to 16:9 instead
of keeping the previous lock.
Downloads bypass stream selection and reach play() as ready-made local
file streams, so audio mode never affected them. Apply the mode at that
choke point instead: swap a local video stream for the separately
downloaded audio track, or play a muxed file with the video track
disabled (vid set per-load in MPVClient, never toggled live). The
transform is bidirectional so stored audio-only queue/history entries
restore full video when the mode is off, and setAudioMode now reloads
downloaded content at the current position in both directions.
The content type picker in the center .principal toolbar slot is what
constrains the .searchable field to the trailing edge. Dropping the
picker when a search was submitted let the toolbar reflow and the search
field re-center. Keep the picker in place and disable it while searching
instead.
Rest the quality/settings sheet at a 0.6 fraction instead of .medium on
iOS so the general section (playback speed / audio mode / lock) at the
bottom of the scroll view is reachable without dragging up to .large.
CloudKit record names carry a source scope suffix so the same channel
or video ID can exist under different sources, but the apply path
matched local entities by bare ID: incoming records could merge into an
entity from a different scope, and a remote deletion of one scope
deleted every local entity with that ID and cascaded scoped deletions
for all other scopes back to CloudKit.
- Match local entities on the full mapper-derived record name when
merging incoming records, materializing records for upload, and
applying remote deletions.
- Remote deletions now remove only the matching entity and no longer
queue CloudKit deletions (no echo, no cross-scope cascade).
- Insert dedupe for CloudKit-applied records is scope-aware, so a
same-ID record from a different source inserts instead of being
silently dropped. Remotely added bookmarks also update the
fast-lookup cache immediately.
- When conflict resolution during a fetch keeps newer local data, the
merge result is queued for upload; previously the server (and other
devices) kept the stale version unless the entity was edited again.
Strict timestamp comparisons prevent re-upload ping-pong between
devices.
refreshFromiCloud already skipped isLocalOnly keys, but the enable-time
paths did not: replaceWithiCloudData overwrote device-specific settings
with another device's values, and syncToiCloud uploaded local-only keys
to the ubiquitous store. Apply the same skip in both.
The recent-channel resolver copied avatarURLString/providerName and the
recent-playlist resolver copied authorID/providerName - fields that do
not exist in the records the mapper writes - while missing the real
ones (thumbnailURLString, subscriberCount, isVerified, videoCount). As
a result, when the local side won a conflict, those fields silently
kept the older server values.
Also drop the notificationsEnabled preservation in the subscription
resolver: the subscription mapper never writes that field since
notification preferences moved to their own record type.
Naturally finished videos only saved their 100% progress locally
(play() deliberately skips the sync-on-switch when videoEndedNaturally
is set), so the watched state never reached other devices in the
autoplay flow. Save completion through updateWatchProgress, which
queues the iCloud sync.
Also queue the current playback position when the app enters
background, so backgrounding mid-video hands the position off to other
devices instead of only syncing on explicit stop or video switch.
Register pending changes with the engine state instead of keeping
app-managed record arrays, and materialize records from local data at
send time in nextRecordZoneChangeBatch. This fixes several data-loss
paths:
- Pending changes now persist inside the engine state serialization,
so saves queued during the debounce window (or while offline) survive
app termination. Legacy record-name keys are migrated once.
- Transient CloudKit errors (zoneBusy, serviceUnavailable, rate limits,
network failures, limitExceeded, batchRequestFailed) re-register the
change for engine-scheduled retry instead of silently dropping it.
- Conflicts are resolved against the exact record that was sent; the
resolved record (carrying the server change tag) is cached and takes
precedence at the next send, and any newer local edit evicts it.
- Queue methods are now synchronous, removing a task-reordering race
where a fast delete/re-add could end up deleting the re-added entity.
- Records are built fresh at send time, so queued changes no longer
upload stale field snapshots, and batch size limits are handled by
the framework.
Persistent per-platform mode that plays only the audio track to save
bandwidth and speed up loads. When enabled, stream selection picks the
best audio-only stream (preferred language, codec, bitrate) and never
fetches the video URL; toggling mid-video reloads at the current
position using the quality-switch path (live vid=no toggling is avoided
due to A/V desync).
- Toggle row in the player quality/settings sheet on iOS, macOS and
tvOS, next to lock controls
- New audioMode control button type (red when active) for player
controls and mini player layouts
- Picking a video quality explicitly turns the mode off; audio track
picks keep it on and now work while audio-only is playing
- Stream URL refresh preserves audio-only playback instead of
resurrecting video
- Queue preloads and history entries resolved before a toggle are
discarded so selection re-runs with the current mode
- tvOS player shows the video thumbnail instead of a black screen
during audio-only playback
- PiP button hidden for audio-only streams
Ordering out a window in native fullscreen skips the exit transition and
strands its black fullscreen space on screen. Route all dismissal paths
(stop/collapse, PiP hide, window close) through exitFullScreenIfNeeded,
which leaves fullscreen first and defers hiding until the transition
completes, with a timeout fallback if AppKit refuses the exit.
Replace the centered avatar/name toolbar title with the
About/Videos/Shorts/Streams/Playlists segmented picker. The channel
avatar and name move to the leading edge next to the view options
button, in their own glass capsule (sharedBackgroundVisibility(.hidden)
+ glassEffect on macOS 26). Add spacing between the channel banner and
the content below now that the inline picker is gone.
Live streams have no meaningful duration, so the mini player's bottom
progress line collapsed to nothing (or showed a misleading partial fill
on DVR streams). Hide the line for live playback and show a red dot +
LIVE indicator next to the author name instead, matching the expanded
player controls. Applies to both the macOS capsule and iOS overlay
layouts.
The read-only defaults list grouped tvOS with iOS as ao=audiounit, but
MPVClient actually sets ao=avfoundation,audiounit on tvOS (avfoundation
first because audiounit can't open 32-channel HDMI Atmos routes).
Display-only fix; no playback behavior change.
On macOS 27 beta, mpv's coreaudio AO fails to initialize (channel layout
set rejected with paramErr -50). The existing ao=coreaudio,avfoundation
fallback restored audio, but exposed an mpv bug: init() registers a HAL
hotplug listener (AudioObjectAddPropertyListener on the system object,
with the raw ao pointer as user data) before the step that fails, and
the coreaudio_error path never unregisters it. mpv then frees the ao
and falls back to avfoundation, leaving a dangling listener per
playback start. The next audio device change (AirPods connect,
sleep/wake device republish) invokes the listener with the freed
pointer and crashes with a use-after-free SIGSEGV on the
HALC_ProxyNotification Call Listener Queue (observed in build 262;
faulting address was reused "texture_" shader string memory).
Gate the AO order by OS version: on macOS 27+ use
avfoundation,coreaudio so the failing coreaudio init never runs; older
macOS keeps the native coreaudio first with avfoundation as fallback.
Update the read-only defaults mirror in MPV options settings to match.
The NavigationStack-level background wasn't enough: macOS draws its own
translucent material behind pushed navigation pages, and Form/List
containers draw a translucent scroll background on top of anything
placed behind them, so several pages kept the wallpaper-tinted look.
Bake the opaque background into SettingsFormContainer (covers all pages
built on it, pushed or not) and add opaqueSettingsFormBackground(),
which hides the container's scroll background before applying the
opaque one, to every macOS-reachable Form/List settings page: player
controls and its sub-editors, sidebar settings, edit source, legacy
data import, subscription/playlist import, customize home and its
shortcut style page, and the log viewer. Contributors, translators and
the remote control device page get the plain opaque background.
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
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
Download row actions were wired only as swipe actions, and
SwipeActionModifier has #if !os(iOS) overloads that return the view
unmodified, so macOS had no way to pause, resume or cancel a download.
The video context menu could not cover it either, since the .downloads
context deliberately hides the built-in cancel item.
Give active download rows their own context menu with pause when queued
or downloading, resume when paused, retry when failed, and a destructive
cancel. Right-click on macOS, long-press on iOS.
Completed rows had a related bug: the delete item called an onDelete
callback that DownloadsView never passed, so it rendered but did nothing
on macOS. Drop the callback and delete via the download manager directly,
which is what already made the same menu work in HomeView.
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
SwiftUI's .preferredColorScheme on the root view could not revert from
dark back to light/system while a sheet was presented, and on macOS it
never applied to secondary window scenes such as Settings. The override
is now applied directly to UIKit/AppKit instead.
- SettingsManager.applyTheme sets overrideUserInterfaceStyle on every
window of every connected scene (iOS) or NSApp.appearance (macOS);
the theme setter calls it, so any future code path that changes the
theme outside the setter must do the same
- AppTheme gains userInterfaceStyle / appearance mappings, with .system
resolving to .unspecified / nil so the OS setting takes over again
- Applied on app appear for the initial launch state, and after both
iCloud sync paths, since theme can arrive as a synced change and is
no longer driven by SwiftUI state
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
Adds an option in Appearance settings to use different accent colors
in light and dark mode, with a toggle to keep a single shared color
(the default, preserving existing behavior).
- New synced settings keys: accentColorDark, customAccentColorDark,
useSeparateDarkAccentColor; dark values fall back to the light
selection until explicitly set, so no migration is needed
- resolvedAccentColor returns a dynamic platform color (UIColor trait
provider / NSColor appearance provider) when the toggle is on, so
the root tint and all direct readers adapt to light/dark
automatically without consumer changes
- Accent Color section shows two stacked preset+custom grids with
Light/Dark headers when enabled, extracted into AccentColorGrid;
CustomAccentColorButton now takes bindings so the macOS color panel
edits whichever target was last opened
- New DarkAccentColorTests covering sync contract, default-off state,
fallbacks, and toggle-off resolution
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
Load local-account subscriptions synchronously on appear so the channels
sidebar is present in the first layout pass, and reserve the sidebar
column with a spinner while async loads are in flight (gated by a
persisted flag so users without enough channels never see a phantom
column).
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 Filter/Export/Clear buttons appeared twice on macOS: inline next
to the search bar and again inside the ellipsis toolbar menu. Show
them once as plain toolbar buttons on macOS; keep the menu on iOS/tvOS.
On macOS 15 Sequoia the error overlay buttons (Retry / Play Next /
error details) and the ended-state replay buttons did nothing when
clicked, while working fine on macOS 26. Layered event logging showed
the click reached SwiftUI but always landed on MacOSPlayerControlsView,
which covers the whole video surface above those overlays: on Sequoia a
view with .onContinuousHover participates in click hit-testing across
its full frame, so even with its inner layers hit-disabled the controls
view formed an invisible wall. Drop the whole controls view out of
hit-testing while the ended/failed overlays own the surface.
Also keep the traffic-light buttons visible on the ended/failed
overlays: they used to fade out with the controls and nothing could
bring them back, leaving the error screen with no way to close the
window.
The hide timer was gated on the pointer leaving the player view, which
never happens in fullscreen, so controls shown when pressing F stayed up
forever. Switch to idle-based hiding: mouse movement shows controls and
resets the 3s timer; controls fade after inactivity even while the
pointer rests over the video, unless it's on one of the control bars.
Entering fullscreen rebaselines hover tracking so the resize-driven
hover re-emission doesn't count as movement.
Also hide the mouse pointer (setHiddenUntilMouseMoves) when controls
hide in fullscreen, restoring it on exit or any mouse move.
macOS 27 beta's AUHAL rejects mpv's channel-layout property with paramErr
(-50), so the coreaudio AO fails to initialize and playback is silent.
Change the macOS ao option from "coreaudio" to "coreaudio,avfoundation"
so mpv self-heals: older macOS keeps using coreaudio unchanged, while the
beta drops to avfoundation (AVSampleBufferAudioRenderer) instead of playing
no sound.
AXe dlopens the private SimulatorKit.framework from
$DEVELOPER_DIR/Library/PrivateFrameworks. Xcode 27 relocated that
framework to Contents/SharedFrameworks, so under a selected Xcode 27
every axe command fails with "Failed to load essential private
frameworks" and all UI assertions fail uniformly.
Detect when the active Xcode lacks SimulatorKit at the path AXe expects
and inject DEVELOPER_DIR pointing at the first installed Xcode that has
it, for every axe subprocess. No global xcode-select switch required.
Claude-Session: https://claude.ai/code/session_01Aqq5bSGrYfjQNvkTEzWcva
On macOS the queue sheet's mode selector now shows both icon and text
and shares a single bottom toolbar bar with the Close button, pinned to
opposite edges.
The default SwiftUI Menu style on macOS 15 renders as a bordered
pull-down button that stretches to fill available width, while
macOS 26 hugs the label. Apply .menuStyle(.borderlessButton) and
.fixedSize() to the video context menu and playlist header menu.
In the separate player window the traffic-light buttons stayed visible
while the controls overlay was hidden. Animate the standard window
buttons' alpha in sync with controls visibility, QuickTime-style.
Applies only to the dedicated player window (inline sheet and overlay
fullscreen presentations keep their parent window's chrome). Buttons
are handed back at full alpha when entering native fullscreen, when the
controls unmount (PiP, debug overlay), and when the view re-parents to
another window.
When the pointer left the player window during playback, the auto-hide
timer set the manual showControls override to false. The override takes
precedence over hover state, so re-entering the window kept controls
hidden until a click toggled them back.
Clear a stale hide override on mouse activity so hovering reveals the
controls again, except while the details panel is open, which keeps
controls hidden on purpose.
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.
Replace the log-content-with-copy sheet with an NSSavePanel that writes
the exported logs to a .txt file, matching the subscriptions and preset
export flows. ShareSheet presentation is now iOS-only; tvOS export is
unchanged.
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
The separate macOS player window (and the inline sheet, which locks to the
same window via applyAspectRatioConstraint) could be dragged arbitrarily
small. Two AppKit behaviors combined to defeat the intended minimum:
- Setting contentAspectRatio makes AppKit's aspect-ratio resize handler take
over live drags and stop enforcing minSize/contentMinSize.
- NSHostingController resets minSize/contentMinSize to zero at runtime, even
with sizingOptions = [].
Enforce the floor in windowWillResize(_:to:) from constants instead, growing
any under-sized proposal back up to a 360pt minimum height (aspect-scaled
width, with a floor for narrow videos) while preserving the proposed ratio.
SPUUpdater is a main-thread-only API; calling resetUpdateCycle() from a
detached utility task violated Sparkle's threading contract and raised
a Swift 6 concurrency error (non-Sendable SPUUpdater captured in a
detached task). A plain main-actor Task keeps the deferral that avoids
running the feed-cache work synchronously inside the didSet.
- Appcast publish: fetch gh-pages into a local branch before creating
the worktree. actions/checkout's narrow refspec meant the branch was
checked out detached, so `git push origin gh-pages` failed with
"src refspec gh-pages does not match any" on a fresh runner.
- Gate the AltStore source update on an iOS build being part of the
release; a mac/tvOS-only release has no IPA and would publish a
broken entry.
- Pass the release tag to update-altstore explicitly (gh's "latest
release" excludes prereleases), parse the build number correctly for
beta tags (2.0.0-beta.263 -> 263, not beta.263), use the real
Yattee-<version>-iOS.ipa asset name, and fail instead of writing a
size-0 entry with a dead URL when the IPA asset is missing.
Previously CKSyncEngine was created with nil state serialization on
every launch, replaying the entire zone change history (hundreds of
merge/delete log lines and re-downloads each start). Now the saved
state is loaded so launches only fetch changes since the last session.
To make incremental sync safe:
- Clear persisted state once when the record schema version increases,
so records previously skipped as unsupported get re-delivered after
an app update.
- Handle remote zone deletion in fetchedDatabaseChanges by recreating
the zone and re-uploading local data, instead of leaving sync dead.
Account change and manual Refresh Sync still clear the state and force
a full fetch as before.
Sheet mode has no container lifecycle hook on either transition, so the
shared render view was stuck until the render watchdog (~10s of black):
- Presenting the sheet: the capsule parks the view while the sheet's
window exists but is not yet visible - viewDidMoveToWindow has already
fired and findTransferTarget rejects non-visible windows, so nothing
picked the parked view up.
- Dismissing the sheet: the dismissed sheet's hierarchy stays alive
holding the view inside a now-invisible window, with no unmount or
park at all.
Add scheduleSharedViewAdoptionRetry() to MPVContainerNSView: for ~1s it
re-checks every 50ms whether the shared view needs re-homing (parked,
or owned by a container whose window lost visibility) and runs the same
recovery the watchdog uses. Deduplicated so overlapping triggers don't
stack loops. Triggered after parking with no transfer target and from
hide()'s no-window branch, which every sheet-mode collapse funnels
through.
The retry ticks pass quiet: true to recoverSharedPlayerViewIfNeeded so
teardown after closing a video (when no container legitimately exists)
no longer spams warnings, and the loop stops once the shared view is
gone.
The capsule's claim on the shared render view is declined during the
player window's fade-out (the window is still visible, so the steal
guard refuses), and nothing retried after orderOut - the capsule stayed
black until the render watchdog recovered the view ~10 seconds later.
Call recoverSharedPlayerViewIfNeeded() in hide()'s cleanup right after
ordering the window out, so the view moves to the capsule as soon as
the collapse finishes. The PiP path is untouched - there the view must
stay in the hidden window.
Closing the player window via its close button left isPlayerCollapsing
stuck true (windowShouldClose has no animation completion to clear it),
so the mini capsule preview kept mounting its render container and
grabbed the shared render view on the next expand - the video rendered
into the 44x26 thumbnail while the player window stayed black.
- Clear isPlayerCollapsing on the next runloop in windowShouldClose
- Give containers in the tracked player window priority to claim the
shared view (even from a visible owner), and refuse steals from the
visible player window by outside containers
- Prefer the largest container in the player window during recovery so
thumbnail-sized surfaces are never picked
- Add a watchdog misplaced-view check in MPVBackend that re-attaches
the render view when it lives outside the visible player window
- Include container bounds and tracked-window flag in attach/decline
logs
In separate-window mode, hide() keeps the ordered-out window's SwiftUI
hierarchy alive (nilling contentViewController crashes AVKit), and its
updateNSView unconditionally re-attached the shared MPVOGLView -
stealing it into a window CoreAnimation never composites. The layer's
silent skip-render fallback then consumed every frame flag, so video
stayed black (climbing vo drops) until app restart.
- Decline stealing the shared view from a visible container into a
non-visible one; owner-initiated transfers bypass the guard
- Only transfer to containers in visible windows or the window still
tracked by ExpandedPlayerWindowManager; park the view otherwise and
reclaim it when a container gains a window
- Log skip-render frame consumption (rate-limited) and track render
health counters (draws/skips/dropped draws)
- Add a watchdog to the playback stats task that detects a stalled
video output and re-attaches the view to a visible container
- Don't mount player layouts during zero-size layout passes
The glass controls capsule can now be dragged anywhere over the video.
Position is stored as a fraction of the container size so it survives
window resizes and aspect-ratio driven sheet resizes, persists across
sessions via SettingsManager, and snaps back magnetically when dropped
near the default bottom-center spot. Travel is clamped so the capsule
stays inside the player and never overlaps the top button row. A
mouseDownCanMoveWindow=false backing view keeps the window's
movable-by-background behavior from swallowing the drag.
Add a queue-count badge to the macOS player controls queue button
(MacOSControlsSectionRenderer) and the shared mini player queue button
(MiniPlayerView), matching the iOS pill button behavior.
The storyboard seek preview was anchored to the top of the control-bar
pill and floated well above it. Anchor the preview and chapter capsule up
from the pill bottom (via geometry height) so they overlap the button row
and sit just above the progress bar, matching iOS proximity.
- Add macOS-specific entitlements via CODE_SIGN_ENTITLEMENTS[sdk=macosx*]:
the iOS aps-environment key is silently dropped from macOS exports,
leaving distribution builds without a push entitlement (no CloudKit
sync pushes). Release now uses Yattee-macOS.entitlements with
com.apple.developer.aps-environment = production.
- Release-DeveloperID uses Yattee-macOS-DeveloperID.entitlements, adding
the Sparkle mach-lookup temporary exceptions (-spks/-spki) required
for the updater to install in a sandboxed app.
- Default Sparkle updater to the beta channel while only beta releases
exist, so testers receive updates without toggling Advanced settings.
- Set NSHumanReadableCopyright for the app target.
Playback menu seek items now follow the durations from the active player
controls preset instead of hardcoded 10s/30s labels. AppEnvironment caches
the active preset's center settings (refreshed on preset change
notifications) so menu commands can read them synchronously.
Add secondary seek durations (default 30s) to CenterSectionSettings,
configurable on macOS in Seek Durations settings, used by Shift+arrows in
the player and Cmd+Shift+arrows in the Playback menu.
Also let Cmd-based key equivalents pass through the player keyboard
monitor so menu shortcuts (e.g. Cmd+Option+arrow previous/next video)
work while the player window has focus, and unify macOS built-in preset
primary seek at 10s to match iOS/tvOS (built-in presets version 8).
Queue items carry a pre-resolved stream, so play() takes the
provided-stream shortcut and skips the full API fetch that populates
storyboards. The follow-up loadOnlineStreams() call fetched the response
containing them but dropped storyboards on the floor.
Keep the storyboards in loadOnlineStreams() and apply them when none are
set (preserving local storyboards for downloaded playback), and bail out
if the current video changed during the fetch.
The PiP restore handler only expanded the player when mini player
video was disabled, which is the iOS behavior where playback can
continue in the mini player. On macOS the mini player is not a
restore target, so always reopen the player window; iOS keeps the
existing logic.
MPVBackend.stop() clears the PiP bridge callbacks on macOS to prevent
crashes during window close, but the backend is reused across videos
and setupPiPIfNeeded is guarded by isPiPSetUp, so the callbacks were
never wired again. The next PiP session then started without notifying
PlayerService (no onPiPStatusChanged), leaving the player window
visible alongside the PiP window, both playing.
Extract the callback wiring into wirePiPBridgeCallbacks() and re-run it
in startPiP() and in setupPiPIfNeeded when already set up.
Switching videos during active PiP left the window at the old video's
size with white space around the new content, for two reasons:
- MPV reports the new video's width and height as separate property
events while the old values are kept across switches, so a transient
mixed size (new width with old height) could reach the PiP capture
path and AVKit sized the window from that frame. Coalesce the PiP
capture-size/aspect update with a short debounce so only settled
dimensions are used.
- On macOS nothing told AVKit about the new aspect: unlike iOS, the
bridge never flushed the sample buffer renderer on an aspect change
during active PiP, and AVKit doesn't resize the macOS PiP window on
its own. Flush the renderer, reset the format description, and
resize the PiP window to the new aspect ratio explicitly.
The PiP-active guard that skips expanding the player when opening a new
video was compiled iOS-only, so on macOS a browser deep link always
re-showed the hidden player window instead of playing the new video in
the existing PiP window. Extend the guard in both openVideo overloads
and the Handoff path to macOS.
Since 1d9c5e3b performRender feeds swap timing to mpv via
mpv_render_context_report_swap, but only after presentRenderbuffer -
which is skipped while PiP is active. Once swap reports stop mid-stream,
display-vdrop's vsync estimation goes stale and mpv drops nearly every
frame (vo drop counter climbing ~19/s on a 24fps stream), turning PiP
into a slideshow.
The frame is genuinely displayed during PiP via the
AVSampleBufferDisplayLayer, so report the swap on that path too.
The content type segmented picker rendered as a Liquid-Glass pill instead
of a native macOS segmented control. Wrapping a `.segmented` Picker in
`.opacity`/`.disabled`/`.accessibilityHidden`/`.fixedSize` (used to keep
toolbar items mounted-but-invisible for stable layout) inserts a hosting
layer that makes the toolbar fall back to the pill rendering.
- SearchView: filters button and content type picker are now always
visible on macOS, rendered bare so `.segmented` keeps native styling.
- SearchView: don't reset the content type to .video on submit on macOS,
so a pre-selected type (e.g. Playlists) is preserved (iOS unchanged).
- InstanceBrowseView: same pill fix, rendering the tab / content type
pickers bare via conditional `if` instead of opacity-masking.
- ViewOptionsSheet / MediaBrowserViewOptionsSheet: use `.formStyle(.grouped)`
for native macOS form styling in the options popover.
Place the view options control on the leading (`.navigation`) edge on
macOS so it is consistently the first/leftmost toolbar button across all
views, instead of sitting on the right next to the search field in
search views and on the left in non-search views.
Each affected view gains a computed `viewOptionsPlacement` (`.navigation`
on macOS, `.primaryAction` elsewhere) so iOS/iPadOS/tvOS keep the
button trailing. Search-style views (Search, InstanceBrowse) declare the
view options item first so it renders left of the search-filters button;
their flexible spacer is kept to right-pin the search field. Channel's
fixed spacer between view options and the channel menu is now iOS-only.
Tapping a recent search (or suggestion, or opening a query deep link) mutates
the searchable text, which fires the onChange handler that clears results and
re-fetches suggestions. That raced against the search Task started by the same
tap: on Sequoia the onChange settled last, cancelling the in-flight search and
leaving suggestions visible instead of results.
Suppress the onChange-driven suggestion fetch for the single programmatic text
mutation via a one-shot flag, so the execute paths go straight to results
regardless of onChange-vs-Task ordering. Manual typing is unaffected.
The .searchable modifier lived inside iOSChannelContent, which body only
renders once the channel network fetch completes, so the search field
appeared only after videos loaded. Hoist the search modifier chain into a
channelSearchable wrapper around the load-state Group in body so it shows
immediately in every state (iOS nav-bar drawer and macOS toolbar).
channelMenuIcon gated the notification lookup behind the network-loaded
channel object, forcing the icon to person.fill while loading and swapping
to bell.fill afterward. Use channel?.id.channelID ?? channelID (the same
fallback as refreshSubscription) so the bell state is correct from the
loading phase, since notificationsEnabled(for:) only needs the ID string.
The Popular/Trending tab picker now lives in the toolbar (principal
placement) instead of inline above the content. Once a search is
submitted it swaps to the search content type picker, with the search
filters button alongside it as a popover, matching the global Search
view. Items stay mounted invisible on macOS 26 so the toolbar layout
stays stable while typing.
Match the global Search view by pinning the searchable field and
trailing buttons to the right edge on macOS 26 via a flexible
ToolbarSpacer at .primaryAction. Applied to History, Bookmarks,
Manage Channels, Downloads, Instance Browse, and PeerTube Explore.
In History, the clear-history item moves from .automatic to
.primaryAction so it groups with the other trailing controls.
A media-source "section" only rendered as a full-width "Browse {name}"
link, identical for all source types and redundant with the shortcut
card. Remove it from the Available Sections add-list and clean up any
already-added media-source sections on load.
Keep HomeSectionItem.mediaSource as a decode-only case so existing saved
homeSectionOrder data (decoded all-or-nothing) still parses; strip those
entries via removeAllHomeMediaSourceSections(). Media-source shortcuts
are unchanged.
Decouple the two concerns that were entangled in a single card-style setting:
- Layout (Compact / Regular) now only controls positioning of the
icon / title / subtitle / counter.
- Color (Soft / Vibrant) is a new independent setting: Soft = tinted
background with palette-colored icon and neutral text; Vibrant =
solid palette fill with white icon/text.
- Palette (Accent first, then Classic/Sunset/Meadow/Berry/Grape/Custom)
now applies to both layouts and both colors, and is always shown.
Adds section headers (Layout / Color / Palette) to the style page and
fixes the Compact preview to show the count subtitle line. Default is
Regular + Soft + Accent.
Tapping the mini bar video thumbnail (with tap action set to PiP) or its
PiP button while the expanded player window was open started PiP playing
alongside the window. Bring the player window to the front instead when
it is open or opening.
The expanded player window hidden for PiP is reused on restore, so its
MPVContainerNSView kept a stale weak reference to the shared render view
that the mini player preview had taken during PiP. On restore the mini
preview released the view as an orphan and the reused container skipped
re-attaching it because its guard only compared view identity.
- Skip re-attach in setPlayerView only when the view is still actually
our subview, matching the iOS container's stolen-view handling
- Transfer the shared render view to another living container on unmount
instead of orphaning it, since SwiftUI is not guaranteed to re-run
updateNSView on a hidden window's content after restore
- Force a repaint when restoring the window hidden for PiP; forced draws
requested while ordered out are dropped and the layer is pull-based
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.
Buttons in the section, mini player, and pill editors can now be
removed via long-press/right-click context menu instead of only
swipe-to-delete. This also gives macOS a delete affordance in the
section editor, which previously had none.
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.
Plain-Form sheets rendered as columns-style content floating inside
oversized fixed frames on macOS. Anchor these panels to their toolbar
buttons as popovers instead, sized to content; iOS/tvOS keep sheets.
- Search and PeerTube explore filters apply live (Cancel never
reverted anything anyway) and dismiss on outside click
- Subscriptions/Manage Channels "Subscriptions Data" opens as a sheet
from the popover's onDisappear via a pending flag, since presenting
while the popover dismisses gets swallowed
- Media browser view options drop navigation chrome on macOS
Relocate the funnel filter button and the content-type picker
(All/Videos/Playlists/Channels) from the inline results strip into the
window toolbar on macOS. The inline strip remains iOS-only.
Like on iOS, the controls appear only after a search is submitted
(gated on hasSearched). To keep the toolbar stable:
- Items stay in the toolbar permanently but are invisible and inert
before a search, so the search field keeps a constant size; their
liquid-glass backgrounds are suppressed with
sharedBackgroundVisibility(.hidden) (macOS 26, with a conditional
fallback on macOS 15).
- A flexible ToolbarSpacer pins the view-options button and search
field to the trailing edge so they no longer shift when items
appear.
The filters type onChange handler moved from the strip to the shared
iOS/macOS branch so both the strip and the toolbar picker reuse it.
With Atmos "Continuous Audio Output" enabled, some HDMI routes report
32 output channels, which mpv's audiounit AO cannot open - playback
stays silent while other (AVPlayer-based) apps work fine.
Update MPVKit to 0.41.0-n8.1.2, which builds mpv's avfoundation AO
(AVSampleBufferAudioRenderer, previously macOS-only) for iOS/tvOS
(mpvkit/MPVKit#73), and prefer it on tvOS via ao=avfoundation,audiounit
so mpv still falls back to audiounit if it fails to initialize. iOS and
macOS audio outputs are unchanged.
The MPVKit pin must be exactVersion: SwiftPM sorts the pre-release
identifier below 0.41.0, so a range starting at 0.41.0 never resolves
to it. Note this tag also bumps FFmpeg from 8.0 to n8.1.1.
1d9c5e3b removed the main-thread EAGLContext bind from setupAsync so
the context is never left current on two threads. But libmpv's
mpv_render_context_create requires a current GL context on the calling
thread (it probes glGetString(GL_VERSION)), so render context creation
failed with MPV_ERROR_UNSUPPORTED (-18) and every video played with no
--vo ("No render context set") on real iOS/tvOS devices.
Bind the context around the createRenderContext call and unbind right
after. The bind is scoped to setup, before the display link starts and
before any frame is rendered, so the never-left-current invariant that
protects the A10X fence fix (#947/#949) still holds.
Verified on Apple TV 4K (3rd gen): createRenderContext succeeds and
video frames render.
Claude-Session: https://claude.ai/code/session_01UEXP5f6F4S1zLdY8fdVuLJ
Closing the separate player window via Cmd+W (or the traffic-light
button) stopped the player but left the queue populated, so the next
video tap showed the Play Now/Play Next/Add to Queue prompt. Clear the
queue in windowShouldClose to match the close button behavior.
The separate player window's show() restore branch reset alphaValue to 0
and faded back in even when the window was already on screen, so every
Play Now while playing made the window flash. Only run the fade when the
window was actually hidden (PiP restore); otherwise just bring it forward.
The macOS CAOpenGLLayer is pull-based: after the shared render view is
re-parented into a reopened player window, a single forced repaint is
issued. draw() consumed the forceDraw flag before validating the
viewport, so when that repaint landed while the layer was still 0x0
(the expanded window builds its content a runloop late), the request
was swallowed. With mpv paused during load, nothing repainted again,
leaving the video permanently black with working audio.
- Validate viewport/framebuffer in draw() before consuming flags;
on failure keep the forced draw armed and retry (bounded, 5x50ms)
- Repaint from setFrameSize on the first non-zero layout while a
forced draw is still pending
- Drop the stale fbo=1 fallback; render only into the validated
live framebuffer binding
- clearToBlack: take the CGL lock and skip glClear when no
framebuffer is bound - clearing FBO 0 on this core-profile context
latched GL_INVALID_FRAMEBUFFER_OPERATION, which libmpv reported on
every load
- Add diagnostics for dropped draws and the first-frame render
Rename the macOS "Control Bar Buttons" settings entry to "Control Bar"
and add an Auto/Light/Dark glass background picker on that page. The
choice is stored per-preset as GlobalLayoutSettings.controlBarTheme and
forces the bar's glass color scheme via glassBackground(colorScheme:).
The Home settings screen copies settings into local @State on appear and
saves on disappear. Pushing the card style page fired onDisappear (saving
pre-edit values), and popping back re-ran loadSettings, clobbering the
style just edited through the bindings — so the selection reverted.
Load settings only once per presentation, and persist style, palette, and
custom colors immediately from the style page via an onSave callback. The
owning view sits covered in the navigation stack where its own onChange
never fires, and swipe-dismissing the settings sheet from the style page
skips its lifecycle entirely, so saving must happen from the visible child.
The player's keyDown monitor only checked that some window was key, so
plain-key shortcuts (m, space, f, arrows) fired while typing into the
Settings window's text fields. Guard on the window actually hosting the
player controls and pass events through while a text field is editing.
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.
Apple TV 4K 1st gen on tvOS 26 crashes to the home screen with a
"blocked by IOFence" gpuEvent at playback start (#949) and stutters
with a starved demuxer cache (#947). Defensive fixes in the shared
iOS/tvOS OpenGL path:
- Drain in-flight GPU work (glFinish) before destroying the
renderbuffer, so a pending presentRenderbuffer fence can't hold the
CAEAGLLayer IOSurface being torn down or reallocated
- Never leave the EAGLContext current on the main thread: framebuffer
create/destroy and the background glFinish now unbind on exit, and
setup no longer binds the context on main at all
- Disable retained backing on tvOS; it exists only for iOS PiP frame
capture and adds per-frame IOSurface fence pressure
- Report swaps to mpv after each present so display-vdrop (the tvOS
default video-sync) gets swap-timing feedback, matching macOS
Add verbose-gated diagnostics for remote triage: playback stats every
10s (cache fill/rate, dropped frames, hwdec, avsync) and a rate-limited
slow-frame warning in performRender.
Addresses #949 and #947
The macOS controls stack (MacOSPlayerControlsView + MacOSControlBar)
never read playerState.isControlsLocked, so locking via the settings
sheet had no effect: buttons, the scrubber, the volume slider, and the
AppKit keyboard shortcuts all kept working.
When locked, dim and disable transport, volume, the progress bar,
queue, the more menu, PiP, the pin/floating toggle, and the title-tap
details button; ignore playback keyboard shortcuts (space/arrows/mute)
while keeping F/Esc for fullscreen. Settings and Close stay enabled so
the user can reopen Settings to unlock.
Replace the three-way macOS Player Mode picker (Separate Window /
Floating Window / Inline) with a single 'Play in a separate window'
toggle in Playback settings, and move the always-on-top choice to a
pin button in the player's top bar (window mode only).
Floating was really a transient window property (NSWindow.level), not
a peer of the window/inline structural choice — so it belongs on a live
control, not in Settings. The pin state persists across sessions.
- Replace MacPlayerMode enum with macPlayerSeparateWindow /
macPlayerFloating bool settings
- Branch runtime presentation and window level off the two bools
- Add pin.fill/pin toggle to MacOSPlayerControlsView top bar
- Update localization; drop obsolete playerMode strings and enum tests
The expanded player window opened at a seeded 16:9 size and was then
resized with an animated setFrame once the real aspect ratio / video
change landed. Because the video and controls are laid out from the
window's live size, they visibly grew/repositioned as the window
animated.
Force the first resize after each open to snap (no animation) so the
player appears at its final fixed layout, and seed the initial window
size and aspect constraint from the real video aspect ratio when it is
already known (e.g. expanding from the mini bar) to avoid even a jump.
Later resizes (switching to a different-aspect video while the window
stays open) still animate.
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.
Colorful shortcut cards now color by grid position instead of shortcut
identity, so colors stay in the same slot when shortcuts are reordered.
Adds a Palette option (Classic default, plus Sunset, Meadow, Berry,
Grape) and a Custom palette with a swatch-list editor (ColorPicker + hex
fields) and a switchable comma-separated text field. Color is resolved
per position and wraps by palette length, wired via a new
homeShortcutColorfulColor environment value and a Color(hex:) extension.
Replace the inline Plain/Accent/Colorful picker in Home customization with
a navigation row that opens a dedicated page. The new page keeps the style
selector and renders a non-interactive preview of every shortcut type so
the chosen style is visible before committing.
- Add an optional styleOverride to HomeShortcutCardView so the preview can
reflect the not-yet-saved selection.
- On macOS, navigate via navigationDestination(isPresented:) instead of a
List-embedded NavigationLink, which otherwise stays stuck in its selected
state after popping back and can't be reopened.
In floating/window mode, interactive resize is aspect-locked via the
window's contentAspectRatio, so no black bars appear. The inline sheet
had no such lock, allowing free resize with pan bars on the sides.
Make applyAspectRatioConstraint static so the sheet path can reuse the
exact same lock the standalone window uses, and apply it to the sheet's
backing NSWindow from SheetWindowResizer whenever the video ratio is
known. Like the standalone window, the resize lock always follows the
real video ratio (independent of the playerSheetAutoResize size setting).
The inline-sheet content declared only a minWidth/minHeight floor, so
.presentationSizing(.fitted) opened the sheet at 640x360 and
SheetWindowResizer corrected it a runloop tick later — a visible
small-then-resize flash when re-opening the sheet while a video plays.
Feed the already-computed aspect-derived size as the frame's ideal size
so .fitted opens the sheet at the correct size immediately when the video
dimensions are known; the resizer's first pass then no-ops on its guard.
The floor (no max) is kept so content still tracks later animated resizes.
Adds a Style picker (Plain / Accent / Colorful) to the Customize Home
sheet, shown when the shortcuts layout is Cards. Defaults to Plain.
- Plain: current look (light accent tint + accent border)
- Accent: solid accent fill, white icon/text
- Colorful: solid fill with a fixed color per shortcut type
The filled styles use a Reminders-style layout (icon top-leading, count
top-trailing, title anchored to the bottom) with a subtle gradient sheen
over the fill color. Cards without a meaningful count (Open Link, Remote,
Subscriptions, instance content, media sources) hide the number; Remote
shows its status dot in the top-right slot instead.
Persisted via SettingsKey.homeShortcutCardStyle, mirroring the existing
homeShortcutLayout accessor.
The player video surface is a single shared MPVOGLView that is re-parented
between the mini-bar container (main window) and the expanded-player sheet
(a separate sheet window). When presenting the sheet, the new container
attaches the shared view before the old mini-bar container tears down, so
the old container's viewWillMove(toSuperview: nil) was ripping the shared
view out of its new home via removeFromSuperview() — leaving it with no
window and a black surface. Symmetric, so it also blanked the mini-bar
preview after collapsing.
Only detach the shared view if it still belongs to the container
(superview === self). Also force a repaint on window reattach so a paused
frame redraws, and extend the onAppear resumeRendering to macOS.
Switch the current-time and duration labels in the macOS transport
control bar from a monospaced design to a rounded design with
.monospacedDigit(), keeping stable digit widths while matching the
softer Liquid Glass look.
The play-next and Picture-in-Picture buttons were folded into the view
condition (hasNext / isPiPPossible), so they popped in and out of the
control bar and shifted the surrounding layout. Always render them (when
their callback is wired) and gate availability with .disabled() instead,
mirroring the existing play-previous button.
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.
In sheet (inline) mode the expanded player kept a fixed size and never
reacted to the video's aspect ratio, unlike window/floating mode. Drive
the sheet's backing NSWindow size from the video aspect ratio, reusing
the same sizing math as the standalone window.
- Extract shared sizing into ExpandedPlayerWindowManager.fittedPlayerSize
and add fittedSheetSize for the sheet path.
- Add SheetWindowResizer (NSViewRepresentable) + .sheetWindowSize modifier
that resizes the hosting sheet window, since .presentationSizing(.fitted)
only fits once at presentation.
- Let the sheet content fill the window and paint the window background
black so animated resizes stay gap-free with no white bars.
The capsule close button (and context-menu close) only stopped playback and
cleared the queue, leaving the separate macOS player window open on a black
frame. Route both through a shared closeVideo() that also sets
isPlayerExpanded = false, mirroring PlaybackCommands.closeVideo, so the
player window is dismissed.
- Replace the full-width material overlay bar with a compact, centered
capsule (Apple Music style) via a macOS-specific macOSCapsuleLayout
- Extend glassBackground() to use real Liquid Glass on macOS 26 with a
material fallback on macOS 15-25 (mirrors the PlayerOverlayButton pattern)
- Remove the duplicate close button (close now comes only from the
configurable button set)
- Keep the capsule visible in the main window while the separate player
window is open (window mode), still hidden in sheet mode
On macOS the expanded player window built ExpandedPlayerSheet directly in
a fresh NSHostingController on every open. AppKit doesn't composite the
window until that heavy view finishes its first layout pass, so the click
felt like it waited for data to load before anything appeared.
Host a lightweight two-phase root (black + spinner) that composites
immediately, then defer building ExpandedPlayerSheet by one runloop so the
window shows at once and fades in with visible loading — matching iOS.
Because assigning contentViewController resizes the window to the hosting
controller's fitting size (tiny for the placeholder), force the content
size back to the seeded 16:9 initialSize so the window opens full-sized
during the loading phase too.
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.
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.
Drop the dashEnabled toggle and its plumbing. Stream auto-selection now
keeps DASH as a candidate but ranks it strictly below progressive and
HLS formats, so DASH is only chosen when it is the only format available
(including live-stream fallback after HLS). The manual quality selector
hides DASH entirely on iOS/tvOS/macOS. Reconciles BackendSwitcher, which
previously treated HLS/DASH equally, to use the same DASH-last ranking.
The Test Connection buttons only probed API/server reachability, which
was misleading: a green result said nothing about whether videos would
actually play. Remove both the remote-server and WebDAV bandwidth test
buttons and all code exclusive to them (testBandwidth, BandwidthTestResult,
and orphaned localization keys). Add-time connectivity validation for
SMB/WebDAV sources is retained.
Legacy v1 instances without an account had no import path after the
switch to explicit account review. Surface them in a separate "Sources"
section of the legacy import view where they can be added with a single
tap (no sign-in) or removed, mirroring the accounts flow. Instances tied
to an account stay in the Accounts section and are not duplicated.
Detection now gates the entry points and first-launch prompt on any
legacy data (accounts or sources), not accounts alone.
The manual account-import flow made the old silent instance-import
pipeline unreachable. Drop the orphaned parsing, reachability checks,
batch import, result/error types, and the old import row view, plus the
httpClient dependency they were the only consumer of. Also remove the
LegacyAccount.password field, which was parsed but never read.
Pre-macOS 26 doesn't reliably surface a detail-pane NavigationStack's
toolbar confirmation items in the window title bar, leaving the
"Add Source" actions invisible. Gate the toolbar placement behind
macOS 26+ and fall back to an inline form/header button on older
macOS across the add-source views and the sources list.
MPVOpenGLLayer called fatalError() when CGLChoosePixelFormat failed for
both the 10-bit and 8-bit (kCGLPFAAccelerated) formats, killing the app at
launch on machines without a usable GPU such as virtual machines.
Add a non-accelerated software pixel format as a last resort and make the
whole OpenGL init path failable: createPixelFormat/createContext return nil
(with an error log) instead of aborting, MPVOpenGLLayer.init becomes init?,
and MPVOGLView holds an optional layer, throwing openGLSetupFailed from
setup(with:) when OpenGL is unavailable. That error is already handled by
MPVBackend.setupMPVAsync, so the app now launches cleanly and only logs that
video playback is unavailable.
Surface audio-delay (±10/±100 ms) and video-sync-mode controls behind an
"A/V Sync" navigation row in Advanced settings rather than inline, keeping the
Advanced page uncluttered. Audio delay applies live to the running MPV
instance; sync mode takes effect on next playback.
On backgrounding, the app flushed pending CloudKit changes in an unguarded Task
while SwiftData autosave could leave an open SQLite transaction. iOS terminates
apps that hold the database lock across suspension (0xdead10cc), which was the
largest crash group on build 261. On iOS, commit pending changes synchronously
to release the lock, and run the async flush inside a beginBackgroundTask
assertion that ends when the flush completes.
Fixes 0xdead10cc SIGKILLs seen in TestFlight build 261 (17 reports).
CloudKitRecordMapper was a standalone actor, so 'await recordMapper.toCKRecord'
from the @MainActor sync engine hopped off the main actor and read live
SwiftData @Model properties (e.g. LocalPlaylistItem.authorName) from the wrong
executor. SwiftData models are bound to the main ModelContext and are not
thread-safe, so this raced the backing store and crashed with EXC_BAD_ACCESS.
Make the mapper @MainActor (it must touch main-isolated models regardless) and
drop the now-redundant awaits. Fixes the crash for all synced model types.
Fixes an EXC_BAD_ACCESS seen in TestFlight build 261.
UIActivityViewController is presented as a popover on iPad and requires an
anchor; without a sourceView/barButtonItem UIKit aborts in
-[UIPopoverPresentationController presentationTransitionWillBegin]. Set
sourceView/sourceRect on both the direct presentation in the swipe-actions
modifier and the ShareSheet representable.
Fixes a SIGABRT seen in TestFlight build 261.
The 'resumed' guard flag was set inside a dispatched MainActor Task rather than
synchronously in the connection state handler. Two states arriving in quick
succession (e.g. .ready then .failed) could both pass the guard before either
Task ran, resuming the continuation twice and trapping. Claim the continuation
synchronously on the serial queue before dispatching the side effects.
Fixes a SIGTRAP seen in TestFlight build 261.
completeMultiFileDownload captured an index via firstIndex, then awaited a
detached file-size calculation. Another concurrently-completing download could
mutate activeDownloads during that suspension, leaving the index stale and
crashing in Array.remove(at:) with an out-of-bounds index. Remove the download
by identity instead.
Fixes a SIGTRAP seen in TestFlight build 261 (5 reports).
YouTube's CDN advertises maxres/sddefault thumbnails for every video but
only generates them for sufficiently high-res uploads, so older/low-res
videos return 404 for those qualities. The view picked the best quality
and showed a blank placeholder on failure with no fallback.
VideoThumbnailView now accepts an ordered fallback chain and advances to
the next candidate via NukeUI's onCompletion when a load fails, so a valid
thumbnail is always shown. DeArrowVideoThumbnail feeds it DeArrow branding
first, then the video's own thumbnails best-quality-first.
SwiftUI evaluates a @State default-value autoclosure more than once,
keeping only the first instance but still running the side effects of
the discarded ones. Each AppEnvironment() built its own DownloadManager,
and each DownloadManager registered a background URLSession under the
same identifier. The download task started on the surviving instance's
session, but iOS delivered the completion delegate callback to the other
(leaked) instance, whose activeDownloads was empty - so the finished
file was dropped and the progress spinner spun forever at 0 KB.
- Make AppEnvironment a process-wide singleton (static let shared) and
reference it from the App's @State, guaranteeing exactly one instance
(and one background session, one DataManager/CloudKit stack).
- Make DownloadManager.setDownloadSettings idempotent: only create the
background session when none exists, so a re-entrant call never
invalidateAndCancels the live session and kills in-flight downloads.
Cellular changes already route through refreshCellularAccessSetting().
On iPad, expanding the player after collapsing left the MPV display link
paused, showing only a static frame. MiniPlayerView pauses rendering when
its video preview hides during expand, and PlayerService.playerSheetDidAppear
is gated behind background playback so it can't reliably resume.
Call resumeRendering() from MPVVideoView.onAppear so the expanded view
always restarts the display link.
Settings, queue, and details panels stayed open over the failure overlay
and error details sheet, obscuring them. Close any open right-side panel
on entry to the failed state.
Annotate makeCoordinator/makeUIView/updateUIView as @MainActor so reading
the @MainActor-typed onTick closure stays within MainActor isolation and
no longer triggers a Sendable conversion warning.
The Settings (quality/audio/subtitles) and Queue panels now slide in
from the right and occupy the right half of the screen, matching the
info/comments details panel introduced in 92cc8b79f. Video stays
visible on the left so the user retains visual context while browsing.
Both panels supply their own ultraThinMaterial backdrop and use a
custom title bar (replacing NavigationStack's auto-title on tvOS) so
the title styling and symmetric padding match across panels and
across pushed destination screens. The Menu button now pops the
quality panel's pushed Video/Audio/Subtitles detail screens before
dismissing the panel itself.
Removes the background Button from the focus tree while either panel
is open so D-pad left/right inside a row no longer escapes focus
into the player and triggers a seek. Initial focus is steered into
the first row programmatically since tvOS doesn't auto-focus inline
overlays the way it does for fullScreenCover.
Doubles the queue thumbnail size on tvOS (160x90) for readability at
the half-screen panel width.
Player settings on tvOS showed the previous video's tracks after queue
advance because availableStreams was never cleared on a video change and
playQueuedVideo only seeded a single pre-resolved stream. Reset the list
on a new video, then fetch the full streams in the background for global
videos, and repoint state.currentStream/currentAudioStream to the
refreshed entries so the picker checkmarks land on the playing tracks.
Lets the Apple TV switch its HDMI output to match the playing video's
frame rate and dynamic range via AVDisplayManager.preferredDisplayCriteria,
driven from MPV's container-fps and video-params/gamma. Two opt-in toggles
(default off) live under Playback → Display on tvOS; both are no-ops on
other platforms. Anchor an AVKit class symbol so the linker keeps AVKit
linked — Swift only autolinks AVFoundation here, and without AVKit the
UIWindow.avDisplayManager category isn't loaded at runtime.
If the MPV backend was retrying a failed load and the user switched to
another video before retries exhausted, the eventual error was published
to the player UI even though that video was no longer active. Guard the
catch block with a loadingVideoID check so stale errors are dropped.
- Skip Invidious integration tests gracefully on .noConnection so a
transient instance outage no longer fails CI
- Point integration tests at i01.v.yattee.stream (the previous test
instance was decommissioned)
- Force UTF-8 on AXe CLI output in the UI test wrapper; ASCII-tagged
bytes were crashing JSON.parse in describe_ui
- Add iOS 26.4 visual baselines for app-launch-home and settings-main
Wrap pushed view in TVSidebarDetailContainer, list custom options
first so focus engages on a row, mark default options focusable so the
list scrolls. Replace toolbar-based Add/Edit sheets with padded VStacks
and inline confirm buttons. Cache customMPVOptions in @Observable
backing storage so writes refresh the list immediately.
Render subscriber count, Subscribe button, and (for subscribed
channels) description in the tvOS loading state instead of just
avatar + name + spinner. Seed the in-memory author cache when
navigating to a channel from a video so the first-time channel
view has a name and avatar to display immediately.
When the same video was already loaded (typically paused), opening it
again via the URL scheme, a deep link, or a remote-control loadVideo
command did nothing — the player just stayed paused. Now the same-video
early-return path resumes playback if paused and seeks to the supplied
startTime, so timestamps from URLs and remotes are honoured even when
the video is already loaded.
URLRouter gains a parseTimestamp helper that reads t/time/start query
params in plain-seconds and YouTube-style (1h2m3s) forms, and the deep
link handler now forwards that timestamp through to openVideo.
Surfaces the existing iOS/macOS Background Playback setting in the tvOS
Playback settings, defaulting to off so audio stops when the user leaves
the app via the TV button. Pauses playback on .background/.inactive when
the toggle is off, regardless of audio route — the user's setting wins
over AirPlay/HomePod handoff. Also auto-shows the tvOS player controls
when returning to foreground so the paused state is immediately visible
and actionable.
Full-screen glass overlay was excessive on 4K displays and fully hid
the video. Wrap the panel in a GeometryReader sized to half the parent
width, slide it in from the trailing edge, and tighten internal
horizontal/top padding now that the content lives in a narrower column.
On tvOS, registering MPRemoteCommandCenter handlers makes the system
classify the app as a long-form video media app. When audio is routed
to AirPlay 2 endpoints (HomePods), the system then enforces a ~2s
look-ahead buffer in the AVAudioSession → AirPlay 2 pipe for
multi-speaker sync. The result is a 2-3 second audio drain on pause
and refill on resume.
The buffer lives downstream of mpv, so no mpv command (ao-reload,
seek-flush, audio-add) can flush it; AVAudioSession setCategory
overrides (mode/policy) and setActive(false)/setActive(true) cycles
are also ignored once the app is media-classified.
Workaround: detect the active audio route via routeChangeNotification
on tvOS. While AirPlay/HomePod is the output, suppress
MPNowPlayingInfoCenter publication and disable every MPRemoteCommand
so tvOS un-classifies us. When the route returns to local outputs,
republish the cached Now Playing info and reconfigure remote commands.
A latch defers all media integration until the audio session has been
activated at least once, so no commands are registered before the
route can be evaluated.
Trade-off: while playing to HomePods, the Control Center widget and
external Siri Remote play/pause are not available — but pause/resume
is responsive.
Throttle SELECT-based scrubbing to seek the underlying frame ~every
150ms instead of waiting 1s after pan-end, so the visible frame keeps
up with the scrub handle. Hide the redundant storyboard panel during
live scrub (the frame itself is now the preview) but keep the chapter
capsule visible. Storyboard panel still shown for D-pad arrow-seek
where the frame doesn't move until commit.
Auto-commit scrub mode after 3s of inactivity, matching
AVPlayerViewController behavior — playback resumes via the existing
scrub-pause wiring instead of staying paused indefinitely.
Route the on-screen play/pause button through handlePlayPause() so it
follows the same visibility and auto-hide timer logic as the Siri Remote
hardware button: timer stops when paused (controls stay pinned) and
restarts on resume.
These views rendered video thumbnails without passing watchProgress, so the
progress bar was silently missing. Apply the existing pattern from
SubscriptionsView: maintain a watchEntriesMap and forward watchProgress(for:)
to VideoRowView/VideoCardView at each call site.
Subscribe to mpv log messages and capture END_FILE error code/string so
load failures bubble up specific causes (HTTP 404/403, DNS failure,
demuxer errors) instead of a generic 10s timeout.
tvOS's sidebarAdaptable TabView leaves the previously-pushed detail view
visible after the user picks another sidebar item, until they manually
press Menu. Broadcast a notification on tab change so any pushed
TVSidebarDetailContainer dismisses itself, and reset each tab's
NavigationPath. Also drop a redundant inner NavigationStack in the tvOS
SettingsView so subpages register on the tab's outer stack.
After iOS reinstall/restore the app container UUID rotates, which left both
the persisted source.url and the security-scoped bookmark pointing at a
no-longer-current path. Files derived a stale absolute path that got appended
onto the resolved bookmark, producing doubled URLs that MPV could not load.
- Resolve the base URL by picking whichever of the bookmark or source.url
actually exists on disk.
- Compute MediaFile relative paths against the resolved root so they survive
later container changes.
- Hold the security-scoped resource access for the source's lifetime via a
shared resolver, so MPV can open files long after the directory enumeration
that resolved the bookmark has returned.
- Normalize legacy absolute paths embedded in old recents/history video IDs
so they re-resolve under the current container.
Removing the embedded framework after every build broke Xcode's
incremental copier on the next build (it would try to copy individual
files into a destination directory tree that no longer existed). The
strip is only meaningful for App Store-bound Release builds, so skip
it in Debug — Sparkle in a local debug bundle is harmless.
The post-embed strip script was intermittently failing with "Directory
not empty" when rm -rf raced against codesign/Spotlight/Xcode touching
the freshly-embedded framework. Atomically rename the framework out of
the embed path first, then rm -rf with a short retry loop, so a clean
build is no longer needed to recover.
Toast cards now follow the finger upward and dismiss on either a
sufficient drag or a fast flick (via predicted-end translation). The
auto-dismiss timer pauses while the user is dragging and re-arms if
they release without dismissing.
Previously a failed video left the user staring at a black screen / thumbnail
with no indication anything went wrong — playbackState went to .failed but
TVPlayerView never read it. Add a focusable glass overlay (Details / Retry /
Play Next or Close) gated on isFailed and a parallel one for retry-exhausted
state, with the regular controls and background tap-target disabled while
either is visible so focus stays inside the overlay. Hide the Copy/Share
toolbar items in ErrorDetailsSheet on tvOS where they aren't useful.
Drop comments restating what the code shows; hoist allowSoftwareDecodedFormats
out of the recommendedVideoStreams filter closure so the bridge property is
read once per render instead of once per stream.
Lets the auto stream selector pick formats whose codec isn't hardware
decoded on the current device. Defaults off; when on, 4K VP9/AV1 can be
auto-selected on Apple TV models without those decoders. Software-decoded
streams also move into the Recommended section so the selection stays
visible without enabling advanced stream details.
Mirrors the existing iOS/macOS option using the shared
subscriptionsShowSidebar AppStorage key. When the sidebar is hidden
the View Options button moves above the feed so it remains reachable.
The horizontal channel chip strip looks awkward on tvOS and the focus
interaction is clunky. Drop it from the tvOS branch of InstanceBrowseView;
other platforms keep it.
The Siri Remote's left/right d-pad only delivered a single discrete
seek per click — holding the button did nothing. A window-level custom
UIGestureRecognizer now tracks the actual press duration and drives a
repeating seek tick (10s → 20s → 30s acceleration) until release,
routing through the existing accumulating-seek paths so the scrubber
preview, debounced commit, and on-screen feedback all keep working.
Settings → Notifications → Manage Channels: wrap the tvOS NavigationLink
destination in TVSidebarDetailContainer(showsDismissButton: true) so the
no-subscriptions, error, and loading states all have a focusable Done.
Channels sidebar tab: lift the tvOS search field + View Options button
out of the loaded-channels branch and render it above every state. The
empty state previously had zero focusable elements, leaving the right
pane blank when swiping in from the sidebar.
TVSidebarDetailContainer now exposes a showsDismissButton flag instead of
always attaching a Done toolbar item. The button is only enabled where a
view can end up with no focusable element on its own — Device
Capabilities (informational rows) and the Import Playlists/Subscriptions
flows.
Wrap Contributors, Translators, Acknowledgements, and Device Capabilities
destinations in TVSidebarDetailContainer for the consistent sidebar look,
and make the Translators/Acknowledgements rows focusable on tvOS by
wrapping them in Buttons so the Menu remote button can pop the stack.
When all playlists/subscriptions were imported, every row collapsed to a
non-focusable checkmark and the Add All toolbar item disappeared, leaving
the view with no focusable element. The Menu button then closed the app
instead of popping the navigation stack.
Wrap the import destinations in TVSidebarDetailContainer for visual
consistency and add a Done toolbar item (cancellationAction) that is
always present on tvOS, reachable from any list row via swipe-up.
The .sheet rendering on tvOS produced a tiny floating modal where the
"Sign In" title wrapped onto two lines and form fields overflowed. Use
.fullScreenCover on tvOS and wrap the login form in
TVSidebarDetailContainer so the title/icon sit in the standard 400pt
left sidebar. iOS and macOS keep the existing sheet presentation.
Pointing AddRemoteServer at a Piped Vue SPA (e.g. the frontend host
rather than the API host) used to fail with a generic
"could not detect instance type" — every JSON probe got the same
index.html back. On the failure path, fetch `/` once more and look
for `<title>Piped</title>`; if matched, return a new
`pipedFrontendDetected` error that tells the user to enter the
Piped API URL instead.
Adds a Piped instance, logs in, and exercises the two settings flows that
hit the regressed endpoints directly — Import Subscriptions (/subscriptions)
and Import Playlists (/user/playlists). Asserts that "session is a required
parameter" never appears in the AX tree, catching the recent header-vs-query
auth regression end to end.
Promotes three tree-walking helpers (id_in_tree?, id_with_prefix_in_tree?,
label_in_tree?) onto UITest::Axe so the spec can fetch the AX tree once per
poll iteration and run all checks against it locally — roughly 6× fewer
`axe` subprocess spawns than calling element_exists? / text_visible? per
check, and a primitive other specs can reuse.
Piped's session token reuses the Authorization header, so a fronting basic
auth proxy can't coexist with logged-in Piped use — the two would clobber
each other's credentials on every authenticated request.
Add a supportsHTTPBasicAuthProxy capability on Instance/InstanceType (false
for Piped, true for everything else) and route it through:
- AddRemoteServerView refuses Piped if detection only succeeded behind basic
auth, surfacing a localized "not supported" error instead of a silently
broken instance, and hides the optional credentials section for Piped.
- EditSourceView hides the basic auth fields for Piped instances and clears
any legacy stored credentials on save, in case a Piped source was added
with credentials before this change.
Commit aed78c13f moved the session token from the Authorization header to a
?authToken= query parameter on /subscriptions, /subscribe, /unsubscribe,
/user/playlists, and /playlists/{id}. Piped's backend only accepts the
?authToken= form on /feed; every other authenticated route reads it from
Authorization (the Java handler names the variable "session", which is why
the rejected requests returned "session is a required parameter"). Restore
the header form for those five routes and leave feed alone.
The proxy auto-detect path (when proxiesVideos is off) HEADed a
googlevideo URL with a 5 s timeout on every video. The verdict is a
property of the network, not the video, so the cost was paid for no
reason on videos 2..N. On a network where the CDN is blocked the full
5 s timeout was added to playback startup every single time.
Two changes:
1) ProxyDetectionCache (actor, per-instance, 10 min TTL). First miss
pays the HEAD once and caches the verdict; subsequent videos hit
the cache synchronously. Concurrent callers share one in-flight
probe. The last-seen sample CDN URL is retained so future probes
don't need a fresh URL from the current video.
2) PlayerService kicks off InvidiousAPI.prewarmProxyDetection() in
parallel with the videoWith... API call. By the time streams come
back, the verdict is usually already cached and proxyStreamsIfNeeded
is a sync lookup. Cheap when there's nothing to prewarm.
Cache invalidation:
- on InstancesManager.update (URL change, proxy toggle flip)
- on InstancesManager.remove
- TTL covers the network-change case for now (no NWPathMonitor yet)
The Sources -> Edit Source -> Proxy toggle now renders for Yattee
Server entries (supportsVideoProxying gains .yatteeServer). When the
toggle is on, playback fetches go through
videoWithProxyStreamsAndCaptionsAndStoryboards with mode .relay, so
the server returns signed /proxy/relay URLs (byte-relay, supports
HTTP Range, no on-disk caching). Downloads keep going through mode
.download so the server-side /proxy/fast/ flow continues to cache
files for repeat use.
InvidiousAPI.proxyStreamsIfNeeded early-returns for .yatteeServer
since proxying is now done at fetch time via ?proxy=true rather than
client-side host rewriting.
Color.accentColor and .foregroundStyle(.tint) resolve to the asset
catalog accent on macOS, so Home shortcut cards, section header
links, the Subscriptions "All channels" header, and the Downloads
per-channel group headers stayed blue when the user picked a
different accent. Read the color from SettingsManager and apply it
directly, matching the pattern already used for the Play button.
Remove the gear toolbar button that opened Settings as a sheet in the
NavigationSplitView sidebar column, and drop the macOS guard hiding
.settings from SidebarMainItem so it can be added to the sidebar and
rendered in the detail column like other items. The dedicated Settings
window (Cmd+,) is unchanged.
Drop the standalone iOS section for Integrations and inline its row into
the main list right above Advanced Settings. Swap the tvOS sidebar order
so Integrations appears before Advanced as well. macOS was already
correctly ordered via SettingsSection enum declaration.
Also swap the icon to puzzlepiece.extension, which better conveys that
this section houses third-party service hookups (SponsorBlock, Return
YouTube Dislike, DeArrow, short-link resolution) rather than being
YouTube-specific.
Hide the Resolve Short Links toggle on tvOS — there's no way to tap
inline description links or reach a system browser there — and tighten
the openInSystemBrowser platform guards so the iOS-only UIApplication
path isn't compiled on tvOS.
Tapping bit.ly/tinyurl/t.co/etc. in a description or comment previously
opened Safari even when the destination was a playable YouTube URL.
Added an opt-in "Resolve Short Links" toggle under YouTube Enhancements
(off by default) that follows the redirect on tap: if the target is a
YouTube/PeerTube/direct-media URL, open it in-app; otherwise prompt the
user before falling back to yt-dlp extraction or the browser.
Also added a confirmation dialog for non-shortener links that only
matched the loose .externalVideo yt-dlp fallback, so arbitrary web
pages in descriptions no longer silently kick off extraction.
Prompts live on NavigationCoordinator and are dual-hosted by YatteeApp
and ExpandedPlayerSheet so they remain visible whether or not the
expanded player is covering the main view.
Bake https://dl.yattee.stream/appcast.xml into SUFeedURL and align the
appcast template and Sparkle setup notes so the gh-pages branch (served
under the custom Cloudflare-CNAMEd domain) is the single place to look
up the feed. Domain is reserved as a generic distribution surface for
Sparkle today and AltStore / other channels later.
Add -Wl,-dead_strip_dylibs to the Release config so the linker drops the
unused Sparkle LC_LOAD_DYLIB, and a Run Script phase that removes the
auto-embedded Sparkle.framework from the app bundle for every config
except Release-DeveloperID. Disable user script sandboxing on the Yattee
target so the rm is permitted. Release-DeveloperID keeps Sparkle linked
and embedded for the notarized Developer ID channel.
New Release-DeveloperID configuration gates Sparkle behind a SPARKLE
compile flag so the App Store Release build stays Sparkle-free. Adds
SPUStandardUpdaterController wrapper, Check for Updates menu command,
Advanced Settings section with beta channel toggle, and a Ruby script
plus GitHub Actions job that signs each release and publishes the
appcast to gh-pages for consumption by Sparkle and Homebrew cask.
Comments now use DescriptionText.attributed so URLs become tappable
and route through the same in-app pipeline as description links, and
timestamp strings seek the player.
Description links to YouTube videos, channels, playlists, and external
video URLs now open in Yattee instead of Safari. When a video is
already playing, tapping a video link surfaces the existing
QueueActionSheet (Play Now / Play Next / Add to Queue) — the sheet is
hosted both at the app root and inside ExpandedPlayerSheet so it
appears above whichever layer is on screen.
Drop the redundant inner NavigationStack on non-tvOS (the outer detail
pane already provides one) and apply .listStyle(.inset) on macOS. Kept
as List to preserve drag-to-reorder for the main navigation section.
Convert detail and filter sheets to shared helpers, add inline Filter /
Export / Clear buttons next to the search bar (toolbar items weren't
surfacing in the settings detail pane), inline the Reset Filters button
at the bottom of the filter sheet, use a 'Close' text button, and trim
the macOS Share Sheet to just the scrollable log with a Copy button.
Convert the main view to shared SettingsFormContainer/Section helpers,
drop the DisclosureGroup in favor of a single always-visible Default
Options section with monospaced trailing values, and redesign the
Add/Edit MPV option sheets on macOS as native dialogs with a Grid
layout, clearer footers, and keyboard-shortcut actions.
Extend SettingsFormSection to accept a @ViewBuilder footer for sections
with dynamic multi-line content (last background refresh, orphaned
files status). Move trailing button accessories (size, progress) out of
button labels so buttons size to their content on macOS.
Includes SponsorBlock, Return YouTube Dislike, and DeArrow sub-screens.
Extend SettingsNavigationRow with an optional trailing content slot so
rows can show enabled/disabled status next to the chevron.
Add SettingsNavigationRow helper that renders destination-pushing rows
as plain list rows with a trailing chevron on macOS, and drop the top
divider in headerless sections so they don't render a stray rule.
Add shared SettingsFormContainer/SettingsFormSection helpers that mirror
the Sources screen styling (uppercase subheadline headers, divider-
bracketed cards, ScrollView + LazyVStack) on macOS while keeping the
standard Form/Section layout on iOS and tvOS.
Convert PlaybackSettingsView and SubtitlesSettingsView to the new
helpers, wrap the macOS Settings detail pane in a NavigationStack so
NavigationLink pushes (Subtitles Appearance) render in the detail
column, fold the macOS-only Player Mode + Auto-resize player controls
into the Behavior section, and drop the unused queue footer.
Drop the iOS-grouped rounded card in MediaBrowserView's section
container for top/bottom dividers on macOS, force .buttonStyle(.plain)
on directory NavigationLinks to avoid default link tinting, and
shrink MediaFileRow's icon frame on macOS. iOS and tvOS unchanged.
Drop the iOS-grouped rounded card, per-row chevron, and oversized
metrics on macOS. Use tighter padding, smaller icon/title fonts,
uppercase section headers, and top/bottom dividers so the list reads
like a native grouped Mac list. Force .buttonStyle(.plain) on row
buttons/NavigationLinks and add .contentShape(Rectangle()) so the
full row is hit-testable without picking up macOS's default link
styling. iOS and tvOS unchanged.
Use grouped Form style with LabeledContent rows and move primary
actions into the sheet toolbar for SMB, WebDAV, Local Folder, Remote
Server and the Edit sheet. iOS and tvOS branches unchanged.
Adds a Show Sidebar toggle (iPad regular and macOS) that controls
channels sidebar visibility. When the sidebar is shown, the channel
strip picker is disabled and the redundant channel header link above
the feed is hidden. Layout picker now uses inline menu style for
consistency with other options.
On iPad in regular horizontal size class, the Subscriptions view now
shows a channels column next to the feed (mirroring macOS/tvOS) instead
of the floating channel strip. iPhone and compact-width iPad keep the
existing strip.
- Renames MacSubscriptionsSidebarRow to SubscriptionsSidebarRow and
shares it across macOS and iOS.
- Uses a custom ScrollView-based sidebar on iPad to avoid iOS 26's
sidebar background extension bleeding into the selection highlight.
- Forces inline toolbar title on iPad regular so scrolling either
column behaves consistently.
Replaces the iOS-style floating channel strip with a dedicated
channels column next to the feed, using HSplitView for a native
draggable divider. Mirrors the tvOS two-column structure.
Replaces the macOS TabView(.sidebarAdaptable) root with NavigationSplitView
so the Settings gear can live in the sidebar column's toolbar (next to the
sidebar toggle) instead of only appearing in HomeView's detail toolbar.
Without sdk:"macosx", gym treats the multiplatform scheme as iOS and
fails with "IPA invalid". Without an explicit destination xcodebuild
picked tvOS. The output_name also needed the .app suffix removed since
gym appends .pkg for macOS app-store exports.
Add macOS-only minimum frame sizing to sheets that wrap a
NavigationStack/Form without intrinsic size, so they render properly
instead of collapsing to just the toolbar. Affects Customize Home,
subscription/channel view options, playlist create/edit, search
filters, media browser options, instance picker, log filters, preset
editor, and legacy data import result.
Scheme-level build entries bypass the pbxproj target-dependency
platformFilter, so tvOS archives tried to build the iOS-only
ShareExtension and failed with "No profiles for *.ShareExtension".
The extension is still built on iOS via the Yattee target's
platform-filtered dependency and embed phase, matching how
YatteeTopShelf is wired for tvOS.
Sidebar buttons in TVSidebarDetailContainer were hard to focus from
the content list. Move the Add Source (and sort/group menu for Media
Sources) to a top HStack wrapped in focusSection(), matching the
pattern used in MediaBrowserView. Default focus lands on the first
source row via @FocusState + FirstRowFocusModifier.
Render a focusable empty state on tvOS Home when no sections have content,
with an "Open Sources" button that switches the sidebar selection. Without
a focusable view the tvOS focus engine had no target, leaving the sidebar
unreachable after the initial iCloud alert was dismissed.
Also wire the selectedSidebarItem onChange handler into the tvOS TabView,
which was missing and prevented programmatic sidebar selection.
When enabled, the Siri remote Menu button stops playback and clears the
queue instead of only collapsing the player, and the explicit top-bar
close (X) button is hidden.
Exposes ContinueWatchingView as an opt-in compact tab bar item,
hidden by default. Uses a short "Continue" label since the full
"Continue Watching" string does not fit as a tab bar title.
- Move Close to a top-right circular icon; bottom row reorganizes into
left (Settings/Info/Comments), center transport (Previous/PlayPause/Next),
and right (Queue) clusters with equal side frames so transport stays
geometrically centered.
- Introduce a circular icon-only `TVTransportButtonStyle` (primary variant
for Play/Pause) mirroring the new Close button look.
- Always render Previous/Next so Play/Pause position is fixed; dim and
disable when unavailable.
- Share `isTransportDisabled` on `PlayerState` and reuse it on iOS and
tvOS; apply it (plus symbol replace transition) to the tvOS Play/Pause
button.
Delete the multi-page onboarding sheet. On first launch the app now
silently imports any v1 instances from UserDefaults (splitting embedded
basic-auth credentials out of the URL and into the Keychain) and then,
if the device is signed in to iCloud, shows a single alert offering to
enable sync. Accepting shows a blocking progress overlay until the
initial upload completes.
Add tvos platformFilter to YatteeTopShelf.appex embed and target
dependency so it's only built for tvOS, and exclude Info-tvOS.plist
from Copy Bundle Resources via synchronized-folder membership
exceptions.
Rewrites /vi/ID/{default|mq|hq|sd}default.jpg to maxresdefault.jpg when
saving/updating a recent playlist, so the card shows a sharper image
than the low-res search-result thumbnail.
- Disable scroll clipping so focused source/channel/playlist cards show full halo
- Remove rounded clip on source picker row that cut the Menu focus effect
- Replace tappable recents header Button with a plain label on tvOS
- Add vertical spacing between recent search items
- Widen recent channel and playlist cards and reserve space for two-line titles
- Increase horizontal spacing between cards so focus halos don't collide
Add a TVSourceRowLabelStyle for consistent icon/text spacing, switch
the Scan Network button to TVFormRowButtonStyle so it matches the
NavigationLink rows, and drop the duplicate navigationTitle in
AddWebDAV/AddSMB views since the title is already shown in the
TVSidebarDetailContainer sidebar.
Use a tvOS-specific Info.plist to exclude iOS CFBundleAlternateIcons
keys that caused ITMS-90471 rejections, and drop empty image slots
from the tvOS brand asset catalog.
Fetch a match profile for the new stream.yattee.app.TopShelf bundle,
switch the extension target to manual signing, and map its profile in
build_app export_options so the tvOS archive signs both the main app
and the Top Shelf extension.
Surfaces Continue Watching, Recent Feed, and Recent Bookmarks in the
Apple TV Home top shelf when Yattee is focused. Tapping a tile opens
the video via the existing yattee://video/{id} deep link.
- New YatteeTopShelf app extension target (tvOS only). LD_ENTRY_POINT is
overridden to _NSExtensionMain; the tv-app-extension product type
defaults to _TVExtensionMain which is for the pre-tvOS-13 legacy API
and crashes modern TVTopShelfContentProvider subclasses at launch.
- Main app writes per-section JSON snapshots (capped at 10 items each)
to a shared App Group UserDefaults suite after bookmark, watch-history,
and feed-cache changes, plus an initial write on launch.
- Enabled-sections list is mirrored to the same App Group so the
extension can respect the user's selection without touching SwiftData.
- Settings → Top Shelf (tvOS only) lets the user toggle sections.
- Deep link playback shows a loading toast while video details are
fetched, and an error toast if no source is configured.
Use a NavigationStack/List layout with a centered thumbnail, stacked
title, and card-styled buttons so the focus ring renders with proper
clearance on tvOS.
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.
handlePendingNavigation guarded the .mediaSource append with
#if os(iOS) || os(macOS), so navigating to video info from a media
source on tvOS was silently dropped. The NavigationStack binding is
already unconditional, so the append is safe on all platforms.
Render section headers as plain Text on tvOS so the focus engine
skips them and moves directly between video cards across sections.
iOS and macOS keep the tappable Button with chevron unchanged.
Match iOS behavior by gating the type switcher and filters menus on an
active query, and drop the .caption font so they render with the same
default button font as View Options.
Surface the existing in-channel search feature on tvOS as a final tab
after Playlists. Selecting it reveals a search field and results area
below the tab row, reusing the same API and result views as iOS/macOS.
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.
Arrow-seek on the focused scrubber previously moved the handle but
showed no storyboard/chapter context. Reuse the existing SELECT-scrub
overlay for arrow-seek too, with a ~2s lingering fade after the last
press so the preview doesn't vanish the instant the seek commits.
Hide navigation title on tvOS, move the new-playlist action into an
inline focus section above the list, and make rows focusable via
NavigationLink so focus can move down from the button and default
focus lands on the first playlist.
Expose the grid columns picker in SubscriptionsView and ManageChannelsView
inline sheets, and track viewWidth in ChannelView's tvOS GeometryReaders so
the shared ViewOptionsSheet can compute a meaningful column range.
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.
The tvOS Main Navigation list relied on .onMove with native Toggle
rows inside editMode, which leaves only the drag handle focusable on
tvOS. Replace it with a TVSidebarMainItemRow modeled on the home
customization screen: explicit up/down chevrons on the left and a
tap-to-toggle checkmark button as the row body. Required items render
disabled with a dimmed checkmark.
Left/right on the Siri Remote now seek instead of revealing controls,
reusing the iOS tap-seek accumulation handler and feedback overlay so
rapid presses compound into a single "+30s" / "-20s" jump. Up/down
still show controls.
Siri Remote already handles play/pause and seeking natively, so the
on-screen skip/play/pause cluster was duplicate UI. Initial and
restored focus now targets the progress bar.
Replace the tvOS bottom action bar with Settings / Info / Comments /
Next / Close. Settings reuses QualitySelectorView (video, audio,
subtitles, speed); Comments opens TVDetailsPanel directly on the
comments tab; Close stops playback and dismisses.
Debug button is hidden by default and can be re-enabled via a new
tvOS-only Advanced Settings > Developer toggle.
Present the settings sheet as a fullScreenCover with a centered
material card, fix the "Normal" hyphenation, and restyle row selection
throughout the quality selector on tvOS: per-row rounded backgrounds
with focus tint + stroke, vertical spacing instead of dividers, and a
focusable speed-rate menu.
Use the tab root's top safe-area inset instead of a fixed 20pt, so the
search/options header in History and Bookmarks clears the floating "Home"
pill drawn by the sidebarAdaptable TabView.
Calling stop() on Menu-button dismiss cleared currentVideo and tore down
the backend, so audio did not continue and the "Now Playing" sidebar
tab never appeared. Match the iOS/macOS dismissal path instead and just
collapse the fullScreenCover.
Extends `SettingsKey.isPlatformSpecific` to cover home, tab bar, and
sidebar layout, plus player details panel and video swipe actions, so
iOS devices sync these with other iOS devices (and tvOS with tvOS,
macOS with macOS) instead of overwriting each other via the shared
iCloud key. Adds a one-shot migration that copies legacy unprefixed
values into the new platform-prefixed slots locally and in iCloud,
preserving protected-key timestamps.
Inset/grouped list style doesn't work well with tvOS focus effects.
Always return .plain on tvOS and hide the list style picker from
appearance settings.
The backgroundStyle.ignoresSafeArea().overlay(ScrollView) pattern
clips the tvOS focus effect at the overlay boundary. On tvOS, render
the ScrollView directly so focus highlights can extend naturally.
Reorganize the tvOS search UI into a single header row with search
field, type filter menu, combined filters menu (sort/date/duration
with reset), and view options button. Removes the separate filter
strip between search and results on tvOS.
Form inside a sheet causes clipped rows and invisible text on focused
items due to white-on-white rendering. Use a plain List on tvOS which
handles the focus styling correctly.
The .pickerStyle(.menu) hid labels. Instead, wrap the Form in a
NavigationStack on tvOS so the default navigation picker style works
and shows both labels and values.
Dividers inside rows conflict with the tvOS focus highlight effect.
Remove dividers and the inset card background/clipShape on tvOS so
the focus effect renders cleanly without visual artifacts.
Unifies the filter strip on tvOS so all filters (sort, date, duration,
content type) use the same inline Menu style instead of mixing menus
with a segmented picker.
The filters sheet is too small and awkward on tvOS. Replace the filter
button with inline Menu pickers for Sort By, Upload Date, and Duration
directly in the filter strip. Applied to both SearchView and
InstanceBrowseView.
Use inline TextField with focusSection instead of .searchable() on tvOS
to prevent keyboard/navigation title overlap. Remove clipShape on recent
search items so tvOS focus effect is not cut off.
- Replace sheets with navigationDestination for Add/Edit Source on tvOS
(tvOS sheets have fixed size that doesn't fit the content)
- Fix focused cell clipping by replacing TVSettingsContainer's frame-based
layout with safeAreaInset, matching the main settings view pattern
- Use standard List with .listStyle(.grouped) for Sources on tvOS
- Add sidebar icons and titles to TVSettingsContainer for all settings
subviews, utilizing the left column space
- Remove redundant large navigation titles on tvOS (shown in sidebar)
- Move Edit Source Save button from toolbar into form above Delete button
for better tvOS focus navigation
parseAudioInfo() returned isOriginal=false for all streams when the
audioTrack object was present in the API response, preventing xtags
parsing from correctly identifying original tracks. This caused the
player to fall through to codec/bitrate sorting, often picking a
locale dub (e.g. Polish) instead of the original English audio.
Now determines isOriginal from both the audioTrack displayName
("original" keyword) and URL xtags (acont=original) for robustness.
Also adds isDefault to InvidiousAudioTrack for future use.
Exercises the read path end-to-end against an Invidious instance fronted
by an HTTP Basic Auth reverse proxy: adds the instance via the new
basic-auth-aware add helper, navigates to Search, runs a query, taps the
first result, waits for the player to expand and start playback, then
closes the player. Confirms that ContentService's per-instance HTTPClient
(with the basic-auth Authorization header baked in via setDefaultHeaders)
is wired correctly through search, video metadata fetch, and stream
loading.
Skips cleanly when INVIDIOUS_BASIC_AUTH_USERNAME / _PASSWORD are not set.
Three end-to-end specs that exercise the new basic-auth flows against a
real Invidious instance fronted by an nginx reverse proxy:
1. add flow: types the URL, hits Detect, fills the basic-auth fields
when the basicAuthRequired UI state appears, taps Retry Detection,
and confirms the instance lands in the Sources list.
2. state assertion: types a URL, taps Detect, and verifies the form
transitions into the basicAuthRequired state (Retry Detection button
present, no detected type) when no credentials were supplied.
3. proxied login: ensures the instance exists, then drives the standard
Invidious login flow with the proxied account credentials. Confirms
the SID Cookie auth coexists with the per-client Authorization
header on the basic-auth-aware HTTPClient.
Test infrastructure additions:
- spec/ui/support/config.rb: env-driven accessors for the basic-auth URL
and proxied-account credentials. No secrets committed.
- spec/ui/support/instance_setup.rb: helpers
add_invidious_with_basic_auth, remove_and_add_invidious_with_basic_auth,
find_basic_auth_text_fields (mirroring find_auth_text_fields), and
fill_field for tapping a discovered field by frame and typing into it.
All three specs skip cleanly when the relevant env vars are not set.
- InstanceDetector: a single 401 from one probe was over-eagerly concluded
as "credentials invalid" / "credentials required". On instances behind a
reverse proxy where one probe path (e.g. Yattee Server's /info) hits a
same-origin redirect, iOS URLSession strips the Authorization header on
the redirect and the request 401s even with valid credentials. Track 401s
across all probes and only conclude basicAuthRequired/basicAuthInvalid
when no probe matched and at least one returned 401.
- InstanceLoginView: the Invidious/Piped login flow constructed an API
client backed by the shared appEnvironment.httpClient, which has no
per-instance basic-auth headers. For instances behind a reverse proxy,
the login POST 401d before reaching the upstream login endpoint. Build a
per-instance HTTPClient with the basic-auth Authorization header baked in
via setDefaultHeaders, mirroring ContentService.httpClientWithBasicAuth.
- InvidiousAPI.login: the login function constructs its own URLSession (to
capture Set-Cookie via a redirect-blocking delegate), so it never
inherits headers from the injected httpClient. Add an optional
extraHeaders parameter and have InstanceLoginView pass the basic-auth
header through when present. PipedAPI.login uses httpClient.fetch and
inherits defaultHeaders correctly, so no change is needed there.
EditSourceView now exposes the basic-auth username/password fields for every
instance type (Invidious, Piped, PeerTube, Yattee Server), keeping the
existing required-credentials UI for Yattee Server and adding an optional
section for the others. Credentials are loaded and persisted via
BasicAuthCredentialsManager regardless of type, and clearing both fields
deletes stored credentials for non-Yattee types.
AddRemoteServerView gains a new basicAuthRequired UI state: when instance
detection hits a 401 (the entire instance is behind a reverse proxy), the
view reveals username/password fields and a Retry Detection button. The
retry calls the detector with the credentials injected as an Authorization
header; on success the form transitions into the normal detected state with
the credentials pre-populated. A repeat 401 shows an inline "invalid
credentials" message instead of restarting the flow. For non-Yattee types,
any credentials entered during the flow are persisted alongside the new
instance.
When an instance sits behind a reverse proxy that requires HTTP Basic Auth,
every detection probe (/info, /api/v1/config, /api/v1/stats, /healthcheck,
/config) returns 401 before reaching the real backend, so the type cannot be
identified. Re-throw APIError.unauthorized from each probe instead of
swallowing it, and have detectWithResult convert the first 401 it sees into
DetectionError.basicAuthRequired. Add a basicAuthHeader parameter so the
caller can retry detection after the user provides credentials; if a retry
also returns 401, surface basicAuthInvalid instead.
Replace the YatteeServerAPI setAuthHeader/buildHeaders pattern (which was
race-prone on the shared actor across multiple instances) with a generic
mechanism: HTTPClient now supports a defaultHeaders dictionary applied to
every request, and ContentService builds a per-instance HTTPClient with the
basic-auth Authorization header baked in whenever credentials are configured.
The same code path now works uniformly for Invidious, Piped, PeerTube, and
Yattee Server, so any instance sitting behind a reverse proxy that requires
HTTP Basic Auth can be authenticated regardless of backend type. Cached
default API actors are still reused when no basic-auth header is needed.
Piped accepts the session token via either an Authorization header or an
authToken query parameter (the /feed endpoint already uses the latter form).
Switch all token-bearing Piped endpoints to the query-parameter form so the
Authorization header is free for HTTP Basic Auth from a fronting reverse
proxy. Affects subscriptions, subscribe, unsubscribe, userPlaylists, and
userPlaylist (including its nextpage pagination loop).
Renames YatteeServerCredentialsManager → BasicAuthCredentialsManager so the
same Keychain-backed username/password storage can be reused for any instance
type that sits behind a reverse proxy requiring HTTP Basic Auth. Adds a
one-time migration that moves existing items from the legacy
'com.yattee.yatteeserver' Keychain service to 'com.yattee.basicauth',
preserving the iCloud-sync attribute. No behavior change for end users.
Videos that haven't premiered yet were triggering repeated notifications
on every background refresh cycle. Filter them out by checking isUpcoming
flag and rejecting videos with future publish dates.
Also decode isUpcoming/premiereTimestamp from Yattee Server feed responses
instead of hardcoding false/nil.
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().
Extract chapter title from inside the storyboard preview into a
standalone ChapterCapsuleView with its own glass capsule background.
The capsule follows the seek position horizontally but independently
clamps to screen edges using alignmentGuide, allowing it to be wider
than the storyboard thumbnail without going offscreen.
Display a floating time pill above the seek bar during dragging
(iOS) and dragging/hovering (macOS) when video has no storyboard
thumbnails. Includes chapter name when available.
- Fix BGTaskScheduler assertion crash on Mac Catalyst by guarding all
iOS background task APIs with isMacCatalystApp check
- Fix iPad popover crash in UIPopoverPresentationController by adding
.presentationCompactAdaptation(.sheet) to all 27 confirmationDialogs
- Fix SwiftData assertion crash when accessing deleted Bookmark model
properties during SwiftUI hit testing in BookmarkRowView
- Fix UICollectionView invalid item count crash on queue swipe-to-delete
by using ID-based removal with withAnimation instead of stale index
- Fix Range crash in storyboard download when storyboardCount is zero
Preserve user:pass credentials in instance URLs so Invidious instances
behind nginx reverse proxies with HTTP basic auth work correctly (#926).
Add displayURL property to mask credentials in the UI.
These settings don't work well on Apple TV, so exclude the
ThemeSection, AccentColorSection, and the .preferredColorScheme/.tint
modifiers from tvOS builds.
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.
Post watchHistoryDidChange notification when a new watch entry is inserted
during local playback progress updates (but not on every progress tick).
Reload Home data when switching back to the Home tab and when the Customize
Home sheet is dismissed.
Remove 32 non-dotted keys (16 unused format specifiers, 16 word keys)
and replace with properly namespaced dotted keys following the existing
convention (common.*, player.*, search.*).
Display the view options button, channel menu, and content type tabs
immediately when the cached header is shown, instead of waiting for
the full channel data to load. The spinner now appears only in the
content area below the tabs.
The background URLSession could be in an invalid state when downloadTask(with:)
is called, because invalidateAndCancel() is asynchronous internally. This adds
an ObjC exception handler to catch NSExceptions from CFNetwork, nil guards on
the session, and safer session lifecycle management (nil after invalidation,
finishTasksAndInvalidate for cellular toggle).
Apple requires BGTaskScheduler.register() to be called during the app
launch sequence before the run loop starts. Moving it from .onAppear
(too late) to init() prevents the crash on TestFlight builds.
Downloads were using direct YouTube CDN URLs even when proxiesVideos
was enabled. Apply the same proxyStreamsIfNeeded used by the player
to the download code path in ContentService.
Malformed items in relatedStreams (e.g., missing title) no longer crash
the entire JSON decode. Reuses the existing PipedVideoItem (renamed from
PipedPlaylistItem) graceful-decoding wrapper for all relatedStreams arrays
in PipedStreamResponse, PipedChannelResponse, and PipedNextPageResponse.
Adds a "Proxy videos" toggle in instance settings that routes video
streams through the instance instead of connecting directly to YouTube
CDN. Includes auto-detection of 403 blocks and live re-application of
proxy settings without requiring app restart or video reload.
Add opaque black background to onboarding on tvOS to prevent Home
screen content from leaking through the fullScreenCover. Replace
toolbar Skip button with plain overlay button to avoid blurred
material style, and add tvOS card button style with default focus
on Continue button.
The release job fails when the build number in the repo already matches
the computed value, causing git commit to exit with code 1 on an empty
changeset. Now we check for staged changes before committing.
The Yattee2.icon/ Icon Composer file is the correct source for app
icons. The previous change to AppIcon was incorrect — AppIcon.appiconset
is an empty legacy placeholder with no actual PNGs.
Without explicit identity, xcodebuild defaults to "iOS Development"
which doesn't exist on CI. Set "Apple Distribution" for App Store
builds and "Developer ID Application" for notarized builds.
Replaced sed-based CODE_SIGN_STYLE override with fastlane's
update_code_signing_settings which also sets PROVISIONING_PROFILE_SPECIFIER.
This fixes the YatteeShareExtension build failure where it couldn't
find a provisioning profile under manual signing.
- Write latest_build_number.txt to repo root using explicit path
(fastlane runs from fastlane/ subdir, so relative path was wrong)
- Remove update_fastlane from before_all to avoid CI instability
Query App Store Connect for the latest TestFlight build number across
all platforms (iOS, tvOS, macOS) and auto-increment it, eliminating
the need for the separate bump-build workflow.
Shows an orange "DEV" capsule next to the iCloud row in Settings and a
development environment notice at the top of iCloud settings, helping
distinguish CloudKit dev environment from production during development.
Pending deletes were lost across app restarts because
recoverPersistedPendingChanges() never reconstructed CKRecord.ID
objects from persisted record names. Additionally, incoming iCloud
records for deleted playlists were blindly applied, and orphaned
playlist items in CloudKit would recreate placeholder playlists.
- Rebuild pendingDeletes array from UserDefaults on recovery
- Guard applyRemoteRecord against records pending local deletion
- Skip deferred items whose parent playlist is pending deletion
- Queue all playlist item deletions when deleting a playlist
- Clean up placeholder playlists for pending-delete playlists
The filter strip was passing the Invidious instance URL as serverURL to
AvatarURLBuilder, which built a Yattee Server-style /avatar/ path that
doesn't exist on Invidious. Now passes the actual Yattee Server URL
(matching SubscriptionsView pattern) and enriches channels from
CachedChannelData as a fallback when the API doesn't return thumbnails.
Single unified "Yattee" scheme replaces per-platform schemes.
Release workflow now has toggleable platform inputs instead of
matrix strategy. Standalone mac notarized workflow removed in
favor of the build_mac_notarized toggle. Share extension bundle
ID updated from Open-in-Yattee to ShareExtension.
When PiP is active, the MPV render view shows a black frame since
rendering goes to the PiP sample buffer layer. Overlay the video
thumbnail (preferring DeArrow) on top to cover the black area,
fading it in/out smoothly when PiP starts/stops.
Use URLComponents/URLQueryItem for standard form-URL encoding instead
of manual percent-encoding with CharacterSet.alphanumerics, which
included non-ASCII Unicode letters and had an unsafe raw-value fallback.
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
When a cancelled load task fell through to `isLoading = false`, it
created a 1-frame gap where the empty view rendered before the
replacement task set `isLoading` back to `true`. Return early on
cancellation so the surviving task controls loading state.
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.
Playlists only loaded the first page of videos. Add full pagination for
both Invidious and Piped playlist endpoints (public and authenticated).
Deduplicate Invidious results by playlist index to handle its overlapping
page windows. Also fix URL encoding in Invidious login to use strict
form-encoding charset.
Move .refreshable from the outer GeometryReader onto the ScrollView
itself so SwiftUI can properly coordinate the scroll offset bounce-back.
The ScrollView was inside an .overlay() which doesn't participate in
the parent's layout system, breaking the offset reset.
Closes#917
Route HTTPS YouTube URLs through yattee://open?url= scheme since simctl
can't trigger Universal Links. Improve wait strategies: use player
expansion check for video tests, tree length threshold for channel/
playlist content loading. Add retry logic to cleanup_after_video.
Test yattee:// custom scheme URLs navigate to correct screens:
playlists, bookmarks, history, downloads, channels, subscriptions,
continue-watching, and search. Handles iOS system confirmation dialog
via coordinate taps since it's invisible to AXe. Settings deep link
is excluded (known app bug - doesn't render when pushed to nav stack).
- Skip onboarding in tests by setting UserDefaults before launch
- Update all addSource.* identifiers to addRemoteServer.* for new flow
- Switch from identifier-based to text-based element lookups (iOS 26 AXe limitation)
- Add Yattee Server credential support in instance setup
- Update baseline screenshots for Home tab and settings
- Create standalone update-altstore.yml workflow (workflow_dispatch + workflow_call)
that gets version from latest GitHub release tag instead of project.pbxproj
- Replace inline update_altstore job in release.yml with workflow_call reference
- Add altstore-source.json with app metadata and initial version entry
- Update README with revised features, TestFlight install link, and new logo assets
xcodebuild's resolvePackageDependencies sometimes fails with
"already exists in file system" when multiple binary xcframework
targets from the same release URL try to extract concurrently.
The failing target varies across runs, confirming a race, not a
missing-file problem. Wipe the three SPM cache roots between
attempts and retry up to three times before giving up; once the
resolve succeeds, fastlane's own resolve step reuses the cache.
Xcode 26.2 (the new default on macos-26) hits a SwiftPM race where
binary xcframework downloads fail with "already exists in file system".
Xcode 26.0.1 — the toolchain the last successful release ran against —
resolved the same deps cleanly. Also download the iOS platform if the
runner image only ships the platform bundled with the default Xcode,
and clear SPM cache more broadly rather than only the artifacts dir.
setup-xcode was switching to /Applications/Xcode_26.0.1.app, which on
the macos-26 runner lacks the iOS 26.0 platform SDK. The runner's
default Xcode has it bundled. Matches rewrite/v2.
The DEVELOPER_KEY_CONTENT secret is stored base64-encoded, so fastlane
needs to be told to decode it before parsing. Matches the approach on
rewrite/v2. Removes the openssl-shell-out workaround from the previous
commits, which was solving the wrong problem.
GitHub secrets store multi-line PEMs as a single line with literal "\n"
sequences. Fastlane's app_store_connect_api_key action un-escapes them
via gsub before use; the helper must do the same before writing the
temp file, otherwise openssl sees garbage.
The hosted macOS runner's OpenSSL rejects Apple's PKCS#8 .p8 key via
OpenSSL::PKey::EC.new with "invalid curve name". Shell out to system
openssl to convert the key to SEC1/traditional PEM before handing it
to fastlane's app_store_connect_api_key action.
Ref: fastlane/fastlane#20593
Commit 37c6f6abb added fi/id/ko/nl/sv to knownRegions but never registered
their .strings files, so Xcode never copied them into the app bundle and the
runtime fell back to English even when the scheme forced one of these
languages. Adds the missing PBXFileReference entries and includes them in
the Localizable.strings PBXVariantGroup.
For context, Invidious performs a type check of its configuration during
bootup, and allows the "domain" to be unspecified, in which case the API
and HTML it generates includes path-only URLs. This is valid: in these
cases we should assume that the Host for those URLs is the same as
the Host we used to access the endpoint called.
This commit adds support for servers configured in such a way, by
defaulting the host of thumbnail URLs, to that used for authentication.
Applied ZStack overlay fix to the channel menu in ChannelVideosView
where the channel name, avatar, subscriber count, and view count
would disappear when tapping the menu.
Uses the same pattern:
- Visible static label with channel info stays on screen
- Invisible Menu overlay with .opacity(0) handles interactions
- Prevents text/avatar disappearing and resizing animations
When switching from MPV to AVPlayer, prioritize HLS and stream formats over non-streamable MP4/AVC1 formats to avoid long loading times.
Changes:
- Added isFastLoadingFormat() helper to AVPlayerBackend
- Modified streamByQualityProfile to prefer fast-loading formats for AVPlayer
- Falls back to non-streamable formats only if no fast-loading option exists
- Ensures quick backend switching without waiting for metadata download
Extended the ZStack overlay fix to all iOS navigation header menus
where text labels would disappear when tapping the menu:
- HomeView: "Home" title menu
- PopularView: "Popular" title with icon menu
- TrendingView: Country/flag title menu
- PlaylistsView: Playlist title with thumbnail menu
- ChannelPlaylistView: Playlist title with thumbnail menu
- OpenVideosView: Playback mode picker menu
All menus now use the same pattern as PlaybackSettings:
- Visible static label layer in ZStack
- Invisible Menu overlay with .opacity(0)
- Prevents text disappearing and resizing animations
When tapping menus in playback settings (playback mode, quality profile,
stream, rate, captions, audio track), the selected value text would
disappear and cause unwanted resizing animations.
Implemented ZStack overlay technique for all iOS menu buttons:
- Visible static label remains on screen
- Invisible Menu overlay (.opacity(0)) handles tap interactions
- Prevents text from disappearing when menu opens
- Eliminates resizing animations on option selection
- Add device checks in Orientation enum to prevent locking on iPad
- Hide "Lock portrait mode" setting on iPad in BrowsingSettings
- Use Constants.isIPhone for consistent device detection
AVPlayer has a fundamental limitation with MP4/AVC1 progressive downloads where the moov atom position affects playback start time. When moov is at the end of the file, AVPlayer must download the entire file before starting playback. MPV doesn't have this limitation.
This commit adds an advanced setting to optionally enable these formats in AVPlayer with appropriate warnings:
- Added new setting: "Enable non-streamable formats (MP4/AVC1)"
- Default: disabled (formats hidden, MPV handles them)
- When enabled: MP4/AVC1 formats up to 1080p appear in AVPlayer quality selector
- Resolution limit: 1080p maximum (higher resolutions can't be played properly)
- Clear warning about slow loading and 1080p limitation
- Automatic stream refresh when setting is toggled
- Full import/export support for the setting
Fixes#870 where YouTube share links incorrectly included the port number
from the user's Invidious instance URL (e.g., http://www.youtube.com:3000
instead of http://www.youtube.com).
Added defensive logic to explicitly clear the port when creating share URLs
with frontend URL strings containing "youtube.com". This ensures YouTube
share links always use the standard format without port numbers, regardless
of the user's instance configuration.
The tap-blocking overlay is only needed on iOS to dismiss the context menu
on tap. Removed it from macOS and tvOS where it was either blocking normal
interactions or not functional due to platform limitations.
Removed fixed height constraint from empty state in FavoriteItemView.
Empty sections now collapse to natural height instead of reserving
full content height (290px iOS/600px tvOS), improving space efficiency.
Improved text contrast on overlay buttons by:
- Applying foreground color directly to button labels to ensure proper override
- Using semi-transparent gray background for unfocused buttons instead of Color.secondary
- Removing accent color overrides from caption text to respect button styling
This ensures readable text in both focused (black on white) and unfocused
(white on gray) states.
Changed context menus from press-and-hold to single-press for better UX:
- Quality profile selection
- Stream quality selection
- Captions selection
- Audio track selection
Updated ControlsOverlayButton to support tap actions via new onSelect parameter.
Replaced .contextMenu modifiers with .alert for instant menu access on tvOS.
Removed hint text and unnecessary 80px padding as single-press is self-evident.
The captions context menu on tvOS was always empty because it relied on
an .onAppear callback that was never triggered. The captionsPicker view
with .onAppear was only rendered on iOS/macOS, not tvOS.
Changes:
- Access captions directly from player.currentVideo?.captions instead of
using a state variable populated via .onAppear
- Fix label logic to show "Disabled" when captions are available but not
selected, and "Not available" only when video has no captions
- Remove unused state variables and helper functions
Prefix caption URLs with /companion when invidiousCompanion is enabled in instance settings. This ensures captions are routed through the companion service, matching the behavior of video streams.
In fullscreen playback, swipe-down and timeline seek gestures now respect a 60pt safe zone at the top of the screen, allowing the system notification center gesture to work without triggering app gestures.
Comments at the bottom of the comments view were not accessible on iOS
without entering fullscreen mode. The issue was caused by the VideoDetails
view being offset by the player height when not in fullscreen, but the
ScrollView padding didn't account for this offset.
Changes:
- Add SafeAreaModel observer for iOS platform
- Update bottom padding to dynamically account for player height offset
and safe area insets based on fullscreen state
- When not in fullscreen: padding = player height + safe area bottom + 20
- When in fullscreen: padding = max(60, safe area bottom + 20)
This ensures all comments and video details content are fully scrollable
and visible regardless of fullscreen state.
This adds a new instance setting for Invidious that filters out videos
without duration information from feeds, popular, trending, search results,
and channel pages. This can be used to hide YouTube Shorts.
The setting is labeled as "Experimental: Hide videos without duration" and
includes an explanation that it can be used to hide shorts.
Key changes:
- Added hideVideosWithoutDuration property to Instance model
- Updated InstancesBridge to serialize/deserialize the new setting
- Added UI toggle in InstanceSettings with explanatory footer text
- Implemented filtering in InvidiousAPI for:
* Popular videos
* Trending videos
* Search results
* Subscription feed
* Channel content
- Videos accessed directly by URL are not filtered
Removed auto-focus logic that was causing keyboard show/hide loop
on iPad with docked keyboard. The keyboard was repeatedly dismissing
immediately after appearing due to interaction between keyboard
notifications, focus state changes, and view updates.
Changes:
- Removed focused state and keyboard observer from SearchModel
- Removed iOS textField reference (kept macOS only)
- Removed auto-focus logic from FocusableSearchTextField on iOS
- Cleaned up unused focus-related code
The search field now works reliably when tapped manually on iPad.
Auto-focus still works on macOS where it doesn't cause issues.
Add startup cleanup to remove trending-related settings when the feature flag is disabled:
- Remove trending from visible sections
- Reset startup section to home if it was set to trending
- Remove all trending favorite items
This ensures users don't have invalid/broken settings referencing the disabled trending feature.
Change the Trending menu command to be completely hidden when the feature flag is disabled, rather than just being disabled and still visible in the UI.
Introduces a feature flag to disable the Trending section across the app. When disabled, all trending-related UI elements, navigation links, and settings are hidden.
Changes:
- Add trendingEnabled feature flag to FeatureFlags.swift (currently disabled)
- Hide Trending tab in AppTabNavigation, Sidebar, and TVNavigationView
- Remove Trending option from visible sections settings
- Remove Trending option from startup section picker
- Disable Trending menu command and keyboard shortcut
- Prevent Trending URL navigation in OpenURLHandler
- Hide Trending in FavoriteItemView navigation
Replace arrow.up.right.circle.fill icon with chart.bar.fill for the Popular section across all navigation contexts (tab bar, sidebar, and view header).
The hide shorts feature is no longer working due to API changes that prevent reliable detection of short videos. This commit introduces a feature flag system to disable the functionality while preserving the ability to easily restore it if the API issue is resolved.
Changes:
- Add FeatureFlags.swift with hideShortsEnabled flag (currently disabled)
- Hide all HideShortsButtons UI elements when flag is disabled
- Disable shorts filtering logic in ContentItemView, FavoriteItemView, and FeedModel
- Preserve hideShorts user preference for future restoration
Tab selection was being set immediately during app configuration, before
the user account had completed sign-in. This caused tabs that require
authentication (like Subscriptions and Playlists) to not be properly
selected on startup since they weren't visible yet.
Changes:
- Add notification system for account configuration completion
- Post notification after all account types finish configuration:
* Accounts with existing tokens
* Accounts requiring sign-in (after network request completes)
* Anonymous/public accounts
* Error cases (missing credentials, network failures)
- Set up observer before account configuration to ensure notification
is received
- Set tab selection only when account is fully configured
Fixed issues with thumbnails being stretched vertically and layout jumping during image loading:
- Simplified VideoCellThumbnail to always use 16:9 aspect ratio with .fill mode
- Added matching 16:9 aspect ratio to placeholder with .fill mode to prevent layout shifts
- Removed quality-based aspect ratio selection (4:3 vs 16:9) in favor of consistent 16:9
- Ensures thumbnails maintain proper proportions on both iOS and macOS
This provides consistent sizing across platforms and eliminates the jump when images finish loading.
Thumbnails were being stretched vertically due to incorrect aspect ratio handling. Fixed by:
- Using .scaledToFill() on thumbnails to fill the container width
- Constraining container to 16:9 aspect ratio with .fit mode
- Adding matching aspect ratio to placeholder to prevent layout shift during loading
This ensures thumbnails maintain proper proportions while filling the full cell width.
Previously, the audio session was initialized immediately when the app launched, causing audio from other apps (like Music) to stop even when no video was playing in Yattee.
Changes:
- Remove audio session initialization from AppDelegate launch
- Remove audio session setup from MPVClient initialization
- Update setAudioSessionActive() to configure audio session category before activation
The audio session is now lazily initialized only when playback actually starts:
- For MPV backend: triggered by FILE_LOADED, PLAYBACK_RESTART, AUDIO_RECONFIG events
- For AVPlayer backend: triggered when play() is called
This allows music from other apps to continue playing until a video is actually played in Yattee.
Reduced video cell and grid item sizes on tvOS to improve layout spacing
and visual consistency. Changed grid item size from 600 to 560 pixels,
and adjusted video cell frame dimensions accordingly.
This commit addresses crashes caused by accessing nil format values on streams:
- QualityProfile.swift: Add guard check for stream.format to prevent nil access crash
- MPVBackend.swift: Add nil check in canPlay method before comparing format
- PlayerStreams.swift: Add nil check before comparing format in asset processing
The crashes occurred when stream.format was nil and accessed as an implicitly unwrapped optional, causing "Unexpectedly found nil while implicitly unwrapping an Optional value" errors.
This commit addresses crashes caused by race conditions when accessing audio track arrays:
- MPVBackend.swift: Use safe index clamping to prevent array out of bounds crashes when selecting audio tracks
- PlayerModel.swift: Add selectedAudioTrack computed property for thread-safe audio track access
- ControlsOverlay.swift: Use safe accessor with "Original" fallback label
- PlaybackSettings.swift: Use safe accessor with "Original" fallback label
This fix resolves approximately 37% of crashes (23 out of 62 crash logs) that were caused by index out of range errors in MPVBackend.playStream at line 345.
Fixed improperly sized and positioned thumbnails by removing duplicate aspect ratio constraints and standardizing to 4:3 format with fill content mode for better display.
Switch from VStack to ZStack layout for better control over detail view positioning in fullscreen mode. Add z-index layering to ensure proper stacking order of player backend and video details.
Changed the default audio track content type from "Unknown" to "Original"
when the content type is not specified. This provides a more accurate
description for the default audio track.
Remove unnecessary edgesIgnoringSafeArea modifier and simplify status bar hiding logic by removing iPad-specific conditional checks, making the fullscreen behavior more consistent across iOS devices.
When switching from AVPlayer to MPV backend, Now Playing controls (play/pause/seek) were disabled because AVPlayer maintained control of the remote command center and audio session. This fix ensures MPV can properly reclaim control.
Key changes:
- Clear AVPlayer's current item when switching to MPV to release media control
- Clear Now Playing info and set playback state to stopped before MPV takes over
- Reset remote command center by removing all targets (including AVPlayer's internal handlers) and re-adding custom handlers
- Force audio session deactivation/reactivation with .notifyOthersOnDeactivation
- Add forceReactivate parameter to setupAudioSessionForNowPlaying() for backend switches
- Ensure stream loading continues after Now Playing setup (don't return early)
The fix properly handles the transition by:
1. Clearing AVPlayer's media session completely
2. Scheduling async Now Playing setup without blocking stream loading
3. Resetting remote command handlers to reclaim control from AVPlayer
4. Re-activating audio session to establish MPV as the active player
The MPV backend now properly displays Now Playing information in iOS Control Center. The fix addresses the issue where the AVAudioSession would become inactive during MPV's playback lifecycle.
Key changes:
- Added setupAudioSessionForNowPlaying() method to activate AVAudioSession with proper playback category and movie playback mode
- Re-activate audio session at critical MPV events: FILE_LOADED, PLAYBACK_RESTART, AUDIO_RECONFIG, and during periodic updates
- Initialize audio session immediately after mpv_initialize() in MPVClient
The audio session must be re-activated at multiple points during playback, not just at initialization, to ensure iOS recognizes the app as playing media.
Expands cache clearing to include DerivedData and .build directories
to prevent corrupted artifact issues during dependency resolution.
Renames workflow file and updates job title to include macOS and
Xcode versions for clarity.
Creates a standalone workflow to build and notarize macOS-only builds
without creating a GitHub release. Uses macOS 15 and Xcode 16.4.
The notarized build is available as a workflow artifact.
Add availability check for tvOS 17.0+ when using Menu with primaryAction parameter. Falls back to simple Button for tvOS 15.0-16.x to maintain backward compatibility with the deployment target.
Add visible labels for all pickers in settings on macOS by wrapping them
in HStack with Text labels and Spacer() for proper alignment.
Fixed pickers:
- BrowsingSettings: Startup section, Thumbnails quality, Player bar gestures
- PlayerControlsSettings: Action button labels
- PlayerSettings: Source, Inspector, Caption size/color/languages, Sidebar
All picker labels now consistently display with left-aligned text and
right-aligned picker controls on macOS.
- Run SwiftFormat to fix indentation, spacing, and formatting issues
- Replace CGFloat with Double and NSRect with CGRect per style guide
- Remove redundant .center alignment specifications
- Remove unnecessary @available checks for satisfied deployment targets
- Fix closure brace indentation for consistency
- Disable closure_end_indentation rule to resolve SwiftFormat conflict
All linting checks now pass with zero errors and warnings.
Wrap favoritesChanged.toggle() calls in MainActor.run blocks to ensure
main actor-isolated state mutations happen on the correct actor context.
This resolves Swift concurrency warnings when updating state from
nonisolated async contexts.
Implemented automatic retry logic with exponential backoff (2, 4, 6 seconds) when file loading fails in AVPlayerBackend. Retries up to 3 times before showing error to user. Retry state is properly reset on successful loads. This matches the retry implementation added to MPVBackend in commit b6df73f9.
- Update macOS build job to use macos-latest and Xcode 26.0.1 (matching iOS/tvOS)
- Update bump-build workflow to use latest action versions (checkout@v4, create-pull-request@v7)
- Add cache-version to ruby/setup-ruby for consistency
This commit enables proper Now Playing Info Center integration on iOS, allowing video playback information to appear in Control Center and Lock Screen with working remote controls.
Key changes:
- Activate audio session on app launch with setCategory(.playback, mode: .moviePlayback) and setActive(true)
- Set up remote commands on first play() call instead of during app initialization to avoid claiming Now Playing slot prematurely
- Remove removeTarget(nil) calls that were claiming Now Playing without content
- Enable remote commands (play, pause, toggle, seek) explicitly and add proper target handlers
- Use backend.isPlaying instead of PlayerModel.isPlaying to avoid race conditions
- Include playback rate (1.0 for playing, 0.0 for paused) in Now Playing info
- Update Now Playing info on main queue for thread safety
- Update Now Playing when switching between backends
- Remove audio session deactivation from pause() and stop() methods
Note: This fix works for AVPlayer backend. MPV backend has fundamental incompatibility with iOS Now Playing system.
Modified HorizontalCells to conditionally apply edgesIgnoringSafeArea based on navigation style. In sidebar mode (iPad), content now respects safe areas and won't overlap with the sidebar. In tab mode (iPhone), content maintains full-width scrolling behavior.
When displaying the video details overlay on iPad in non-fullscreen windows,
add 65px of left padding (50px for system controls width + 15px spacing) to
prevent content from overlapping with iPad system controls.
When the iPad window is resized (not fullscreen), player controls now have 10px horizontal padding from the edges for better spacing and visual comfort.
Remove incorrect safe area insets from offsetY calculation that was
causing unequal black bars (smaller top, larger bottom). Now properly
centers video with equal padding like AVPlayer backend.
Adds fullscreen detection utility to Constants.swift to determine if the window occupies the full screen on iOS. Uses this to conditionally add leading padding to player controls on iPad in non-fullscreen windows, preventing overlap with system window controls.
Changed UIRequiresFullScreen to NO to allow pixel-perfect window
resizing on iPad. Also moved ITSAppUsesNonExemptEncryption to
project settings for cleaner configuration.
When playing video fullscreen in a resizable window on iPad, the player
height was being forced to UIScreen.main.bounds.size.height, which is
the full screen size. In resizable windows, this caused the player
container to extend beyond the visible window bounds, clipping controls
at the bottom.
Now on iPad, the player uses natural geometry provided by its container
which respects actual window bounds, while iPhone continues using
screen-based calculation for proper fullscreen behavior.
Added plain button style for rate increase/decrease buttons on iOS. Fixed safe area insets in VerticalCells to respect sidebar navigation style on iOS.
Refactored glUpdate to use requestRedraw method for better control. Added needsRedraw flag to prevent redundant display calls. Enabled asynchronous drawing on VideoLayer for improved performance. Modified displayLinkCallback to only report swap without triggering display to avoid flooding the main thread.
Added guard check to return early if currentTime is nil in getTimeUpdates. Simplified optional unwrapping by using the guarded currentTime value throughout the method.
Added height reservation to FavoriteItemView to prevent layout shifts during content loading. Changed HomeView to use LazyVStack for better performance. Converted QueueView from LazyVStack to VStack. Disabled animations on content count changes across multiple views to prevent jarring layout transitions. Added width constraint to stream button in PlaybackSettings.
Removed border overlay on search text field for macOS 26+ to match new design guidelines. Added conditional padding to sort label for better alignment on macOS 26+.
Standardized picker and button sizing with consistent alignment and control sizes. Added SettingsPickerModifier to all macOS pickers with menu style. Improved rate buttons with proper sizing and alignment. Added text truncation for stream descriptions to prevent overflow.
Restructured PlayerControls view hierarchy by extracting controls content into a separate computed property for better code organization. Added shouldShowCustomControls property to VideoPlayerView to properly determine when custom controls should be shown vs system controls. Updated hover logic to only show/hide custom controls when appropriate.
Added comprehensive nil checks for stream resolution values across PlayerBackend, QualityProfile, and PlayerQueue to prevent crashes when streams have missing resolution metadata. Also added backend nil checks in PlayerQueue.
Implemented automatic retry logic with exponential backoff (2, 4, 6 seconds) when file loading fails in MPVBackend. Retries up to 3 times before showing error to user. Retry state is properly reset on successful loads.
- Make glassEffect iOS-only as it's not available on other platforms
- Use ultraThinMaterial fallback for macOS and tvOS
- Fixes build error in GitHub Actions with Xcode 16.4
Added checks to skip the framework conversion script when running in
CI environments (GitHub Actions). The script now exits early if either
CI or GITHUB_ACTIONS environment variables are set.
This ensures:
- Script only runs for local development builds
- GitHub Actions builds use frameworks as-is from MPVKit
- iOS/tvOS builds on macOS 15 with Xcode 16.4 work without conversion
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit implements a workaround for MPVKit shipping frameworks as
shallow bundles, which are incompatible with macOS Developer ID
distribution requirements.
Changes:
1. Raised macOS deployment target to 14.0
- Matches MPVKit's minimum requirement
- Previous: 11.0
- New: 14.0
2. Added Run Script phase to convert frameworks
- Converts MPVKit frameworks from shallow to versioned bundles
- Required for macOS Developer ID code signing
- Runs after framework embedding
- Converts all 28 MPVKit frameworks during build
3. Modified fastlane build process
- Build and archive without export
- Create PKG directly from archive
- Avoids extended attribute issues from export process
4. Pinned MPVKit to specific commit
- Commit: e7e914a70e943f0d4f050c9ede793af8f6e74ad7
- Ensures consistent framework structure
Known Issues:
- Some frameworks (Libplacebo, Libluajit) have signature issues after
conversion that still prevent successful notarization
- This is a workaround; the root issue should be fixed in MPVKit by
providing macOS-compatible versioned bundle frameworks
See minimal reproduction project at:
/tmp/MPVKit-Notarization-Issue/MPVKitNotarizationTest/
Related: MPVKit should provide macOS-specific XCFrameworks with
versioned bundles for proper Developer ID distribution support.
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add print_log: true to notarize action to display detailed error
messages when notarization fails in GitHub Actions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit resolves multiple build errors caused by using APIs that
require newer OS versions than the deployment targets (macOS 11.0 and
tvOS 15.0).
macOS fixes:
- Add missing init(frame:) initializer to PlayerLayerView
- Add availability checks for textSelection modifier (macOS 12.0+)
- Add availability checks for AttributedString (macOS 12.0+)
- Add availability checks for listStyle.inset(alternatesRowBackgrounds:) (macOS 12.0+)
- Add availability checks for focusScope modifier (macOS 12.0+)
- Correct listRowSeparator availability from macOS 12.0 to 13.0
tvOS fixes:
- Use older onChange(of:) signature compatible with tvOS 15.0
- Add availability check for Menu with primaryAction (tvOS 17.0+)
All changes include appropriate fallbacks for older OS versions to
maintain backward compatibility.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Bundler 2.6.3 requires Ruby >= 3.1.0, but workflows were using Ruby 3.0.7,
causing build failures. Updated both release and bump-build workflows to use
Ruby 3.1.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Adds five new languages to the Xcode project knownRegions that have at least 50% translation coverage from Weblate:
- Finnish (fi): 100%
- Indonesian (id): 100%
- Korean (ko): 100%
- Dutch (nl): 81.9%
- Swedish (sv): 77.4%
Languages with less than 50% coverage (ars, kab, sk) were not added.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Replace sheet presentation with fullScreenCover for Settings and Accounts views on tvOS
- Add proper background color to Settings and Accounts screens on tvOS
- Clean up trailing whitespace in HomeView
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add TVOSPlainToggleStyle for cleaner toggle appearance on tvOS
- Remove focus overlays from settings navigation links and buttons
- Apply plain button and list styles across all settings screens
- Implement custom system controls picker for tvOS to avoid focus overlay
- Update SettingsPickerModifier with platform-specific styling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Refactor orientation logic when entering fullscreen to better handle
button-initiated vs gesture-initiated transitions:
- Consolidate orientation determination into a single expression that
considers whether fullscreen was initiated by button or gesture
- When initiated by button, always use rotateToLandscapeOnEnterFullScreen
preference
- When initiated by gesture, respect current device orientation if already
in landscape, otherwise use preference
- Apply .landscape lock only for button-initiated transitions, .all for
gesture-initiated (when not orientation locked)
This provides more intuitive behavior where button taps rotate to preferred
orientation, while gestures respect current device orientation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Improve spacing in PlaybackSettings view by adding top padding to the
playback speed section for better visual separation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses multiple SwiftUI performance bottlenecks identified
through code analysis, focusing on view rendering efficiency, list
performance, and memory usage optimization.
Key improvements:
- HomeView: Optimize async task management using structured concurrency
with async let to handle multiple Defaults updates in a single task
- VideoCell: Remove GeometryReader from VideoCellThumbnail to eliminate
layout thrashing; change @ObservedObject to computed property for shared
ThumbnailsModel
- ThumbnailView: Cache URL extension computation in init() instead of
recalculating on every body evaluation
- FavoriteItemView: Replace filter().prefix() with early-exit loop and
capacity reservation for significant performance gain on large lists
- ContentItemView: Optimize FetchRequest creation with direct predicate
construction only for video items, empty predicate for others
- VideoPlayerView: Fix playerSize didSet trigger by moving
updateSidebarQueue() calls to explicit onChange/onAppear handlers
- FeedView: Replace .unique() with Set-based deduplication for O(n)
performance and reduced allocations
- VerticalCells: Remove expensive sorting on every redraw; items should
be pre-sorted from source
These optimizations follow SwiftUI best practices by minimizing expensive
computations in view bodies, caching computed values, using efficient data
structures, and avoiding unnecessary redraws and layout passes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Refactor controls bar background styling with platform-specific effects:
- Add Liquid Glass effect for iOS 26+ using new glassEffect API
- Fallback to ultraThinMaterial for iOS 15-25
- Fallback to blurred black overlay for iOS 14 and earlier
- Extract background logic into reusable applyControlsBackground modifier
- Adjust controls bar vertical offset for better alignment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Apply compact row styling to channel list items on iOS to reduce vertical spacing and improve visual density. Uses listRowSpacing(0) on iOS 15+ and custom insets for consistent padding across all iOS versions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
SearchTextField improvements:
- Add flexible width constraints (minWidth: 250, maxWidth: .infinity) for macOS
- Restructure iOS layout to use HStack instead of ZStack for better alignment
- Add invisible spacer to maintain consistent width when clear button is hidden
- Adjust padding for more balanced appearance
- Remove fixed width from fieldBorder to support flexible sizing
SearchView improvements:
- Wrap in GeometryReader to calculate available width dynamically
- Add searchFieldWidth() helper to compute optimal search field width
- Account for navigation buttons and internal padding
- Apply dynamic width to both FocusableSearchTextField and SearchTextField
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add logging for video and audio asset URLs during stream filtering to help debug stream selection issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add proper validation and fallback logic for audio track selection in MPV backend:
- Validate audio track index is within bounds before switching
- Handle streams without separate audio tracks (single asset streams)
- Reset selectedAudioTrackIndex if out of bounds
- Add fallback path for streams without audioTracks array
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract and provide all available audio tracks (ORIGINAL, DUBBED, etc.) from Piped API instead of only using the first ORIGINAL track. This allows users to select between different audio languages and track types.
Changes:
- Extract all M4A audio tracks grouped by type and language
- Keep highest bitrate stream for each unique track combination
- Sort tracks with ORIGINAL first, then others alphabetically
- Pass audio tracks array to Stream for player selection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Clear the published string when a proper publishedAt date is extracted from uploadDate to prevent duplicate or inconsistent date display. Only fallback to string-based published date when no structured date is available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update companion API endpoint URLs to use /companion/latest_version instead of /latest_version to match the correct API path structure.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- fix subtitle disabling not working
- make subtitle adding/removing async
- make subtitle menu non blocking
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
- have a separate body view for each os
- macOS: set navigation title for search
- macOS: set min width to 835 for main content
- macOS: set main window min height to 600
- macOS: don’t have text behind the cancel button
- split SearchTextField into macOS and iOS/tvOS
- iOS: move search menu to the right
- iOS: unified search field
- make min width a constant
- add option to disable search suggestions
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
Currently, we have a hard-coded list of resolutions. Since Invidious reports the actual resolution of a stream and does not hard-code them to a fixed value anymore, resolutions that are not in the list won’t be handled, and the stream cannot be played back.
Instead of hard-coding even more resolutions (and inadvertently might not cover all), we revert the list back to a finite set of resolutions, the users can select from. All other resolutions are handled dynamically and compared to the existing set of defined resolutions when selecting the best stream for playback.
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
- use displays refresh rate
- execute needs drawing with higher priority
- run create() with higher priority
- determine the number of threads used for rendering
- enable VSYNC and change video-sync to display-resample
- iOS/tvOS: set new display refresh rate on change
- run setSize with higher priority
- add more options to MPVClient
- get refresh rate updates
- sync refresh rate to fps
- update CADisplayLink to current refresh rate
- update refresh rate on macOS
- Add experimental feature to sync display with content fps
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
- iPad: rotate to device orientation on startup
- fixed controls in portrait fullscreen
- iOS: don’t call setNeedsDrawing multiple times
- On iOS we call set needs drawing only once.
- Added cooldown time to MPV.Client setNeedsDrawing to avoid multiple successive calls
- make fullscreen animation smoother
- dragGesture now calls toggleFullScreenAction
- fix tvOS and macOS build
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
On macOS optimisation level -O3 seems to be a bit aggressive and can cause crashes when opening MPV.
- fixes#783
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
Currently when returning from background, the app is fullscreen but the video is in portrait mode.
Now when entering background we leave fullscreen and enter it again the the app is in foreground.
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
- MP4 contains av1 encoded video which is not hardware accelerated.
- MP4 format removed from AVPlayer
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
- Don’t use System Controls with AVPlayer by default
- Changed default order of the formats
- no default AVPlayer profiles except for tvOS
- new constants isIOS, isTvOS, isMacOS
- ArtWork for nowPlaying is .medium on iPhone .maxres on all other platforms
- changes to the player bar defaults
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
This adds user, password and port to the proxy and thumbnail url, if they exist.
fixes#614 & #731
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
This reverts commit 59da0e71b6.
Revert "Merge pull request #758 from stonerl/new-app-icons-second-try"
This reverts commit 7b26fdf400, reversing
changes made to 67b41e36d5.
Revert "Merge pull request #756 from stonerl/new-app-icons-second-try"
This reverts commit b51eadc7a9, reversing
changes made to 0c1fb02d50.
- added support for adaptive/tinted Icons on iOS.
- closes#700
- new macOS icon set, courtesy of @Kongolabongo (Discourse)
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
with #745 I left som testing changes in the PR that resulted in currentChapter index not being updated. This is fixed now.
Also, the ScrollViewReader waiter 0.5s before jumping to the current Chapter. So it is always drawn correctly.
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
Invidious now reports the actual resolution and doesn’t hardcode them anymore.
See: https://github.com/iv-org/invidious/pull/4586
- Extended the list of possible resolutions in the StreamModel
- trigger videoLoadFailureHandler if no streams are available
- more logging for backend.bestPlayable
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
This should circumvent edge cases where videos are marked as watch when they failed to play back.
Fixes#660
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
The upper 5% of the screen ignore swipe down gestures when in fullscreen, to avoid leaving fullscreen when opening the notification center.
fixes#702
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
- ThumbnailsModel optionally returns the quality
- Have constants for 4:3 and 16:9 aspect ratio
- Add high quality options for thumbnails
- Rename Highest quality to Best quality
- make 4:3 thumbnails fill the VideoCell
- use .maxes instead of .maxresdefault (the latter sometimes returns white images)
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
fixes#691
add `--initial-audio-sync=<yes|no>` to MPV settings. Might fix#690 but needs to be toggled by the user. We leave the standard settings.
Also added links to the icons on macOS.
- The reloading of items in the favorite widgets is now done async, so it does not block the UI when opening Home.
- Tasks that check for changes of the widget and favorites are now terminated when another view e.g. Subscriptions is opened.
- We only update the Favourites when the player is not in the foreground, to avoid unresponsiveness.
We don't test every single stream anymore. If we get an 200, 206 or 403 we immediately stop testing streams. Unknown streams are also skipped. This speeds up starting playback, since we don'T have to wait for the network anymore.
Three configurations exist, mapped to distribution channels:
| Configuration | Sparkle (`#if SPARKLE`) | Used for |
|---|---|---|
| `Debug` | off | local development, tests |
| `Release` | off | App Store / TestFlight (`fastlane mac beta`) — must stay Sparkle-free, App Review rejects auto-update frameworks |
| `Release-DeveloperID` | **on** | Developer ID notarized build (`fastlane mac build_and_notarize`), distributed via GitHub Releases + Homebrew cask, receives Sparkle updates |
All Sparkle-dependent code must be wrapped in `#if SPARKLE ... #endif` so the `Release` variant links zero Sparkle symbols. When adding new Sparkle features, test both configs build clean on macOS.
## Code Style
**Language:** Swift 5.0+ with strict concurrency (Swift 6 mode enabled)
**UI:** SwiftUI with `@Observable` macro for view models (not `ObservableObject`)
**Concurrency:** Use `actor` for services, `@MainActor` for UI-related code, `async/await` everywhere
**Testing:** Swift Testing framework (`@Test`, `@Suite`, `#expect`) - NOT XCTest
## Imports & Organization
**Import order:** Foundation first, then SwiftUI, then @testable imports
**File headers:** Include `// FileName.swift`, `// Yattee`, and brief comment describing purpose
**MARK comments:** Use `// MARK: - Section Name` to organize code sections
**Sendable:** All models, errors, and actors must conform to `Sendable`
## Types & Naming
**Models:** Immutable structs with `Codable, Hashable, Sendable` conformance
**Services:** Use `actor` for thread-safe services, `final class` for `@Observable` view models
**Enums:** Use associated values for typed errors (see `APIError.swift`)
**Optionals:** Prefer guard-let unwrapping; use `if let` for simple cases
**Naming:** camelCase for variables/functions, PascalCase for types, clear descriptive names
**Run single spec:**`SKIP_BUILD=1 KEEP_SIMULATOR=1 bundle exec rspec spec/ui/smoke/search_spec.rb`
**Accessibility labels vs identifiers:** On iOS 26+, `.accessibilityIdentifier()` doesn't work reliably on `Group`, `ScrollView`, and some container views (AXUniqueId comes back empty). Use `.accessibilityLabel()` instead, which maps to `AXLabel` and can be detected via AXe's `text_visible?()` method.
**iOS 26 TabView search:** The search field is integrated into the bottom tab bar with `Tab(role: .search)`. Typing `\n` doesn't submit - use hardware key press via `press_return` (AXe key 40).
**ScrollView children:** Video rows inside `LazyVStack`/`ScrollView` aren't exposed in the accessibility tree. Use coordinate-based tapping instead.
* Fix thumbnails failing to load on tvOS by @patelhiren in https://github.com/yattee/yattee/pull/688
* speed up sorting for Stream by @stonerl in https://github.com/yattee/yattee/pull/681
* faster chapter extraction by @stonerl in https://github.com/yattee/yattee/pull/682
* Invidious: add images to chapters by @stonerl in https://github.com/yattee/yattee/pull/685
* Improved Captions handling by @stonerl in https://github.com/yattee/yattee/pull/684
* Add User-Agent to request by @stonerl in https://github.com/yattee/yattee/pull/680
* MPV: speed up playback start by @stonerl in https://github.com/yattee/yattee/pull/689
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/683
## What's Changed
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
* Added Settings Import/Export
* Export all settings, instances and accounts
* Import selected elements from the file
* Include unencrypted passwords in the export or provide them during the import
* Import via URL for tvOS
* Added Controls setting "Action button labels" icon or icon and text
* Added Advanced setting for MPV: "deinterlace"
* Add help text to all header buttons (by @rickykresslein)
* History Setting: hide the recent activity in the sidebar or limit the number of items shown (by @rickykresslein)
* Fix issues with empty comments (by @stonerl)
* Improved Invidious comments (by @stonerl)
* Don't show related in sidebar when disabled in settings by @stonerl in https://github.com/yattee/yattee/pull/635
* Handle audio session interrupts by other media by @stonerl in https://github.com/yattee/yattee/pull/640
* Only show Queue header in sidebar view by @stonerl in https://github.com/yattee/yattee/pull/642
* SponsorBlock Improvements by @stonerl in https://github.com/yattee/yattee/pull/639
* Chapter title on jump by @stonerl in https://github.com/yattee/yattee/pull/655
* Restart finished video by @stonerl in https://github.com/yattee/yattee/pull/646
* SponsorBlock jump to end instead of pausing by @stonerl in https://github.com/yattee/yattee/pull/648
* Call correct class of SDImageAWebPCoder by @stonerl in https://github.com/yattee/yattee/pull/664
* Fix handling and displaying captions by @stonerl in https://github.com/yattee/yattee/pull/636
* Advanced settings: make number fields .numPad by @stonerl in https://github.com/yattee/yattee/pull/661
* Preserve time on stream change by @stonerl in https://github.com/yattee/yattee/pull/651
* Switch to previous backend when leaving PiP by @stonerl in https://github.com/yattee/yattee/pull/641
* Handle deep links by @timonus in https://github.com/yattee/yattee/pull/645
* Music Mode: don't bindPlayerToLayer when entering foreground by @stonerl in https://github.com/yattee/yattee/pull/644
* Allow user to disable thumbnails and jump to current chapter in horizontal view by @stonerl in https://github.com/yattee/yattee/pull/665
* Rework qualitiy settings by @stonerl in https://github.com/yattee/yattee/pull/650
* HLS: set target bitrate / AVPlayer: higher resolution by @stonerl in https://github.com/yattee/yattee/pull/667
* Fix #619: Remove ports from shared YouTube links by @0x000C in https://github.com/yattee/yattee/pull/627
* XCode enable IDEPreferLogStreaming by @stonerl in https://github.com/yattee/yattee/pull/638
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
* HomeView: Changes to Favourites and History Widget by @stonerl in https://github.com/yattee/yattee/pull/672
* Snappy UI - Offloading non UI task to background threads by @stonerl in https://github.com/yattee/yattee/pull/671
* Fix PiP Mode Not Working Using MPV by @stonerl in https://github.com/yattee/yattee/pull/676
* Advanced Settings: cache-pause-initial by @stonerl in https://github.com/yattee/yattee/pull/679
* Changed description for Format reordering by @stonerl in https://github.com/yattee/yattee/pull/677
* Add Chinese (Traditional) localization (by @rexcsk)
* Localization fixes
* Updated localizations
* Upgraded dependencies
* Fixed reported crash
* Other minor changes and improvements
**macOS support** — Yattee 2 arrives on the Mac with its first public beta. It brings the full Yattee 2 experience to the desktop with a native interface: a dedicated player window with fullscreen and stay on top option, Picture in Picture, customizable and movable player controls, keyboard shortcuts, and other features present in the iOS app.
**Big thanks to the current, past and future project contributors!**
The macOS app is available via [TestFlight](https://yattee.stream/beta2), or as a notarized direct download from [GitHub Releases](https://github.com/yattee/yattee/releases) that keeps itself up to date with built-in automatic updates.
### General
#### New Features
* Add audio-only music mode, available in video player settings and player controls button
* Add custom accent color setting with system color picker and separate light and dark mode colors
* Redesign Home shortcut cards with layout, color, and palette options and a live style preview
* Add Edit Shortcuts and Hide options to Home shortcut context menus
* Add pause, resume, and cancel context menu to download rows
* Add manual legacy account import and allow importing account-less legacy instances as sources
#### Improvements
* Rework iCloud sync engine for more reliable syncing; resume sync from saved state instead of re-fetching everything
* Sync watch progress when a video plays to the end and when the app goes to the background
* Fall back to lower-quality thumbnails when higher-resolution variants are unavailable
* Apply the selected theme at the window level so it takes effect everywhere
#### Bug Fixes
* Fix disabling Background Playback having no effect
* Fix downloads never finishing and a crash on download completion
* Fix missing storyboards when advancing to the next queued video
* Fix live videos shown in the mini player bar
* Fix crash when opening the share sheet on iPad
* Fix rare data loss when the app is suspended in the background
* Fix iCloud sync conflicts for recent channels, playlists, and watch progress
* Fix Sources home shortcut counting disabled instances
### iOS
* Use toolbar search placement on iPad channel view
- **Invidious**: `https://invidious.home.arekf.net/` - Use this instance for testing API calls
- **Yattee Server**: `https://main.s.yattee.stream` - Local self-hosted Yattee server for backend testing
## Related Projects
### Yattee Server
Location: `~/Developer/yattee-server`
A self-hosted API server powered by yt-dlp that provides an Invidious-compatible API for YouTube content. Used as an alternative backend when Invidious/Piped instances are blocked or unavailable.
**Key features:**
- Invidious-compatible API endpoints (`/api/v1/videos`, `/api/v1/channels`, `/api/v1/search`, etc.)
- Uses yt-dlp with deno for YouTube JS challenge solving
- Returns direct YouTube CDN stream URLs
- Optional backing Invidious instance for trending, popular, and search suggestions
**API endpoints:**
-`GET /api/v1/videos/{video_id}` - Video metadata and streams
-`GET /api/v1/channels/{channel_id}` - Channel info
- scheme name to build is Yattee. use generic platform build instead of specific sim/device id
## UI Testing with AXe
The project uses a Ruby/RSpec-based UI testing framework with [AXe](https://github.com/cameroncooke/AXe) for simulator automation and visual regression testing.
### Running UI Tests
```bash
# Install dependencies (first time)
bundle install
# Run all UI tests
./bin/ui-test
# Skip build (faster iteration)
./bin/ui-test --skip-build
# Keep simulator running after tests
./bin/ui-test --keep-simulator
# Generate new baseline screenshots
./bin/ui-test --generate-baseline
# Run on a different device
./bin/ui-test --device "iPad Pro 13-inch (M5)"
```
### Creating Tests for New Features
When implementing a new feature, create a UI test to verify it works:
text:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus feugiat mi, suscipit pharetra lectus dapibus vel. Vivamus orci erat, sagittis sit amet dui vel, feugiat cursus ante. Pellentesque eget orci tortor. Suspendisse pulvinar orci tortor, eu scelerisque neque consequat nec. Aliquam sit amet turpis et nunc placerat finibus eget sit amet justo. Nullam tincidunt ornare neque. Donec ornare, arcu at elementum pulvinar, urna elit pharetra diam, vel ultrices lacus diam at lorem. Sed vel maximus dolor. Morbi massa est, interdum quis justo sit amet, dapibus bibendum tellus. Integer at purus nec neque tincidunt convallis sit amet eu odio. Duis et ante vitae sem tincidunt facilisis sit amet ac mauris. Quisque varius non nisi vel placerat. Nulla orci metus, imperdiet ac accumsan sed, pellentesque eget nisl. Praesent a suscipit lacus, ut finibus orci. Nulla ut eros commodo, fermentum purus at, porta leo. In finibus luctus nulla, eget posuere eros mollis vel. ",
description:"The best channel that ever existed.\nThe best channel that ever existed. The best channel that ever existed. The best channel that ever existed. The best channel that ever existed. ",
message:message??"\(response?.response?.statusCode??-1) - \(response?.error?.errorDescription??"unknown")\nIf this issue persists, try removing and adding your account again in Settings."
message:message??"\(response?.response?.statusCode??-1) - \(response?.error?.errorDescription??"unknown")\nIf this issue persists, try removing and adding your account again in Settings."
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.