fix: restore sidebar header and balance footer sizing

This commit is contained in:
2026-10-18 19:37:22 +02:00
parent 09d659d2cc
commit 4ca6e80cb0
3 changed files with 41 additions and 14 deletions
+27
View File
@@ -70,6 +70,30 @@ private enum WindowLayoutCheck {
sawIntermediateFrame = false sawIntermediateFrame = false
} }
static func sidebarHeaderIsVisible(in window: NSWindow) -> Bool {
guard let content = window.contentView else { return false }
let header = NSRect(x: content.bounds.maxX - 84,
y: content.isFlipped ? content.bounds.minY : content.bounds.maxY - 32,
width: 84, height: 32)
guard let bitmap = content.bitmapImageRepForCachingDisplay(in: header) else { return false }
content.cacheDisplay(in: header, to: bitmap)
let scale = CGFloat(bitmap.pixelsWide) / header.width
// Each control must paint a visible glyph, not merely exist in the view tree.
return (0..<3).allSatisfy { index in
var visiblePixels = 0
for x in Int(CGFloat(index * 28) * scale)..<Int(CGFloat(index * 28 + 24) * scale) {
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 {
visiblePixels += 1
}
}
}
return visiblePixels >= 8
}
}
static func advance() { static func advance() {
let model = PlayerModel.shared let model = PlayerModel.shared
guard ContinuousClock.now < deadline else { finish("Window layout did not settle at step \(step)"); return } guard ContinuousClock.now < deadline else { finish("Window layout did not settle at step \(step)"); return }
@@ -95,6 +119,9 @@ private enum WindowLayoutCheck {
if windowIdentity == nil { windowIdentity = ObjectIdentifier(window) } if windowIdentity == nil { windowIdentity = ObjectIdentifier(window) }
guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return } guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return }
window.layoutIfNeeded() window.layoutIfNeeded()
if model.showsQueue && !model.isCompact && !sidebarHeaderIsVisible(in: window) {
finish("Sidebar pin, Cast or settings glyph is not visible at step \(step)"); return
}
switch step % 8 { switch step % 8 {
case 0, 4: case 0, 4:
if step > 0 { if step > 0 {
+2 -2
View File
@@ -61,7 +61,7 @@ Active playback prevents idle system sleep. Local video also keeps the display a
Use the **Queue** icon immediately left of the compact-mode button in the playback controls, or **View → Show Queue**, to show the editable sidebar. The **+** button beside the queue count opens **Add to Queue…**; it and dropping files onto the sidebar append without replacing the queue or interrupting the current item. Drag rows to reorder, remove them with the minus button, and double-click a row to play it. The current item is marked; context menus also offer Play, Remove and Move Up/Down. Use the **Queue** icon immediately left of the compact-mode button in the playback controls, or **View → Show Queue**, to show the editable sidebar. The **+** button beside the queue count opens **Add to Queue…**; it and dropping files onto the sidebar append without replacing the queue or interrupting the current item. Drag rows to reorder, remove them with the minus button, and double-click a row to play it. The current item is marked; context menus also offer Play, Remove and Move Up/Down.
The fixed bottom row holds the queue label/count and **+** on the left, with previous/next controls on the right. The **+** is a 20-point rounded-square button with an accent-tinted background and an 8-point gap after the queue count. The fixed bottom row holds the queue label/count and **+** on the left, with previous/next controls on the right. Its text and icons share a 15-point size. The **+** is a 20-point rounded-square button with an accent-tinted background and an 8-point gap after the queue count.
Queue rows use equal-width leading and remove-icon slots, with 8-point outer insets matching the gaps between icons and filenames and no additional horizontal scroll-content margin. Queue rows use equal-width leading and remove-icon slots, with 8-point outer insets matching the gaps between icons and filenames and no additional horizontal scroll-content margin.
@@ -69,7 +69,7 @@ In a normal window, showing the queue expands the window outward while preservin
Resizing with the queue open preserves the video's aspect ratio while keeping the sidebar at a fixed width; closing the queue retains ordinary aspect-locked video resizing. Resizing with the queue open preserves the video's aspect ratio while keeping the sidebar at a fixed width; closing the queue retains ordinary aspect-locked video resizing.
While the sidebar is open, its persistent top header holds the pin, Cast and settings controls without a surrounding glass container, with matching top and right button insets; these and the bottom queue controls do not fade with the video controls. Closing the sidebar returns the pin, Cast and settings controls to the auto-hiding video titlebar with its glass container. The queue toggle stays in the playback controls. Space and configured playback shortcuts continue working with queue-list focus; Tab navigation, deliberately focused buttons/sliders and text entry keep their native keyboard behavior. While the sidebar is open, its persistent top-right overlay holds the pin, Cast and settings controls above the list without a surrounding glass container, with matching top and right button insets; these and the bottom queue controls do not fade with the video controls. The native layout check verifies that all three header glyphs actually render. Closing the sidebar returns the pin, Cast and settings controls to the auto-hiding video titlebar with its glass container. The queue toggle stays in the playback controls. Space and configured playback shortcuts continue working with queue-list focus; Tab navigation, deliberately focused buttons/sliders and text entry keep their native keyboard behavior.
Playback advances automatically and stops at the end, without wrapping. **Previous Queue Item** and **Next Queue Item** are available in the sidebar and Playback menu and are disabled at their respective ends. Reordering or removing another item does not interrupt playback; removing the current item plays its next neighbor, or stops if there is none. A playback error stays on that item rather than skipping it. Playback advances automatically and stops at the end, without wrapping. **Previous Queue Item** and **Next Queue Item** are available in the sidebar and Playback menu and are disabled at their respective ends. Reordering or removing another item does not interrupt playback; removing the current item plays its next neighbor, or stops if there is none. A playback error stays on that item rather than skipping it.
+12 -12
View File
@@ -2052,9 +2052,9 @@ private struct PlayerContent: View {
: String(format: "%02d:%02d", value / 60, value % 60) : String(format: "%02d:%02d", value / 60, value % 60)
} }
private func iconButton(_ symbol: String, _ label: String, action: @escaping () -> Void) -> some View { private func iconButton(_ symbol: String, _ label: String, size: CGFloat = 17, action: @escaping () -> Void) -> some View {
Button(action: action) { Button(action: action) {
Image(systemName: symbol).font(.system(size: 17)).frame(width: 24, height: 24) Image(systemName: symbol).font(.system(size: size)).frame(width: 24, height: 24)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.help(label) .help(label)
@@ -2286,13 +2286,7 @@ private struct PlayerContent: View {
private var queueSidebar: some View { private var queueSidebar: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 4) { Color.clear.frame(height: 32)
Spacer(minLength: 4)
titleControls
}
.padding(.leading, 12)
.padding(.trailing, 4)
.frame(height: 32)
List { List {
ForEach(model.queue.items) { item in ForEach(model.queue.items) { item in
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -2349,10 +2343,11 @@ private struct PlayerContent: View {
Text("Queue").fontWeight(.semibold) Text("Queue").fontWeight(.semibold)
Text("\(model.queue.items.count)").foregroundStyle(.secondary) Text("\(model.queue.items.count)").foregroundStyle(.secondary)
} }
.font(.system(size: 13)) .font(.system(size: 15))
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
Button(action: model.addQueuePanel) { Button(action: model.addQueuePanel) {
Image(systemName: "plus") Image(systemName: "plus")
.font(.system(size: 15))
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.background(Color(SunsetPalette.accent).opacity(0.1), in: RoundedRectangle(cornerRadius: 6)) .background(Color(SunsetPalette.accent).opacity(0.1), in: RoundedRectangle(cornerRadius: 6))
} }
@@ -2363,9 +2358,9 @@ private struct PlayerContent: View {
.accessibilityLabel("Add to Queue…") .accessibilityLabel("Add to Queue…")
.disabled(!model.canOpen) .disabled(!model.canOpen)
Spacer() Spacer()
iconButton("backward.end.fill", "Previous Queue Item") { model.advanceQueue(-1) } iconButton("backward.end.fill", "Previous Queue Item", size: 15) { model.advanceQueue(-1) }
.disabled(!model.canPreviousQueueItem) .disabled(!model.canPreviousQueueItem)
iconButton("forward.end.fill", "Next Queue Item") { model.advanceQueue(1) } iconButton("forward.end.fill", "Next Queue Item", size: 15) { model.advanceQueue(1) }
.disabled(!model.canNextQueueItem) .disabled(!model.canNextQueueItem)
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
@@ -2373,6 +2368,11 @@ private struct PlayerContent: View {
} }
.frame(width: 260) .frame(width: 260)
.background(PlayerGlass(cornerRadius: 0)) .background(PlayerGlass(cornerRadius: 0))
.overlay(alignment: .topTrailing) {
titleControls
.padding(.trailing, 4)
.frame(height: 32)
}
.dropDestination(for: URL.self) { urls, _ in .dropDestination(for: URL.self) { urls, _ in
guard model.canOpen, urls.contains(where: \.isFileURL) else { return false } guard model.canOpen, urls.contains(where: \.isFileURL) else { return false }
model.addToQueue(urls) model.addToQueue(urls)