diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index f1682543..61c0139c 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -953,6 +953,11 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier { private func handleAspectRatioChange(oldValue: Double?, newValue: Double?) { guard let newValue, newValue > 0 else { return } + + // Always keep the manual-resize aspect lock in sync, regardless of the + // auto-resize setting — the user always wants resize to be ratio-locked. + ExpandedPlayerWindowManager.shared.lockAspectRatio(newValue) + guard appEnvironment?.settingsManager.playerSheetAutoResize == true else { return } let shouldAnimate = oldValue != nil @@ -982,9 +987,12 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier { } private func handleMacOSAppear() { - if appEnvironment?.settingsManager.playerSheetAutoResize == true, - let aspectRatio = playerState?.videoAspectRatio, - aspectRatio > 0 { + guard let aspectRatio = playerState?.videoAspectRatio, aspectRatio > 0 else { return } + + // Lock aspect ratio for manual resize unconditionally. + ExpandedPlayerWindowManager.shared.lockAspectRatio(aspectRatio) + + if appEnvironment?.settingsManager.playerSheetAutoResize == true { ExpandedPlayerWindowManager.shared.resizeToFitAspectRatio(aspectRatio, animated: false) } } diff --git a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift index 11ca633d..2890293b 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift @@ -21,9 +21,10 @@ final class ExpandedPlayerWindowManager: NSObject { // Configuration private let minWidth: CGFloat = 640 - private let minHeight: CGFloat = 480 + private let minHeight: CGFloat = 360 private let maxScreenRatio: CGFloat = 0.7 private let targetVideoHeight: CGFloat = 720 + private let defaultAspectRatio: Double = 16.0 / 9.0 var isPresented: Bool { playerWindow != nil @@ -89,9 +90,12 @@ final class ExpandedPlayerWindowManager: NSObject { window.titleVisibility = .hidden window.isMovableByWindowBackground = true window.backgroundColor = NSColor.windowBackgroundColor - window.minSize = NSSize(width: minWidth, height: minHeight) window.contentViewController = hostingController + // 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(defaultAspectRatio, to: window) + // Set up window delegate for close handling // Make ExpandedPlayerWindowManager itself the delegate to avoid lifecycle issues window.delegate = self @@ -256,6 +260,9 @@ final class ExpandedPlayerWindowManager: NSObject { guard let window = playerWindow else { return } guard aspectRatio > 0 else { return } + // Always update the aspect-ratio lock, even if we end up not resizing here. + applyAspectRatioConstraint(aspectRatio, to: window) + // Get screen bounds guard let screen = window.screen ?? NSScreen.main else { return } let screenFrame = screen.visibleFrame @@ -278,8 +285,32 @@ final class ExpandedPlayerWindowManager: NSObject { window.setFrame(adjustedFrame, display: true, animate: animated) } + /// Locks the window's resize behavior to the given aspect ratio without + /// changing the current frame. Use this when the auto-resize setting is + /// disabled but we still want manual resizing to be ratio-locked. + func lockAspectRatio(_ aspectRatio: Double) { + guard let window = playerWindow else { return } + guard aspectRatio > 0 else { return } + 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) { + guard aspectRatio > 0 else { return } + + // contentAspectRatio is expressed as a ratio; using (aspectRatio, 1) keeps it exact. + window.contentAspectRatio = NSSize(width: aspectRatio, height: 1) + + // Derive a minimum size that lies on the same ratio so the lower bound + // doesn't force the window off-ratio (which would re-introduce bars). + // Anchor on minHeight and scale width by aspect. + let derivedMinWidth = max(minHeight * CGFloat(aspectRatio), 320) + window.minSize = NSSize(width: derivedMinWidth, height: minHeight) + } + private func configureWindowLevel(_ window: NSWindow, floating: Bool) { if floating { window.level = .floating