diff --git a/Yattee/ContentView.swift b/Yattee/ContentView.swift index d390f7a2..b3531fc6 100644 --- a/Yattee/ContentView.swift +++ b/Yattee/ContentView.swift @@ -94,6 +94,7 @@ struct ContentView: View { set: { appEnvironment.navigationCoordinator.isPlayerExpanded = $0 } )) { let size = expandedSheetSize(appEnvironment: appEnvironment) + let lockAspect = sheetLockAspectRatio(appEnvironment: appEnvironment) ExpandedPlayerSheet() // Floor keeps the content flexible so it tracks the window's // animated resize with no gap — do NOT pin an exact (max) size: @@ -109,8 +110,9 @@ struct ContentView: View { .presentationSizing(.fitted) // `.presentationSizing(.fitted)` only fits the sheet once, at // presentation. Resize the backing window directly when the - // aspect-ratio-derived size changes so the sheet tracks the video. - .sheetWindowSize(size) + // aspect-ratio-derived size changes so the sheet tracks the video, + // and lock interactive resize to the video ratio (no black bars). + .sheetWindowSize(size, aspectRatio: lockAspect) } #elseif os(tvOS) .fullScreenCover(isPresented: Binding( @@ -142,6 +144,16 @@ struct ContentView: View { } return ExpandedPlayerWindowManager.fittedSheetSize(for: aspect) } + + /// Real video aspect ratio to lock the sheet's interactive resize to, or `0` + /// when unknown. Unlike the sheet *size* (which honors `playerSheetAutoResize`), + /// the resize lock always follows the actual video ratio — mirroring the + /// standalone window, which locks unconditionally. Reading `videoAspectRatio` + /// here registers the @Observable dependency that keeps the lock in sync. + private func sheetLockAspectRatio(appEnvironment: AppEnvironment) -> Double { + let ratio = appEnvironment.playerService.state.videoAspectRatio ?? 0 + return ratio > 0 ? ratio : 0 + } #endif #if os(macOS) diff --git a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift index e45e269e..d3899424 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift @@ -106,7 +106,7 @@ final class ExpandedPlayerWindowManager: NSObject { // Lock manual resize to the video aspect ratio. Seeded with 16:9 here; // updated as soon as the real video aspect ratio is known. - applyAspectRatioConstraint(Self.defaultAspectRatio, to: window) + Self.applyAspectRatioConstraint(Self.defaultAspectRatio, to: window) // Force the intended size. Assigning `contentViewController` above resizes // the window to the hosting controller's fitting size — and the lightweight @@ -281,7 +281,7 @@ final class ExpandedPlayerWindowManager: NSObject { guard aspectRatio > 0 else { return } // Always update the aspect-ratio lock, even if we end up not resizing here. - applyAspectRatioConstraint(aspectRatio, to: window) + Self.applyAspectRatioConstraint(aspectRatio, to: window) // Get screen bounds guard let screen = window.screen ?? NSScreen.main else { return } @@ -311,14 +311,16 @@ final class ExpandedPlayerWindowManager: NSObject { func lockAspectRatio(_ aspectRatio: Double) { guard let window = playerWindow else { return } guard aspectRatio > 0 else { return } - applyAspectRatioConstraint(aspectRatio, to: window) + Self.applyAspectRatioConstraint(aspectRatio, to: window) } // MARK: - Private Helpers /// Sets `contentAspectRatio` and a ratio-consistent `minSize` on the window /// so that interactive resize couples width and height proportionally. - private func applyAspectRatioConstraint(_ aspectRatio: Double, to window: NSWindow) { + /// Static so the inline-sheet path (`SheetWindowResizer`) can lock the sheet's + /// backing window to the same ratio the standalone window uses. + static func applyAspectRatioConstraint(_ aspectRatio: Double, to window: NSWindow) { guard aspectRatio > 0 else { return } // contentAspectRatio is expressed as a ratio; using (aspectRatio, 1) keeps it exact. @@ -517,6 +519,10 @@ private struct ExpandedPlayerWindowRoot: View { /// attached sheet naturally sits. struct SheetWindowResizer: NSViewRepresentable { let targetSize: CGSize + /// Real video aspect ratio (width / height) to lock interactive resize to, or + /// `0` when unknown. Mirrors the standalone window's `contentAspectRatio` lock + /// so dragging the sheet edge keeps the video's ratio instead of adding bars. + let aspectRatio: Double func makeCoordinator() -> Coordinator { Coordinator() } @@ -530,6 +536,7 @@ struct SheetWindowResizer: NSViewRepresentable { func updateNSView(_ nsView: NSView, context: Context) { let target = targetSize + let aspect = aspectRatio let coordinator = context.coordinator // The window isn't attached during the same runloop tick as the SwiftUI // update, so defer the resize until the view is in a window. @@ -545,6 +552,13 @@ struct SheetWindowResizer: NSViewRepresentable { window.isOpaque = true } + // Lock interactive resize to the video ratio so dragging the sheet's + // edge keeps the aspect (no black bars), matching the standalone + // window. Applied every pass so it tracks aspect-ratio changes. + if aspect > 0 { + ExpandedPlayerWindowManager.applyAspectRatioConstraint(aspect, to: window) + } + let current = window.frame guard abs(current.width - target.width) > 1 || abs(current.height - target.height) > 1 else { return } @@ -567,9 +581,11 @@ struct SheetWindowResizer: NSViewRepresentable { } extension View { - /// Resizes the hosting sheet window to `size` when it changes (macOS sheets). - func sheetWindowSize(_ size: CGSize) -> some View { - background(SheetWindowResizer(targetSize: size)) + /// Resizes the hosting sheet window to `size` when it changes and locks its + /// interactive resize to `aspectRatio` (macOS sheets). Pass `aspectRatio == 0` + /// to leave resize unconstrained. + func sheetWindowSize(_ size: CGSize, aspectRatio: Double) -> some View { + background(SheetWindowResizer(targetSize: size, aspectRatio: aspectRatio)) } } #endif