Redesign macOS now-playing bar as centered Liquid Glass capsule

- Replace the full-width material overlay bar with a compact, centered
  capsule (Apple Music style) via a macOS-specific macOSCapsuleLayout
- Extend glassBackground() to use real Liquid Glass on macOS 26 with a
  material fallback on macOS 15-25 (mirrors the PlayerOverlayButton pattern)
- Remove the duplicate close button (close now comes only from the
  configurable button set)
- Keep the capsule visible in the main window while the separate player
  window is open (window mode), still hidden in sheet mode
This commit is contained in:
Arkadiusz Fal
2026-05-22 23:43:51 +02:00
parent a480b73e7c
commit 84c17382ce
3 changed files with 74 additions and 9 deletions

View File

@@ -119,14 +119,17 @@ struct ContentView: View {
let playerState = appEnvironment.playerService.state
let hasActiveVideo = playerState.currentVideo != nil
let isExpanded = appEnvironment.navigationCoordinator.isPlayerExpanded
// The expanded player is a separate window in window mode, so keep the capsule
// visible alongside it. In sheet mode the sheet covers the window, so hide it.
let usesWindow = appEnvironment.settingsManager.macPlayerMode.usesWindow
if hasActiveVideo && !isExpanded {
if hasActiveVideo && (!isExpanded || usesWindow) {
VStack(spacing: 0) {
Spacer()
MiniPlayerView()
}
// Add padding for tab bar
.padding(.bottom, 49)
// Float the capsule above the bottom edge (macOS uses a sidebar, not a tab bar)
.padding(.bottom, 16)
// Use move-only transition (no opacity) to prevent thumbnail flash during collapse
.transition(.move(edge: .bottom))
.animation(.spring(response: 0.3), value: hasActiveVideo)

View File

@@ -64,23 +64,23 @@ struct GlassBackgroundModifier: ViewModifier {
}
func body(content: Content) -> some View {
#if os(iOS)
if #available(iOS 26.0, *) {
#if os(iOS) || os(macOS)
if #available(iOS 26.0, macOS 26.0, *) {
content.modifier(LiquidGlassModifier(style: style, shape: shape, colorScheme: colorScheme))
} else {
content.modifier(FallbackGlassModifier(shape: shape, material: fallbackMaterial, colorScheme: colorScheme))
}
#else
// macOS / tvOS don't have glassEffect, always use fallback material
// tvOS doesn't have glassEffect, always use fallback material
content.modifier(FallbackGlassModifier(shape: shape, material: fallbackMaterial, colorScheme: colorScheme))
#endif
}
}
// MARK: - iOS 26+ Liquid Glass
// MARK: - iOS 26+ / macOS 26+ Liquid Glass
#if os(iOS)
@available(iOS 26.0, *)
#if os(iOS) || os(macOS)
@available(iOS 26.0, macOS 26.0, *)
private struct LiquidGlassModifier: ViewModifier {
let style: GlassStyle
let shape: GlassShape

View File

@@ -115,11 +115,15 @@ struct MiniPlayerView: View {
var body: some View {
Group {
#if os(macOS)
macOSCapsuleLayout
#else
if isTabAccessory {
accessoryLayout
} else {
overlayLayout
}
#endif
}
.accessibilityIdentifier("player.miniPlayer")
.accessibilityLabel("player.miniPlayer")
@@ -254,6 +258,64 @@ struct MiniPlayerView: View {
}
}
// MARK: - macOS Capsule Layout
#if os(macOS)
/// Compact, centered capsule shown in the main window on macOS (Music.app style).
/// The expanded player lives in a separate window, so this stays visible alongside it.
private var macOSCapsuleLayout: some View {
HStack(spacing: 10) {
// Video preview - tap for PiP (or expand if PiP unavailable)
videoPreviewView
.frame(width: 44, height: 26)
.clipShape(RoundedRectangle(cornerRadius: 4))
.contentShape(Rectangle())
.onTapGesture {
handleVideoPreviewTap()
}
// Title/author area - tap to expand player (restores from PiP first if needed)
VStack(alignment: .leading, spacing: 1) {
MarqueeText(
text: displayTitle,
font: .subheadline,
fontWeight: .medium,
foregroundStyle: .primary
)
if let authorName = currentVideo?.author.name, !authorName.isEmpty {
MarqueeText(
text: authorName,
font: .caption,
foregroundStyle: .secondary
)
}
}
.contentShape(Rectangle())
.onTapGesture {
expandPlayerWithPiPRestore()
}
// Dynamic buttons from settings (includes close by default - no separate close button)
buttonsView
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.frame(maxWidth: 420)
.glassBackground(.regular, in: .capsule, fallback: .regularMaterial)
.overlay(alignment: .bottom) {
GeometryReader { geo in
Rectangle()
.fill(.red)
.frame(width: geo.size.width * (playerState?.progress ?? 0), height: 2)
}
.frame(height: 2)
}
.clipShape(Capsule())
.shadow(color: .black.opacity(0.25), radius: 8, y: 4)
}
#endif
// MARK: - Shared Components
/// Video preview that shows live video when available, or thumbnail as fallback.