feat: add expandable compact queue and sidebar shortcut

This commit is contained in:
2026-10-22 18:11:13 +02:00
parent 6ed7db8167
commit 98e6dcb3c5
11 changed files with 330 additions and 39 deletions
+119 -4
View File
@@ -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