feat: add expandable compact queue and sidebar shortcut
This commit is contained in:
+119
-4
@@ -590,6 +590,7 @@ private enum WindowLayoutCheck {
|
||||
static var step = 0
|
||||
static var windowIdentity: ObjectIdentifier?
|
||||
static var normalFrame = NSRect.zero
|
||||
static var compactFrame = NSRect.zero
|
||||
static var animationStart: NSRect?
|
||||
static var animationTarget = NSRect.zero
|
||||
static var sawIntermediateFrame = false
|
||||
@@ -598,6 +599,7 @@ private enum WindowLayoutCheck {
|
||||
static var queueVideoSize: NSSize?
|
||||
static var observingQueue = false
|
||||
static var animationQueueWidth: CGFloat = 0
|
||||
static var compactQueueOnly: Bool { CommandLine.arguments.contains("--compact-queue-only") }
|
||||
static var expectsAnimation: Bool { !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion }
|
||||
// Infrastructure timeout only; assertions depend on native transition completion.
|
||||
static let deadline = ContinuousClock.now.advanced(by: .seconds(30))
|
||||
@@ -677,7 +679,7 @@ private enum WindowLayoutCheck {
|
||||
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
|
||||
if model.compactWindow.isTransitioning || model.isQueueTransitioning {
|
||||
window.layoutIfNeeded()
|
||||
if model.isQueueTransitioning && !sidebarHeaderIsVisible(in: window) {
|
||||
if model.isQueueTransitioning && !model.isCompact && !sidebarHeaderIsVisible(in: window) {
|
||||
finish("Top-right controls left the window corner during queue animation at step \(step)"); return
|
||||
}
|
||||
if let size = queueVideoSize, let video = model.videoView {
|
||||
@@ -693,8 +695,8 @@ private enum WindowLayoutCheck {
|
||||
// These run-loop-delivered requests must remain responsive mid-animation.
|
||||
if model.isQueueTransitioning {
|
||||
let frame = window.frame
|
||||
model.showsQueue.toggle()
|
||||
model.showsQueue.toggle()
|
||||
model.perform(model.queueAction)
|
||||
model.perform(model.queueAction)
|
||||
guard sameFrame(window.frame, frame) else { finish("Queue reversal jumped the window"); return }
|
||||
} else {
|
||||
model.toggleCompact()
|
||||
@@ -720,9 +722,20 @@ private enum WindowLayoutCheck {
|
||||
guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return }
|
||||
if model.fullScreenIntent != nil { return }
|
||||
window.layoutIfNeeded()
|
||||
if compactQueueOnly && step == 0 {
|
||||
if !model.showsQueue {
|
||||
guard toggleQueue(model, in: window) else { finish("Normal queue shortcut was not handled"); return }
|
||||
return
|
||||
}
|
||||
step = 36
|
||||
}
|
||||
if model.showsQueue && !model.isCompact && !sidebarHeaderIsVisible(in: window) {
|
||||
finish("Sidebar pin, Cast or settings glyph is not visible at step \(step)"); return
|
||||
}
|
||||
if step >= 36 {
|
||||
advanceCompactQueue(model, window: window)
|
||||
return
|
||||
}
|
||||
if step >= 33 {
|
||||
switch step {
|
||||
case 33, 34:
|
||||
@@ -771,7 +784,9 @@ private enum WindowLayoutCheck {
|
||||
observeAnimation(from: window, to: target)
|
||||
observingQueue = true
|
||||
queueVideoSize = model.videoView?.bounds.size
|
||||
model.showsQueue = showsQueue
|
||||
guard toggleQueue(model, in: window), model.showsQueue == showsQueue else {
|
||||
finish("Native queue shortcut failed in normal mode"); return
|
||||
}
|
||||
}
|
||||
case 1, 5:
|
||||
window.setFrame(NSRect(x: 100, y: 200, width: 763, height: 368), display: true)
|
||||
@@ -800,6 +815,106 @@ private enum WindowLayoutCheck {
|
||||
}
|
||||
step += 1
|
||||
}
|
||||
static func toggleQueue(_ model: PlayerModel, in window: NSWindow, repeatKey: Bool = false) -> Bool {
|
||||
let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command, .shift],
|
||||
timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||
characters: "L", charactersIgnoringModifiers: "l", isARepeat: repeatKey, keyCode: 37)!
|
||||
return model.handleKeyEvent(event, in: window)
|
||||
}
|
||||
|
||||
static func observeCompactQueue(_ model: PlayerModel, window: NSWindow) {
|
||||
let target = model.compactWindow.queueLayout(visible: !model.queueVisible)!
|
||||
observeAnimation(from: window, to: target.frame)
|
||||
observingQueue = true
|
||||
queueVideoSize = model.videoView?.bounds.size
|
||||
if !toggleQueue(model, in: window) { finish("Native queue shortcut was not handled") }
|
||||
}
|
||||
|
||||
static func compactQueueHasPaintedContent(_ model: PlayerModel, window: NSWindow) -> Bool {
|
||||
guard let content = window.contentView else { return false }
|
||||
let height = model.compactWindow.queueHeight
|
||||
let region = NSRect(x: 0, y: content.isFlipped ? content.bounds.maxY - height : 0,
|
||||
width: content.bounds.width, height: height)
|
||||
guard let bitmap = content.bitmapImageRepForCachingDisplay(in: region) else { return false }
|
||||
content.cacheDisplay(in: region, to: bitmap)
|
||||
var textPixels = 0
|
||||
for x in 0..<bitmap.pixelsWide {
|
||||
for y in 0..<bitmap.pixelsHigh {
|
||||
if let color = bitmap.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB),
|
||||
color.alphaComponent > 0.5,
|
||||
min(color.redComponent, color.greenComponent, color.blueComponent) > 0.5 {
|
||||
textPixels += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return textPixels > 30
|
||||
}
|
||||
|
||||
static func advanceCompactQueue(_ model: PlayerModel, window: NSWindow) {
|
||||
switch step {
|
||||
case 36, 42:
|
||||
guard !model.isFullScreen, !model.isCompact else { finish("Expanded compact/fullscreen restoration failed"); return }
|
||||
if step == 42, !sameFrame(window.frame, normalFrame) {
|
||||
finish("Expanded queue corrupted the normal restoration frame"); return
|
||||
}
|
||||
normalFrame = window.frame
|
||||
observeAnimation(from: window, to: CompactWindowController.initialFrame(
|
||||
aspect: model.state.video_aspect_ratio,
|
||||
visibleFrame: (window.screen ?? NSScreen.main!).visibleFrame, width: 320))
|
||||
model.toggleCompact()
|
||||
case 37, 43:
|
||||
guard model.isCompact, !model.queueVisible, model.compactWindow.queueHeight == 0 else {
|
||||
finish("Compact entry did not default to a hidden queue"); return
|
||||
}
|
||||
compactFrame = window.frame
|
||||
observeCompactQueue(model, window: window)
|
||||
case 38, 40, 44:
|
||||
let height = model.compactWindow.queueHeight
|
||||
guard model.queueVisible, height > 0, model.showsQueue,
|
||||
abs(window.frame.maxY - compactFrame.maxY) < 1,
|
||||
abs(window.frame.width / (window.frame.height - height) - model.state.video_aspect_ratio) < 0.01,
|
||||
let video = model.videoView, abs(video.bounds.height - compactFrame.height) < 2 else {
|
||||
finish("Compact queue did not expand below the unchanged video at step \(step)"); return
|
||||
}
|
||||
guard compactQueueHasPaintedContent(model, window: window) else {
|
||||
finish("Expanded compact queue did not paint its native rows and footer"); return
|
||||
}
|
||||
guard toggleQueue(model, in: window, repeatKey: true), model.queueVisible else {
|
||||
finish("Held queue shortcut toggled repeatedly"); return
|
||||
}
|
||||
if step == 38 {
|
||||
observeCompactQueue(model, window: window)
|
||||
} else if step == 40 {
|
||||
if compactQueueOnly {
|
||||
observeAnimation(from: window, to: normalFrame)
|
||||
model.toggleCompact()
|
||||
step = 41
|
||||
} else {
|
||||
// The explicit fullscreen action must discard dropdown geometry before entering the native Space.
|
||||
model.toggleFullScreen()
|
||||
}
|
||||
} else {
|
||||
observeAnimation(from: window, to: normalFrame)
|
||||
model.toggleCompact()
|
||||
}
|
||||
case 39:
|
||||
guard !model.queueVisible, sameFrame(window.frame, compactFrame) else {
|
||||
finish("Compact queue did not collapse to its original video frame"); return
|
||||
}
|
||||
observeCompactQueue(model, window: window)
|
||||
case 41:
|
||||
guard model.isFullScreen, !model.isCompact, model.compactWindow.queueHeight == 0 else {
|
||||
finish("Fullscreen retained compact dropdown geometry"); return
|
||||
}
|
||||
model.toggleFullScreen()
|
||||
default:
|
||||
guard !model.isCompact, model.queueVisible, sameFrame(window.frame, normalFrame) else {
|
||||
finish("Normal queue visibility or restoration frame was lost"); return
|
||||
}
|
||||
finish(); return
|
||||
}
|
||||
step += 1
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user