diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index c459e14..8c71423 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -544,6 +544,7 @@ struct ContentView: View { .clipped() .clipShape(RoundedRectangle(cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed)) .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) + .albumArtFlip(angle: musicManager.flipAngle) .frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12)) } .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))) diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index 30276ea..b539d8b 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -183,6 +183,7 @@ struct LockScreenMusicPanel: View { .transition(.scale.combined(with: .opacity)) } } + .albumArtFlip(angle: musicManager.flipAngle) .frame(width: size, height: size) .background(albumArtBackground(cornerRadius: cornerRadius)) } diff --git a/DynamicIsland/components/MinimalisticMusicView.swift b/DynamicIsland/components/MinimalisticMusicView.swift index e297b96..23ebb07 100644 --- a/DynamicIsland/components/MinimalisticMusicView.swift +++ b/DynamicIsland/components/MinimalisticMusicView.swift @@ -58,6 +58,7 @@ struct MinimalisticMusicView: View { ) .clipped() .clipShape(RoundedRectangle(cornerRadius: 18)) // Dramatically increased corner radius for minimalistic mode + .albumArtFlip(angle: musicManager.flipAngle) .frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12)) } .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))) diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index febb879..a18243f 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -268,11 +268,11 @@ struct MinimalisticAlbumArtView: View { Image(nsImage: musicManager.albumArt) .resizable() .aspectRatio(contentMode: .fill) - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: musicManager.isFlipping) ) .clipped() .clipShape(RoundedRectangle(cornerRadius: 12)) .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) + .albumArtFlip(angle: musicManager.flipAngle) } .buttonStyle(PlainButtonStyle()) .opacity(musicManager.isPlaying ? 1 : 0.4) diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index dbbcf18..f999438 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -65,6 +65,7 @@ struct AlbumArtView: View { albumArtImage appIconOverlay } + .albumArtFlip(angle: musicManager.flipAngle) } .buttonStyle(PlainButtonStyle()) .scaleEffect(musicManager.isPlaying ? 1 : 0.85) diff --git a/DynamicIsland/extensions/AlbumArtFlipModifier.swift b/DynamicIsland/extensions/AlbumArtFlipModifier.swift new file mode 100644 index 0000000..8ccc299 --- /dev/null +++ b/DynamicIsland/extensions/AlbumArtFlipModifier.swift @@ -0,0 +1 @@ +// Deprecated placeholder: albumArtFlip modifier now lives alongside MusicManager. diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index d51b228..3691163 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -55,8 +55,8 @@ class MusicManager: ObservableObject { private var lastArtworkAlbum: String = "Self Love" private var lastArtworkBundleIdentifier: String? = nil - @Published var isFlipping: Bool = false - private var flipWorkItem: DispatchWorkItem? + @Published var flipAngle: Double = 0 + private let flipAnimationDuration: TimeInterval = 0.45 @Published var isTransitioning: Bool = false private var transitionWorkItem: DispatchWorkItem? @@ -93,7 +93,6 @@ class MusicManager: ObservableObject { debounceIdleTask?.cancel() cancellables.removeAll() controllerCancellables.removeAll() - flipWorkItem?.cancel() transitionWorkItem?.cancel() // Release active controller @@ -159,9 +158,6 @@ class MusicManager: ObservableObject { } private func setActiveController(_ controller: any MediaControllerProtocol) { - // Cancel any existing flip animation - flipWorkItem?.cancel() - // Set new active controller activeController = controller @@ -270,19 +266,9 @@ class MusicManager: ObservableObject { } private func triggerFlipAnimation() { - // Cancel any existing animation - flipWorkItem?.cancel() - - // Create a new animation - let workItem = DispatchWorkItem { [weak self] in - self?.isFlipping = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self?.isFlipping = false - } + withAnimation(.easeInOut(duration: flipAnimationDuration)) { + flipAngle += 180 } - - flipWorkItem = workItem - DispatchQueue.main.async(execute: workItem) } private func updateArtwork(_ artworkData: Data) { @@ -447,3 +433,35 @@ class MusicManager: ObservableObject { } } } + +// MARK: - Album Art Flip Helper + +private struct AlbumArtFlipModifier: ViewModifier { + let angle: Double + + private var normalizedAngle: Double { + var value = angle.truncatingRemainder(dividingBy: 360) + if value < 0 { value += 360 } + return value + } + + private var mirrorScale: CGFloat { + (normalizedAngle > 90 && normalizedAngle < 270) ? -1 : 1 + } + + func body(content: Content) -> some View { + content + .rotation3DEffect( + .degrees(angle), + axis: (x: 0, y: 1, z: 0), + perspective: 0.65 + ) + .scaleEffect(x: mirrorScale, y: 1) + } +} + +extension View { + func albumArtFlip(angle: Double) -> some View { + modifier(AlbumArtFlipModifier(angle: angle)) + } +}