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.