fix: anchor title controls outside the animated queue clip

This commit is contained in:
2026-10-18 22:08:41 +02:00
parent 22f13dd08d
commit 2e5255e194
4 changed files with 27 additions and 16 deletions
+3
View File
@@ -110,6 +110,9 @@ private enum WindowLayoutCheck {
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return } let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
if model.compactWindow.isTransitioning || model.isQueueTransitioning { if model.compactWindow.isTransitioning || model.isQueueTransitioning {
window.layoutIfNeeded() window.layoutIfNeeded()
if model.isQueueTransitioning && !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 { if let size = queueVideoSize, let video = model.videoView {
guard abs(video.bounds.width - size.width) < 2, abs(video.bounds.height - size.height) < 2 else { guard abs(video.bounds.width - size.width) < 2, abs(video.bounds.height - size.height) < 2 else {
finish("Queue animation changed video size at step \(step): \(video.bounds.size), expected \(size)"); return finish("Queue animation changed video size at step \(step): \(video.bounds.size), expected \(size)"); return
+1 -1
View File
@@ -71,7 +71,7 @@ The queue expands and collapses with a 250 ms ease-in/out reveal. In a normal wi
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-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. One window-level top-right overlay holds the pin, Cast and settings controls, keeping them visible at the moving window corner throughout queue expansion, collapse and reversals. Their glass container fades out as the sidebar opens and back in as it closes. With the sidebar open, the controls sit above the list with matching top and right button insets; these and the bottom queue controls do not fade with the video controls. Once the sidebar closes, the top-right controls resume auto-hiding with the video titlebar. The native layout check samples all three rendered header glyphs at the window corner during transitions as well as after opening. 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.
The sidebar, top controls and queue footer share an opaque playlist background. The top backing fades downward over 20 points. A noninteractive 52-point blank row at the start of the list clears the 32-point control row and its fade for the first entry, while the list viewport extends behind the controls so scrolling rows pass beneath the gradient rather than a hard clipping edge. The footer backing fades upward into the list by the same amount, keeping scrolled filenames from interfering with the controls without hard edges. The sidebar, top controls and queue footer share an opaque playlist background. The top backing fades downward over 20 points. A noninteractive 52-point blank row at the start of the list clears the 32-point control row and its fade for the first entry, while the list viewport extends behind the controls so scrolling rows pass beneath the gradient rather than a hard clipping edge. The footer backing fades upward into the list by the same amount, keeping scrolled filenames from interfering with the controls without hard edges.
+22 -14
View File
@@ -2142,12 +2142,9 @@ private struct PlayerContent: View {
.frame(height: 24) .frame(height: 24)
.background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false)) .background(PlayerGlass(cornerRadius: 8).clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false))
Spacer(minLength: 12) Spacer(minLength: 12)
if !model.showsQueue {
titleControls
}
} }
.padding(.leading, model.isFullScreen ? 16 : 86) .padding(.leading, model.isFullScreen ? 16 : 86)
.padding(.trailing, 4) .padding(.trailing, 92 - 88 * model.queueWidth / 260)
.frame(height: 32) .frame(height: 32)
.foregroundStyle(Color(SunsetPalette.text)) .foregroundStyle(Color(SunsetPalette.text))
.onHover(perform: model.setTitleHovered) .onHover(perform: model.setTitleHovered)
@@ -2168,13 +2165,13 @@ private struct PlayerContent: View {
CastDeviceMenu(model: model) CastDeviceMenu(model: model)
actionsMenu actionsMenu
} }
.padding(.horizontal, model.showsQueue ? 0 : 4) .padding(.horizontal, 4 * (1 - model.queueWidth / 260))
.frame(height: 24) .frame(height: 24)
.background { .background {
if !model.showsQueue { PlayerGlass(cornerRadius: 8)
PlayerGlass(cornerRadius: 8) .clipShape(RoundedRectangle(cornerRadius: 8))
.clipShape(RoundedRectangle(cornerRadius: 8)).allowsHitTesting(false) .opacity(1 - model.queueWidth / 260)
} .allowsHitTesting(false)
} }
} }
@@ -2485,12 +2482,9 @@ private struct PlayerContent: View {
} }
.frame(width: 260) .frame(width: 260)
.background(queueBackground) .background(queueBackground)
.overlay(alignment: .topTrailing) { .overlay(alignment: .top) {
titleControls queueChromeBackground(top: true)
.padding(.trailing, 4)
.frame(height: 32) .frame(height: 32)
.frame(maxWidth: .infinity, alignment: .trailing)
.background { queueChromeBackground(top: true) }
} }
.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 }
@@ -2501,6 +2495,7 @@ private struct PlayerContent: View {
} }
var body: some View { var body: some View {
let titleControlsVisible = model.showsQueue || model.isQueueTransitioning || model.controlsVisible
HStack(spacing: 0) { HStack(spacing: 0) {
videoContent videoContent
if !model.isCompact && !model.compactWindow.isTransitioning && model.queueWidth > 0 { if !model.isCompact && !model.compactWindow.isTransitioning && model.queueWidth > 0 {
@@ -2513,6 +2508,19 @@ private struct PlayerContent: View {
} }
.frame(minWidth: model.isCompact || model.compactWindow.isTransitioning ? 160 : 400 + model.queueWidth, .frame(minWidth: model.isCompact || model.compactWindow.isTransitioning ? 160 : 400 + model.queueWidth,
minHeight: model.isCompact || model.compactWindow.isTransitioning ? model.compactContentMinimumHeight : 240) minHeight: model.isCompact || model.compactWindow.isTransitioning ? model.compactContentMinimumHeight : 240)
.overlay(alignment: .topTrailing) {
if !model.isCompact && !model.compactWindow.isTransitioning {
titleControls
.padding(.trailing, 4)
.frame(height: 32)
.onHover(perform: model.setTitleHovered)
.animation(model.isQueueTransitioning ? nil : controlsVisibilityAnimation) { content in
content.opacity(titleControlsVisible ? 1 : 0)
}
.allowsHitTesting(titleControlsVisible)
.accessibilityHidden(!titleControlsVisible)
}
}
.ignoresSafeArea() .ignoresSafeArea()
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.foregroundStyle(Color(SunsetPalette.text)) .foregroundStyle(Color(SunsetPalette.text))
+1 -1
View File
@@ -9,6 +9,6 @@
- [x] takes an awfully long time seek in local videos which i find weird, since seeking in streamed youtube videos is pretty much instant - [x] takes an awfully long time seek in local videos which i find weird, since seeking in streamed youtube videos is pretty much instant
- local seek preroll skips disposable non-reference pictures without sacrificing exact target frames; measured 1080p/10-second-GOP paused seek improved from 541 ms to 400 ms and playing seek from 682 ms to 488 ms; timestamp/pixel checks passed across six decoder formats; long-GOP reference decoding remains necessary, and the originally affected files still need manual verification - local seek preroll skips disposable non-reference pictures without sacrificing exact target frames; measured 1080p/10-second-GOP paused seek improved from 541 ms to 400 ms and playing seek from 682 ms to 488 ms; timestamp/pixel checks passed across six decoder formats; long-GOP reference decoding remains necessary, and the originally affected files still need manual verification
- [x] make the sidebar expand/collapse with a nice transition - [x] make the sidebar expand/collapse with a nice transition
- synchronized 250 ms sidebar/window reveal, reversible mid-transition, with immediate Reduce Motion changes; native checks passed for stable video size, reversals, screen-edge fitting, fullscreen and compact restoration; animation feel and Reduce Motion still need manual verification - synchronized 250 ms sidebar/window reveal, reversible mid-transition, with immediate Reduce Motion changes; top-right controls stay anchored to the moving window corner; native checks passed for rendered controls during transitions, stable video size, reversals, screen-edge fitting, fullscreen and compact restoration; animation feel and Reduce Motion still need manual verification
- [ ] in the "choose what to play" screen, we should change the youtube button to general streaming - [ ] in the "choose what to play" screen, we should change the youtube button to general streaming
- we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly) - we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly)