From 6646ce9d09ae80d8508d436402e621fd8967bbfb Mon Sep 17 00:00:00 2001 From: iamharshdev Date: Sun, 4 Aug 2024 01:45:10 +0530 Subject: [PATCH] Release V1: Add system-wide media playing support and new animations """ - Added support for all sorts of media playing system-wide. - Improved animations for a smoother user experience. - Introduced new states for better functionality. - Added a happy face animation to make the notch more fun. """ --- boringNotch.xcodeproj/project.pbxproj | 48 ++++- boringNotch/ContentView.swift | 11 +- boringNotch/Info.plist | 8 + boringNotch/animations/drop.swift | 29 +++ boringNotch/components/AnimatedFace.swift | 82 +++++++ boringNotch/components/BoringNotch.swift | 117 ++++++---- .../components/BottomRoundedRectangle.swift | 7 +- boringNotch/components/BuyMeCoffee.swift | 36 ++++ boringNotch/components/EmptyState.swift | 26 +++ boringNotch/components/MusicVisualizer.swift | 3 +- boringNotch/components/NotchShape.swift | 45 ++++ boringNotch/enums/generic.swift | 13 ++ boringNotch/helpers/MusicAPI.swift | 34 --- boringNotch/helpers/NowPlayingInfo.swift | 6 - boringNotch/managers/MusicManager.swift | 203 ++++++++++-------- boringNotch/managers/PlaybackManager.swift | 56 +++++ 16 files changed, 534 insertions(+), 190 deletions(-) create mode 100644 boringNotch/Info.plist create mode 100644 boringNotch/animations/drop.swift create mode 100644 boringNotch/components/AnimatedFace.swift create mode 100644 boringNotch/components/BuyMeCoffee.swift create mode 100644 boringNotch/components/EmptyState.swift create mode 100644 boringNotch/components/NotchShape.swift create mode 100644 boringNotch/enums/generic.swift delete mode 100644 boringNotch/helpers/MusicAPI.swift delete mode 100644 boringNotch/helpers/NowPlayingInfo.swift create mode 100644 boringNotch/managers/PlaybackManager.swift diff --git a/boringNotch.xcodeproj/project.pbxproj b/boringNotch.xcodeproj/project.pbxproj index b37aafc..de37f4b 100644 --- a/boringNotch.xcodeproj/project.pbxproj +++ b/boringNotch.xcodeproj/project.pbxproj @@ -9,19 +9,24 @@ /* Begin PBXBuildFile section */ 147163982C5D35B70068B555 /* MusicVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147163972C5D35B70068B555 /* MusicVisualizer.swift */; }; 1471639A2C5D35FF0068B555 /* MusicManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147163992C5D35FF0068B555 /* MusicManager.swift */; }; - 1471639D2C5D364F0068B555 /* BottomRoundedRectangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1471639C2C5D364F0068B555 /* BottomRoundedRectangle.swift */; }; 1471639F2C5D368B0068B555 /* BoringNotch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1471639E2C5D368B0068B555 /* BoringNotch.swift */; }; 147163AE2C5D7A080068B555 /* NowPlaying.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 147163AB2C5D752E0068B555 /* NowPlaying.scpt */; }; 14CEF4162C5CAED300855D72 /* boringNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CEF4152C5CAED300855D72 /* boringNotchApp.swift */; }; 14CEF4182C5CAED300855D72 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CEF4172C5CAED300855D72 /* ContentView.swift */; }; 14CEF41A2C5CAED400855D72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14CEF4192C5CAED400855D72 /* Assets.xcassets */; }; 14CEF41D2C5CAED400855D72 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14CEF41C2C5CAED400855D72 /* Preview Assets.xcassets */; }; + 14D570B62C5E961A0011E668 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570B52C5E961A0011E668 /* NotchShape.swift */; }; + 14D570B92C5E98A20011E668 /* drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570B82C5E98A20011E668 /* drop.swift */; }; + 14D570BC2C5E98EB0011E668 /* generic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570BB2C5E98EB0011E668 /* generic.swift */; }; + 14D570BE2C5EA0270011E668 /* PlaybackManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570BD2C5EA0270011E668 /* PlaybackManager.swift */; }; + 14D570C02C5EA5870011E668 /* AnimatedFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570BF2C5EA5870011E668 /* AnimatedFace.swift */; }; + 14D570C22C5EAFBF0011E668 /* EmptyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570C12C5EAFBF0011E668 /* EmptyState.swift */; }; + 14D570C42C5EBE170011E668 /* BuyMeCoffee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570C32C5EBE170011E668 /* BuyMeCoffee.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 147163972C5D35B70068B555 /* MusicVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicVisualizer.swift; sourceTree = ""; }; 147163992C5D35FF0068B555 /* MusicManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicManager.swift; sourceTree = ""; }; - 1471639C2C5D364F0068B555 /* BottomRoundedRectangle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRoundedRectangle.swift; sourceTree = ""; }; 1471639E2C5D368B0068B555 /* BoringNotch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringNotch.swift; sourceTree = ""; }; 147163AB2C5D752E0068B555 /* NowPlaying.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = NowPlaying.scpt; sourceTree = ""; }; 14CEF4122C5CAED300855D72 /* boringNotch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = boringNotch.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -30,6 +35,13 @@ 14CEF4192C5CAED400855D72 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14CEF41C2C5CAED400855D72 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 14CEF41E2C5CAED400855D72 /* boringNotch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = boringNotch.entitlements; sourceTree = ""; }; + 14D570B52C5E961A0011E668 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = ""; }; + 14D570B82C5E98A20011E668 /* drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = drop.swift; sourceTree = ""; }; + 14D570BB2C5E98EB0011E668 /* generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = generic.swift; sourceTree = ""; }; + 14D570BD2C5EA0270011E668 /* PlaybackManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackManager.swift; sourceTree = ""; }; + 14D570BF2C5EA5870011E668 /* AnimatedFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedFace.swift; sourceTree = ""; }; + 14D570C12C5EAFBF0011E668 /* EmptyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyState.swift; sourceTree = ""; }; + 14D570C32C5EBE170011E668 /* BuyMeCoffee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyMeCoffee.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -47,8 +59,11 @@ isa = PBXGroup; children = ( 147163972C5D35B70068B555 /* MusicVisualizer.swift */, - 1471639C2C5D364F0068B555 /* BottomRoundedRectangle.swift */, 1471639E2C5D368B0068B555 /* BoringNotch.swift */, + 14D570B52C5E961A0011E668 /* NotchShape.swift */, + 14D570BF2C5EA5870011E668 /* AnimatedFace.swift */, + 14D570C12C5EAFBF0011E668 /* EmptyState.swift */, + 14D570C32C5EBE170011E668 /* BuyMeCoffee.swift */, ); path = components; sourceTree = ""; @@ -56,6 +71,7 @@ 147163B52C5D804B0068B555 /* managers */ = { isa = PBXGroup; children = ( + 14D570BD2C5EA0270011E668 /* PlaybackManager.swift */, 147163992C5D35FF0068B555 /* MusicManager.swift */, ); path = managers; @@ -81,6 +97,8 @@ 14CEF4142C5CAED300855D72 /* boringNotch */ = { isa = PBXGroup; children = ( + 14D570BA2C5E98E30011E668 /* enums */, + 14D570B72C5E98960011E668 /* animations */, 147163B52C5D804B0068B555 /* managers */, 1471639B2C5D362F0068B555 /* components */, 14CEF4152C5CAED300855D72 /* boringNotchApp.swift */, @@ -100,6 +118,22 @@ path = "Preview Content"; sourceTree = ""; }; + 14D570B72C5E98960011E668 /* animations */ = { + isa = PBXGroup; + children = ( + 14D570B82C5E98A20011E668 /* drop.swift */, + ); + path = animations; + sourceTree = ""; + }; + 14D570BA2C5E98E30011E668 /* enums */ = { + isa = PBXGroup; + children = ( + 14D570BB2C5E98EB0011E668 /* generic.swift */, + ); + path = enums; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -171,12 +205,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1471639D2C5D364F0068B555 /* BottomRoundedRectangle.swift in Sources */, 147163982C5D35B70068B555 /* MusicVisualizer.swift in Sources */, + 14D570C02C5EA5870011E668 /* AnimatedFace.swift in Sources */, + 14D570B92C5E98A20011E668 /* drop.swift in Sources */, + 14D570BE2C5EA0270011E668 /* PlaybackManager.swift in Sources */, 1471639A2C5D35FF0068B555 /* MusicManager.swift in Sources */, + 14D570BC2C5E98EB0011E668 /* generic.swift in Sources */, + 14D570C42C5EBE170011E668 /* BuyMeCoffee.swift in Sources */, 1471639F2C5D368B0068B555 /* BoringNotch.swift in Sources */, 14CEF4182C5CAED300855D72 /* ContentView.swift in Sources */, + 14D570B62C5E961A0011E668 /* NotchShape.swift in Sources */, 14CEF4162C5CAED300855D72 /* boringNotchApp.swift in Sources */, + 14D570C22C5EAFBF0011E668 /* EmptyState.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/boringNotch/ContentView.swift b/boringNotch/ContentView.swift index ac41777..2a66034 100644 --- a/boringNotch/ContentView.swift +++ b/boringNotch/ContentView.swift @@ -2,20 +2,11 @@ import SwiftUI import AVFoundation import Combine -// MARK: - Data Models - -struct Song: Identifiable { - let id = UUID() - let title: String - let artist: String - let albumArt: String -} - struct ContentView: View { let onHover: () -> Void var body: some View { BoringNotch(onHover: onHover) - .frame(maxWidth: .infinity, maxHeight: 200) + .frame(maxWidth: .infinity, maxHeight: 250) .background(Color.clear) .edgesIgnoringSafeArea(.top) } diff --git a/boringNotch/Info.plist b/boringNotch/Info.plist new file mode 100644 index 0000000..edc481c --- /dev/null +++ b/boringNotch/Info.plist @@ -0,0 +1,8 @@ + + + + + NSAppleEventsUsageDescription + Read current playing music from system events + + diff --git a/boringNotch/animations/drop.swift b/boringNotch/animations/drop.swift new file mode 100644 index 0000000..ee8eb5a --- /dev/null +++ b/boringNotch/animations/drop.swift @@ -0,0 +1,29 @@ +// +// drop.swift +// boringNotch +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + +import Foundation +import SwiftUI + + +public class BoringAnimations { + @Published var notchStyle: Style = .notch + + init() { + self.notchStyle = .notch + } + + var animation: Animation { + if #available(macOS 14.0, *), notchStyle == .notch { + Animation.spring(.bouncy(duration: 0.4)) + } else { + Animation.timingCurve(0.16, 1, 0.3, 1, duration: 0.7) + } + } + + // TODO: Move all animations to this file + +} diff --git a/boringNotch/components/AnimatedFace.swift b/boringNotch/components/AnimatedFace.swift new file mode 100644 index 0000000..0877df2 --- /dev/null +++ b/boringNotch/components/AnimatedFace.swift @@ -0,0 +1,82 @@ +// +// AnimatedFace.swift +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + +import SwiftUI + +struct MinimalFaceFeatures: View { + @State private var isBlinking = false + @State var height:CGFloat = 20; + @State var width:CGFloat = 30; + + var body: some View { + VStack(spacing: 4) { // Adjusted spacing to fit within 30x30 + // Eyes + HStack(spacing: 4) { // Adjusted spacing to fit within 30x30 + Eye(isBlinking: $isBlinking) + Eye(isBlinking: $isBlinking) + } + + // Nose and mouth combined + VStack(spacing: 2) { // Adjusted spacing to fit within 30x30 + // Nose + RoundedRectangle(cornerRadius: 2) + .fill(Color.white) + .frame(width: 3, height: 10) + + // Mouth (happy) + GeometryReader { geometry in + Path { path in + let width = geometry.size.width + let height = geometry.size.height + path.move(to: CGPoint(x: 0, y: height / 2)) + path.addQuadCurve(to: CGPoint(x: width, y: height / 2), control: CGPoint(x: width / 2, y: height)) + } + .stroke(Color.white, lineWidth: 2) + } + .frame(width: 18, height: 10) + } + } + .frame(width: self.width, height: self.height) // Maximum size of face + .onAppear { + startBlinking() + } + } + + func startBlinking() { + Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in + withAnimation(.easeInOut(duration: 0.1)) { + isBlinking = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation(.easeInOut(duration: 0.1)) { + isBlinking = false + } + } + } + } +} + +struct Eye: View { + @Binding var isBlinking: Bool + + var body: some View { + RoundedRectangle(cornerRadius: 10) + .fill(Color.white) + .frame(width: 4, height: isBlinking ? 1 : 4) + .frame(maxWidth: 15, maxHeight: 15) // Adjusted max size + .animation(.easeInOut(duration: 0.1), value: isBlinking) + } +} + +struct MinimalFaceFeatures_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.black + MinimalFaceFeatures() + } + .previewLayout(.fixed(width: 60, height: 60)) // Adjusted preview size for better visibility + } +} diff --git a/boringNotch/components/BoringNotch.swift b/boringNotch/components/BoringNotch.swift index 440b3f9..4c498a4 100644 --- a/boringNotch/components/BoringNotch.swift +++ b/boringNotch/components/BoringNotch.swift @@ -10,65 +10,106 @@ import SwiftUI struct BoringNotch: View { let onHover: () -> Void @State private var isExpanded = false + @State var showEmptyState = false @StateObject private var musicManager = MusicManager() + var boringAnimations = BoringAnimations() var body: some View { ZStack { - BottomRoundedRectangle( - radius: 12) - .fill(Color.black) - .frame(width: isExpanded ? 500 : 290, height: isExpanded ? 200: 40) - .animation(.spring(), value: isExpanded) + NotchShape(cornerRadius: isExpanded ? 30: 10) + .fill(Color.black) + .frame(width: isExpanded ? 500 : 290, height: isExpanded ? 250: 40) + .animation(.spring(), value: isExpanded) + .shadow(color: .black.opacity(0.5), radius: 10) VStack { if isExpanded { Spacer() + } - HStack { - AsyncImage(url: URL(string: "https://i.scdn.co/image/ab67616d0000b2737d37ca425dc0d46cd4f79113")).frame(width: isExpanded ? 80: 20, height:isExpanded ?80: 20).scaledToFit().cornerRadius(4) + + + HStack(spacing: 4) { + + if musicManager.isPlaying || musicManager.lastUpdated.timeIntervalSinceNow > -10 { + + Image(nsImage: musicManager.albumArt).frame(width: isExpanded ? 80: 20, height:isExpanded ?80: 20).cornerRadius(isExpanded ?16:4).aspectRatio(contentMode: .fit) + // Fit the image within the frame + } + + if isExpanded { - VStack(alignment: .leading) { - Text(musicManager.songTitle) - .font(.caption) - .foregroundColor(.white) - Text(musicManager.artistName) - .font(.caption2) - .foregroundColor(.gray) - HStack(spacing: 15) { - Button(action: { - musicManager.previousTrack() - }) { - Image(systemName: "backward.fill") + if musicManager.isPlaying == true || musicManager.lastUpdated.timeIntervalSinceNow > -10 { + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading){ + Text(musicManager.songTitle) + .font(.caption) .foregroundColor(.white) + Text(musicManager.artistName) + .font(.caption2) + .foregroundColor(.gray) } - Button(action: { - musicManager.togglePlayPause() - }) { - Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill") - .foregroundColor(.white) - }.tint(.clear) - Button(action: { - musicManager.nextTrack() - }) { - Image(systemName: "forward.fill") - .foregroundColor(.white) - }.tint(.clear) - } + HStack(spacing: 15) { + Button(action: { + musicManager.previousTrack() + }) { + Image(systemName: "backward.fill") + .foregroundColor(.white).font(.title2) + }.buttonStyle(PlainButtonStyle()) + Button(action: { + musicManager.togglePlayPause() + }) { + Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill") + .foregroundColor(.white).font(.title) + }.buttonStyle(PlainButtonStyle()) + Button(action: { + musicManager.nextTrack() + }) { + Image(systemName: "forward.fill") + .foregroundColor(.white).font(.title2) + }.buttonStyle(PlainButtonStyle()) + } + }.transition(.blurReplace.animation(.spring(.bouncy(duration: 0.3)))) } - .transition(.opacity) + + if musicManager.isPlaying == false && musicManager.lastUpdated.timeIntervalSinceNow < -10 { + EmptyStateView(message: "Play some jams, ladies, and watch me shine! New features coming soon! 🎶 🚀") + } + } Spacer() - MusicVisualizer() - .frame(width: 30) - }.frame(width: isExpanded ? 480 : 280) - .padding(.horizontal, 10).padding(.vertical, 20) + if musicManager.isPlaying == false && isExpanded == false { + + MinimalFaceFeatures().transition(.blurReplace.animation(.spring(.bouncy(duration: 0.3)))) + + } + + if musicManager.isPlaying { + + MusicVisualizer() + .frame(width: 30).padding(.horizontal, isExpanded ? 8 : 2) + + } + + + + }.frame(width: isExpanded ? 440 : 270) + .padding(.horizontal, 10).padding(.vertical, isExpanded ? 10: 20) + + + // if isExpanded { + // HStack { + // BuyMeCoffee().transition(.blurReplace.animation(.spring(.bouncy(duration: 0.3)))) + // + // } + // } } } .onHover { hovering in - withAnimation(.spring()) { + withAnimation(boringAnimations.animation) { isExpanded = hovering onHover() } diff --git a/boringNotch/components/BottomRoundedRectangle.swift b/boringNotch/components/BottomRoundedRectangle.swift index ca60387..fa951e7 100644 --- a/boringNotch/components/BottomRoundedRectangle.swift +++ b/boringNotch/components/BottomRoundedRectangle.swift @@ -1,9 +1,4 @@ -// -// BottomRoundedRectangle.swift -// boringNotch -// -// Created by Harsh Vardhan Goswami on 02/08/24. -// + import SwiftUI diff --git a/boringNotch/components/BuyMeCoffee.swift b/boringNotch/components/BuyMeCoffee.swift new file mode 100644 index 0000000..d3799f6 --- /dev/null +++ b/boringNotch/components/BuyMeCoffee.swift @@ -0,0 +1,36 @@ +// +// BuyMeCoffee.swift +// boringNotch +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + +import SwiftUI +import Foundation + +struct BuyMeCoffee:View { + var body: some View { + VStack { + Button(action: { + // Open "Buy Me a Coffee" URL + if let url = URL(string: "https://www.buymeacoffee.com/yourusername") { + NSWorkspace.shared.open(url) + } + }) { + HStack { + Image(systemName: "cup.and.saucer.fill") // Coffee cup icon + .font(.system(size: 10)) + Text("Buy Me a Coffee") + .font(.system(size: 10)) + }.padding( + .horizontal, 4).padding(.vertical, 6) + .background(Color.white) // Background color + .foregroundColor(.black) // Text color + .cornerRadius(8) // Rounded corners + .shadow(radius: 5) // Shadow effect + } + .buttonStyle(PlainButtonStyle()) // Ensure default button styling is overridden + } + + } +} diff --git a/boringNotch/components/EmptyState.swift b/boringNotch/components/EmptyState.swift new file mode 100644 index 0000000..a7f2894 --- /dev/null +++ b/boringNotch/components/EmptyState.swift @@ -0,0 +1,26 @@ +// +// EmptyState.swift +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + +import SwiftUI + +struct EmptyStateView: View { + var message: String + @State private var isVisible = true + + var body: some View { + HStack { + MinimalFaceFeatures( + height: 100, width: 100) + Text(message) + .font(.system(size:14)) + .foregroundColor(.gray) + }.transition(.blurReplace.animation(.spring(.bouncy(duration: 0.3)))) // Smooth animation + } +} + +#Preview { + EmptyStateView(message: "Play some music babies") +} diff --git a/boringNotch/components/MusicVisualizer.swift b/boringNotch/components/MusicVisualizer.swift index b0470d7..608cdb1 100644 --- a/boringNotch/components/MusicVisualizer.swift +++ b/boringNotch/components/MusicVisualizer.swift @@ -19,8 +19,9 @@ struct MusicVisualizer: View { .frame(width: 3, height: amplitudes[index]) } } + .transition(.scale.animation(.spring(.bouncy(duration: 0.6)))) .onReceive(timer) { _ in - withAnimation(.easeInOut(duration: 0.1)) { + withAnimation(.spring(.bouncy(duration: 0.6))) { for i in 0..<5 { amplitudes[i] = CGFloat.random(in: 5...20) } diff --git a/boringNotch/components/NotchShape.swift b/boringNotch/components/NotchShape.swift new file mode 100644 index 0000000..85250d7 --- /dev/null +++ b/boringNotch/components/NotchShape.swift @@ -0,0 +1,45 @@ +// +// NotchShape.swift +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + +import SwiftUI + +struct NotchShape: Shape { + var topCornerRadius: CGFloat { + bottomCornerRadius - 5 + } + + var bottomCornerRadius: CGFloat + + init(cornerRadius: CGFloat? = nil) { + if cornerRadius == nil { + self.bottomCornerRadius = 10 + } else { + self.bottomCornerRadius = cornerRadius! + } + } + + func path(in rect: CGRect) -> Path { + var path = Path() + + path.addArc(center: CGPoint(x: rect.minX, y: topCornerRadius), radius: topCornerRadius, startAngle: .degrees(-90), endAngle: .degrees(0), clockwise: false) + path.addLine(to: CGPoint(x: rect.minX + topCornerRadius, y: rect.maxY - bottomCornerRadius)) + path.addArc(center: CGPoint(x: rect.minX + topCornerRadius + bottomCornerRadius, y: rect.maxY - bottomCornerRadius), radius: bottomCornerRadius, startAngle: .degrees(180), endAngle: .degrees(90), clockwise: true) + path.addLine(to: CGPoint(x: rect.maxX - topCornerRadius - bottomCornerRadius, y: rect.maxY)) + path.addArc(center: CGPoint(x: rect.maxX - topCornerRadius - bottomCornerRadius, y: rect.maxY - bottomCornerRadius), radius: bottomCornerRadius, startAngle: .degrees(90), endAngle: .degrees(0), clockwise: true) + path.addLine(to: CGPoint(x: rect.maxX - topCornerRadius, y: rect.minY + bottomCornerRadius)) + + path.addArc(center: CGPoint(x: rect.maxX, y: topCornerRadius), radius: topCornerRadius, startAngle: .degrees(180), endAngle: .degrees(270), clockwise: false) + path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) + + return path + } +} + +#Preview { + NotchShape() + .frame(width: 200, height: 32) + .padding(10) +} diff --git a/boringNotch/enums/generic.swift b/boringNotch/enums/generic.swift new file mode 100644 index 0000000..34afbee --- /dev/null +++ b/boringNotch/enums/generic.swift @@ -0,0 +1,13 @@ +// +// generic.swift +// boringNotch +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + +import Foundation + +public enum Style { + case notch + case floating +} diff --git a/boringNotch/helpers/MusicAPI.swift b/boringNotch/helpers/MusicAPI.swift deleted file mode 100644 index 1f6ee10..0000000 --- a/boringNotch/helpers/MusicAPI.swift +++ /dev/null @@ -1,34 +0,0 @@ -func fetchNowPlayingInfo() { - guard let scriptURL = Bundle.main.url(forResource: "NowPlaying", withExtension: "scpt") else { return } - var error: NSDictionary? - if let script = NSAppleScript(contentsOf: scriptURL, error: &error) { - if let output = script.executeAndReturnError(&error).stringValue { - parseNowPlayingInfo(output) - } else if let error = error { - print("Error executing AppleScript: \(error)") - } - } - } - - - func parseNowPlayingInfo(_ info: String) { - let components = info.split(separator: "||").map { String($0) } - guard components.count == 5 else { - print("Invalid now playing info format") - return - } - let trackName = components[0] - let artistName = components[1] - let albumName = components[2] - let artworkDataString = components[3] - let currentApp = components[4] - - // Handle artwork data (convert from Base64 if necessary) - var artwork: NSImage? = nil - if !artworkDataString.isEmpty, let artworkData = Data(base64Encoded: artworkDataString) { - artwork = NSImage(data: artworkData) - } - - nowPlayingInfo = NowPlayingInfo(trackName: trackName, artistName: artistName, albumName: albumName, artwork: artwork) - // Update your UI with this information - } \ No newline at end of file diff --git a/boringNotch/helpers/NowPlayingInfo.swift b/boringNotch/helpers/NowPlayingInfo.swift deleted file mode 100644 index bc11a10..0000000 --- a/boringNotch/helpers/NowPlayingInfo.swift +++ /dev/null @@ -1,6 +0,0 @@ -struct NowPlayingInfo { - var trackName: String - var artistName: String - var albumName: String - var artwork: NSImage? -} \ No newline at end of file diff --git a/boringNotch/managers/MusicManager.swift b/boringNotch/managers/MusicManager.swift index 4c87bd6..d1c5b02 100644 --- a/boringNotch/managers/MusicManager.swift +++ b/boringNotch/managers/MusicManager.swift @@ -1,3 +1,11 @@ +// +// MusicManager.swift +// boringNotch +// +// Created by Harsh Vardhan Goswami on 03/08/24. +// + + import SwiftUI import Combine import AppKit @@ -5,123 +13,136 @@ import AppKit class MusicManager: ObservableObject { private var cancellables = Set() - @Published var songTitle: String = "Blinding Lights" - @Published var artistName: String = "The Weeknd" - @Published var albumArt: String = "music.note" + @Published var songTitle: String = "I;m Handome" + @Published var artistName: String = "Me" + // use default image url for now + // i'm getting Value of optional type 'NSImage?' must be unwrapped to a value of type 'NSImage' + @Published var albumArt: NSImage = NSImage( + systemSymbolName: "music.note", + accessibilityDescription: "Album Art" + )! @Published var isPlaying = false + @Published var album: String = "Self Love" + @Published var playbackManager = PlaybackManager() + @Published var lastUpdated: Date = Date() init() { setupNowPlayingObserver() fetchNowPlayingInfo() } + private func setupNowPlayingObserver() { - NotificationCenter.default.addObserver( - self, - selector: #selector(fetchNowPlayingInfo), - name: NSNotification.Name("com.apple.iTunes.playerInfo"), - object: nil - ) - - NotificationCenter.default.addObserver( - self, - selector: #selector(fetchNowPlayingInfo), - name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"), - object: nil - ) + // every 5 seconds, fetch now playing info + Timer.publish(every: 5, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.fetchNowPlayingInfo() + } + .store(in: &cancellables) } - @objc private func fetchNowPlayingInfo() { - let scriptPath = Bundle.main.path(forResource: "NowPlaying", ofType: "scpt") + @objc func fetchNowPlayingInfo() { + print("Called fetchNowPlayingInfo") + // Load framework + guard let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")) else { return } - guard let scriptPath = scriptPath else { - print("Script path not found.") - return - } + // Get a Swift function for MRMediaRemoteGetNowPlayingInfo + guard let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString) else { return } + typealias MRMediaRemoteGetNowPlayingInfoFunction = @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void + let MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(MRMediaRemoteGetNowPlayingInfoPointer, to: MRMediaRemoteGetNowPlayingInfoFunction.self) - let scriptURL = URL(fileURLWithPath: scriptPath) + // Get a Swift function for MRNowPlayingClientGetBundleIdentifier + guard let MRNowPlayingClientGetBundleIdentifierPointer = CFBundleGetFunctionPointerForName(bundle, "MRNowPlayingClientGetBundleIdentifier" as CFString) else { return } + typealias MRNowPlayingClientGetBundleIdentifierFunction = @convention(c) (AnyObject?) -> String + let MRNowPlayingClientGetBundleIdentifier = unsafeBitCast(MRNowPlayingClientGetBundleIdentifierPointer, to: MRNowPlayingClientGetBundleIdentifierFunction.self) - do { - let script = try NSAppleScript(contentsOf: scriptURL, error: nil) - var error: NSDictionary? - if let output = script?.executeAndReturnError(&error).stringValue { - parseNowPlayingInfo(output) - } else if let error = error { - print("AppleScript Error: \(error)") + // Get song info + MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main) { [weak self] information in + guard let self = self else { self?.isPlaying = false; return } + + // check if the song is paused + if let state = information["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Int { + + + // don't update lastUpdated if the song is paused and the state is same as the previous one + if !self.isPlaying && state == 0 { + return + } + + if state == 0 { + self.lastUpdated = Date() + } + + self.isPlaying = state == 1 + playbackManager.isPlaying = state == 1 + + } - } catch { - print("Error loading AppleScript: \(error)") - } - } - - - private func parseNowPlayingInfo(_ info: String) { - let components = info.components(separatedBy: "||") - print(components) - if components.count == 4 { - songTitle = components[0] - artistName = components[1] - albumArt = components[2] - isPlaying = (components[3] == "playing") - } else { - songTitle = "Unknown Title" - artistName = "Unknown Artist" - albumArt = "music.note" - isPlaying = false + + // check if the song is same as the previous one + if let title = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String, + title == self.songTitle { + return + } + + if let artist = information["kMRMediaRemoteNowPlayingInfoArtist"] as? String { + self.artistName = artist + } + + if let title = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String { + self.songTitle = title + } + + if let album = information["kMRMediaRemoteNowPlayingInfoAlbum"] as? String { + print("Album: \(album)") + self.album = album + } + + if let duration = information["kMRMediaRemoteNowPlayingInfoDuration"] as? String { + print("Duration: \(duration)") + } + + if let artworkData = information["kMRMediaRemoteNowPlayingInfoArtworkData"] as? Data, + let artworkImage = NSImage(data: artworkData) { + self.albumArt = artworkImage + print("artworkData : \(artworkData)") + } + + // Get bundle identifier + let _MRNowPlayingClientProtobuf: AnyClass? = NSClassFromString("MRClient") + let handle: UnsafeMutableRawPointer! = dlopen("/usr/lib/libobjc.A.dylib", RTLD_NOW) + let object = unsafeBitCast(dlsym(handle, "objc_msgSend"), to: (@convention(c) (AnyClass?, Selector?) -> AnyObject).self)(_MRNowPlayingClientProtobuf, Selector(("alloc"))) + unsafeBitCast(dlsym(handle, "objc_msgSend"), to: (@convention(c) (AnyObject?, Selector?, Any?) -> Void).self)(object, Selector(("initWithData:")), information["kMRMediaRemoteNowPlayingInfoClientPropertiesData"] as AnyObject?) + let bundleIdentifier = MRNowPlayingClientGetBundleIdentifier(object) + dlclose(handle) } } func togglePlayPause() { + // Implement play/pause functionality + playbackManager.playPause() + isPlaying = playbackManager.isPlaying + if isPlaying { - executeAppleScript(script: "tell application \"System Events\" to tell process \"\(currentPlayerProcess())\" to keystroke space") - } else { - executeAppleScript(script: "tell application \"System Events\" to tell process \"\(currentPlayerProcess())\" to keystroke space") + fetchNowPlayingInfo() + } + + if !isPlaying { + lastUpdated = Date() } } func nextTrack() { - let script = """ - tell application "System Events" - if (exists (processes where name is "Music")) then - tell application "Music" to next track - else if (exists (processes where name is "Spotify")) then - tell application "Spotify" to next track - end if - end tell - """ - executeAppleScript(script: script) + // Implement next track functionality + playbackManager.nextTrack() + fetchNowPlayingInfo() } func previousTrack() { - let script = """ - tell application "System Events" - if (exists (processes where name is "Music")) then - tell application "Music" to previous track - else if (exists (processes where name is "Spotify")) then - tell application "Spotify" to previous track - end if - end tell - """ - executeAppleScript(script: script) - } - - private func executeAppleScript(script: String) { - var error: NSDictionary? - if let scriptObject = NSAppleScript(source: script) { - scriptObject.executeAndReturnError(&error) - if let error = error { - print("AppleScript Error: \(error)") - } - } + // Implement previous track functionality + playbackManager.previousTrack() + fetchNowPlayingInfo() } - private func currentPlayerProcess() -> String { - if NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music").count > 0 { - return "Music" - } else if NSRunningApplication.runningApplications(withBundleIdentifier: "com.spotify.client").count > 0 { - return "Spotify" - } else { - return "" - } - } } diff --git a/boringNotch/managers/PlaybackManager.swift b/boringNotch/managers/PlaybackManager.swift new file mode 100644 index 0000000..16f04f3 --- /dev/null +++ b/boringNotch/managers/PlaybackManager.swift @@ -0,0 +1,56 @@ +// +// PlaybackManager.swift +// boringNotch +// +// Created by Harsh Vardhan Goswami on 04/08/24. +// + + +import SwiftUI +import AppKit +import Combine + +class PlaybackManager: ObservableObject { + @Published var isPlaying = false + @Published var MrMediaRemoteSendCommandFunction:@convention(c) (Int, AnyObject?) -> Void + + init() { + self.isPlaying = false; + self.MrMediaRemoteSendCommandFunction = {_,_ in } + handleLoadMediaHandlerApis() + } + + private func handleLoadMediaHandlerApis(){ + // Load framework + guard let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")) else { return } + + // Get a Swift function for MRMediaRemoteSendCommand + guard let MRMediaRemoteSendCommandPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteSendCommand" as CFString) else { return } + + typealias MRMediaRemoteSendCommandFunction = @convention(c) (Int, AnyObject?) -> Void + + MrMediaRemoteSendCommandFunction = unsafeBitCast(MRMediaRemoteSendCommandPointer, to: MRMediaRemoteSendCommandFunction.self) + } + + + func playPause() { + if self.isPlaying { + MrMediaRemoteSendCommandFunction(2, nil) + self.isPlaying = false; + } else { + MrMediaRemoteSendCommandFunction(1, nil) + MrMediaRemoteSendCommandFunction(16, nil) + self.isPlaying = true + } + } + + func nextTrack() { + // Implement next track action + MrMediaRemoteSendCommandFunction(4, nil) + } + + func previousTrack() { + // Implement previous track action + MrMediaRemoteSendCommandFunction(3, nil) + } +}