mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
Open macOS player window immediately with loading feedback
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.
This commit is contained in:
@@ -67,12 +67,24 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
// Get the current player mode for window configuration
|
||||
let mode = appEnvironment.settingsManager.macPlayerMode
|
||||
|
||||
// Create the player view
|
||||
let playerView = ExpandedPlayerSheet()
|
||||
// Host a lightweight two-phase root instead of ExpandedPlayerSheet directly.
|
||||
// AppKit won't composite the window to screen until the hosted SwiftUI view
|
||||
// finishes its first layout pass; ExpandedPlayerSheet is heavy (MPV setup,
|
||||
// full controls, layout math), so building it inline leaves the alpha 0→1
|
||||
// fade with nothing to draw and the window only pops in after the render.
|
||||
// ExpandedPlayerWindowRoot paints black + spinner immediately, then defers
|
||||
// building ExpandedPlayerSheet by one runloop so the window appears at once.
|
||||
let playerView = ExpandedPlayerWindowRoot()
|
||||
.appEnvironment(appEnvironment)
|
||||
|
||||
// Create hosting controller
|
||||
let hostingController = NSHostingController(rootView: playerView)
|
||||
// Don't let the hosting controller drive the window size from the SwiftUI
|
||||
// content's fitting size. The lightweight loading root (just a spinner) has a
|
||||
// tiny ideal size, which would otherwise shrink the window and then grow it
|
||||
// when ExpandedPlayerSheet builds. Window sizing is owned by `initialSize`
|
||||
// below and `resizeToFitAspectRatio` once the real video ratio is known.
|
||||
hostingController.sizingOptions = []
|
||||
|
||||
// Calculate initial window size
|
||||
let initialSize = calculateInitialWindowSize()
|
||||
@@ -96,6 +108,14 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
// updated as soon as the real video aspect ratio is known.
|
||||
applyAspectRatioConstraint(defaultAspectRatio, to: window)
|
||||
|
||||
// Force the intended size. Assigning `contentViewController` above resizes
|
||||
// the window to the hosting controller's fitting size — and the lightweight
|
||||
// loading root has a tiny fitting size, which produced a tiny window that
|
||||
// only grew once `resizeToFitAspectRatio` fired after streams loaded. Set
|
||||
// the content size back to `initialSize` so the window opens full-sized
|
||||
// during the loading phase too.
|
||||
window.setContentSize(initialSize)
|
||||
|
||||
// Set up window delegate for close handling
|
||||
// Make ExpandedPlayerWindowManager itself the delegate to avoid lifecycle issues
|
||||
window.delegate = self
|
||||
@@ -431,4 +451,40 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Two-Phase Window Root
|
||||
|
||||
/// Root view hosted in the expanded player window.
|
||||
///
|
||||
/// Shows a cheap black background + spinner on the first frame so the window
|
||||
/// composites and fades in immediately, then defers building the heavy
|
||||
/// `ExpandedPlayerSheet` by one runloop — after the window is already on screen.
|
||||
/// This is the macOS equivalent of the instant loading feedback iOS shows while
|
||||
/// its player window renders, and removes the perceptible "dead gap" between the
|
||||
/// click and the window appearing.
|
||||
private struct ExpandedPlayerWindowRoot: View {
|
||||
@State private var showFullPlayer = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Fills the window and matches the player's black background so the
|
||||
// swap to ExpandedPlayerSheet is seamless.
|
||||
Color.black
|
||||
|
||||
if showFullPlayer {
|
||||
ExpandedPlayerSheet()
|
||||
} else {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.tint(.white)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
// Defer so the cheap branch composites first, then build the real
|
||||
// player while the window is already visible.
|
||||
DispatchQueue.main.async { showFullPlayer = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user