mirror of
https://github.com/yattee/yattee.git
synced 2026-07-21 06:42:01 +00:00
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:
@@ -119,14 +119,17 @@ struct ContentView: View {
|
|||||||
let playerState = appEnvironment.playerService.state
|
let playerState = appEnvironment.playerService.state
|
||||||
let hasActiveVideo = playerState.currentVideo != nil
|
let hasActiveVideo = playerState.currentVideo != nil
|
||||||
let isExpanded = appEnvironment.navigationCoordinator.isPlayerExpanded
|
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) {
|
VStack(spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
MiniPlayerView()
|
MiniPlayerView()
|
||||||
}
|
}
|
||||||
// Add padding for tab bar
|
// Float the capsule above the bottom edge (macOS uses a sidebar, not a tab bar)
|
||||||
.padding(.bottom, 49)
|
.padding(.bottom, 16)
|
||||||
// Use move-only transition (no opacity) to prevent thumbnail flash during collapse
|
// Use move-only transition (no opacity) to prevent thumbnail flash during collapse
|
||||||
.transition(.move(edge: .bottom))
|
.transition(.move(edge: .bottom))
|
||||||
.animation(.spring(response: 0.3), value: hasActiveVideo)
|
.animation(.spring(response: 0.3), value: hasActiveVideo)
|
||||||
|
|||||||
@@ -64,23 +64,23 @@ struct GlassBackgroundModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
#if os(iOS)
|
#if os(iOS) || os(macOS)
|
||||||
if #available(iOS 26.0, *) {
|
if #available(iOS 26.0, macOS 26.0, *) {
|
||||||
content.modifier(LiquidGlassModifier(style: style, shape: shape, colorScheme: colorScheme))
|
content.modifier(LiquidGlassModifier(style: style, shape: shape, colorScheme: colorScheme))
|
||||||
} else {
|
} else {
|
||||||
content.modifier(FallbackGlassModifier(shape: shape, material: fallbackMaterial, colorScheme: colorScheme))
|
content.modifier(FallbackGlassModifier(shape: shape, material: fallbackMaterial, colorScheme: colorScheme))
|
||||||
}
|
}
|
||||||
#else
|
#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))
|
content.modifier(FallbackGlassModifier(shape: shape, material: fallbackMaterial, colorScheme: colorScheme))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - iOS 26+ Liquid Glass
|
// MARK: - iOS 26+ / macOS 26+ Liquid Glass
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS) || os(macOS)
|
||||||
@available(iOS 26.0, *)
|
@available(iOS 26.0, macOS 26.0, *)
|
||||||
private struct LiquidGlassModifier: ViewModifier {
|
private struct LiquidGlassModifier: ViewModifier {
|
||||||
let style: GlassStyle
|
let style: GlassStyle
|
||||||
let shape: GlassShape
|
let shape: GlassShape
|
||||||
|
|||||||
@@ -115,11 +115,15 @@ struct MiniPlayerView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
|
#if os(macOS)
|
||||||
|
macOSCapsuleLayout
|
||||||
|
#else
|
||||||
if isTabAccessory {
|
if isTabAccessory {
|
||||||
accessoryLayout
|
accessoryLayout
|
||||||
} else {
|
} else {
|
||||||
overlayLayout
|
overlayLayout
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.accessibilityIdentifier("player.miniPlayer")
|
.accessibilityIdentifier("player.miniPlayer")
|
||||||
.accessibilityLabel("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
|
// MARK: - Shared Components
|
||||||
|
|
||||||
/// Video preview that shows live video when available, or thumbnail as fallback.
|
/// Video preview that shows live video when available, or thumbnail as fallback.
|
||||||
|
|||||||
Reference in New Issue
Block a user