diff --git a/Yattee/ContentView.swift b/Yattee/ContentView.swift index 5c861dd5..ec701908 100644 --- a/Yattee/ContentView.swift +++ b/Yattee/ContentView.swift @@ -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) diff --git a/Yattee/Views/Components/GlassBackground.swift b/Yattee/Views/Components/GlassBackground.swift index 5e37c30f..2e2fcc6e 100644 --- a/Yattee/Views/Components/GlassBackground.swift +++ b/Yattee/Views/Components/GlassBackground.swift @@ -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 diff --git a/Yattee/Views/Player/MiniPlayerView.swift b/Yattee/Views/Player/MiniPlayerView.swift index 49a47139..4576b8fc 100644 --- a/Yattee/Views/Player/MiniPlayerView.swift +++ b/Yattee/Views/Player/MiniPlayerView.swift @@ -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.