diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..1925113
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..64e1baa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,64 @@
+# Xcode
+#
+build/
+DerivedData/
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+*.xcscmblueprint
+
+# SwiftPM
+#
+.build/
+Package.resolved
+
+# CocoaPods
+#
+Pods/
+Podfile.lock
+
+# Carthage
+#
+Carthage/Build/
+
+# Fastlane
+#
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+
+# Archives
+#
+*.xcarchive
+
+# App data
+#
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+# Playgrounds
+#
+timeline.xctimeline
+playground.xcworkspace
+
+# Xcode Server
+#
+integration.json
+
+# Other
+#
+*.moved-aside
+*.xcuserstate
+
+# GitHub instructions and agent files
+#
+.github/
+rough_note.md
\ No newline at end of file
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/Contents.json b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/Contents.json
index 3f00db4..519bc19 100644
--- a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,51 +1,61 @@
{
"images" : [
{
+ "filename" : "notch-stage-icon2 2.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
+ "filename" : "notch-stage-icon2 5.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
+ "filename" : "notch-stage-icon2 6.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
+ "filename" : "notch-stage-icon2 11.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
+ "filename" : "notch-stage-icon2 12.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
+ "filename" : "notch-stage-icon2 13.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
+ "filename" : "notch-stage-icon2 7.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
+ "filename" : "notch-stage-icon2 8.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
+ "filename" : "notch-stage-icon2 9.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
+ "filename" : "notch-stage-icon2 10.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 10.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 10.png
new file mode 100644
index 0000000..f114f25
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 10.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 11.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 11.png
new file mode 100644
index 0000000..166997a
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 11.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 12.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 12.png
new file mode 100644
index 0000000..e0ab74d
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 12.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 13.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 13.png
new file mode 100644
index 0000000..c2d2b43
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 13.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 2.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 2.png
new file mode 100644
index 0000000..2b897b3
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 2.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 5.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 5.png
new file mode 100644
index 0000000..394b128
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 5.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 6.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 6.png
new file mode 100644
index 0000000..394b128
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 6.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 7.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 7.png
new file mode 100644
index 0000000..c2d2b43
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 7.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 8.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 8.png
new file mode 100644
index 0000000..770e71c
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 8.png differ
diff --git a/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 9.png b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 9.png
new file mode 100644
index 0000000..770e71c
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/AppIcon.appiconset/notch-stage-icon2 9.png differ
diff --git a/DynamicIsland/Assets.xcassets/Github.imageset/Contents.json b/DynamicIsland/Assets.xcassets/Github.imageset/Contents.json
new file mode 100644
index 0000000..d750f10
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/Github.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "GitHub Mark White.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "GitHub Mark White 1.svg",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "GitHub Mark White 2.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White 1.svg b/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White 1.svg
new file mode 100644
index 0000000..d5e6491
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White 1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White 2.svg b/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White 2.svg
new file mode 100644
index 0000000..d5e6491
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White 2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White.svg b/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White.svg
new file mode 100644
index 0000000..d5e6491
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/Github.imageset/GitHub Mark White.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/DynamicIsland/Assets.xcassets/bolt.imageset/Contents.json b/DynamicIsland/Assets.xcassets/bolt.imageset/Contents.json
new file mode 100644
index 0000000..6909fcc
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/bolt.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "bolt.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/bolt.imageset/bolt.png b/DynamicIsland/Assets.xcassets/bolt.imageset/bolt.png
new file mode 100644
index 0000000..9544fe6
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/bolt.imageset/bolt.png differ
diff --git a/DynamicIsland/Assets.xcassets/chrome.imageset/Contents.json b/DynamicIsland/Assets.xcassets/chrome.imageset/Contents.json
new file mode 100644
index 0000000..f0f188f
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/chrome.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "Google Chrome macOS BigSur Icon.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "Google Chrome macOS BigSur Icon 1.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "Google Chrome macOS BigSur Icon 2.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon 1.png b/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon 1.png
new file mode 100644
index 0000000..1cce1cd
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon 1.png differ
diff --git a/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon 2.png b/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon 2.png
new file mode 100644
index 0000000..1cce1cd
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon 2.png differ
diff --git a/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon.png b/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon.png
new file mode 100644
index 0000000..1cce1cd
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/chrome.imageset/Google Chrome macOS BigSur Icon.png differ
diff --git a/DynamicIsland/Assets.xcassets/defaultmusic.imageset/Contents.json b/DynamicIsland/Assets.xcassets/defaultmusic.imageset/Contents.json
new file mode 100644
index 0000000..a19a549
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/defaultmusic.imageset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/dynamicisland.imageset/Contents.json b/DynamicIsland/Assets.xcassets/dynamicisland.imageset/Contents.json
new file mode 100644
index 0000000..d6b705b
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/dynamicisland.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "TheBoringTeam.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/dynamicisland.imageset/TheBoringTeam.svg b/DynamicIsland/Assets.xcassets/dynamicisland.imageset/TheBoringTeam.svg
new file mode 100644
index 0000000..032a961
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/dynamicisland.imageset/TheBoringTeam.svg
@@ -0,0 +1,3 @@
+
diff --git a/DynamicIsland/Assets.xcassets/logo.imageset/256-mac 1.png b/DynamicIsland/Assets.xcassets/logo.imageset/256-mac 1.png
new file mode 100644
index 0000000..3bc82b0
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/logo.imageset/256-mac 1.png differ
diff --git a/DynamicIsland/Assets.xcassets/logo.imageset/Contents.json b/DynamicIsland/Assets.xcassets/logo.imageset/Contents.json
new file mode 100644
index 0000000..e05b766
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/logo.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "256-mac 1.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/logo2.imageset/BoringNotch icon.png b/DynamicIsland/Assets.xcassets/logo2.imageset/BoringNotch icon.png
new file mode 100644
index 0000000..d233e5f
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/logo2.imageset/BoringNotch icon.png differ
diff --git a/DynamicIsland/Assets.xcassets/logo2.imageset/Contents.json b/DynamicIsland/Assets.xcassets/logo2.imageset/Contents.json
new file mode 100644
index 0000000..37d84e3
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/logo2.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "BoringNotch icon.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/plug.imageset/Contents.json b/DynamicIsland/Assets.xcassets/plug.imageset/Contents.json
new file mode 100644
index 0000000..7356dce
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/plug.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "plug.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/plug.imageset/plug.png b/DynamicIsland/Assets.xcassets/plug.imageset/plug.png
new file mode 100644
index 0000000..c4ddd2a
Binary files /dev/null and b/DynamicIsland/Assets.xcassets/plug.imageset/plug.png differ
diff --git a/DynamicIsland/Assets.xcassets/sparkle.imageset/Contents.json b/DynamicIsland/Assets.xcassets/sparkle.imageset/Contents.json
new file mode 100644
index 0000000..138a4dc
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/sparkle.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "sparkle.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/sparkle.imageset/sparkle.svg b/DynamicIsland/Assets.xcassets/sparkle.imageset/sparkle.svg
new file mode 100644
index 0000000..5a4a86e
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/sparkle.imageset/sparkle.svg
@@ -0,0 +1,18 @@
+
diff --git a/DynamicIsland/Assets.xcassets/spotlight.imageset/Contents.json b/DynamicIsland/Assets.xcassets/spotlight.imageset/Contents.json
new file mode 100644
index 0000000..896a107
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/spotlight.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "spotlight.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DynamicIsland/Assets.xcassets/spotlight.imageset/spotlight.svg b/DynamicIsland/Assets.xcassets/spotlight.imageset/spotlight.svg
new file mode 100644
index 0000000..272ceec
--- /dev/null
+++ b/DynamicIsland/Assets.xcassets/spotlight.imageset/spotlight.svg
@@ -0,0 +1,35 @@
+
diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift
index 3810b35..db43dfe 100644
--- a/DynamicIsland/ContentView.swift
+++ b/DynamicIsland/ContentView.swift
@@ -1,24 +1,521 @@
//
// ContentView.swift
-// DynamicIsland
+// boringNotchApp
//
-// Created by admin63 on 01/11/24.
+// Created by Harsh Vardhan Goswami on 02/08/24
+// Modified by Richard Kunkli on 24/08/2024.
//
+import AVFoundation
+import Combine
+import Defaults
+import KeyboardShortcuts
import SwiftUI
+import SwiftUIIntrospect
struct ContentView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @StateObject var webcamManager: WebcamManager = .init()
+
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @ObservedObject var musicManager = MusicManager.shared
+ @ObservedObject var batteryModel = BatteryStatusViewModel.shared
+
+ @State private var isHovering: Bool = false
+ @State private var hoverWorkItem: DispatchWorkItem?
+ @State private var debounceWorkItem: DispatchWorkItem?
+
+ @State private var isHoverStateChanging: Bool = false
+
+ @State private var gestureProgress: CGFloat = .zero
+
+ @State private var haptics: Bool = false
+
+ @Namespace var albumArtNamespace
+
+ @Default(.useMusicVisualizer) var useMusicVisualizer
+
+ @Default(.showNotHumanFace) var showNotHumanFace
+ @Default(.useModernCloseAnimation) var useModernCloseAnimation
+
+ private let extendedHoverPadding: CGFloat = 30
+ private let zeroHeightHoverPadding: CGFloat = 10
+
var body: some View {
- VStack {
- Image(systemName: "globe")
- .imageScale(.large)
- .foregroundStyle(.tint)
- Text("Hello, world!")
+ ZStack(alignment: .top) {
+ let mainLayout = NotchLayout()
+ .frame(alignment: .top)
+ .padding(.horizontal, vm.notchState == .open
+ ? Defaults[.cornerRadiusScaling] ? (cornerRadiusInsets.opened.top - 5) : (cornerRadiusInsets.opened.bottom - 5)
+ : cornerRadiusInsets.closed.bottom
+ )
+ .padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
+ .background(.black)
+ .mask {
+ ((vm.notchState == .open) && Defaults[.cornerRadiusScaling])
+ ? NotchShape(topCornerRadius: cornerRadiusInsets.opened.top, bottomCornerRadius: cornerRadiusInsets.opened.bottom)
+ .drawingGroup()
+ : NotchShape(topCornerRadius: cornerRadiusInsets.closed.top, bottomCornerRadius: cornerRadiusInsets.closed.bottom)
+ .drawingGroup()
+ }
+ .padding(.bottom, vm.notchState == .open && Defaults[.extendHoverArea] ? 0 : (vm.effectiveClosedNotchHeight == 0)
+ ? zeroHeightHoverPadding
+ : 0
+ )
+
+ mainLayout
+ .conditionalModifier(!useModernCloseAnimation) { view in
+ let hoverAnimationAnimation = Animation.bouncy.speed(1.2)
+ let notchStateAnimation = Animation.spring.speed(1.2)
+ return view
+ .animation(hoverAnimationAnimation, value: isHovering)
+ .animation(notchStateAnimation, value: vm.notchState)
+ .animation(.smooth, value: gestureProgress)
+ .transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
+ }
+ .conditionalModifier(useModernCloseAnimation) { view in
+ let hoverAnimationAnimation = Animation.bouncy.speed(1.2)
+ let notchStateAnimation = Animation.spring.speed(1.2)
+ return view
+ .animation(hoverAnimationAnimation, value: isHovering)
+ .animation(notchStateAnimation, value: vm.notchState)
+ }
+ .conditionalModifier(Defaults[.openNotchOnHover]) { view in
+ view.onHover { hovering in
+ handleHover(hovering)
+ }
+ }
+ .conditionalModifier(!Defaults[.openNotchOnHover]) { view in
+ view
+ .onHover { hovering in
+ withAnimation(vm.animation) {
+ isHovering = hovering
+ }
+
+ // Only close if mouse leaves and the notch is open
+ if !hovering && vm.notchState == .open {
+ vm.close()
+ }
+ }
+ .onTapGesture {
+ if (vm.notchState == .closed) && Defaults[.enableHaptics] {
+ haptics.toggle()
+ }
+ doOpen()
+ }
+ .conditionalModifier(Defaults[.enableGestures]) { view in
+ view
+ .panGesture(direction: .down) { translation, phase in
+ handleDownGesture(translation: translation, phase: phase)
+ }
+ }
+ }
+ .conditionalModifier(Defaults[.closeGestureEnabled] && Defaults[.enableGestures]) { view in
+ view
+ .panGesture(direction: .up) { translation, phase in
+ handleUpGesture(translation: translation, phase: phase)
+ }
+ }
+ .onAppear(perform: {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ withAnimation(vm.animation) {
+ if coordinator.firstLaunch {
+ doOpen()
+ }
+ }
+ }
+ })
+ .onChange(of: vm.notchState) { _, newState in
+ // Reset hover state when notch state changes
+ if newState == .closed && isHovering {
+ // Only reset visually, without triggering the hover logic again
+ isHoverStateChanging = true
+ withAnimation {
+ isHovering = false
+ }
+ // Reset the flag after the animation completes
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ isHoverStateChanging = false
+ }
+ }
+ }
+ .onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ if !newPopoverState && !isHovering && vm.notchState == .open {
+ vm.close()
+ }
+ }
+ }
+ .sensoryFeedback(.alignment, trigger: haptics)
+ .contextMenu {
+ SettingsLink(label: {
+ Text("Settings")
+ })
+ .keyboardShortcut(KeyEquivalent(","), modifiers: .command)
+// Button("Edit") { // Doesnt work....
+// let dn = DynamicNotch(content: EditPanelView())
+// dn.toggle()
+// }
+// #if DEBUG
+// .disabled(false)
+// #else
+// .disabled(true)
+// #endif
+// .keyboardShortcut("E", modifiers: .command)
+ }
+ }
+ .frame(maxWidth: openNotchSize.width, maxHeight: openNotchSize.height, alignment: .top)
+ .shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5)
+ .background(dragDetector)
+ .environmentObject(vm)
+ .environmentObject(webcamManager)
+ }
+
+ @ViewBuilder
+ func NotchLayout() -> some View {
+ VStack(alignment: .leading) {
+ VStack(alignment: .leading) {
+ if coordinator.firstLaunch {
+ Spacer()
+ HelloAnimation().frame(width: 200, height: 80).onAppear(perform: {
+ vm.closeHello()
+ })
+ .padding(.top, 40)
+ Spacer()
+ } else {
+ if coordinator.expandingView.type == .battery && coordinator.expandingView.show && vm.notchState == .closed && Defaults[.showPowerStatusNotifications] {
+ HStack(spacing: 0) {
+ HStack {
+ Text(batteryModel.statusText)
+ .font(.subheadline)
+ }
+
+ Rectangle()
+ .fill(.black)
+ .frame(width: vm.closedNotchSize.width + 10)
+
+ HStack {
+ DynamicIslandBatteryView(
+ batteryWidth: 30,
+ isCharging: batteryModel.isCharging,
+ isInLowPowerMode: batteryModel.isInLowPowerMode,
+ isPluggedIn: batteryModel.isPluggedIn,
+ levelBattery: batteryModel.levelBattery,
+ isForNotification: true
+ )
+ }
+ .frame(width: 76, alignment: .trailing)
+ }
+ .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
+ } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) {
+ InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
+ .transition(.opacity)
+ } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed {
+ MusicLiveActivity()
+ } else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
+ DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle)
+ } else if vm.notchState == .open {
+ DynamicIslandHeader()
+ .frame(height: max(24, vm.effectiveClosedNotchHeight))
+ .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
+ .animation(.spring(response: 1, dampingFraction: 1, blendDuration: 0.8), value: vm.notchState)
+ } else {
+ Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight)
+ }
+
+ if coordinator.sneakPeek.show {
+ if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && !Defaults[.inlineHUD] {
+ SystemEventIndicatorModifier(eventType: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, sendEventBack: { _ in
+ //
+ })
+ .padding(.bottom, 10)
+ .padding(.leading, 4)
+ .padding(.trailing, 8)
+ }
+ // Old sneak peek music
+ else if coordinator.sneakPeek.type == .music {
+ if vm.notchState == .closed && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard {
+ HStack(alignment: .center) {
+ Image(systemName: "music.note")
+ GeometryReader { geo in
+ MarqueeText(.constant(musicManager.songTitle + " - " + musicManager.artistName), textColor: .gray, minDuration: 1, frameWidth: geo.size.width)
+ }
+ }
+ .foregroundStyle(.gray)
+ .padding(.bottom, 10)
+ }
+ }
+ }
+ }
+ }
+ .conditionalModifier((coordinator.sneakPeek.show && (coordinator.sneakPeek.type == .music) && vm.notchState == .closed && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard) || (coordinator.sneakPeek.show && (coordinator.sneakPeek.type != .music) && (vm.notchState == .closed))) { view in
+ view
+ .fixedSize()
+ }
+ .zIndex(2)
+
+ ZStack {
+ if vm.notchState == .open {
+ switch coordinator.currentView {
+ case .home:
+ NotchHomeView(albumArtNamespace: albumArtNamespace)
+ case .shelf:
+ NotchShelfView()
+ }
+ }
+ }
+ .zIndex(1)
+ .allowsHitTesting(vm.notchState == .open)
+ .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
+ .opacity(abs(gestureProgress) > 0.3 ? min(abs(gestureProgress * 2), 0.8) : 1)
+ }
+ }
+
+ @ViewBuilder
+ func DynamicIslandFaceAnimation() -> some View {
+ HStack {
+ HStack {
+ Rectangle()
+ .fill(.clear)
+ .frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12))
+ Rectangle()
+ .fill(.black)
+ .frame(width: vm.closedNotchSize.width - 20)
+ MinimalFaceFeatures()
+ }
+ }.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
+ }
+
+ @ViewBuilder
+ func MusicLiveActivity() -> some View {
+ HStack {
+ HStack {
+ Color.clear
+ .aspectRatio(1, contentMode: .fit)
+ .background(
+ Image(nsImage: musicManager.albumArt)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ )
+ .clipped()
+ .clipShape(RoundedRectangle(cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed))
+ .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
+ .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)))
+
+ Rectangle()
+ .fill(.black)
+ .overlay(
+ HStack(alignment: .top){
+ if(coordinator.expandingView.show && coordinator.expandingView.type == .music) {
+ MarqueeText(
+ .constant(musicManager.songTitle),
+ textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray,
+ minDuration: 0.4,
+ frameWidth: 100
+ )
+ .opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
+ Spacer(minLength: vm.closedNotchSize.width)
+ // Song Artist
+ Text(musicManager.artistName)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .foregroundStyle(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray)
+ .opacity((coordinator.expandingView.show && coordinator.expandingView.type == .music && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
+ }
+ }
+ )
+ .frame(width: (coordinator.expandingView.show && coordinator.expandingView.type == .music && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 380 : vm.closedNotchSize.width + (isHovering ? 8 : 0))
+
+
+ HStack {
+ if useMusicVisualizer {
+ Rectangle()
+ .fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient)
+ .frame(width: 50, alignment: .center)
+ .matchedGeometryEffect(id: "spectrum", in: albumArtNamespace)
+ .mask {
+ AudioSpectrumView(isPlaying: $musicManager.isPlaying)
+ .frame(width: 16, height: 12)
+ }
+ .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2),
+ height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center)
+ } else {
+ LottieAnimationView()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+ .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2),
+ height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center)
+ }
+ .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
+ }
+
+ @ViewBuilder
+ var dragDetector: some View {
+ if Defaults[.dynamicShelf] {
+ Color.clear
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .contentShape(Rectangle())
+ .onDrop(of: [.data], isTargeted: $vm.dragDetectorTargeting) { _ in true }
+ .onChange(of: vm.anyDropZoneTargeting) { _, isTargeted in
+ if isTargeted, vm.notchState == .closed {
+ coordinator.currentView = .shelf
+ doOpen()
+ } else if !isTargeted {
+ print("DROP EVENT", vm.dropEvent)
+ if vm.dropEvent {
+ vm.dropEvent = false
+ return
+ }
+
+ vm.dropEvent = false
+ vm.close()
+ }
+ }
+ } else {
+ EmptyView()
+ }
+ }
+
+ private func doOpen() {
+ withAnimation(.bouncy.speed(1.2)) {
+ vm.open()
+ }
+ }
+
+ // MARK: - Hover Management
+
+ /// Handle hover state changes with debouncing
+ private func handleHover(_ hovering: Bool) {
+ // Don't process events if we're already transitioning
+ if isHoverStateChanging { return }
+
+ // Cancel any pending tasks
+ hoverWorkItem?.cancel()
+ hoverWorkItem = nil
+ debounceWorkItem?.cancel()
+ debounceWorkItem = nil
+
+ if hovering {
+ // Handle mouse enter
+ withAnimation(.bouncy.speed(1.2)) {
+ isHovering = true
+ }
+
+ // Only provide haptic feedback if notch is closed
+ if vm.notchState == .closed && Defaults[.enableHaptics] {
+ haptics.toggle()
+ }
+
+ // Don't open notch if there's a sneak peek showing
+ if coordinator.sneakPeek.show {
+ return
+ }
+
+ // Delay opening the notch
+ let task = DispatchWorkItem {
+ // ContentView is a struct, so we don't use weak self here
+ guard vm.notchState == .closed, isHovering else { return }
+ doOpen()
+ }
+
+ hoverWorkItem = task
+ DispatchQueue.main.asyncAfter(
+ deadline: .now() + Defaults[.minimumHoverDuration],
+ execute: task
+ )
+ } else {
+ // Handle mouse exit with debounce to prevent flickering
+ let debounce = DispatchWorkItem {
+ // ContentView is a struct, so we don't use weak self here
+
+ // Update visual state
+ withAnimation(.bouncy.speed(1.2)) {
+ isHovering = false
+ }
+
+ // Close the notch if it's open and battery popover is not active
+ if vm.notchState == .open && !vm.isBatteryPopoverActive {
+ vm.close()
+ }
+ }
+
+ debounceWorkItem = debounce
+ // Add a small delay to debounce rapid mouse movements
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: debounce)
+ }
+ }
+
+ // MARK: - Gesture Handling
+
+ private func handleDownGesture(translation: CGFloat, phase: NSEvent.Phase) {
+ guard vm.notchState == .closed else { return }
+
+ withAnimation(.smooth) {
+ gestureProgress = (translation / Defaults[.gestureSensitivity]) * 20
+ }
+
+ if phase == .ended {
+ withAnimation(.smooth) {
+ gestureProgress = .zero
+ }
+ }
+
+ if translation > Defaults[.gestureSensitivity] {
+ if Defaults[.enableHaptics] {
+ haptics.toggle()
+ }
+ withAnimation(.smooth) {
+ gestureProgress = .zero
+ }
+ doOpen()
+ }
+ }
+
+ private func handleUpGesture(translation: CGFloat, phase: NSEvent.Phase) {
+ if vm.notchState == .open && !vm.isHoveringCalendar {
+ withAnimation(.smooth) {
+ gestureProgress = (translation / Defaults[.gestureSensitivity]) * -20
+ }
+
+ if phase == .ended {
+ withAnimation(.smooth) {
+ gestureProgress = .zero
+ }
+ }
+
+ if translation > Defaults[.gestureSensitivity] {
+ withAnimation(.smooth) {
+ gestureProgress = .zero
+ isHovering = false
+ }
+ vm.close()
+
+ if Defaults[.enableHaptics] {
+ haptics.toggle()
+ }
+ }
}
- .padding()
}
}
-#Preview {
- ContentView()
+struct FullScreenDropDelegate: DropDelegate {
+ @Binding var isTargeted: Bool
+ let onDrop: () -> Void
+
+ func dropEntered(info _: DropInfo) {
+ isTargeted = true
+ }
+
+ func dropExited(info _: DropInfo) {
+ isTargeted = false
+ }
+
+ func performDrop(info _: DropInfo) -> Bool {
+ isTargeted = false
+ onDrop()
+ return true
+ }
}
diff --git a/DynamicIsland/DynamicIsland.entitlements b/DynamicIsland/DynamicIsland.entitlements
index 8d636b5..6d10c93 100644
--- a/DynamicIsland/DynamicIsland.entitlements
+++ b/DynamicIsland/DynamicIsland.entitlements
@@ -2,24 +2,29 @@
- com.apple.security.app-sandbox
-
- com.apple.security.automation.apple-events
-
- com.apple.security.device.camera
-
- com.apple.security.files.user-selected.read-write
-
- com.apple.security.network.client
-
- com.apple.security.network.server
-
- com.apple.security.personal-information.calendars
-
- com.apple.security.temporary-exception.mach-lookup.global-name
-
- $(PRODUCT_BUNDLE_IDENTIFIER)-spks
- $(PRODUCT_BUNDLE_IDENTIFIER)-spki
-
+ com.apple.security.temporary-exception.apple-events
+
+ com.spotify.client
+ com.apple.Music
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.automation.apple-events
+
+ com.apple.security.device.camera
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.personal-information.calendars
+
+ com.apple.security.temporary-exception.mach-lookup.global-name
+
+ $(PRODUCT_BUNDLE_IDENTIFIER)-spks
+ $(PRODUCT_BUNDLE_IDENTIFIER)-spki
+
diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift
index 9a8db69..cc1052c 100644
--- a/DynamicIsland/DynamicIslandApp.swift
+++ b/DynamicIsland/DynamicIslandApp.swift
@@ -2,16 +2,398 @@
// DynamicIslandApp.swift
// DynamicIsland
//
-// Created by admin63 on 01/11/24.
+// Created by Harsh Vardhan Goswami on 02/08/24.
//
+import AVFoundation
+import Combine
+import KeyboardShortcuts
+import Sparkle
import SwiftUI
+import Defaults
@main
-struct DynamicIslandApp: App {
+struct DynamicNotchApp: App {
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+ @Default(.menubarIcon) var showMenuBarIcon
+ @Environment(\.openWindow) var openWindow
+ let updaterController: SPUStandardUpdaterController
+
+ init() {
+ updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
+ }
+
var body: some Scene {
- WindowGroup {
- ContentView()
+ MenuBarExtra("dynamic.island", systemImage: "sparkle", isInserted: $showMenuBarIcon) {
+ SettingsLink(label: {
+ Text("Settings")
+ })
+ .keyboardShortcut(KeyEquivalent(","), modifiers: .command)
+ if false {
+ Button("Activate License") {
+ openWindow(id: "activation")
+ }
+ }
+ CheckForUpdatesView(updater: updaterController.updater)
+ Divider()
+ Button("Restart Dynamic Island") {
+ guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
+
+ let workspace = NSWorkspace.shared
+
+ if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) {
+
+ let configuration = NSWorkspace.OpenConfiguration()
+ configuration.createsNewApplicationInstance = true
+
+ workspace.openApplication(at: appURL, configuration: configuration)
+ }
+
+ NSApplication.shared.terminate(nil)
+ }
+ Button("Quit", role: .destructive) {
+ NSApp.terminate(nil)
+ }
+ .keyboardShortcut(KeyEquivalent("Q"), modifiers: .command)
}
+
+ Settings {
+ SettingsView(updaterController: updaterController)
+ }
+ .defaultSize(CGSize(width: 750, height: 700))
+
+ Window("Onboarding", id: "onboarding") {
+ ProOnboard()
+ }
+ .windowStyle(.hiddenTitleBar)
+ .windowResizability(.contentSize)
+
+ Window("Activation", id: "activation") {
+ ActivationWindow()
+ }
+ .windowStyle(.hiddenTitleBar)
+ .windowResizability(.contentSize)
+ }
+}
+
+class AppDelegate: NSObject, NSApplicationDelegate {
+ var statusItem: NSStatusItem?
+ var windows: [NSScreen: NSWindow] = [:]
+ var viewModels: [NSScreen: DynamicIslandViewModel] = [:]
+ var window: NSWindow?
+ let vm: DynamicIslandViewModel = .init()
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ var whatsNewWindow: NSWindow?
+ var timer: Timer?
+ let calenderManager = CalendarManager()
+ var closeNotchWorkItem: DispatchWorkItem?
+ private var previousScreens: [NSScreen]?
+ @Environment(\.openWindow) var openWindow
+
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return false
+ }
+
+ func applicationWillTerminate(_ notification: Notification) {
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ @objc func onScreenLocked(_: Notification) {
+ print("Screen locked")
+ cleanupWindows()
+ }
+
+ @objc func onScreenUnlocked(_: Notification) {
+ print("Screen unlocked")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ self?.cleanupWindows()
+ self?.adjustWindowPosition(changeAlpha: true)
+ }
+ }
+
+ private func cleanupWindows(shouldInvert: Bool = false) {
+ if shouldInvert ? !Defaults[.showOnAllDisplays] : Defaults[.showOnAllDisplays] {
+ for window in windows.values {
+ window.close()
+ NotchSpaceManager.shared.notchSpace.windows.remove(window)
+ }
+ windows.removeAll()
+ viewModels.removeAll()
+ } else if let window = window {
+ window.close()
+ NotchSpaceManager.shared.notchSpace.windows.remove(window)
+ self.window = nil
+ }
+ }
+
+ private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel) -> NSWindow {
+ let window = DynamicIslandWindow(
+ contentRect: NSRect(x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height),
+ styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow],
+ backing: .buffered,
+ defer: false
+ )
+
+ window.contentView = NSHostingView(
+ rootView: ContentView()
+ .environmentObject(viewModel)
+ )
+
+ window.orderFrontRegardless()
+ NotchSpaceManager.shared.notchSpace.windows.insert(window)
+ return window
+ }
+
+ private func positionWindow(_ window: NSWindow, on screen: NSScreen, changeAlpha: Bool = false) {
+ if changeAlpha {
+ window.alphaValue = 0
+ }
+
+ DispatchQueue.main.async { [weak window] in
+ guard let window = window else { return }
+ let screenFrame = screen.frame
+ window.setFrameOrigin(NSPoint(
+ x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2,
+ y: screenFrame.origin.y + screenFrame.height - window.frame.height
+ ))
+ window.alphaValue = 1
+ }
+ }
+
+ func applicationDidFinishLaunching(_ notification: Notification) {
+
+ coordinator.setupWorkersNotificationObservers();
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(screenConfigurationDidChange),
+ name: NSApplication.didChangeScreenParametersNotification,
+ object: nil
+ )
+
+ NotificationCenter.default.addObserver(forName: Notification.Name.selectedScreenChanged, object: nil, queue: nil) { [weak self] _ in
+ self?.adjustWindowPosition(changeAlpha: true)
+ }
+
+ NotificationCenter.default.addObserver(forName: Notification.Name.notchHeightChanged, object: nil, queue: nil) { [weak self] _ in
+ self?.adjustWindowPosition()
+ }
+
+ NotificationCenter.default.addObserver(forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil) { [weak self] _ in
+ guard let self = self, let window = self.window else { return }
+ window.alphaValue = self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
+ }
+
+ NotificationCenter.default.addObserver(forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil) { [weak self] _ in
+ guard let self = self else { return }
+ self.cleanupWindows(shouldInvert: true)
+
+ if(!Defaults[.showOnAllDisplays]) {
+ let viewModel = self.vm
+ let window = self.createDynamicIslandWindow(for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel)
+ self.window = window
+ self.adjustWindowPosition(changeAlpha: true)
+ } else {
+ self.adjustWindowPosition()
+ }
+ }
+
+ DistributedNotificationCenter.default().addObserver(self, selector: #selector(onScreenLocked(_:)), name: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil)
+ DistributedNotificationCenter.default().addObserver(self, selector: #selector(onScreenUnlocked(_:)), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil)
+
+ KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in
+ guard let self = self else { return }
+ self.coordinator.toggleSneakPeek(
+ status: !self.coordinator.sneakPeek.show,
+ type: .music,
+ duration: 3.0
+ )
+ }
+
+ KeyboardShortcuts.onKeyDown(for: .toggleNotchOpen) { [weak self] in
+ guard let self = self else { return }
+
+ let mouseLocation = NSEvent.mouseLocation
+
+ var viewModel = self.vm
+
+ if(Defaults[.showOnAllDisplays]) {
+ for screen in NSScreen.screens {
+ if screen.frame.contains(mouseLocation) {
+ if let screenViewModel = self.viewModels[screen] {
+ viewModel = screenViewModel
+ break
+ }
+ }
+ }
+ }
+
+ self.closeNotchWorkItem?.cancel()
+ self.closeNotchWorkItem = nil
+
+ switch viewModel.notchState {
+ case .closed:
+ viewModel.open()
+
+ let workItem = DispatchWorkItem { [weak viewModel] in
+ viewModel?.close()
+ }
+ self.closeNotchWorkItem = workItem
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: workItem)
+ case .open:
+ viewModel.close()
+ }
+ }
+
+ if !Defaults[.showOnAllDisplays] {
+ let viewModel = self.vm
+ let window = createDynamicIslandWindow(for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel)
+ self.window = window
+ adjustWindowPosition(changeAlpha: true)
+ } else {
+ adjustWindowPosition(changeAlpha: true)
+ }
+
+ if coordinator.firstLaunch {
+ DispatchQueue.main.async {
+ self.openWindow(id: "onboarding")
+ }
+ playWelcomeSound()
+ }
+
+ previousScreens = NSScreen.screens
+ }
+
+ func playWelcomeSound() {
+ let audioPlayer = AudioPlayer()
+ audioPlayer.play(fileName: "dynamic", fileExtension: "m4a")
+ }
+
+ func deviceHasNotch() -> Bool {
+ if #available(macOS 12.0, *) {
+ for screen in NSScreen.screens {
+ if screen.safeAreaInsets.top > 0 {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ @objc func screenConfigurationDidChange() {
+ let currentScreens = NSScreen.screens
+
+ let screensChanged = currentScreens.count != previousScreens?.count ||
+ Set(currentScreens.map { $0.localizedName }) != Set(previousScreens?.map { $0.localizedName } ?? []) ||
+ Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? [])
+
+ previousScreens = currentScreens
+
+ if screensChanged {
+ DispatchQueue.main.async { [weak self] in
+ self?.cleanupWindows()
+ self?.adjustWindowPosition()
+ }
+ }
+ }
+
+ @objc func adjustWindowPosition(changeAlpha: Bool = false) {
+ if Defaults[.showOnAllDisplays] {
+ let currentScreens = Set(NSScreen.screens)
+
+ for screen in windows.keys where !currentScreens.contains(screen) {
+ if let window = windows[screen] {
+ window.close()
+ NotchSpaceManager.shared.notchSpace.windows.remove(window)
+ windows.removeValue(forKey: screen)
+ viewModels.removeValue(forKey: screen)
+ }
+ }
+
+ for screen in currentScreens {
+ if windows[screen] == nil {
+ let viewModel = DynamicIslandViewModel(screen: screen.localizedName)
+ let window = createDynamicIslandWindow(for: screen, with: viewModel)
+
+ windows[screen] = window
+ viewModels[screen] = viewModel
+ }
+
+ if let window = windows[screen], let viewModel = viewModels[screen] {
+ positionWindow(window, on: screen, changeAlpha: changeAlpha)
+
+ if viewModel.notchState == .closed {
+ viewModel.close()
+ }
+ }
+ }
+ } else {
+ let selectedScreen: NSScreen
+
+ if let preferredScreen = NSScreen.screens.first(where: { $0.localizedName == coordinator.preferredScreen }) {
+ coordinator.selectedScreen = coordinator.preferredScreen
+ selectedScreen = preferredScreen
+ } else if Defaults[.automaticallySwitchDisplay], let mainScreen = NSScreen.main {
+ coordinator.selectedScreen = mainScreen.localizedName
+ selectedScreen = mainScreen
+ } else {
+ if let window = window {
+ window.alphaValue = 0
+ }
+ return
+ }
+
+ vm.screen = selectedScreen.localizedName
+ vm.notchSize = getClosedNotchSize(screen: selectedScreen.localizedName)
+
+ if window == nil {
+ window = createDynamicIslandWindow(for: selectedScreen, with: vm)
+ }
+
+ if let window = window {
+ positionWindow(window, on: selectedScreen, changeAlpha: changeAlpha)
+
+ if vm.notchState == .closed {
+ vm.close()
+ }
+ }
+ }
+ }
+
+ @objc func togglePopover(_ sender: Any?) {
+ if window?.isVisible == true {
+ window?.orderOut(nil)
+ } else {
+ window?.orderFrontRegardless()
+ }
+ }
+
+ @objc func showMenu() {
+ statusItem?.menu?.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
+ }
+
+ @objc func quitAction() {
+ NSApplication.shared.terminate(nil)
+ }
+}
+
+extension Notification.Name {
+ static let selectedScreenChanged = Notification.Name("SelectedScreenChanged")
+ static let notchHeightChanged = Notification.Name("NotchHeightChanged")
+ static let showOnAllDisplaysChanged = Notification.Name("showOnAllDisplaysChanged")
+ static let automaticallySwitchDisplayChanged = Notification.Name("automaticallySwitchDisplayChanged")
+}
+
+extension CGRect: @retroactive Hashable {
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(origin.x)
+ hasher.combine(origin.y)
+ hasher.combine(size.width)
+ hasher.combine(size.height)
+ }
+
+ public static func == (lhs: CGRect, rhs: CGRect) -> Bool {
+ return lhs.origin == rhs.origin && lhs.size == rhs.size
}
}
diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift
new file mode 100644
index 0000000..8e5ace5
--- /dev/null
+++ b/DynamicIsland/DynamicIslandViewCoordinator.swift
@@ -0,0 +1,214 @@
+//
+// DynamicIslandViewCoordinator.swift
+// DynamicIsland
+//
+// Created by Alexander on 2024-11-20.
+//
+
+import Combine
+import Defaults
+import SwiftUI
+import TheBoringWorkerNotifier
+
+enum SneakContentType {
+ case brightness
+ case volume
+ case backlight
+ case music
+ case mic
+ case battery
+ case download
+}
+
+struct sneakPeek {
+ var show: Bool = false
+ var type: SneakContentType = .music
+ var value: CGFloat = 0
+ var icon: String = ""
+}
+
+struct SharedSneakPeek: Codable {
+ var show: Bool
+ var type: String
+ var value: String
+ var icon: String
+}
+
+enum BrowserType {
+ case chromium
+ case safari
+}
+
+struct ExpandedItem {
+ var show: Bool = false
+ var type: SneakContentType = .battery
+ var value: CGFloat = 0
+ var browser: BrowserType = .chromium
+}
+
+class DynamicIslandViewCoordinator: ObservableObject {
+ static let shared = DynamicIslandViewCoordinator()
+ var notifier: TheBoringWorkerNotifier = .init()
+
+ @Published var currentView: NotchViews = .home
+ private var sneakPeekDispatch: DispatchWorkItem?
+ private var expandingViewDispatch: DispatchWorkItem?
+
+
+ @AppStorage("firstLaunch") var firstLaunch: Bool = true
+ @AppStorage("showWhatsNew") var showWhatsNew: Bool = true
+ @AppStorage("musicLiveActivityEnabled") var musicLiveActivityEnabled: Bool = true
+ @AppStorage("currentMicStatus") var currentMicStatus: Bool = true
+
+ @AppStorage("alwaysShowTabs") var alwaysShowTabs: Bool = true {
+ didSet {
+ if !alwaysShowTabs {
+ openLastTabByDefault = false
+ if TrayDrop.shared.isEmpty || !Defaults[.openShelfByDefault] {
+ currentView = .home
+ }
+ }
+ }
+ }
+
+ @AppStorage("openLastTabByDefault") var openLastTabByDefault: Bool = false {
+ didSet {
+ if openLastTabByDefault {
+ alwaysShowTabs = true
+ }
+ }
+ }
+
+ @AppStorage("hudReplacement") var hudReplacement: Bool = true {
+ didSet {
+ notifier.postNotification(name: notifier.toggleHudReplacementNotification.name, userInfo: nil)
+ }
+ }
+
+ @AppStorage("preferred_screen_name") var preferredScreen = NSScreen.main?.localizedName ?? "Unknown" {
+ didSet {
+ selectedScreen = preferredScreen
+ NotificationCenter.default.post(name: Notification.Name.selectedScreenChanged, object: nil)
+ }
+ }
+
+ @Published var selectedScreen: String = NSScreen.main?.localizedName ?? "Unknown"
+
+ @Published var optionKeyPressed: Bool = true
+
+ private init() {
+ selectedScreen = preferredScreen
+ notifier = TheBoringWorkerNotifier()
+ }
+
+ func setupWorkersNotificationObservers() {
+ notifier.setupObserver(notification: notifier.micStatusNotification, handler: initialMicStatus)
+ notifier.setupObserver(notification: notifier.sneakPeakNotification, handler: sneakPeekEvent)
+ }
+
+ @objc func sneakPeekEvent(_ notification: Notification) {
+ let decoder = JSONDecoder()
+ if let decodedData = try? decoder.decode(SharedSneakPeek.self, from: notification.userInfo?.first?.value as! Data) {
+ let contentType = decodedData.type == "brightness" ? SneakContentType.brightness : decodedData.type == "volume" ? SneakContentType.volume : decodedData.type == "backlight" ? SneakContentType.backlight : decodedData.type == "mic" ? SneakContentType.mic : SneakContentType.brightness
+
+ let value = CGFloat((NumberFormatter().number(from: decodedData.value) ?? 0.0).floatValue)
+ let icon = decodedData.icon
+
+ print(decodedData)
+
+ toggleSneakPeek(status: decodedData.show, type: contentType, value: value, icon: icon)
+
+ } else {
+ print("Failed to decode JSON data")
+ }
+ }
+
+ func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") {
+ sneakPeekDuration = duration
+ if type != .music {
+ // close()
+ if !hudReplacement {
+ return
+ }
+ }
+ DispatchQueue.main.async {
+ withAnimation(.smooth) {
+ self.sneakPeek.show = status
+ self.sneakPeek.type = type
+ self.sneakPeek.value = value
+ self.sneakPeek.icon = icon
+ }
+ }
+
+ if type == .mic {
+ currentMicStatus = value == 1
+ }
+ }
+
+ private var sneakPeekDuration: TimeInterval = 1.5
+
+ @Published var sneakPeek: sneakPeek = .init() {
+ didSet {
+ if sneakPeek.show {
+ sneakPeekDispatch?.cancel()
+
+ sneakPeekDispatch = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ withAnimation {
+ self.toggleSneakPeek(status: false, type: SneakContentType.music)
+ self.sneakPeekDuration = 1.5
+ }
+ }
+ DispatchQueue.main
+ .asyncAfter(
+ deadline: .now() + self.sneakPeekDuration,
+ execute: sneakPeekDispatch!
+ )
+ }
+ }
+ }
+
+ func toggleExpandingView(status: Bool, type: SneakContentType, value: CGFloat = 0, browser: BrowserType = .chromium) {
+ if expandingView.show {
+ withAnimation(.smooth) {
+ self.expandingView.show = false
+ }
+ }
+ DispatchQueue.main.async {
+ withAnimation(.smooth) {
+ self.expandingView.show = status
+ self.expandingView.type = type
+ self.expandingView.value = value
+ self.expandingView.browser = browser
+ }
+ }
+ }
+
+ @Published var expandingView: ExpandedItem = .init() {
+ didSet {
+ if expandingView.show {
+ expandingViewDispatch?.cancel()
+
+ expandingViewDispatch = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.toggleExpandingView(status: false, type: SneakContentType.battery)
+ }
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + (expandingView.type == .download ? 2 : 3), execute: expandingViewDispatch!)
+ }
+ }
+ }
+
+
+ @objc func initialMicStatus(_ notification: Notification) {
+ currentMicStatus = notification.userInfo?.first?.value as! Bool
+ }
+
+ func toggleMic() {
+ notifier.postNotification(name: notifier.toggleMicNotification.name, userInfo: nil)
+ }
+
+ func showEmpty() {
+ currentView = .home
+ }
+}
diff --git a/DynamicIsland/Info.plist b/DynamicIsland/Info.plist
index d1565e5..d07ae61 100644
--- a/DynamicIsland/Info.plist
+++ b/DynamicIsland/Info.plist
@@ -7,6 +7,8 @@
NSAllowsArbitraryLoads
+ NSAppleEventsUsageDescription
+ Dynamic Island uses AppleScripts to control Spotify and Apple Music
NSCalendarsFullAccessUsageDescription
Show calender events inside the notch
SUEnableDownloaderService
@@ -16,7 +18,7 @@
SUFeedURL
https://TheBoredTeam.github.io/boring.notch/appcast.xml
SUPublicEDKey
- LlPWpDWaN5DLDCuB45z9xwxVXkdIqJhOT0eP5CiMsto=
+ B1Y47t8C/v8ImurYA+9arEsuCrpxwJSviekiflMElbI=
UTImportedTypeDeclarations
diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings
index 382dd8c..94c1c62 100644
--- a/DynamicIsland/Localizable.xcstrings
+++ b/DynamicIsland/Localizable.xcstrings
@@ -3,6 +3,9 @@
"strings" : {
" β %lld" : {
+ },
+ "'Now Playing' was the only option on previous versions and works with all media apps." : {
+
},
"(%@)" : {
"localizations" : {
@@ -40,9 +43,6 @@
},
"β’ New feature 1" : {
- },
- "β οΈ Important" : {
-
},
"About" : {
"localizations" : {
@@ -63,6 +63,9 @@
}
}
}
+ },
+ "Access Denied" : {
+
},
"Activate" : {
@@ -72,9 +75,15 @@
},
"Activation" : {
+ },
+ "Add" : {
+
},
"Add manually" : {
+ },
+ "Add new visualizer" : {
+
},
"Additional features" : {
@@ -87,6 +96,9 @@
},
"All-day" : {
+ },
+ "Always hide in fullscreen" : {
+
},
"Always show tabs" : {
@@ -119,6 +131,15 @@
}
}
}
+ },
+ "Battery Information" : {
+
+ },
+ "Battery Settings" : {
+
+ },
+ "Battery Status" : {
+
},
"Boost your productivity with Clipboard Manager" : {
@@ -164,6 +185,15 @@
}
}
}
+ },
+ "Buy us a coffee" : {
+
+ },
+ "Calendar" : {
+
+ },
+ "Calendar access is denied. Please enable it in System Settings." : {
+
},
"Cancel" : {
@@ -177,6 +207,9 @@
}
}
}
+ },
+ "Charging on Hold: Desktop Mode" : {
+
},
"Check for updates" : {
"extractionState" : "stale",
@@ -207,9 +240,6 @@
}
}
}
- },
- "CPU" : {
-
},
"Custom color" : {
"localizations" : {
@@ -223,9 +253,15 @@
},
"Custom height" : {
+ },
+ "Custom music live activity animation" : {
+
},
"Custom notch size - %.0f" : {
+ },
+ "Custom vizualizers (Lottie)" : {
+
},
"Default" : {
@@ -280,9 +316,6 @@
},
"Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" : {
- },
- "Edit" : {
-
},
"Edit layout" : {
@@ -317,6 +350,9 @@
},
"Enable music live activity" : {
+ },
+ "Enable sneak peek" : {
+
},
"Enhance your experience with HUDs" : {
@@ -326,9 +362,6 @@
},
"Error" : {
- },
- "ERROR: VIEW NOT DEFINED" : {
-
},
"Exclude apps" : {
"localizations" : {
@@ -345,9 +378,6 @@
},
"Extensions" : {
- },
- "Fullscreen media playback detection" : {
-
},
"General" : {
"localizations" : {
@@ -383,6 +413,12 @@
},
"Gradient" : {
+ },
+ "Hide BoringNotch Options" : {
+
+ },
+ "Hide only when NowPlaying app is in fullscreen" : {
+
},
"Hierarchical" : {
@@ -390,10 +426,7 @@
"High" : {
},
- "Hover over the notch after changing the height to see the effect." : {
-
- },
- "Hover over the notch after changing the screen to adapt the new size" : {
+ "https://github.com/th-ch/youtube-music" : {
},
"HUD style" : {
@@ -427,9 +460,15 @@
},
"License key" : {
+ },
+ "Lottie JSON URL" : {
+
},
"Low" : {
+ },
+ "Low Power Mode" : {
+
},
"Made with π«Άπ» by not so boring not.people" : {
"localizations" : {
@@ -446,6 +485,9 @@
},
"Match real notch size" : {
+ },
+ "Max Capacity: %lld%%" : {
+
},
"Media" : {
"localizations" : {
@@ -480,10 +522,10 @@
}
}
},
- "Medium" : {
+ "Media Source" : {
},
- "Memory" : {
+ "Medium" : {
},
"Menubar icon" : {
@@ -511,14 +553,26 @@
},
"More" : {
+ },
+ "Music Source" : {
+
},
"muted" : {
+ },
+ "Name" : {
+
},
"Need Restart" : {
},
- "Network" : {
+ "Never hide" : {
+
+ },
+ "No custom animation available" : {
+
+ },
+ "No custom visualizer" : {
},
"No events today" : {
@@ -541,14 +595,17 @@
"No extension installed" : {
},
- "Non-notch displays" : {
-
- },
- "Non-notch screen height" : {
+ "Non-notch display height" : {
},
"Notch behavior" : {
+ },
+ "Notch display height" : {
+
+ },
+ "Notch Height" : {
+
},
"OK" : {
@@ -578,6 +635,9 @@
}
}
}
+ },
+ "Open System Settings" : {
+
},
"Percentage" : {
"localizations" : {
@@ -588,6 +648,9 @@
}
}
}
+ },
+ "Plugged In" : {
+
},
"Progress bar" : {
"localizations" : {
@@ -658,6 +721,15 @@
}
}
}
+ },
+ "Select Calendars" : {
+
+ },
+ "selected" : {
+
+ },
+ "Selected animation" : {
+
},
"Settings" : {
"localizations" : {
@@ -685,6 +757,9 @@
}
}
}
+ },
+ "Show calendar" : {
+
},
"Show charging indicator" : {
"extractionState" : "stale",
@@ -721,12 +796,18 @@
},
"Show on a specific display" : {
+ },
+ "Show on all displays" : {
+
},
"Slider color" : {
},
"Sneak Peek shows the media title and artist under the notch for a few seconds." : {
+ },
+ "Sneak Peek Style" : {
+
},
"Software updates" : {
"localizations" : {
@@ -737,6 +818,9 @@
}
}
}
+ },
+ "Speed" : {
+
},
"Square" : {
@@ -758,14 +842,13 @@
},
"TheBoringNotch" : {
- "localizations" : {
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "TheDynamicIsland"
- }
- }
- }
+
+ },
+ "Time to Full Charge: %lld min" : {
+
+ },
+ "Toggle Notch Open:" : {
+
},
"Toggle Sneak Peek:" : {
@@ -787,6 +870,9 @@
},
"Upgrade to Pro" : {
+ },
+ "Use music visualizer spectrogram" : {
+
},
"Version" : {
"localizations" : {
@@ -816,6 +902,9 @@
},
"What's New" : {
+ },
+ "YouTube Music requires this third-party app to be installed: " : {
+
}
},
"version" : "1.0"
diff --git a/DynamicIsland/MediaControllers/AppleMusicController.swift b/DynamicIsland/MediaControllers/AppleMusicController.swift
new file mode 100644
index 0000000..2c34757
--- /dev/null
+++ b/DynamicIsland/MediaControllers/AppleMusicController.swift
@@ -0,0 +1,154 @@
+//
+// AppleMusicController.swift
+// boringNotch
+//
+// Created by Alexander Greco on 2025-03-29.
+//
+
+import Foundation
+import Combine
+import SwiftUI
+
+class AppleMusicController: MediaControllerProtocol {
+ // MARK: - Properties
+ @Published private var playbackState: PlaybackState = PlaybackState(
+ bundleIdentifier: "com.apple.Music"
+ )
+
+ var playbackStatePublisher: Published.Publisher { $playbackState }
+
+ // MARK: - Initialization
+ init() {
+ setupPlaybackStateChangeObserver()
+ DispatchQueue.main.async { [weak self] in
+ if self?.isActive() == true {
+ self?.updatePlaybackInfo()
+ }
+ }
+ }
+
+ private func setupPlaybackStateChangeObserver() {
+ DistributedNotificationCenter.default().addObserver(
+ self,
+ selector: #selector(updatePlaybackInfo),
+ name: NSNotification.Name("com.apple.Music.playerInfo"),
+ object: nil
+ )
+ }
+
+ deinit {
+ DistributedNotificationCenter.default().removeObserver(
+ self,
+ name: NSNotification.Name("com.apple.Music.playerInfo"),
+ object: nil
+ )
+
+ }
+
+ // MARK: - Protocol Implementation
+ func play() {
+ executeCommand("play")
+ }
+
+ func pause() {
+ executeCommand("pause")
+ }
+
+ func togglePlay() {
+ executeCommand("playpause")
+ }
+
+ func nextTrack() {
+ executeCommand("next track")
+ }
+
+ func previousTrack() {
+ executeCommand("previous track")
+ }
+
+ func seek(to time: Double) {
+ executeCommand("set player position to \(time)")
+ updatePlaybackInfo()
+ }
+
+ func isActive() -> Bool {
+ let runningApps = NSWorkspace.shared.runningApplications
+ return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier }
+ }
+
+ // MARK: - Private Methods
+ @objc func updatePlaybackInfo() {
+ guard let descriptor = fetchPlaybackInfo() else { return }
+ guard descriptor.numberOfItems >= 8 else { return }
+
+ let isPlaying = descriptor.atIndex(1)?.booleanValue ?? false
+ let currentTrack = descriptor.atIndex(2)?.stringValue ?? "Unknown"
+ let currentTrackArtist = descriptor.atIndex(3)?.stringValue ?? "Unknown"
+ let currentTrackAlbum = descriptor.atIndex(4)?.stringValue ?? "Unknown"
+ let currentTime = descriptor.atIndex(5)?.doubleValue ?? 0
+ let duration = descriptor.atIndex(6)?.doubleValue ?? 0
+ let isShuffled = descriptor.atIndex(7)?.booleanValue ?? false
+ let isRepeating = descriptor.atIndex(8)?.booleanValue ?? false
+ let artworkData = descriptor.atIndex(9)?.data as Data?
+
+ let updatedState = PlaybackState(
+ bundleIdentifier: "com.apple.Music",
+ isPlaying: isPlaying,
+ title: currentTrack,
+ artist: currentTrackArtist,
+ album: currentTrackAlbum,
+ currentTime: currentTime,
+ duration: duration,
+ playbackRate: 1,
+ isShuffled: isShuffled,
+ isRepeating: isRepeating,
+ lastUpdated: Date(),
+ artwork: (artworkData?.isEmpty ?? true) ? nil : artworkData
+ )
+
+ DispatchQueue.main.async { [weak self] in
+ self?.playbackState = updatedState
+ }
+ }
+
+ private func executeCommand(_ command: String) {
+ let script = "tell application \"Music\" to \(command)"
+ Task {
+ try? await AppleScriptHelper.executeVoid(script)
+ }
+ }
+
+ private func fetchPlaybackInfo() -> NSAppleEventDescriptor? {
+ let script = """
+ tell application "Music"
+ set isRunning to true
+ try
+ set playerState to player state is playing
+ set currentTrackName to name of current track
+ set currentTrackArtist to artist of current track
+ set currentTrackAlbum to album of current track
+ set trackPosition to player position
+ set trackDuration to duration of current track
+ set shuffleState to false
+ set repeatState to false
+ try
+ set artData to data of artwork 1 of current track
+ on error
+ set artData to ""
+ end try
+ return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatState, artData}
+ on error
+ return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, false, ""}
+ end try
+ end tell
+ """
+ var descriptor: NSAppleEventDescriptor? = nil
+ let semaphore = DispatchSemaphore(value: 0)
+ Task {
+ descriptor = try? await AppleScriptHelper.execute(script)
+ semaphore.signal()
+ }
+ semaphore.wait()
+ return descriptor
+ }
+}
diff --git a/DynamicIsland/MediaControllers/MediaControllerProtocol.swift b/DynamicIsland/MediaControllers/MediaControllerProtocol.swift
new file mode 100644
index 0000000..825042e
--- /dev/null
+++ b/DynamicIsland/MediaControllers/MediaControllerProtocol.swift
@@ -0,0 +1,21 @@
+//
+// MediaControllerProtocol.swift
+// boringNotch
+//
+// Created by Alexander Greco on 2025-03-29.
+//
+
+import Foundation
+import AppKit
+
+protocol MediaControllerProtocol: ObservableObject {
+ var playbackStatePublisher: Published.Publisher { get }
+ func play()
+ func pause()
+ func seek(to time: Double)
+ func nextTrack()
+ func previousTrack()
+ func togglePlay()
+ func isActive() -> Bool
+ func updatePlaybackInfo()
+}
diff --git a/DynamicIsland/MediaControllers/NowPlayingController.swift b/DynamicIsland/MediaControllers/NowPlayingController.swift
new file mode 100644
index 0000000..df3749c
--- /dev/null
+++ b/DynamicIsland/MediaControllers/NowPlayingController.swift
@@ -0,0 +1,197 @@
+//
+// NowPlayingController.swift
+// boringNotch
+//
+// Created by Alexander on 2025-03-29.
+//
+
+import Combine
+import AppKit
+import Foundation
+
+class NowPlayingController: ObservableObject, MediaControllerProtocol {
+ // MARK: - Properties
+ @Published var playbackState: PlaybackState = .init(
+ bundleIdentifier: "com.apple.Music"
+ )
+
+ var playbackStatePublisher: Published.Publisher { $playbackState }
+ private var cancellables = Set()
+ private var lastMusicItem: (title: String, artist: String, album: String, duration: TimeInterval, artworkData: Data?)?
+
+ // MARK: - Media Remote Functions
+ private let mediaRemoteBundle: CFBundle
+ private let MRMediaRemoteGetNowPlayingInfo: @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void
+ private let MRMediaRemoteRegisterForNowPlayingNotifications: @convention(c) (DispatchQueue) -> Void
+ private let MRMediaRemoteGetNowPlayingApplicationIsPlaying: @convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void
+ private let MRMediaRemoteGetNowPlayingClient: @convention(c) (DispatchQueue, @escaping (AnyObject?) -> Void) -> Void
+ private let MRNowPlayingClientGetBundleIdentifier: @convention(c) (AnyObject?) -> String?
+ private let MRNowPlayingClientGetParentAppBundleIdentifier: @convention(c) (AnyObject?) -> String?
+ private let MRMediaRemoteSendCommandFunction:@convention(c) (Int, AnyObject?) -> Void
+ private let MRMediaRemoteSetElapsedTimeFunction: @convention(c) (Double) -> Void
+
+ // MARK: - Initialization
+ init?() {
+ guard let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")),
+ let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString),
+ let MRMediaRemoteRegisterForNowPlayingNotificationsPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteRegisterForNowPlayingNotifications" as CFString),
+ let MRMediaRemoteGetNowPlayingApplicationIsPlayingPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying" as CFString),
+ let MRMediaRemoteGetNowPlayingClientPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingClient" as CFString),
+ let MRNowPlayingClientGetBundleIdentifierPointer = CFBundleGetFunctionPointerForName(bundle, "MRNowPlayingClientGetBundleIdentifier" as CFString),
+ let MRNowPlayingClientGetParentAppBundleIdentifierPointer = CFBundleGetFunctionPointerForName(bundle, "MRNowPlayingClientGetParentAppBundleIdentifier" as CFString),
+ let MRMediaRemoteSendCommandPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteSendCommand" as CFString),
+ let MRMediaRemoteSetElapsedTimePointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteSetElapsedTime" as CFString)
+ else { return nil }
+
+ mediaRemoteBundle = bundle
+ MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(MRMediaRemoteGetNowPlayingInfoPointer, to: (@convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void).self)
+ MRMediaRemoteRegisterForNowPlayingNotifications = unsafeBitCast(MRMediaRemoteRegisterForNowPlayingNotificationsPointer, to: (@convention(c) (DispatchQueue) -> Void).self)
+ MRMediaRemoteGetNowPlayingApplicationIsPlaying = unsafeBitCast(MRMediaRemoteGetNowPlayingApplicationIsPlayingPointer, to: (@convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void).self)
+ MRMediaRemoteGetNowPlayingClient = unsafeBitCast(MRMediaRemoteGetNowPlayingClientPointer, to: (@convention(c) (DispatchQueue, @escaping (AnyObject?) -> Void) -> Void).self)
+ MRNowPlayingClientGetBundleIdentifier = unsafeBitCast(MRNowPlayingClientGetBundleIdentifierPointer, to: (@convention(c) (AnyObject?) -> String?).self)
+ MRNowPlayingClientGetParentAppBundleIdentifier = unsafeBitCast(MRNowPlayingClientGetParentAppBundleIdentifierPointer, to: (@convention(c) (AnyObject?) -> String?).self)
+ MRMediaRemoteSendCommandFunction = unsafeBitCast(MRMediaRemoteSendCommandPointer, to: (@convention(c) (Int, AnyObject?) -> Void).self)
+ MRMediaRemoteSetElapsedTimeFunction = unsafeBitCast(MRMediaRemoteSetElapsedTimePointer, to: (@convention(c) (Double) -> Void).self)
+
+ setupNowPlayingObserver()
+ updatePlaybackInfo()
+ }
+
+ deinit {
+ // Clean up all Combine subscribers
+ cancellables.removeAll()
+
+ // Remove distributed notification observers
+ DistributedNotificationCenter.default().removeObserver(
+ self,
+ name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"),
+ object: nil
+ )
+
+ DistributedNotificationCenter.default().removeObserver(
+ self,
+ name: NSNotification.Name("com.apple.Music.playerInfo"),
+ object: nil
+ )
+ }
+
+ // MARK: - Protocol Implementation
+ func play() {
+ MRMediaRemoteSendCommandFunction(0, nil)
+ }
+
+ func pause() {
+ MRMediaRemoteSendCommandFunction(1, nil)
+ }
+
+ func togglePlay() {
+ MRMediaRemoteSendCommandFunction(2, nil)
+ }
+
+ func nextTrack() {
+ MRMediaRemoteSendCommandFunction(4, nil)
+ }
+
+ func previousTrack() {
+ MRMediaRemoteSendCommandFunction(5, nil)
+ }
+
+ func seek(to time: Double) {
+ MRMediaRemoteSetElapsedTimeFunction(time)
+ }
+
+ func isActive() -> Bool {
+ return true
+ }
+
+ // MARK: - Setup Methods
+ private func setupNowPlayingObserver() {
+ MRMediaRemoteRegisterForNowPlayingNotifications(DispatchQueue.main)
+
+ NotificationCenter.default.publisher(for: NSNotification.Name("kMRMediaRemoteNowPlayingInfoDidChangeNotification"))
+ .sink { [weak self] _ in self?.updatePlaybackInfo() }
+ .store(in: &cancellables)
+
+ NotificationCenter.default.publisher(for: NSNotification.Name("kMRMediaRemoteNowPlayingApplicationDidChangeNotification"))
+ .sink { [weak self] _ in self?.updateApp() }
+ .store(in: &cancellables)
+
+ DistributedNotificationCenter.default().addObserver(
+ self,
+ selector: #selector(updatePlaybackInfo),
+ name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"),
+ object: nil
+ )
+
+ DistributedNotificationCenter.default().addObserver(
+ self,
+ selector: #selector(updatePlaybackInfo),
+ name: NSNotification.Name("com.apple.Music.playerInfo"),
+ object: nil
+ )
+ }
+
+ // MARK: - Update Methods
+ @objc func updateApp() {
+ // Get the now playing client
+ MRMediaRemoteGetNowPlayingClient(DispatchQueue.main) { [weak self] clientObj in
+ guard let clientObj = clientObj else {
+ DispatchQueue.main.async {
+ self?.playbackState.bundleIdentifier = "com.apple.Music" // Default fallback
+ }
+ return
+ }
+
+ // Try to get parent bundle ID first, then fall back to direct bundle ID
+ var appBundleID = self?.MRNowPlayingClientGetParentAppBundleIdentifier(clientObj)
+ if appBundleID == nil {
+ appBundleID = self?.MRNowPlayingClientGetBundleIdentifier(clientObj)
+ }
+
+ // Special case for WebKit.GPU which is often Safari
+ if appBundleID == "com.apple.WebKit.GPU" {
+ appBundleID = "com.apple.Safari"
+ }
+
+ DispatchQueue.main.async {
+ self?.playbackState.bundleIdentifier = appBundleID ?? "com.apple.Music"
+ }
+ }
+ }
+
+ @objc func updatePlaybackInfo() {
+ updateApp()
+
+ MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main) { [weak self] information in
+ guard let self = self else { return }
+
+ let title = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String ?? ""
+ let artist = information["kMRMediaRemoteNowPlayingInfoArtist"] as? String ?? ""
+ let album = information["kMRMediaRemoteNowPlayingInfoAlbum"] as? String ?? ""
+ let duration = information["kMRMediaRemoteNowPlayingInfoDuration"] as? TimeInterval ?? 0
+ let currentTime = information["kMRMediaRemoteNowPlayingInfoElapsedTime"] as? TimeInterval ?? 0
+ let artworkData = information["kMRMediaRemoteNowPlayingInfoArtworkData"] as? Data
+ let timestamp = information["kMRMediaRemoteNowPlayingInfoTimestamp"] as? Date ?? Date()
+ let playbackRate = information["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Double ?? 1
+
+ // Update playback state
+ DispatchQueue.main.async {
+ self.playbackState.title = title
+ self.playbackState.artist = artist
+ self.playbackState.album = album
+ self.playbackState.duration = duration
+ self.playbackState.currentTime = currentTime
+ self.playbackState.artwork = artworkData
+ self.playbackState.lastUpdated = timestamp
+ self.playbackState.playbackRate = playbackRate
+
+ // Check playback state
+ self.MRMediaRemoteGetNowPlayingApplicationIsPlaying(DispatchQueue.main) { [weak self] isPlaying in
+ DispatchQueue.main.async {
+ self?.playbackState.isPlaying = isPlaying
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/MediaControllers/SpotifyController.swift b/DynamicIsland/MediaControllers/SpotifyController.swift
new file mode 100644
index 0000000..20a31c3
--- /dev/null
+++ b/DynamicIsland/MediaControllers/SpotifyController.swift
@@ -0,0 +1,168 @@
+//
+// SpotifyController.swift
+// boringNotch
+//
+// Created by Alexander Greco on 2025-03-29.
+//
+
+import Foundation
+import Combine
+import SwiftUI
+
+class SpotifyController: MediaControllerProtocol {
+ // MARK: - Properties
+ @Published private var playbackState: PlaybackState = PlaybackState(
+ bundleIdentifier: "com.spotify.client"
+ )
+
+ var playbackStatePublisher: Published.Publisher { $playbackState }
+
+ init() {
+ setupPlaybackStateChangeObserver()
+ DispatchQueue.main.async { [weak self] in
+ if self?.isActive() == true {
+ self?.updatePlaybackInfo()
+ }
+ }
+ }
+
+ private func setupPlaybackStateChangeObserver() {
+ DistributedNotificationCenter.default().addObserver(
+ self,
+ selector: #selector(updatePlaybackInfo),
+ name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"),
+ object: nil
+ )
+ }
+
+ deinit {
+ // Remove notification observer when controller is deallocated
+ DistributedNotificationCenter.default().removeObserver(
+ self,
+ name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"),
+ object: nil
+ )
+ }
+
+ // MARK: - Protocol Implementation
+ func play() {
+ executeCommand("play")
+ }
+
+ func pause() {
+ executeCommand("pause")
+ }
+
+ func togglePlay() {
+ executeCommand("playpause")
+ }
+
+ func nextTrack() {
+ executeCommand("next track")
+ }
+
+ func previousTrack() {
+ executeCommand("previous track")
+ }
+
+ func seek(to time: Double) {
+ executeCommand("set player position to \(time)")
+ updatePlaybackInfo()
+ }
+
+ func isActive() -> Bool {
+ let runningApps = NSWorkspace.shared.runningApplications
+ return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier }
+ }
+
+ // MARK: - Private Methods
+ @objc func updatePlaybackInfo() {
+ guard let descriptor = fetchPlaybackInfo() else { return }
+ guard descriptor.numberOfItems >= 9 else { return }
+
+ let isPlaying = descriptor.atIndex(1)?.booleanValue ?? false
+ let currentTrack = descriptor.atIndex(2)?.stringValue ?? "Unknown"
+ let currentTrackArtist = descriptor.atIndex(3)?.stringValue ?? "Unknown"
+ let currentTrackAlbum = descriptor.atIndex(4)?.stringValue ?? "Unknown"
+ let currentTime = descriptor.atIndex(5)?.doubleValue ?? 0
+ let duration = (descriptor.atIndex(6)?.doubleValue ?? 0)/1000
+ let isShuffled = descriptor.atIndex(7)?.booleanValue ?? false
+ let isRepeating = descriptor.atIndex(8)?.booleanValue ?? false
+ let artworkURL = descriptor.atIndex(9)?.stringValue ?? ""
+
+ let state = PlaybackState(
+ bundleIdentifier: "com.spotify.client",
+ isPlaying: isPlaying,
+ title: currentTrack,
+ artist: currentTrackArtist,
+ album: currentTrackAlbum,
+ currentTime: currentTime,
+ duration: duration,
+ playbackRate: 1,
+ isShuffled: isShuffled,
+ isRepeating: isRepeating,
+ lastUpdated: Date()
+ )
+
+ DispatchQueue.main.async { [weak self] in
+ self?.playbackState = state
+ }
+
+ // Load artwork asynchronously and update the state when complete
+ if !artworkURL.isEmpty, let url = URL(string: artworkURL) {
+ DispatchQueue.global(qos: .background).async { [weak self] in
+ do {
+ let artworkData = try Data(contentsOf: url)
+ DispatchQueue.main.async {
+ // Create a new state with the artwork data and update
+ var updatedState = state
+ updatedState.artwork = artworkData
+ self?.playbackState = updatedState
+ }
+ } catch {
+ print("Failed to load artwork: \(error)")
+ }
+ }
+ } else {
+ self.playbackState = state
+ }
+ }
+
+ private func executeCommand(_ command: String) {
+ let script = "tell application \"Spotify\" to \(command)"
+ Task {
+ try? await AppleScriptHelper.executeVoid(script)
+ }
+ }
+
+ private func fetchPlaybackInfo() -> NSAppleEventDescriptor? {
+ let script = """
+ tell application "Spotify"
+ set isRunning to true
+ try
+ set playerState to player state is playing
+ set currentTrackName to name of current track
+ set currentTrackArtist to artist of current track
+ set currentTrackAlbum to album of current track
+ set trackPosition to player position
+ set trackDuration to duration of current track
+ set shuffleState to shuffling
+ set repeatState to repeating
+ set artworkURL to artwork url of current track
+ return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatState, artworkURL}
+ on error
+ return {false, "Unknown", "Unknown", "Unknown", 0, 0, false, false, ""}
+ end try
+ end tell
+ """
+
+ var descriptor: NSAppleEventDescriptor? = nil
+ let semaphore = DispatchSemaphore(value: 0)
+ Task {
+ descriptor = try? await AppleScriptHelper.execute(script)
+ semaphore.signal()
+ }
+ semaphore.wait()
+ return descriptor
+ }
+}
diff --git a/DynamicIsland/MediaControllers/YouTubeMusicController.swift b/DynamicIsland/MediaControllers/YouTubeMusicController.swift
new file mode 100644
index 0000000..fc40adb
--- /dev/null
+++ b/DynamicIsland/MediaControllers/YouTubeMusicController.swift
@@ -0,0 +1,386 @@
+//
+// YouTubeMusicController.swift
+// boringNotch
+//
+// Created By Alexander Greco on 2025-03-30.
+//
+
+import Foundation
+import Combine
+import SwiftUI
+
+class YouTubeMusicController: MediaControllerProtocol {
+ // MARK: - Properties
+ @Published var playbackState: PlaybackState = .init(
+ bundleIdentifier: "com.github.th-ch.youtube-music",
+ isPlaying: true
+ )
+
+ var playbackStatePublisher: Published.Publisher { $playbackState }
+ private let baseURL = "http://localhost:26538"
+ private var accessToken: String?
+ private var refreshTimer: Timer?
+ private var cancellables = Set()
+ private var playbackInfoCancellable: AnyCancellable?
+ private var isAuthenticating = false
+ private let authQueue = DispatchQueue(label: "com.boringnotch.youtubemusicauth", qos: .background)
+
+ init() {
+ setupAppStateObservers()
+ authQueue.async { [weak self] in
+ self?.authenticateAndSetup()
+ }
+ }
+
+ deinit {
+ stopPeriodicUpdates()
+ cancellables.forEach { $0.cancel() }
+ NSWorkspace.shared.notificationCenter.removeObserver(self)
+ }
+
+ // MARK: - Authentication
+
+ private func authenticateAndSetup() {
+ // Prevent multiple concurrent authentication attempts
+ guard !isAuthenticating else { return }
+
+ isAuthenticating = true
+ getAccessToken { [weak self] success in
+ guard let self = self else { return }
+
+ DispatchQueue.main.async {
+ self.isAuthenticating = false
+ if success {
+ self.startPeriodicUpdates()
+ self.updatePlaybackInfo()
+ } else {
+ // Retry authentication after a delay if it fails
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5) { [weak self] in
+ self?.authenticateAndSetup()
+ }
+ }
+ }
+ }
+ }
+
+ private func getAccessToken(completion: @escaping (Bool) -> Void) {
+ guard let url = URL(string: "\(baseURL)/auth/boringNotch") else {
+ completion(false)
+ return
+ }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+
+ URLSession.shared.dataTaskPublisher(for: request)
+ .map { data, response -> Data in
+ return data
+ }
+ .decode(type: AuthResponse.self, decoder: JSONDecoder())
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { completionStatus in
+ switch completionStatus {
+ case .failure(let error):
+ completion(false)
+ case .finished:
+ break
+ }
+ }, receiveValue: { [weak self] response in
+ self?.accessToken = response.accessToken
+ completion(true)
+ })
+ .store(in: &cancellables)
+ }
+
+ // MARK: - Protocol Implementation
+
+ func play() {
+ sendCommand(endpoint: "/play", method: "POST")
+ updatePlaybackInfo()
+ }
+
+ func pause() {
+ sendCommand(endpoint: "/pause", method: "POST")
+ updatePlaybackInfo()
+ }
+
+ func togglePlay() {
+ if !isActive() {
+ if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: playbackState.bundleIdentifier) {
+ NSWorkspace.shared.open(url)
+ }
+ }
+ sendCommand(endpoint: "/toggle-play", method: "POST")
+ updatePlaybackInfo()
+ }
+
+ func nextTrack() {
+ sendCommand(endpoint: "/next", method: "POST")
+ updatePlaybackInfo()
+ }
+
+ func previousTrack() {
+ sendCommand(endpoint: "/previous", method: "POST")
+ updatePlaybackInfo()
+ }
+
+ func seek(to time: Double) {
+ guard let url = URL(string: "\(baseURL)/seek-to") else { return }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ if let token = accessToken {
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ } else {
+ authenticateAndSetup()
+ return
+ }
+
+ // Create the request body with the seconds value
+ let seekData = ["seconds": time]
+
+ do {
+ request.httpBody = try JSONEncoder().encode(seekData)
+ } catch {
+ return
+ }
+
+ URLSession.shared.dataTaskPublisher(for: request)
+ .tryMap { data, response -> Data in
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw URLError(.badServerResponse)
+ }
+
+ if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
+ throw URLError(.userAuthenticationRequired)
+ } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
+ throw URLError(.badServerResponse)
+ }
+
+ return data
+ }
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { [weak self] completion in
+ if case .failure(let error) = completion {
+ if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired {
+ self?.accessToken = nil
+ self?.authenticateAndSetup()
+ }
+ }
+ }, receiveValue: { [weak self] _ in
+ self?.updatePlaybackInfo()
+ })
+ .store(in: &cancellables)
+ }
+
+ func isActive() -> Bool {
+ let runningApps = NSWorkspace.shared.runningApplications
+ return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier }
+ }
+
+ // MARK: - Private Methods
+
+ private func setupAppStateObservers() {
+ // Register for app launch notifications
+ NSWorkspace.shared.notificationCenter.addObserver(
+ self,
+ selector: #selector(handleAppStateChange),
+ name: NSWorkspace.didLaunchApplicationNotification,
+ object: nil
+ )
+
+ // Register for app termination notifications
+ NSWorkspace.shared.notificationCenter.addObserver(
+ self,
+ selector: #selector(handleAppStateChange),
+ name: NSWorkspace.didTerminateApplicationNotification,
+ object: nil
+ )
+ }
+
+ @objc private func handleAppStateChange(notification: Notification) {
+ guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
+ app.bundleIdentifier == playbackState.bundleIdentifier else {
+ return
+ }
+
+ if notification.name == NSWorkspace.didLaunchApplicationNotification {
+ startPeriodicUpdates()
+ } else if notification.name == NSWorkspace.didTerminateApplicationNotification {
+ // App has terminated - update state and stop timer
+ stopPeriodicUpdates()
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.playbackState = PlaybackState(
+ bundleIdentifier: self.playbackState.bundleIdentifier,
+ isPlaying: false
+ )
+ }
+ }
+ }
+
+ private func startPeriodicUpdates() {
+ stopPeriodicUpdates() // Ensure we don't create multiple timers
+
+ // Only start the timer if the app is actually running
+ guard isActive() else { return }
+
+ refreshTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
+ self?.updatePlaybackInfo()
+ }
+ refreshTimer?.tolerance = 0.5
+
+ // Initial update when timer starts
+ updatePlaybackInfo()
+ }
+
+ private func stopPeriodicUpdates() {
+ refreshTimer?.invalidate()
+ refreshTimer = nil
+ }
+
+ func updatePlaybackInfo() {
+ if !isActive() {
+ playbackState = PlaybackState(bundleIdentifier: playbackState.bundleIdentifier, isPlaying: false)
+ stopPeriodicUpdates()
+ return
+ }
+ guard let request = createAuthenticatedRequest(for: "/api/v1/song") else { return }
+
+ let backgroundQueue = DispatchQueue.global(qos: .utility)
+
+ backgroundQueue.async { [weak self] in
+ guard let self = self else { return }
+
+ playbackInfoCancellable?.cancel()
+
+ playbackInfoCancellable = URLSession.shared.dataTaskPublisher(for: request)
+ .tryMap { data, response -> Data in
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw URLError(.badServerResponse)
+ }
+
+ // Check for authentication errors
+ if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
+ // Re-authenticate and retry later
+ DispatchQueue.main.async { [weak self] in
+ self?.accessToken = nil
+ self?.authenticateAndSetup()
+ }
+ throw URLError(.userAuthenticationRequired)
+ } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
+ throw URLError(.badServerResponse)
+ }
+
+ return data
+ }
+ .decode(type: PlaybackResponse.self, decoder: JSONDecoder())
+ .subscribe(on: backgroundQueue)
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] response in
+ self?.updatePlaybackState(with: response)
+ })
+ }
+ }
+
+ private func updatePlaybackState(with response: PlaybackResponse) {
+ playbackState.isPlaying = !response.isPaused
+ playbackState.title = response.title
+ playbackState.artist = response.artist
+ playbackState.album = response.album ?? ""
+ playbackState.currentTime = response.elapsedSeconds
+ playbackState.duration = response.songDuration
+ playbackState.lastUpdated = Date()
+
+ // Load artwork if available
+ if let artworkURL = response.imageSrc, let url = URL(string: artworkURL) {
+ DispatchQueue.global(qos: .background).async { [weak self] in
+ do {
+ let artworkData = try Data(contentsOf: url)
+ DispatchQueue.main.async {
+ self?.playbackState.artwork = artworkData
+ }
+ } catch { return }
+ }
+ }
+ }
+
+ private func sendCommand(endpoint: String, method: String = "GET") {
+ guard let url = URL(string: "\(baseURL)/api/v1\(endpoint)") else { return }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = method
+
+ if let token = accessToken {
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ } else {
+ authenticateAndSetup()
+ return
+ }
+
+ URLSession.shared.dataTaskPublisher(for: request)
+ .tryMap { data, response -> Data in
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw URLError(.badServerResponse)
+ }
+
+ if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
+ throw URLError(.userAuthenticationRequired)
+ } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
+ throw URLError(.badServerResponse)
+ }
+
+ return data
+ }
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { [weak self] completion in
+ if case .failure(let error) = completion {
+ if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired {
+ // Authentication error - token might be expired
+ self?.accessToken = nil
+ self?.authenticateAndSetup()
+
+ // Try the command again after authentication
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2) { [weak self] in
+ self?.sendCommand(endpoint: endpoint, method: method)
+ }
+ }
+ }
+ }, receiveValue: { [weak self] _ in
+ self?.updatePlaybackInfo()
+ })
+ .store(in: &cancellables)
+ }
+
+ private func createAuthenticatedRequest(for endpoint: String) -> URLRequest? {
+ guard let token = accessToken, let url = URL(string: "\(baseURL)\(endpoint)") else {
+ if accessToken == nil {
+ // Token is missing, try to authenticate again
+ authenticateAndSetup()
+ }
+ return nil
+ }
+
+ var request = URLRequest(url: url)
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ return request
+ }
+}
+
+// MARK: - API Response Models
+
+struct AuthResponse: Codable {
+ let accessToken: String
+}
+
+struct PlaybackResponse: Codable {
+ let isPaused: Bool
+ let title: String
+ let artist: String
+ let album: String?
+ let elapsedSeconds: Double
+ let songDuration: Double
+ let imageSrc: String?
+}
diff --git a/DynamicIsland/Shortcuts/ShortcutConstants.swift b/DynamicIsland/Shortcuts/ShortcutConstants.swift
new file mode 100644
index 0000000..4e476a2
--- /dev/null
+++ b/DynamicIsland/Shortcuts/ShortcutConstants.swift
@@ -0,0 +1,18 @@
+//
+// Constants.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 16/08/2024.
+//
+
+import KeyboardShortcuts
+import SwiftUI
+
+extension KeyboardShortcuts.Name {
+ static let clipboardHistoryPanel = Self("clipboardHistoryPanel", default: .init(.c, modifiers: [.shift, .command]))
+ static let toggleMicrophone = Self("toggleMicrophone", default: .init(.f5, modifiers: [.function]))
+ static let decreaseBacklight = Self("decreaseBacklight", default: .init(.f1, modifiers: [.command]))
+ static let increaseBacklight = Self("increaseBacklight", default: .init(.f2, modifiers: [.command]))
+ static let toggleSneakPeek = Self("toggleSneakPeek", default: .init(.h, modifiers: [.command, .shift]))
+ static let toggleNotchOpen = Self("toggleNotchOpen", default: .init(.i, modifiers: [.command, .shift]))
+}
diff --git a/DynamicIsland/animations/HelloAnimation.swift b/DynamicIsland/animations/HelloAnimation.swift
new file mode 100644
index 0000000..6fc5966
--- /dev/null
+++ b/DynamicIsland/animations/HelloAnimation.swift
@@ -0,0 +1,152 @@
+//
+// HelloAnimation.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+// HelloAnimation.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 08/08/24.
+//
+
+import SwiftUI
+
+struct HelloShape: Shape {
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let width = rect.size.width
+ let height = rect.size.height
+ path.move(to: CGPoint(x: 0.00095*width, y: 0.88718*height))
+ path.addCurve(to: CGPoint(x: 0.19536*width, y: 0.31015*height), control1: CGPoint(x: 0.00993*width, y: 0.87738*height), control2: CGPoint(x: 0.16556*width, y: 0.56785*height))
+ path.addCurve(to: CGPoint(x: 0.15043*width, y: 0.04964*height), control1: CGPoint(x: 0.22517*width, y: 0.05245*height), control2: CGPoint(x: 0.1859*width, y: -0.068*height))
+ path.addCurve(to: CGPoint(x: 0.10028*width, y: 0.932*height), control1: CGPoint(x: 0.11495*width, y: 0.16729*height), control2: CGPoint(x: 0.09792*width, y: 1.02023*height))
+ path.addCurve(to: CGPoint(x: 0.18354*width, y: 0.47822*height), control1: CGPoint(x: 0.10265*width, y: 0.84376*height), control2: CGPoint(x: 0.12157*width, y: 0.47822*height))
+ path.addCurve(to: CGPoint(x: 0.22327*width, y: 0.88718*height), control1: CGPoint(x: 0.25733*width, y: 0.51463*height), control2: CGPoint(x: 0.19915*width, y: 0.81575*height))
+ path.addCurve(to: CGPoint(x: 0.38553*width, y: 0.71351*height), control1: CGPoint(x: 0.2474*width, y: 0.95861*height), control2: CGPoint(x: 0.33586*width, y: 0.89978*height))
+ path.addCurve(to: CGPoint(x: 0.35998*width, y: 0.45441*height), control1: CGPoint(x: 0.43519*width, y: 0.52724*height), control2: CGPoint(x: 0.38978*width, y: 0.4306*height))
+ path.addCurve(to: CGPoint(x: 0.35478*width, y: 0.87317*height), control1: CGPoint(x: 0.33018*width, y: 0.47822*height), control2: CGPoint(x: 0.27956*width, y: 0.71631*height))
+ path.addCurve(to: CGPoint(x: 0.53453*width, y: 0.62808*height), control1: CGPoint(x: 0.42999*width, y: 1.03004*height), control2: CGPoint(x: 0.51892*width, y: 0.6939*height))
+ path.addCurve(to: CGPoint(x: 0.57332*width, y: 0.00623*height), control1: CGPoint(x: 0.55014*width, y: 0.56225*height), control2: CGPoint(x: 0.63955*width, y: 0.05805*height))
+ path.addCurve(to: CGPoint(x: 0.48723*width, y: 0.60146*height), control1: CGPoint(x: 0.5071*width, y: -0.04559*height), control2: CGPoint(x: 0.48486*width, y: 0.50623*height))
+ path.addCurve(to: CGPoint(x: 0.54588*width, y: 0.91239*height), control1: CGPoint(x: 0.48959*width, y: 0.6967*height), control2: CGPoint(x: 0.50378*width, y: 0.87597*height))
+ path.addCurve(to: CGPoint(x: 0.70719*width, y: 0.51043*height), control1: CGPoint(x: 0.58798*width, y: 0.9488*height), control2: CGPoint(x: 0.6807*width, y: 0.64768*height))
+ path.addCurve(to: CGPoint(x: 0.73273*width, y: 0.01323*height), control1: CGPoint(x: 0.73368*width, y: 0.37317*height), control2: CGPoint(x: 0.76679*width, y: 0.03984*height))
+ path.addCurve(to: CGPoint(x: 0.67171*width, y: 0.15048*height), control1: CGPoint(x: 0.69868*width, y: -0.01338*height), control2: CGPoint(x: 0.68259*width, y: 0.07205*height))
+ path.addCurve(to: CGPoint(x: 0.69678*width, y: 0.92639*height), control1: CGPoint(x: 0.66083*width, y: 0.22892*height), control2: CGPoint(x: 0.62204*width, y: 0.86057*height))
+ path.addCurve(to: CGPoint(x: 0.87275*width, y: 0.47822*height), control1: CGPoint(x: 0.77152*width, y: 0.99222*height), control2: CGPoint(x: 0.78855*width, y: 0.42997*height))
+ path.addCurve(to: CGPoint(x: 0.91438*width, y: 0.89776*height), control1: CGPoint(x: 0.9734*width, y: 0.51043*height), control2: CGPoint(x: 0.92329*width, y: 0.85998*height))
+ path.addCurve(to: CGPoint(x: 0.79943*width, y: 0.69608*height), control1: CGPoint(x: 0.87047*width, y: 1.08403*height), control2: CGPoint(x: 0.77956*width, y: height))
+ path.addCurve(to: CGPoint(x: 0.92006*width, y: 0.53081*height), control1: CGPoint(x: 0.81523*width, y: 0.45436*height), control2: CGPoint(x: 0.86282*width, y: 0.43277*height))
+ path.addCurve(to: CGPoint(x: 0.99905*width, y: 0.432*height), control1: CGPoint(x: 0.95979*width, y: 0.57703*height), control2: CGPoint(x: 0.98959*width, y: 0.4944*height))
+ return path
+ }
+}
+
+extension ShapeStyle where Self == AngularGradient {
+ static var hello: some ShapeStyle {
+ LinearGradient(
+ stops: [
+ .init(color: .blue, location: 0.0),
+ .init(color: .purple, location: 0.2),
+ .init(color: .red, location: 0.4),
+ .init(color: .mint, location: 0.5),
+ .init(color: .indigo, location: 0.7),
+ .init(color: .pink, location: 0.9),
+ .init(color: .blue, location: 1.0)
+ ],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ }
+}
+
+struct GlowingSnake<
+ Content: Shape,
+ Fill: ShapeStyle
+>: View, Animatable {
+
+ var progress: Double
+ var delay: Double = 1.0
+ var fill: Fill
+ var lineWidth = 4.0
+ var blurRadius = 8.0
+
+ @ViewBuilder var shape: Content
+
+ var animatableData: Double {
+ get { progress }
+ set { progress = newValue }
+ }
+
+ var body: some View {
+ shape
+ .trim(
+ from: {
+ if progress > 1 - delay {
+ 2 * progress - 1.0
+ } else if progress > delay {
+ progress - delay
+ } else {
+ .zero
+ }
+ }(),
+ to: progress
+ )
+ .glow(
+ fill: fill,
+ lineWidth: lineWidth,
+ blurRadius: blurRadius
+ )
+ }
+}
+
+struct HelloAnimation: View {
+ @State private var progress: Double = 0.0
+
+ var body: some View {
+ GlowingSnake(
+ progress: progress,
+ fill: .hello,
+ lineWidth: 8,
+ blurRadius: 8.0,
+ shape: { HelloShape() }
+ )
+ .onAppear {
+ withAnimation(
+ .easeInOut(duration: 4.0)
+ ) {
+ progress = 1.0
+ }
+ }
+ }
+}
+
+extension View where Self: Shape {
+ func glow(
+ fill: some ShapeStyle,
+ lineWidth: Double,
+ blurRadius: Double = 8.0,
+ lineCap: CGLineCap = .round
+ ) -> some View {
+ self
+ .stroke(style: StrokeStyle(lineWidth: lineWidth / 2, lineCap: lineCap))
+ .fill(fill)
+ .overlay {
+ self
+ .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: lineCap))
+ .fill(fill)
+ .blur(radius: blurRadius)
+ }
+ .overlay {
+ self
+ .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: lineCap))
+ .fill(fill)
+ .blur(radius: blurRadius / 2)
+ }
+ }
+}
+
+#Preview {
+ HelloAnimation()
+ .frame(width: 300, height: 100)
+}
diff --git a/DynamicIsland/animations/drop.swift b/DynamicIsland/animations/drop.swift
new file mode 100644
index 0000000..eafce26
--- /dev/null
+++ b/DynamicIsland/animations/drop.swift
@@ -0,0 +1,29 @@
+//
+// drop.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+//
+
+import Foundation
+import SwiftUI
+
+
+public class DynamicIslandAnimations {
+ @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/DynamicIsland/components/AnimatedFace.swift b/DynamicIsland/components/AnimatedFace.swift
new file mode 100644
index 0000000..51913b7
--- /dev/null
+++ b/DynamicIsland/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: 4)
+
+ // 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: 14, 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(.spring(duration: 0.2)) {
+ isBlinking = true
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ withAnimation(.spring(duration: 0.2)) {
+ 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/DynamicIsland/components/BottomRoundedRectangle.swift b/DynamicIsland/components/BottomRoundedRectangle.swift
new file mode 100644
index 0000000..fa951e7
--- /dev/null
+++ b/DynamicIsland/components/BottomRoundedRectangle.swift
@@ -0,0 +1,39 @@
+
+
+import SwiftUI
+
+
+struct BottomRoundedRectangle: Shape {
+ var radius: CGFloat
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+
+ // Top left corner
+ path.move(to: CGPoint(x: rect.minX, y: rect.minY))
+
+ // Top right corner
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
+
+ // Bottom right corner (rounded)
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - radius))
+ path.addArc(center: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius),
+ radius: radius,
+ startAngle: Angle(degrees: 0),
+ endAngle: Angle(degrees: 90),
+ clockwise: false)
+
+ // Bottom left corner (rounded)
+ path.addLine(to: CGPoint(x: rect.minX + radius, y: rect.maxY))
+ path.addArc(center: CGPoint(x: rect.minX + radius, y: rect.maxY - radius),
+ radius: radius,
+ startAngle: Angle(degrees: 90),
+ endAngle: Angle(degrees: 180),
+ clockwise: false)
+
+ // Back to top left to close the path
+ path.closeSubpath()
+
+ return path
+ }
+}
diff --git a/DynamicIsland/components/Calender/BoringCalendar.swift b/DynamicIsland/components/Calender/BoringCalendar.swift
new file mode 100644
index 0000000..1a68776
--- /dev/null
+++ b/DynamicIsland/components/Calender/BoringCalendar.swift
@@ -0,0 +1,315 @@
+//
+// BoringCalender.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 08/09/24.
+//
+
+import EventKit
+import SwiftUI
+import Defaults
+
+struct Config: Equatable {
+// var count: Int = 10 // 3 days past + today + 7 days future
+ var past: Int = 3
+ var future: Int = 7
+ var steps: Int = 1 // Each step is one day
+ var spacing: CGFloat = 1
+ var showsText: Bool = true
+ var offset: Int = 2 // Number of dates to the left of the selected date
+}
+
+struct WheelPicker: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @Binding var selectedDate: Date
+ @State private var scrollPosition: Int?
+ @State private var haptics: Bool = false
+ @State private var byClick: Bool = false
+ let config: Config
+
+ var body: some View {
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: config.spacing) {
+ let totalSteps = config.steps * (config.past + config.future)
+ let spacerNum = config.offset
+ ForEach(0.. totalSteps + spacerNum - 1){
+ Spacer().frame(width: 24, height: 24).id(index)
+ } else {
+ let offset = -config.offset - config.past
+ let date = dateForIndex(index, offset: offset)
+ let isSelected = isDateSelected(index, offset: offset)
+ dateButton(date: date, isSelected: isSelected, offset: offset){
+ selectedDate = date
+ byClick = true
+ withAnimation{
+ scrollPosition = indexForDate(date, offset: offset) - config.offset
+ }
+ //haptics.toggle() // Causes double haptic when click
+ }
+ }
+ }
+ }
+ .frame(height: 50)
+ .scrollTargetLayout()
+ }
+ .scrollIndicators(.never)
+ .scrollPosition(id: $scrollPosition, anchor: .leading)
+ .scrollTargetBehavior(.viewAligned) // Ensures scroll view snaps to button center
+ .safeAreaPadding(.horizontal)
+ .sensoryFeedback(.alignment, trigger: haptics)
+ .onChange(of: scrollPosition) { oldValue, newValue in
+ if(!byClick){
+ handleScrollChange(newValue: newValue, config: config)
+ }else{
+ byClick = false
+ }
+ }
+ .onAppear {
+ scrollToToday(config: config)
+ }
+ }
+
+ private func dateButton(date: Date, isSelected: Bool, offset: Int, onClick:@escaping()->Void) -> some View {
+ Button(action: onClick) {
+ VStack(spacing: 2) {
+ dayText(date: dateToString(for: date), isSelected: isSelected)
+ dateCircle(date: date, isSelected: isSelected)
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ .id(indexForDate(date, offset: offset))
+ }
+
+ private func dayText(date: String, isSelected: Bool) -> some View {
+ Text(date)
+ .font(.caption2)
+ .foregroundStyle(
+ isSelected ? Defaults[.accentColor] : .gray
+ )
+ }
+
+ private func dateCircle(date: Date, isSelected: Bool) -> some View {
+ ZStack {
+ Circle()
+ .fill(isSelected ? Defaults[.accentColor] : .clear)
+ .frame(width: 24, height: 24)
+ Text("\(date.date)")
+ .font(.title3)
+ .foregroundStyle(isSelected ? .white : .gray)
+ }
+ }
+
+ func handleScrollChange(newValue: Int?, config: Config) {
+ let offset = -config.offset - config.past
+ let todayIndex = indexForDate(Date(), offset: offset)
+ guard let newIndex = newValue else { return }
+ let targetDateIndex = newIndex + config.offset
+ switch targetDateIndex{
+ case todayIndex-config.past.. Int {
+ let calendar = Calendar.current
+ let startDate = calendar.startOfDay(for: calendar.date(byAdding: .day, value: offset, to: Date()) ?? Date())
+ let targetDate = calendar.startOfDay(for: date)
+ let daysDifference = calendar.dateComponents([.day], from: startDate, to: targetDate).day ?? 0
+ return daysDifference
+ }
+
+ private func dateForIndex(_ index: Int, offset: Int) -> Date {
+ let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date()
+ return Calendar.current.date(byAdding: .day, value: index, to: startDate) ?? Date()
+ }
+
+ private func dayForIndex(_ index: Int, offset: Int) -> String {
+ let date = dateForIndex(index, offset: offset)
+ return dateToString(for: date)
+ }
+
+ private func dateToString(for date: Date) -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "E"
+ return formatter.string(from: date)
+ }
+
+ private func isDateSelected(_ index: Int, offset: Int) -> Bool {
+ Calendar.current.isDate(dateForIndex(index, offset: offset), inSameDayAs: selectedDate)
+ }
+}
+
+struct CalendarView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @StateObject private var calendarManager = CalendarManager()
+ @State private var selectedDate = Date()
+
+ var body: some View {
+ VStack(spacing: 8) {
+ HStack {
+ Text("\(selectedDate, format: .dateTime.month())")
+ .font(.system(size: 18))
+ .fontWeight(.semibold)
+ ZStack {
+ WheelPicker(selectedDate: $selectedDate, config: Config())
+ HStack {
+ LinearGradient(
+ colors: [.black, .clear], startPoint: .leading, endPoint: .trailing
+ )
+ .frame(width: 20)
+ Spacer()
+ LinearGradient(
+ colors: [.clear, .black], startPoint: .leading, endPoint: .trailing
+ )
+ .frame(width: 20)
+ }
+ }
+ }
+ if calendarManager.events.isEmpty {
+ EmptyEventsView()
+ } else {
+ EventListView(events: calendarManager.events)
+ }
+ }
+ .listRowBackground(Color.clear)
+ .onChange(of: selectedDate) { _, newDate in
+ calendarManager.updateCurrentDate(newDate)
+ }
+ .onChange(of: vm.notchState) { _, _ in
+ calendarManager.updateCurrentDate(Date.now)
+ }
+ .onAppear {
+ calendarManager.updateCurrentDate(Date.now)
+ }
+ }
+}
+
+struct EmptyEventsView: View {
+ var body: some View {
+ ScrollView {
+ Text("No events today")
+ .font(.headline)
+ .foregroundStyle(.white)
+ Text("Enjoy your free time!")
+ .font(.subheadline)
+ .foregroundColor(.gray)
+ }
+ }
+}
+
+struct EventListView: View {
+ @Environment(\.openURL) private var openURL
+ let events: [EKEvent]
+
+ var body: some View {
+ ScrollView(showsIndicators: false) {
+ HStack(alignment: .top) {
+ VStack(alignment: .trailing, spacing: 5) {
+ ForEach(events.indices, id: \.self) { index in
+ VStack(alignment: .trailing) {
+ if isAllDayEvent(
+ start: events[index].startDate, end: events[index].endDate)
+ {
+ Text("All-day")
+ } else {
+ Text("\(events[index].startDate, style: .time)")
+ Text("\(events[index].endDate, style: .time)")
+ }
+ }
+ .multilineTextAlignment(.trailing)
+ .padding(.bottom, 8)
+ .font(.caption2)
+ }
+ }
+
+ VStack(alignment: .leading, spacing: 5) {
+ ForEach(events.indices, id: \.self) { index in
+ HStack(alignment: .top) {
+ VStack(spacing: 5) {
+ Image(
+ systemName: isEventEnded(events[index].endDate)
+ ? "checkmark.circle" : "circle"
+ )
+ .foregroundColor(isEventEnded(events[index].endDate) ? .green : .gray)
+ .font(.footnote)
+ Rectangle()
+ .frame(width: 1)
+ .foregroundStyle(.gray.opacity(0.5))
+ .opacity(index == events.count - 1 ? 0 : 1)
+ }
+ .padding(.top, 1)
+
+ Button {
+ if let url = generateEventURL(for: events[index]) {
+ openURL(url)
+ }
+ } label: {
+ Text(events[index].title)
+ .font(.footnote)
+ .foregroundStyle(.gray)
+ }
+ .buttonStyle(.plain)
+
+ Spacer(minLength: 0)
+ }
+ .opacity(isEventEnded(events[index].endDate) ? 0.6 : 1)
+ }
+ }
+ }
+ }
+ .scrollIndicators(.never)
+ .scrollTargetBehavior(.viewAligned)
+ }
+
+ private func isAllDayEvent(start: Date, end: Date) -> Bool {
+ let calendar = Calendar.current
+
+ let startComponents = calendar.dateComponents([.hour, .minute], from: start)
+ let endComponents = calendar.dateComponents([.hour, .minute], from: end)
+
+ return startComponents.hour == 0 && startComponents.minute == 0 && endComponents.hour == 23
+ && endComponents.minute == 59
+ }
+
+ private func isEventEnded(_ end: Date) -> Bool {
+ return Date.now > end
+ }
+
+ private func generateEventURL(for event: EKEvent) -> URL? {
+ var dateComponent = ""
+ if event.hasRecurrenceRules {
+ if let startDate = event.startDate {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
+ formatter.timeZone = TimeZone.current
+ if !event.isAllDay {
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
+ }
+ dateComponent = "/\(formatter.string(from: startDate))"
+ }
+ }
+ return URL(string: "ical://ekevent\(dateComponent)/\(event.calendarItemIdentifier)?method=show&options=more")
+ }
+}
+
+// Keep the CalendarManager, EmptyEventsView, and EventListView as they were in the previous implementation
+
+#Preview {
+ CalendarView()
+ .frame(width: 250)
+ .padding(.horizontal)
+ .background(.black)
+ .environmentObject(DynamicIslandViewModel())
+}
diff --git a/DynamicIsland/components/DynamicIslandSystemTiles.swift b/DynamicIsland/components/DynamicIslandSystemTiles.swift
new file mode 100644
index 0000000..367adb8
--- /dev/null
+++ b/DynamicIsland/components/DynamicIslandSystemTiles.swift
@@ -0,0 +1,109 @@
+//
+// DynamicIslandSystemTiles.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+// BoringSystemTiles.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 16/08/24.
+//
+
+import Foundation
+import SwiftUI
+import Defaults
+
+struct SystemItemButton: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @State var icon: String = "gear"
+ var onTap: () -> Void
+ @State var label: String?
+ @State var showEmojis: Bool = true
+ @State var emoji: String = "π§"
+
+ var body: some View {
+ Button(action: onTap) {
+ if Defaults[.tileShowLabels] {
+ HStack {
+ if !showEmojis {
+ Image(systemName: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 10)
+ .foregroundStyle(.gray)
+ }
+
+ Text((showEmojis ? "\(emoji) " : "") + label!)
+ .font(.caption2)
+ .fontWeight(.regular)
+ .foregroundStyle(.gray)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .allowsTightening(true)
+ .minimumScaleFactor(0.7)
+ .lineLimit(1)
+ }
+ } else {
+ Color.clear
+ .overlay {
+ Image(systemName: icon)
+ .foregroundStyle(.gray)
+ }
+ .aspectRatio(1, contentMode: .fit)
+ }
+ }
+ .buttonStyle(BouncingButtonStyle(vm: vm))
+ }
+}
+
+func logout() {
+ DispatchQueue.global(qos: .background).async {
+ let appleScript = """
+ tell application "System Events" to log out
+ """
+
+ var error: NSDictionary?
+ if let scriptObject = NSAppleScript(source: appleScript) {
+ scriptObject.executeAndReturnError(&error)
+ if let error = error {
+ print("Error: \(error)")
+ }
+ }
+ }
+}
+
+struct DynamicIslandSystemTiles: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+
+ struct ItemButton {
+ var icon: String
+ var onTap: () -> Void
+ }
+
+ var body: some View {
+ Grid {
+ GridRow {
+// SystemItemButton(icon: "clipboard", onTap: {
+// vm.openClipboard()
+// }, label: "Clipboard History", showEmojis: Defaults[.showEmojis], emoji: "β¨")
+ // SystemItemButton(icon: "keyboard", onTap: {
+ // vm?.close()
+ // vm?.togglesneakPeek(status: true, type: .backlight, value: 1)
+ // }, label: "π‘ Keyboard Backlight")
+ }
+ GridRow {
+ SystemItemButton(icon: coordinator.currentMicStatus ? "mic" : "mic.slash", onTap: {
+ coordinator.toggleMic()
+ vm.close()
+ }, label: "Toggle Microphone", showEmojis: Defaults[.showEmojis], emoji: coordinator.currentMicStatus ? "π" : "π€«")
+ // SystemItemButton(icon: "lock", onTap: {
+ // logout()
+ // }, label: "π Lock My Device")
+ }
+ }
+ }
+}
+
+#Preview {
+ DynamicIslandSystemTiles().padding()
+}
diff --git a/DynamicIsland/components/EmptyState.swift b/DynamicIsland/components/EmptyState.swift
new file mode 100644
index 0000000..9ca8292
--- /dev/null
+++ b/DynamicIsland/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: 70, width: 80)
+ 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/DynamicIsland/components/HoverButton.swift b/DynamicIsland/components/HoverButton.swift
new file mode 100644
index 0000000..7b68448
--- /dev/null
+++ b/DynamicIsland/components/HoverButton.swift
@@ -0,0 +1,46 @@
+//
+// HoverButton.swift
+// boringNotch
+//
+// Created by Kraigo on 04.09.2024.
+//
+
+import SwiftUI
+
+struct HoverButton: View {
+ var icon: String
+ var iconColor: Color = .white;
+ var scale: Image.Scale = .medium
+ var action: () -> Void
+ var contentTransition: ContentTransition = .symbolEffect;
+
+ @State private var isHovering = false
+
+ var body: some View {
+ let size = CGFloat(scale == .large ? 40 : 30)
+
+ Button(action: action) {
+ Rectangle()
+ .fill(.clear)
+ .contentShape(Rectangle())
+ .frame(width: size, height: size)
+ .overlay {
+ Capsule()
+ .fill(isHovering ? Color.gray.opacity(0.2) : .clear)
+ .frame(width: size, height: size)
+ .overlay {
+ Image(systemName: icon)
+ .foregroundColor(iconColor)
+ .contentTransition(contentTransition)
+ .font(scale == .large ? .largeTitle : .body)
+ }
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ .onHover { hovering in
+ withAnimation(.smooth(duration: 0.3)) {
+ isHovering = hovering
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/components/Live activities/DownloadView.swift b/DynamicIsland/components/Live activities/DownloadView.swift
new file mode 100644
index 0000000..2e6104b
--- /dev/null
+++ b/DynamicIsland/components/Live activities/DownloadView.swift
@@ -0,0 +1,52 @@
+//
+// DownloadView.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 17/08/24.
+//
+
+import Foundation
+import SwiftUI
+
+enum Browser {
+ case safari
+ case chrome
+}
+
+struct DownloadFile {
+ var name: String
+ var size: Int
+ var formattedSize: String
+ var browser: Browser
+}
+
+class DownloadWatcher: ObservableObject {
+ @Published var downloadFiles: [DownloadFile] = []
+}
+
+struct DownloadArea: View {
+ @EnvironmentObject var watcher: DownloadWatcher
+
+ var body: some View {
+ HStack(alignment: .center) {
+ HStack {
+ if watcher.downloadFiles.first!.browser == .safari {
+ AppIcon(for: "com.apple.safari")
+ } else {
+ Image(.chrome).resizable().scaledToFit().frame(width: 30, height: 30)
+ }
+ VStack(alignment: .leading) {
+ Text("Download")
+ Text("In progress").font(.system(.footnote)).foregroundStyle(.gray)
+ }
+ }
+ Spacer()
+ HStack(spacing: 12) {
+ VStack(alignment: .trailing) {
+ Text(watcher.downloadFiles.first!.formattedSize)
+ Text(watcher.downloadFiles.first!.name).font(.caption2).foregroundStyle(.gray)
+ }
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/components/Live activities/DynamicIslandBattery.swift b/DynamicIsland/components/Live activities/DynamicIslandBattery.swift
new file mode 100644
index 0000000..997b89a
--- /dev/null
+++ b/DynamicIsland/components/Live activities/DynamicIslandBattery.swift
@@ -0,0 +1,256 @@
+import SwiftUI
+import Defaults
+
+/// A view that displays the battery status with an icon and charging indicator.
+struct BatteryView: View {
+
+ var levelBattery: Float
+ var isPluggedIn: Bool
+ var isCharging: Bool
+ var isInLowPowerMode: Bool
+ var batteryWidth: CGFloat = 26
+ var isForNotification: Bool
+
+ var animationStyle: BoringAnimations = BoringAnimations()
+
+ var icon: String = "battery.0"
+
+ /// Determines the icon to display when charging.
+ var iconStatus: String {
+ if isCharging {
+ return "bolt"
+ }
+ else if isPluggedIn {
+ return "plug"
+ }
+ else {
+ return ""
+ }
+ }
+
+ /// Determines the color of the battery based on its status.
+ var batteryColor: Color {
+ if isInLowPowerMode {
+ return .yellow
+ } else if levelBattery <= 20 && !isCharging && !isPluggedIn {
+ return .red
+ } else if isCharging || isPluggedIn || levelBattery == 100 {
+ return .green
+ } else {
+ return .white
+ }
+ }
+
+ var body: some View {
+ ZStack(alignment: .leading) {
+
+ Image(systemName: icon)
+ .resizable()
+ .fontWeight(.thin)
+ .aspectRatio(contentMode: .fit)
+ .foregroundColor(.white.opacity(0.5))
+ .frame(
+ width: batteryWidth + 1
+ )
+
+ RoundedRectangle(cornerRadius: 2.5)
+ .fill(batteryColor)
+ .frame(
+ width: CGFloat(((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6))),
+ height: (batteryWidth - 2.75) - 18
+ )
+ .padding(.leading, 2)
+
+ if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) {
+ ZStack {
+ Image(iconStatus)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .foregroundColor(.white)
+ .frame(
+ width: 17,
+ height: 17
+ )
+ }
+ .frame(width: batteryWidth, height: batteryWidth)
+ }
+ }
+ }
+}
+
+/// A view that displays detailed battery information and settings.
+struct BatteryMenuView: View {
+
+ var isPluggedIn: Bool
+ var isCharging: Bool
+ var levelBattery: Float
+ var maxCapacity: Float
+ var timeToFullCharge: Int
+ var isInLowPowerMode: Bool
+ var onDismiss: () -> Void
+
+ @Environment(\.openURL) private var openURL
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+
+ HStack {
+ Text("Battery Status")
+ .font(.headline)
+ .fontWeight(.semibold)
+ Spacer()
+ Text("\(Int(levelBattery))%")
+ .font(.headline)
+ .fontWeight(.semibold)
+ }
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Max Capacity: \(Int(maxCapacity))%")
+ .font(.subheadline)
+ .fontWeight(.regular)
+ if isInLowPowerMode {
+ Label("Low Power Mode", systemImage: "bolt.circle")
+ .font(.subheadline)
+ .fontWeight(.regular)
+ }
+ if isCharging {
+ Label("Charging", systemImage: "bolt.fill")
+ .font(.subheadline)
+ .fontWeight(.regular)
+ }
+ if isPluggedIn {
+ Label("Plugged In", systemImage: "powerplug.fill")
+ .font(.subheadline)
+ .fontWeight(.regular)
+ }
+ if timeToFullCharge > 0 {
+ Label("Time to Full Charge: \(timeToFullCharge) min", systemImage: "clock")
+ .font(.subheadline)
+ .fontWeight(.regular)
+ }
+ if !isCharging && isPluggedIn && levelBattery >= 80 {
+ Label("Charging on Hold: Desktop Mode", systemImage: "desktopcomputer")
+ .font(.subheadline)
+ .fontWeight(.regular)
+ }
+
+ }
+ .padding(.vertical, 8)
+
+ Divider().background(Color.white)
+
+ Button(action: openBatteryPreferences) {
+ Label("Battery Settings", systemImage: "gearshape")
+ .fontWeight(.regular)
+ }
+ .frame(maxWidth: .infinity)
+ .buttonStyle(.plain)
+ .padding(.vertical, 8)
+ }
+ .padding()
+ .frame(width: 280)
+ .foregroundColor(.white)
+ }
+
+ private func openBatteryPreferences() {
+ if let url = URL(string: "x-apple.systempreferences:com.apple.preference.battery") {
+ openURL(url)
+ onDismiss()
+ }
+ }
+}
+
+
+/// A view that displays the battery status and allows interaction to show detailed information.
+struct DynamicIslandBatteryView: View {
+
+ @State var batteryWidth: CGFloat = 26
+ var isCharging: Bool = false
+ var isInLowPowerMode: Bool = false
+ var isPluggedIn: Bool = false
+ var levelBattery: Float = 0
+ var maxCapacity: Float = 0
+ var timeToFullCharge: Int = 0
+ @State var isForNotification: Bool = false
+
+ @State private var showPopupMenu: Bool = false
+ @State private var isPressed: Bool = false
+ @State private var isHoveringPopover: Bool = false
+
+ @EnvironmentObject var vm: DynamicIslandViewModel
+
+ var body: some View {
+ HStack {
+ if Defaults[.showBatteryPercentage] {
+ Text("\(Int32(levelBattery))%")
+ .font(.callout)
+ .foregroundStyle(.white)
+ }
+ BatteryView(
+ levelBattery: levelBattery,
+ isPluggedIn: isPluggedIn,
+ isCharging: isCharging,
+ isInLowPowerMode: isInLowPowerMode,
+ batteryWidth: batteryWidth,
+ isForNotification: isForNotification
+ )
+ }
+ .scaleEffect(isPressed ? 0.95 : 1.0)
+ .animation(.easeInOut(duration: 0.2), value: isPressed)
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { _ in
+ withAnimation {
+ isPressed = true
+ }
+ }
+ .onEnded { _ in
+ withAnimation {
+ isPressed = false
+ showPopupMenu.toggle()
+ }
+ }
+ )
+ .popover(
+ isPresented: $showPopupMenu,
+ arrowEdge: .bottom) {
+ BatteryMenuView(
+ isPluggedIn: isPluggedIn,
+ isCharging: isCharging,
+ levelBattery: levelBattery,
+ maxCapacity: maxCapacity,
+ timeToFullCharge: timeToFullCharge,
+ isInLowPowerMode: isInLowPowerMode,
+ onDismiss: {
+ showPopupMenu = false
+ }
+ )
+ .onHover { hovering in
+ isHoveringPopover = hovering
+ }
+ }
+ .onChange(of: showPopupMenu) { _, _ in
+ updateBatteryPopoverActiveState()
+ }
+ .onChange(of: isHoveringPopover) { _, _ in
+ updateBatteryPopoverActiveState()
+ }
+ }
+
+ private func updateBatteryPopoverActiveState() {
+ vm.isBatteryPopoverActive = showPopupMenu && isHoveringPopover
+ }
+}
+
+#Preview {
+ DynamicIslandBatteryView(
+ batteryWidth: 30,
+ isCharging: false,
+ isInLowPowerMode: false,
+ isPluggedIn: true,
+ levelBattery: 80,
+ maxCapacity: 100,
+ timeToFullCharge: 10,
+ isForNotification: false
+ ).frame(width: 200, height: 200)
+}
diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift
new file mode 100644
index 0000000..5fbedc7
--- /dev/null
+++ b/DynamicIsland/components/Live activities/InlineHUD.swift
@@ -0,0 +1,147 @@
+//
+// InlineHUDs.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 14/09/2024.
+//
+
+import SwiftUI
+
+struct InlineHUD: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @Binding var type: SneakContentType
+ @Binding var value: CGFloat
+ @Binding var icon: String
+ @Binding var hoverAnimation: Bool
+ @Binding var gestureProgress: CGFloat
+ var body: some View {
+ HStack {
+ HStack(spacing: 5) {
+ Group {
+ switch (type) {
+ case .volume:
+ if icon.isEmpty {
+ Image(systemName: SpeakerSymbol(value))
+ .contentTransition(.interpolate)
+ .symbolVariant(value > 0 ? .none : .slash)
+ .frame(width: 20, height: 15, alignment: .leading)
+ } else {
+ Image(systemName: icon)
+ .contentTransition(.interpolate)
+ .opacity(value.isZero ? 0.6 : 1)
+ .scaleEffect(value.isZero ? 0.85 : 1)
+ .frame(width: 20, height: 15, alignment: .leading)
+ }
+ case .brightness:
+ Image(systemName: BrightnessSymbol(value))
+ .contentTransition(.interpolate)
+ .frame(width: 20, height: 15, alignment: .center)
+ case .backlight:
+ Image(systemName: "keyboard")
+ .contentTransition(.interpolate)
+ .frame(width: 20, height: 15, alignment: .center)
+ case .mic:
+ Image(systemName: "mic")
+ .symbolRenderingMode(.hierarchical)
+ .symbolVariant(value > 0 ? .none : .slash)
+ .contentTransition(.interpolate)
+ .frame(width: 20, height: 15, alignment: .center)
+ default:
+ EmptyView()
+ }
+ }
+ .foregroundStyle(.white)
+ .symbolVariant(.fill)
+
+ Text(Type2Name(type))
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .lineLimit(1)
+ .allowsTightening(true)
+ .contentTransition(.numericText())
+ }
+ .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading)
+
+ Rectangle()
+ .fill(.black)
+ .frame(width: vm.closedNotchSize.width - 20)
+
+ HStack {
+ if (type == .mic) {
+ Text(value.isZero ? "muted" : "unmuted")
+ .foregroundStyle(.gray)
+ .lineLimit(1)
+ .allowsTightening(true)
+ .multilineTextAlignment(.trailing)
+ .frame(maxWidth: .infinity, alignment: .trailing)
+ .contentTransition(.interpolate)
+ } else {
+ HStack {
+ DraggableProgressBar(value: $value)
+ if (type == .volume && value.isZero) {
+ Text("muted")
+ .font(.caption)
+ .fontWeight(.medium)
+ .foregroundStyle(.gray)
+ .lineLimit(1)
+ .allowsTightening(true)
+ .multilineTextAlignment(.trailing)
+ }
+ }
+ }
+ }
+ .padding(.trailing, 4)
+ .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.closedNotchSize.height - (hoverAnimation ? 0 : 12), alignment: .center)
+ }
+ .frame(height: vm.closedNotchSize.height + (hoverAnimation ? 8 : 0), alignment: .center)
+ }
+
+ func SpeakerSymbol(_ value: CGFloat) -> String {
+ switch(value) {
+ case 0:
+ return "speaker"
+ case 0...0.3:
+ return "speaker.wave.1"
+ case 0.3...0.8:
+ return "speaker.wave.2"
+ case 0.8...1:
+ return "speaker.wave.3"
+ default:
+ return "speaker.wave.2"
+ }
+ }
+
+ func BrightnessSymbol(_ value: CGFloat) -> String {
+ switch(value) {
+ case 0...0.6:
+ return "sun.min"
+ case 0.6...1:
+ return "sun.max"
+ default:
+ return "sun.min"
+ }
+ }
+
+ func Type2Name(_ type: SneakContentType) -> String {
+ switch(type) {
+ case .volume:
+ return "Volume"
+ case .brightness:
+ return "Brightness"
+ case .backlight:
+ return "Backlight"
+ case .mic:
+ return "Mic"
+ default:
+ return ""
+ }
+ }
+}
+
+#Preview {
+ InlineHUD(type: .constant(.brightness), value: .constant(0.4), icon: .constant(""), hoverAnimation: .constant(false), gestureProgress: .constant(0))
+ .padding(.horizontal, 8)
+ .background(Color.black)
+ .padding()
+ .environmentObject(DynamicIslandViewModel())
+}
diff --git a/DynamicIsland/components/Live activities/LiveActivityModifier.swift b/DynamicIsland/components/Live activities/LiveActivityModifier.swift
new file mode 100644
index 0000000..e10fc0c
--- /dev/null
+++ b/DynamicIsland/components/Live activities/LiveActivityModifier.swift
@@ -0,0 +1,43 @@
+//
+// LiveActivityModifier.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 12/08/2024.
+//
+
+import SwiftUI
+
+enum ActivityType {
+ case mediaPlayback
+ case charging
+ case download
+}
+
+struct LiveActivityModifier: ViewModifier {
+ let `for`: ActivityType
+ let leftContent: () -> Left
+ let rightContent: () -> Right
+
+ func body(content: Content) -> some View {
+ content
+ .overlay(
+ HStack {
+ leftContent()
+ Spacer()
+ //.frame(minWidth: vm.closedNotchSize.width)
+ rightContent()
+ }
+ .padding()
+ )
+ }
+}
+
+extension View {
+ func liveActivity(
+ for activityId: ActivityType,
+ @ViewBuilder left: @escaping () -> Left,
+ @ViewBuilder right: @escaping () -> Right
+ ) -> some View {
+ self.modifier(LiveActivityModifier(for: activityId, leftContent: left, rightContent: right))
+ }
+}
diff --git a/DynamicIsland/components/Live activities/MarqueeTextView.swift b/DynamicIsland/components/Live activities/MarqueeTextView.swift
new file mode 100644
index 0000000..4bb7bf3
--- /dev/null
+++ b/DynamicIsland/components/Live activities/MarqueeTextView.swift
@@ -0,0 +1,93 @@
+//
+// MarqueeTextView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 08/08/2024.
+//
+
+import SwiftUI
+
+struct SizePreferenceKey: PreferenceKey {
+ static var defaultValue: CGSize = .zero
+ static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
+ value = nextValue()
+ }
+}
+
+struct MeasureSizeModifier: ViewModifier {
+ func body(content: Content) -> some View {
+ content.background(GeometryReader { geometry in
+ Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
+ })
+ }
+}
+
+struct MarqueeText: View {
+ @Binding var text: String
+ let font: Font
+ let nsFont: NSFont.TextStyle
+ let textColor: Color
+ let backgroundColor: Color
+ let minDuration: Double
+ let frameWidth: CGFloat
+
+ @State private var animate = false
+ @State private var textSize: CGSize = .zero
+ @State private var offset: CGFloat = 0
+
+ init(_ text: Binding, font: Font = .body, nsFont: NSFont.TextStyle = .body, textColor: Color = .primary, backgroundColor: Color = .clear, minDuration: Double = 3.0, frameWidth: CGFloat = 200) {
+ _text = text
+ self.font = font
+ self.nsFont = nsFont
+ self.textColor = textColor
+ self.backgroundColor = backgroundColor
+ self.minDuration = minDuration
+ self.frameWidth = frameWidth
+ }
+
+ private var needsScrolling: Bool {
+ textSize.width > frameWidth
+ }
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack(alignment: .leading) {
+ HStack(spacing: 20) {
+ Text(text)
+ Text(text)
+ .opacity(needsScrolling ? 1 : 0)
+ }
+ .id(text)
+ .font(font)
+ .foregroundColor(textColor)
+ .fixedSize(horizontal: true, vertical: false)
+ .offset(x: self.animate ? offset : 0)
+ .animation(
+ self.animate ?
+ .linear(duration: Double(textSize.width / 30))
+ .delay(minDuration)
+ .repeatForever(autoreverses: false) : .none,
+ value: self.animate
+ )
+ .background(backgroundColor)
+ .modifier(MeasureSizeModifier())
+ .onPreferenceChange(SizePreferenceKey.self) { size in
+ self.textSize = CGSize(width: size.width / 2, height: NSFont.preferredFont(forTextStyle: nsFont).pointSize)
+ self.animate = false
+ self.offset = 0
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.01){
+ if needsScrolling {
+ self.animate = true
+ self.offset = -(textSize.width + 10)
+
+ }
+ }
+ }
+ }
+ .frame(width: frameWidth, alignment: .leading)
+ .clipped()
+ }
+ .frame(height: textSize.height * 1.3)
+
+ }
+}
diff --git a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift
new file mode 100644
index 0000000..db2f196
--- /dev/null
+++ b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift
@@ -0,0 +1,144 @@
+ //
+ // SystemEventIndicatorModifier.swift
+ // boringNotch
+ //
+ // Created by Richard Kunkli on 12/08/2024.
+ //
+
+import SwiftUI
+import Defaults
+
+struct SystemEventIndicatorModifier: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @Binding var eventType: SneakContentType
+ @Binding var value: CGFloat {
+ didSet {
+ DispatchQueue.main.async {
+ self.sendEventBack(value)
+ self.vm.objectWillChange.send()
+ }
+ }
+ }
+ @Binding var icon: String
+ let showSlider: Bool = false
+ var sendEventBack: (CGFloat) -> Void
+
+ var body: some View {
+ HStack(spacing: 14) {
+ switch (eventType) {
+ case .volume:
+ if icon.isEmpty {
+ Image(systemName: SpeakerSymbol(value))
+ .contentTransition(.interpolate)
+ .symbolVariant(value > 0 ? .none : .slash)
+ .frame(width: 20, height: 15, alignment: .leading)
+ } else {
+ Image(systemName: icon)
+ .contentTransition(.interpolate)
+ .opacity(value.isZero ? 0.6 : 1)
+ .scaleEffect(value.isZero ? 0.85 : 1)
+ .frame(width: 20, height: 15, alignment: .leading)
+ }
+ case .brightness:
+ Image(systemName: "sun.max.fill")
+ .contentTransition(.symbolEffect)
+ .frame(width: 20, height: 15)
+ .foregroundStyle(.white)
+ case .backlight:
+ Image(systemName: "keyboard")
+ .contentTransition(.symbolEffect)
+ .frame(width: 20, height: 15)
+ .foregroundStyle(.white)
+ case .mic:
+ Image(systemName: "mic")
+ .symbolVariant(value > 0 ? .none : .slash)
+ .contentTransition(.interpolate)
+ .frame(width: 20, height: 15)
+ .foregroundStyle(.white)
+ default:
+ EmptyView()
+ }
+ if (eventType != .mic) {
+ DraggableProgressBar(value: $value)
+ } else {
+ Text("Mic \(value > 0 ? "unmuted" : "muted")")
+ .foregroundStyle(.gray)
+ .lineLimit(1)
+ .allowsTightening(true)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .symbolVariant(.fill)
+ .imageScale(.large)
+ }
+
+ func SpeakerSymbol(_ value: CGFloat) -> String {
+ switch(value) {
+ case 0:
+ return "speaker.slash"
+ case 0...0.3:
+ return "speaker.wave.1"
+ case 0.3...0.8:
+ return "speaker.wave.2"
+ case 0.8...1:
+ return "speaker.wave.3"
+ default:
+ return "speaker.wave.2"
+ }
+ }
+}
+
+struct DraggableProgressBar: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @Binding var value: CGFloat
+
+ @State private var isDragging = false
+ @State private var dragOffset: CGFloat = 0
+
+ var body: some View {
+ VStack {
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ Capsule()
+ .fill(.tertiary)
+ Group {
+ if Defaults[.enableGradient] {
+ Capsule()
+ .fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading))
+ .frame(width: max(0, min(geo.size.width * value, geo.size.width)))
+ .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
+ } else {
+ Capsule()
+ .fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white)
+ .frame(width: max(0, min(geo.size.width * value, geo.size.width)))
+ .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
+ }
+ }
+ .opacity(value.isZero ? 0 : 1)
+ }
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { gesture in
+ withAnimation(.smooth(duration: 0.3)) {
+ isDragging = true
+ updateValue(gesture: gesture, in: geo)
+ }
+ }
+ .onEnded { _ in
+ withAnimation(.smooth(duration: 0.3)) {
+ isDragging = false
+ }
+ }
+ )
+ }
+ .frame(height: Defaults[.inlineHUD] ? isDragging ? 8 : 5 : isDragging ? 9 : 6)
+ }
+ }
+
+ private func updateValue(gesture: DragGesture.Value, in geometry: GeometryProxy) {
+ let dragPosition = gesture.location.x
+ let newValue = dragPosition / geometry.size.width
+
+ value = max(0, min(newValue, 1))
+ }
+}
diff --git a/DynamicIsland/components/Music/LottieAnimationView.swift b/DynamicIsland/components/Music/LottieAnimationView.swift
new file mode 100644
index 0000000..b0b7433
--- /dev/null
+++ b/DynamicIsland/components/Music/LottieAnimationView.swift
@@ -0,0 +1,33 @@
+//
+// LottieAnimationView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 2024. 10. 29..
+//
+
+import SwiftUI
+import Lottie
+import LottieUI
+import Defaults
+
+struct LottieAnimationView: View {
+ let state1 = LUStateData(type: .loadedFrom(URL(string: "https://assets9.lottiefiles.com/packages/lf20_mniampqn.json")!), speed: 1.0, loopMode: .loop)
+ @Default(.selectedVisualizer) var selectedVisualizer
+ var body: some View {
+ if selectedVisualizer == nil {
+ LottieView(state: state1)
+ } else {
+ LottieView(
+ state: LUStateData(
+ type: .loadedFrom(selectedVisualizer!.url),
+ speed: selectedVisualizer!.speed,
+ loopMode: .loop
+ )
+ )
+ }
+ }
+}
+
+#Preview {
+ LottieAnimationView()
+}
diff --git a/DynamicIsland/components/Music/MusicVisualizer.swift b/DynamicIsland/components/Music/MusicVisualizer.swift
new file mode 100644
index 0000000..a9baf98
--- /dev/null
+++ b/DynamicIsland/components/Music/MusicVisualizer.swift
@@ -0,0 +1,115 @@
+//
+// MusicVisualizer.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 02/08/24.
+//
+import AppKit
+import Cocoa
+import SwiftUI
+
+class AudioSpectrum: NSView {
+ private var barLayers: [CAShapeLayer] = []
+ private var isPlaying: Bool = true
+ private var animationTimer: Timer?
+
+ override init(frame frameRect: NSRect) {
+ super.init(frame: frameRect)
+ wantsLayer = true
+ setupBars()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ wantsLayer = true
+ setupBars()
+ }
+
+ private func setupBars() {
+ let barWidth: CGFloat = 2
+ let barCount = 4
+ let spacing: CGFloat = barWidth
+ let totalWidth = CGFloat(barCount) * (barWidth + spacing)
+ let totalHeight: CGFloat = 14
+ frame.size = CGSize(width: totalWidth, height: totalHeight)
+
+ for i in 0 ..< barCount {
+ let xPosition = CGFloat(i) * (barWidth + spacing)
+ let barLayer = CAShapeLayer()
+ barLayer.frame = CGRect(x: xPosition, y: 0, width: barWidth, height: totalHeight)
+ barLayer.position = CGPoint(x: xPosition + barWidth / 2, y: totalHeight / 2)
+ barLayer.fillColor = NSColor.white.cgColor
+
+ let path = NSBezierPath(roundedRect: CGRect(x: 0, y: 0, width: barWidth, height: totalHeight),
+ xRadius: barWidth / 2,
+ yRadius: barWidth / 2)
+ barLayer.path = path.cgPath
+
+ barLayers.append(barLayer)
+ layer?.addSublayer(barLayer)
+ }
+ }
+
+ private func startAnimating() {
+ guard animationTimer == nil else { return }
+ animationTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [weak self] _ in
+ self?.updateBars()
+ }
+ }
+
+ private func stopAnimating() {
+ animationTimer?.invalidate()
+ animationTimer = nil
+ resetBars()
+ }
+
+ private func updateBars() {
+ for barLayer in barLayers {
+ let animation = CABasicAnimation(keyPath: "transform.scale.y")
+ animation.fromValue = barLayer.presentation()?.value(forKeyPath: "transform.scale.y") ?? 0.35
+ animation.toValue = CGFloat.random(in: 0.35 ... 1.0)
+ animation.duration = 0.3
+ animation.autoreverses = true
+ animation.fillMode = .forwards
+ animation.isRemovedOnCompletion = false
+
+ barLayer.add(animation, forKey: "scaleY")
+ }
+ }
+
+ private func resetBars() {
+ for barLayer in barLayers {
+ barLayer.removeAllAnimations()
+ barLayer.transform = CATransform3DMakeScale(1, 0.35, 1)
+ }
+ }
+
+ func setPlaying(_ playing: Bool) {
+ isPlaying = playing
+ if isPlaying {
+ startAnimating()
+ } else {
+ stopAnimating()
+ }
+ }
+}
+
+struct AudioSpectrumView: NSViewRepresentable {
+ @Binding var isPlaying: Bool
+
+ func makeNSView(context: Context) -> AudioSpectrum {
+ let spectrum = AudioSpectrum()
+ spectrum.setPlaying(isPlaying)
+ return spectrum
+ }
+
+ func updateNSView(_ nsView: AudioSpectrum, context: Context) {
+ nsView.setPlaying(isPlaying)
+ }
+}
+
+#Preview {
+ AudioSpectrumView(isPlaying: .constant(true))
+ .frame(width: 16, height: 20)
+ .padding()
+}
diff --git a/DynamicIsland/components/Notch/BoringExtrasMenu.swift b/DynamicIsland/components/Notch/BoringExtrasMenu.swift
new file mode 100644
index 0000000..101c2da
--- /dev/null
+++ b/DynamicIsland/components/Notch/BoringExtrasMenu.swift
@@ -0,0 +1,107 @@
+//
+// BoringExtrasMenu.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+//
+
+import SwiftUI
+
+struct BoringLargeButtons: View {
+ var action: () -> Void
+ var icon: Image
+ var title: String
+ var body: some View {
+ Button (
+ action:action,
+ label: {
+ ZStack {
+ RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70)
+ VStack(spacing: 8) {
+ icon.resizable()
+ .aspectRatio(contentMode: .fit).frame(width:20)
+ Text(title).font(.body)
+ }
+ }
+ }).buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10)
+ }
+}
+
+struct BoringExtrasMenu : View {
+ @ObservedObject var vm: DynamicIslandViewModel
+
+ var body: some View {
+ VStack{
+ HStack(spacing: 20) {
+ hide
+ settings
+ close
+ }
+ }
+ }
+
+ var github: some View {
+ BoringLargeButtons(
+ action: {
+ NSWorkspace.shared.open(productPage)
+ },
+ icon: Image(.github),
+ title: "Checkout"
+ )
+ }
+
+ var donate: some View {
+ BoringLargeButtons(
+ action: {
+ NSWorkspace.shared.open(sponsorPage)
+ },
+ icon: Image(systemName: "heart.fill"),
+ title: "Love Us"
+ )
+ }
+
+ var settings: some View {
+ SettingsLink(label: {
+ ZStack {
+ RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70)
+ VStack(spacing: 8) {
+ Image(systemName: "gear").resizable()
+ .aspectRatio(contentMode: .fit).frame(width:20)
+ Text("Settings").font(.body)
+ }
+ }
+ })
+ .buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10)
+ }
+
+ var hide: some View {
+ BoringLargeButtons(
+ action: {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
+ //vm.openMusic()
+ }
+ },
+ icon: Image(systemName: "arrow.down.forward.and.arrow.up.backward"),
+ title: "Hide"
+ )
+ }
+
+ var close: some View {
+ BoringLargeButtons(
+ action: {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
+ NSApp.terminate(nil)
+ }
+ }
+ },
+ icon: Image(systemName: "xmark"),
+ title: "Exit"
+ )
+ }
+}
+
+
+#Preview {
+ BoringExtrasMenu(vm: .init())
+}
diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift
new file mode 100644
index 0000000..4c1177c
--- /dev/null
+++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift
@@ -0,0 +1,85 @@
+//
+// DynamicIslandHeader.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+//
+
+import Defaults
+import SwiftUI
+
+struct DynamicIslandHeader: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @ObservedObject var batteryModel = BatteryStatusViewModel.shared
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @StateObject var tvm = TrayDrop.shared
+ var body: some View {
+ HStack(spacing: 0) {
+ HStack {
+ if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.dynamicShelf] {
+ TabSelectionView()
+ } else if vm.notchState == .open {
+ EmptyView()
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .opacity(vm.notchState == .closed ? 0 : 1)
+ .blur(radius: vm.notchState == .closed ? 20 : 0)
+ .animation(.smooth.delay(0.1), value: vm.notchState)
+ .zIndex(2)
+
+ if vm.notchState == .open {
+ Rectangle()
+ .fill(NSScreen.screens
+ .first(where: { $0.localizedName == coordinator.selectedScreen })?.safeAreaInsets.top ?? 0 > 0 ? .black : .clear)
+ .frame(width: vm.closedNotchSize.width)
+ .mask {
+ NotchShape()
+ }
+ }
+
+ HStack(spacing: 4) {
+ if vm.notchState == .open {
+ if Defaults[.settingsIconInNotch] {
+ SettingsLink(label: {
+ Capsule()
+ .fill(.black)
+ .frame(width: 30, height: 30)
+ .overlay {
+ Image(systemName: "gear")
+ .foregroundColor(.white)
+ .padding()
+ .imageScale(.medium)
+ }
+ })
+ .buttonStyle(PlainButtonStyle())
+ }
+ if Defaults[.showBatteryIndicator] {
+ DynamicIslandBatteryView(
+ batteryWidth: 30,
+ isCharging: batteryModel.isCharging,
+ isInLowPowerMode: batteryModel.isInLowPowerMode,
+ isPluggedIn: batteryModel.isPluggedIn,
+ levelBattery: batteryModel.levelBattery,
+ maxCapacity: batteryModel.maxCapacity,
+ timeToFullCharge: batteryModel.timeToFullCharge,
+ isForNotification: false
+ )
+ }
+ }
+ }
+ .font(.system(.headline, design: .rounded))
+ .frame(maxWidth: .infinity, alignment: .trailing)
+ .opacity(vm.notchState == .closed ? 0 : 1)
+ .blur(radius: vm.notchState == .closed ? 20 : 0)
+ .animation(.smooth.delay(0.1), value: vm.notchState)
+ .zIndex(2)
+ }
+ .foregroundColor(.gray)
+ .environmentObject(vm)
+ }
+}
+
+#Preview {
+ DynamicIslandHeader().environmentObject(DynamicIslandViewModel())
+}
diff --git a/DynamicIsland/components/Notch/DynamicIslandWindow.swift b/DynamicIsland/components/Notch/DynamicIslandWindow.swift
new file mode 100644
index 0000000..df55f02
--- /dev/null
+++ b/DynamicIsland/components/Notch/DynamicIslandWindow.swift
@@ -0,0 +1,50 @@
+//
+// DynamicIslandWindow.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 06/08/24.
+//
+
+import Cocoa
+
+class DynamicIslandWindow: NSPanel {
+ override init(
+ contentRect: NSRect,
+ styleMask: NSWindow.StyleMask,
+ backing: NSWindow.BackingStoreType,
+ defer flag: Bool
+ ) {
+ super.init(
+ contentRect: contentRect,
+ styleMask: styleMask,
+ backing: backing,
+ defer: flag
+ )
+
+ isFloatingPanel = true
+ isOpaque = false
+ titleVisibility = .hidden
+ titlebarAppearsTransparent = true
+ backgroundColor = .clear
+ isMovable = false
+
+ collectionBehavior = [
+ .fullScreenAuxiliary,
+ .stationary,
+ .canJoinAllSpaces,
+ .ignoresCycle,
+ ]
+
+ isReleasedWhenClosed = false
+ level = .mainMenu + 3
+ hasShadow = false
+ }
+
+ override var canBecomeKey: Bool {
+ false
+ }
+
+ override var canBecomeMain: Bool {
+ false
+ }
+}
diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift
new file mode 100644
index 0000000..86efe80
--- /dev/null
+++ b/DynamicIsland/components/Notch/NotchHomeView.swift
@@ -0,0 +1,342 @@
+//
+// NotchHomeView.swift
+// boringNotch
+//
+// Created by Hugo Persson on 2024-08-18.
+// Modified by Harsh Vardhan Goswami & Richard Kunkli
+//
+
+import Combine
+import Defaults
+import SwiftUI
+
+// MARK: - Music Player Components
+
+struct MusicPlayerView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ let albumArtNamespace: Namespace.ID
+
+ var body: some View {
+ HStack {
+ AlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace)
+ MusicControlsView().drawingGroup().compositingGroup()
+ }
+ }
+}
+
+struct AlbumArtView: View {
+ @ObservedObject var musicManager = MusicManager.shared
+ @ObservedObject var vm: DynamicIslandViewModel
+ let albumArtNamespace: Namespace.ID
+
+ var body: some View {
+ ZStack(alignment: .bottomTrailing) {
+ if Defaults[.lightingEffect] {
+ albumArtBackground
+ }
+ albumArtButton
+ }
+ }
+
+ private var albumArtBackground: some View {
+ Color.clear
+ .aspectRatio(1, contentMode: .fit)
+ .background(
+ Image(nsImage: musicManager.albumArt)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ )
+ .clipped()
+ .clipShape(RoundedRectangle(cornerRadius: Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed))
+ .scaleEffect(x: 1.3, y: 1.4)
+ .rotationEffect(.degrees(92))
+ .blur(radius: 35)
+ .opacity(min(0.6, 1 - max(musicManager.albumArt.getBrightness(), 0.3)))
+ }
+
+ private var albumArtButton: some View {
+ Button {
+ musicManager.openMusicApp()
+ } label: {
+ ZStack(alignment: .bottomTrailing) {
+ albumArtImage
+ appIconOverlay
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ .opacity(musicManager.isPlaying ? 1 : 0.4)
+ .scaleEffect(musicManager.isPlaying ? 1 : 0.85)
+ }
+
+ private var albumArtImage: some View {
+ Color.clear
+ .aspectRatio(1, contentMode: .fit)
+ .background(
+ Image(nsImage: musicManager.albumArt)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .animation(.spring(response: 0.4, dampingFraction: 0.8), value: musicManager.isFlipping)
+ )
+ .clipped()
+ .clipShape(RoundedRectangle(cornerRadius: Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed))
+ .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
+ }
+
+ @ViewBuilder
+ private var appIconOverlay: some View {
+ if vm.notchState == .open && !musicManager.usingAppIconForArtwork {
+ AppIcon(for: musicManager.bundleIdentifier ?? "com.apple.Music")
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 30, height: 30)
+ .offset(x: 10, y: 10)
+ .transition(.scale.combined(with: .opacity).animation(.bouncy.delay(0.3)))
+ }
+ }
+}
+
+struct MusicControlsView: View {
+ @ObservedObject var musicManager = MusicManager.shared
+ @State private var sliderValue: Double = 0
+ @State private var dragging: Bool = false
+ @State private var lastDragged: Date = .distantPast
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ songInfoAndSlider
+ playbackControls
+ }
+ .buttonStyle(PlainButtonStyle())
+ .frame(minWidth: Defaults[.showMirror] && Defaults[.showCalendar] ? 140 : 180)
+ }
+
+ private var songInfoAndSlider: some View {
+ GeometryReader { geo in
+ VStack(alignment: .leading, spacing: 4) {
+ songInfo(width: geo.size.width)
+ musicSlider
+ }
+ }
+ .padding(.top, 10)
+ .padding(.leading, 5)
+ }
+
+ private func songInfo(width: CGFloat) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+ MarqueeText($musicManager.songTitle, font: .headline, nsFont: .headline, textColor: .white, frameWidth: width)
+ MarqueeText(
+ $musicManager.artistName,
+ font: .headline,
+ nsFont: .headline,
+ textColor: Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor)
+ .ensureMinimumBrightness(factor: 0.6) : .gray,
+ frameWidth: width
+ )
+ .fontWeight(.medium)
+ }
+ }
+
+ private var musicSlider: some View {
+ TimelineView(.animation(minimumInterval: musicManager.playbackRate > 0 ? 0.1 : nil)) { timeline in
+ MusicSliderView(
+ sliderValue: $sliderValue,
+ duration: $musicManager.songDuration,
+ lastDragged: $lastDragged,
+ color: musicManager.avgColor,
+ dragging: $dragging,
+ currentDate: timeline.date,
+ lastUpdated: musicManager.lastUpdated,
+ ignoreLastUpdated: musicManager.ignoreLastUpdated,
+ timestampDate: musicManager.timestampDate,
+ elapsedTime: musicManager.elapsedTime,
+ playbackRate: musicManager.playbackRate,
+ isPlaying: musicManager.isPlaying
+ ) { newValue in
+ MusicManager.shared.seek(to: newValue)
+ }
+ .padding(.top, 5)
+ .frame(height: 36)
+ }
+ }
+
+ private var playbackControls: some View {
+ HStack(spacing: 8) {
+ HoverButton(icon: "backward.fill", scale: .medium) {
+ MusicManager.shared.previousTrack()
+ }
+ HoverButton(icon: musicManager.isPlaying ? "pause.fill" : "play.fill", scale: .large) {
+ MusicManager.shared.togglePlay()
+ }
+ HoverButton(icon: "forward.fill", scale: .medium) {
+ MusicManager.shared.nextTrack()
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+}
+
+// MARK: - Main View
+
+struct NotchHomeView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @EnvironmentObject var webcamManager: WebcamManager
+ @ObservedObject var batteryModel = BatteryStatusViewModel.shared
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ let albumArtNamespace: Namespace.ID
+
+ var body: some View {
+ Group {
+ if !coordinator.firstLaunch {
+ mainContent
+ }
+ }
+ .transition(.opacity.combined(with: .blurReplace))
+ }
+
+ private var mainContent: some View {
+ HStack(alignment: .top, spacing: 20) {
+ MusicPlayerView(albumArtNamespace: albumArtNamespace)
+
+ if Defaults[.showCalendar] {
+ CalendarView()
+ .onHover { isHovering in
+ vm.isHoveringCalendar = isHovering
+ }
+ .environmentObject(vm)
+ }
+
+ if Defaults[.showMirror] && webcamManager.cameraAvailable {
+ CameraPreviewView(webcamManager: webcamManager)
+ .scaledToFit()
+ .opacity(vm.notchState == .closed ? 0 : 1)
+ .blur(radius: vm.notchState == .closed ? 20 : 0)
+ }
+ }
+ .transition(.opacity.animation(.smooth.speed(0.9))
+ .combined(with: .blurReplace.animation(.smooth.speed(0.9)))
+ .combined(with: .move(edge: .top)))
+ .blur(radius: vm.notchState == .closed ? 30 : 0)
+ }
+}
+
+struct MusicSliderView: View {
+ @Binding var sliderValue: Double
+ @Binding var duration: Double
+ @Binding var lastDragged: Date
+ var color: NSColor
+ @Binding var dragging: Bool
+ let currentDate: Date
+ let lastUpdated: Date
+ let ignoreLastUpdated: Bool
+ let timestampDate: Date
+ let elapsedTime: Double
+ let playbackRate: Double
+ let isPlaying: Bool
+ var onValueChange: (Double) -> Void
+
+ var currentElapsedTime: Double {
+ // A small buffer is needed to ensure a meaningful difference between the two dates
+ guard !dragging, timestampDate.timeIntervalSince(lastDragged) > -0.0001, (timestampDate > lastUpdated || ignoreLastUpdated) else { return sliderValue }
+ let timeDifference = isPlaying ? currentDate.timeIntervalSince(timestampDate) : 0
+ let elapsed = elapsedTime + (timeDifference * playbackRate)
+ return min(elapsed, duration)
+ }
+
+ var body: some View {
+ VStack {
+ CustomSlider(
+ value: $sliderValue,
+ range: 0 ... duration,
+ color: Defaults[.sliderColor] == SliderColorEnum.albumArt ? Color(
+ nsColor: color
+ ).ensureMinimumBrightness(factor: 0.8) : Defaults[.sliderColor] == SliderColorEnum.accent ? Defaults[.accentColor] : .white,
+ dragging: $dragging,
+ lastDragged: $lastDragged,
+ onValueChange: onValueChange
+ )
+ .frame(height: 10, alignment: .center)
+ HStack {
+ Text(timeString(from: sliderValue))
+ Spacer()
+ Text(timeString(from: duration))
+ }
+ .fontWeight(.medium)
+ .foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: color)
+ .ensureMinimumBrightness(factor: 0.6) : .gray)
+ .font(.caption)
+ }
+ .onChange(of: currentDate) {
+ sliderValue = currentElapsedTime
+ }
+ }
+
+ func timeString(from seconds: Double) -> String {
+ let totalMinutes = Int(seconds) / 60
+ let remainingSeconds = Int(seconds) % 60
+ let hours = totalMinutes / 60
+ let minutes = totalMinutes % 60
+
+ if hours > 0 {
+ return String(format: "%d:%02d:%02d", hours, minutes, remainingSeconds)
+ } else {
+ return String(format: "%d:%02d", minutes, remainingSeconds)
+ }
+ }
+}
+
+struct CustomSlider: View {
+ @Binding var value: Double
+ var range: ClosedRange
+ var color: Color = .white
+ @Binding var dragging: Bool
+ @Binding var lastDragged: Date
+ var onValueChange: ((Double) -> Void)?
+ var thumbSize: CGFloat = 12
+
+ var body: some View {
+ GeometryReader { geometry in
+ let width = geometry.size.width
+ let height = CGFloat(dragging ? 9 : 5)
+ let rangeSpan = range.upperBound - range.lowerBound
+
+ let filledTrackWidth = min(rangeSpan == .zero ? 0 : ((value - range.lowerBound) / rangeSpan) * width, width)
+
+ ZStack(alignment: .leading) {
+ // Background track
+ Rectangle()
+ .fill(.gray.opacity(0.3))
+ .frame(height: height)
+
+ // Filled track
+ Rectangle()
+ .fill(color)
+ .frame(width: filledTrackWidth, height: height)
+ }
+ .cornerRadius(height / 2)
+ .frame(height: 10)
+ .contentShape(Rectangle())
+ .highPriorityGesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { gesture in
+ withAnimation {
+ dragging = true
+ }
+ let newValue = range.lowerBound + Double(gesture.location.x / width) * rangeSpan
+ value = min(max(newValue, range.lowerBound), range.upperBound)
+ }
+ .onEnded { _ in
+ onValueChange?(value)
+ dragging = false
+ lastDragged = Date()
+ }
+ )
+ .animation(.bouncy.speed(1.4), value: dragging)
+ }
+ }
+}
+
+#Preview {
+ NotchHomeView(albumArtNamespace: Namespace().wrappedValue)
+ .environmentObject(DynamicIslandViewModel())
+ .environmentObject(WebcamManager())
+}
diff --git a/DynamicIsland/components/Notch/NotchShape.swift b/DynamicIsland/components/Notch/NotchShape.swift
new file mode 100644
index 0000000..be2cfa9
--- /dev/null
+++ b/DynamicIsland/components/Notch/NotchShape.swift
@@ -0,0 +1,126 @@
+//
+// NotchShape.swift
+// boringNotch
+//
+// Created by Kai Azim on 2023-08-24.
+// Original source: https://github.com/MrKai77/DynamicNotchKit
+// Modified by Alexander Greco on 2025-05-18.
+
+import SwiftUI
+
+struct NotchShape: Shape {
+ private var topCornerRadius: CGFloat
+ private var bottomCornerRadius: CGFloat
+
+ init(
+ topCornerRadius: CGFloat? = nil,
+ bottomCornerRadius: CGFloat? = nil
+ ) {
+ self.topCornerRadius = topCornerRadius ?? 6
+ self.bottomCornerRadius = bottomCornerRadius ?? 14
+ }
+
+ var animatableData: AnimatablePair {
+ get {
+ .init(
+ topCornerRadius,
+ bottomCornerRadius
+ )
+ }
+ set {
+ topCornerRadius = newValue.first
+ bottomCornerRadius = newValue.second
+ }
+ }
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+
+ path.move(
+ to: CGPoint(
+ x: rect.minX,
+ y: rect.minY
+ )
+ )
+
+ path.addQuadCurve(
+ to: CGPoint(
+ x: rect.minX + topCornerRadius,
+ y: rect.minY + topCornerRadius
+ ),
+ control: CGPoint(
+ x: rect.minX + topCornerRadius,
+ y: rect.minY
+ )
+ )
+
+ path.addLine(
+ to: CGPoint(
+ x: rect.minX + topCornerRadius,
+ y: rect.maxY - bottomCornerRadius
+ )
+ )
+
+ path.addQuadCurve(
+ to: CGPoint(
+ x: rect.minX + topCornerRadius + bottomCornerRadius,
+ y: rect.maxY
+ ),
+ control: CGPoint(
+ x: rect.minX + topCornerRadius,
+ y: rect.maxY
+ )
+ )
+
+ path.addLine(
+ to: CGPoint(
+ x: rect.maxX - topCornerRadius - bottomCornerRadius,
+ y: rect.maxY
+ )
+ )
+
+ path.addQuadCurve(
+ to: CGPoint(
+ x: rect.maxX - topCornerRadius,
+ y: rect.maxY - bottomCornerRadius
+ ),
+ control: CGPoint(
+ x: rect.maxX - topCornerRadius,
+ y: rect.maxY
+ )
+ )
+
+ path.addLine(
+ to: CGPoint(
+ x: rect.maxX - topCornerRadius,
+ y: rect.minY + topCornerRadius
+ )
+ )
+
+ path.addQuadCurve(
+ to: CGPoint(
+ x: rect.maxX,
+ y: rect.minY
+ ),
+ control: CGPoint(
+ x: rect.maxX - topCornerRadius,
+ y: rect.minY
+ )
+ )
+
+ path.addLine(
+ to: CGPoint(
+ x: rect.minX,
+ y: rect.minY
+ )
+ )
+
+ return path
+ }
+}
+
+#Preview {
+ NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
+ .frame(width: 200, height: 32)
+ .padding(10)
+}
diff --git a/DynamicIsland/components/Notch/NotchShelfView.swift b/DynamicIsland/components/Notch/NotchShelfView.swift
new file mode 100644
index 0000000..5ec7528
--- /dev/null
+++ b/DynamicIsland/components/Notch/NotchShelfView.swift
@@ -0,0 +1,64 @@
+
+import SwiftUI
+
+struct NotchShelfView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @StateObject var tvm = TrayDrop.shared
+
+ var body: some View {
+ HStack {
+ AirDropView()
+ panel
+ .onDrop(of: [.data], isTargeted: $vm.dropZoneTargeting) { providers in
+ vm.dropEvent = true
+ DispatchQueue.global().async {
+ tvm.load(providers)
+ }
+ return true
+ }
+ }
+ }
+
+ var panel: some View {
+ RoundedRectangle(cornerRadius: 16)
+ .strokeBorder(style: StrokeStyle(lineWidth: 4, dash: [10]))
+ .foregroundStyle(.white.opacity(0.1))
+ .overlay {
+ content
+ .padding()
+ }
+ .animation(vm.animation, value: tvm.items)
+ .animation(vm.animation, value: tvm.isLoading)
+ }
+
+ var content: some View {
+ Group {
+ if tvm.isEmpty {
+ VStack(spacing: 10) {
+ Image(systemName: "tray.and.arrow.down")
+ .symbolVariant(.fill)
+ .symbolRenderingMode(.hierarchical)
+ .foregroundStyle(.white, .gray)
+ .imageScale(.large)
+
+ Text("Drop files here")
+ .foregroundStyle(.gray)
+ .font(.system(.title3, design: .rounded))
+ .fontWeight(.medium)
+ }
+ } else {
+ ScrollView(.horizontal) {
+ HStack(spacing: spacing) {
+ ForEach(tvm.items) { item in
+ DropItemView(item: item)
+ }
+ }
+ .padding(spacing)
+ }
+ .padding(-spacing)
+ .scrollIndicators(.never)
+ }
+ }
+ }
+}
+
diff --git a/DynamicIsland/components/Onboarding/ActivationView.swift b/DynamicIsland/components/Onboarding/ActivationView.swift
new file mode 100644
index 0000000..bafb5b2
--- /dev/null
+++ b/DynamicIsland/components/Onboarding/ActivationView.swift
@@ -0,0 +1,80 @@
+//
+// OnboardingSettings.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 2024. 09. 26..
+//
+
+import SwiftUI
+
+struct ActivationWindow: View {
+ @State private var email: String = ""
+ @State private var key: String = ""
+ var body: some View {
+ VStack {
+ Image("logo")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(height: 80)
+ .padding(.top, 30)
+ .padding(.bottom, 10)
+ Text("Activate your license")
+ .font(.largeTitle.bold())
+ .fontDesign(.rounded)
+ Text("Transform your notch truly yours")
+ .foregroundStyle(.secondary)
+ .font(.title2)
+ .padding(.bottom, 20)
+ Group {
+ TextField("Email address", text: $email)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
+ TextField("License key", text: $key)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
+ }
+ .textFieldStyle(PlainTextFieldStyle())
+ .scrollContentBackground(.hidden)
+ .toggleStyle(.switch)
+ Spacer()
+ VStack(alignment: .center, spacing: 14) {
+ HStack(alignment: .center) {
+ HStack {
+ Button {} label: {
+ Text("Cancel")
+ .padding(.horizontal, 18)
+ }
+ .buttonStyle(AccessoryBarButtonStyle())
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ Image("dynamicisland")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(height: 18)
+ .offset(y: 4)
+ .blendMode(.overlay)
+ HStack {
+ Button {} label: {
+ Text("Activate")
+ .padding(.horizontal, 18)
+ }
+ .buttonStyle(BorderedProminentButtonStyle())
+ }
+ .frame(maxWidth: .infinity, alignment: .trailing)
+
+ }
+ .controlSize(.extraLarge)
+ }
+ }
+ .padding()
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
+ .ignoresSafeArea()
+ .background {
+ VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
+ .ignoresSafeArea()
+ }
+ .frame(width: 350, height: 350)
+ }
+}
diff --git a/DynamicIsland/components/Onboarding/ProOnboarding.swift b/DynamicIsland/components/Onboarding/ProOnboarding.swift
new file mode 100644
index 0000000..55a6bce
--- /dev/null
+++ b/DynamicIsland/components/Onboarding/ProOnboarding.swift
@@ -0,0 +1,105 @@
+//
+// ProOnboarding.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 2024. 09. 26..
+//
+
+import SwiftUI
+import SwiftUIIntrospect
+
+struct ProOnboard: View {
+ var body: some View {
+ ZStack(alignment: .top) {
+ ZStack {
+ Image("spotlight")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .padding(.bottom)
+ .blur(radius: 3)
+ .offset(y: -5)
+ .background(SparkleView().opacity(0.6))
+ VStack(spacing: 8) {
+ Image("logo")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 100, height: 100)
+ .padding(.bottom, 8)
+ Text("TheDynamicIsland")
+ .font(.system(.largeTitle, design: .serif))
+ Text("Welcome")
+ .font(.title)
+ .foregroundStyle(.secondary)
+ .padding(.bottom, 30)
+ if false {
+ Text("PRO")
+ .font(.system(size: 18, design: .rounded))
+ .fontWeight(.bold)
+ .foregroundStyle(.white)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 3)
+ .background(
+ Capsule()
+ .fill(LinearGradient(colors: [.white.opacity(0.7), .white.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing))
+ .strokeBorder(LinearGradient(stops: [.init(color: .white.opacity(0.7), location: 0.3), .init(color: .clear, location: 0.6)], startPoint: .topLeading, endPoint: .bottomTrailing))
+ .blendMode(.overlay)
+ )
+ .padding(.bottom, 30)
+ }
+ Button {
+ if let url = URL(string: "https://buymeacoffee.com/jfxh67wvfxq") {
+ NSWorkspace.shared.open(url)
+ }
+ } label: {
+ Label("Buy us a coffee", systemImage: "cup.and.saucer.fill")
+ .font(.headline)
+ .foregroundColor(Color.black)
+ .padding(.horizontal, 20)
+ .padding(.vertical, 10)
+ .background(Color.yellow)
+ .cornerRadius(8)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .padding(.bottom, 20)
+
+ Button {
+ NSApp.keyWindow?.close()
+ } label: {
+ Text("Get started")
+ .padding(.horizontal, 20)
+ .padding(.vertical, 6)
+ }
+ .buttonStyle(BorderedProminentButtonStyle())
+ }
+ .padding(.top)
+ }
+
+ Image("dynamicisland")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(height: 22)
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
+ .padding()
+ .padding(.bottom, 36)
+ .blendMode(.overlay)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
+ .ignoresSafeArea()
+ .background {
+ VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
+ .ignoresSafeArea()
+ }
+ .onAppear {
+ NSApp.mainWindow?.standardWindowButton(.zoomButton)?.isHidden = true
+ NSApp.mainWindow?.standardWindowButton(.closeButton)?.isHidden = true
+ NSApp.mainWindow?.standardWindowButton(.miniaturizeButton)?.isHidden = true
+ NSApp.mainWindow?.styleMask.remove(.resizable)
+ }
+ .frame(width: 350, height: 500)
+ .introspect(.window, on: .macOS(.v14, .v15)) { window in
+ window.titlebarAppearsTransparent = true
+ window.titleVisibility = .hidden
+ window.center()
+ }
+ }
+}
diff --git a/DynamicIsland/components/Onboarding/SparkleView.swift b/DynamicIsland/components/Onboarding/SparkleView.swift
new file mode 100644
index 0000000..5fb190e
--- /dev/null
+++ b/DynamicIsland/components/Onboarding/SparkleView.swift
@@ -0,0 +1,76 @@
+//
+// SparkleView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 2024. 09. 26..
+//
+
+import SwiftUI
+import AppKit
+
+class SparkleNSView: NSView {
+ private var emitterLayer: CAEmitterLayer?
+
+ override init(frame frameRect: NSRect) {
+ super.init(frame: frameRect)
+ self.wantsLayer = true
+ setupEmitterLayer()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func setupEmitterLayer() {
+ let emitterLayer = CAEmitterLayer()
+ emitterLayer.emitterShape = .rectangle
+ emitterLayer.emitterMode = .surface
+ emitterLayer.renderMode = .oldestFirst
+
+ let cell = CAEmitterCell()
+ cell.contents = NSImage(named: "sparkle")?.cgImage(forProposedRect: nil, context: nil, hints: nil)
+ cell.birthRate = 50
+ cell.lifetime = 5
+ cell.velocity = 10
+ cell.velocityRange = 5
+ cell.emissionRange = .pi * 2
+ cell.scale = 0.2
+ cell.scaleRange = 0.1
+ cell.alphaSpeed = -0.5
+ cell.yAcceleration = 10 // Add a slight downward motion
+
+ emitterLayer.emitterCells = [cell]
+
+ self.layer?.addSublayer(emitterLayer)
+ self.emitterLayer = emitterLayer
+
+ updateEmitterForCurrentBounds()
+ }
+
+ private func updateEmitterForCurrentBounds() {
+ guard let emitterLayer = self.emitterLayer else { return }
+
+ emitterLayer.frame = self.bounds
+ emitterLayer.emitterSize = self.bounds.size
+ emitterLayer.emitterPosition = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
+
+ // Adjust birth rate based on view size
+ let area = bounds.width * bounds.height
+ let baseBirthRate: Float = 50
+ let adjustedBirthRate = 20 // Assuming 200x200 as base size
+ emitterLayer.emitterCells?.first?.birthRate = Float(adjustedBirthRate)
+ }
+
+ override func setFrameSize(_ newSize: NSSize) {
+ super.setFrameSize(newSize)
+ updateEmitterForCurrentBounds()
+ }
+}
+
+struct SparkleView: NSViewRepresentable {
+ func makeNSView(context: Context) -> SparkleNSView {
+ return SparkleNSView()
+ }
+
+ func updateNSView(_ nsView: SparkleNSView, context: Context) {}
+}
diff --git a/DynamicIsland/components/ProgressIndicator.swift b/DynamicIsland/components/ProgressIndicator.swift
new file mode 100644
index 0000000..04b6607
--- /dev/null
+++ b/DynamicIsland/components/ProgressIndicator.swift
@@ -0,0 +1,63 @@
+ //
+ // ProgressIndicator.swift
+ // boringNotch
+ //
+ // Created by Harsh Vardhan Goswami on 11/08/24.
+ //
+
+import Foundation
+import SwiftUI
+
+struct CircularProgressView: View {
+ let progress: Double
+ let color: Color
+
+ var body: some View {
+ ZStack {
+ Circle()
+ .stroke(
+ Color.white.opacity(0.2),
+ lineWidth: 6
+ )
+ Circle()
+ .trim(from: 0, to: progress)
+ .stroke(
+ color,
+ // 1
+ style: StrokeStyle(
+ lineWidth: 6,
+ lineCap: .round
+ )
+ )
+ .rotationEffect(.degrees(-90))
+ }
+ }
+}
+
+enum ProgressIndicatorType {
+ case circle
+ case text
+}
+
+
+ // based on type .circle or .text
+struct ProgressIndicator: View {
+ var type: ProgressIndicatorType
+ var progress: Double
+ var color: Color
+
+ var body: some View {
+ switch type {
+ case .circle:
+ CircularProgressView(progress: progress, color: color).frame(
+ width: 20, height: 20)
+ case .text:
+ Text("\(Int(progress * 100))%")
+ }
+ }
+}
+
+#Preview {
+ ProgressIndicator(type: .circle, progress: 0.8, color: Color.blue).padding()
+ .frame(width: 200, height: 200)
+}
diff --git a/DynamicIsland/components/Settings/EditPanelView.swift b/DynamicIsland/components/Settings/EditPanelView.swift
new file mode 100644
index 0000000..0d3c025
--- /dev/null
+++ b/DynamicIsland/components/Settings/EditPanelView.swift
@@ -0,0 +1,54 @@
+//
+// EditPanelView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 12/08/2024.
+//
+
+import SwiftUI
+
+struct EditPanelView: View {
+ @State var wallpaperPath: URL?
+ var body: some View {
+ VStack {
+ HStack {
+ Text("Edit layout")
+ .font(.system(.largeTitle, design: .rounded))
+ .foregroundColor(.white.opacity(0.5))
+ Spacer()
+ Button {
+ exit(0)
+ } label: {
+ Label("Close", systemImage: "xmark")
+ }
+ .controlSize(.extraLarge)
+ .buttonStyle(AccessoryBarButtonStyle())
+ }
+ .padding()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
+ }
+}
+
+#Preview {
+ EditPanelView()
+}
+
+struct VisualEffectView: NSViewRepresentable {
+ let material: NSVisualEffectView.Material
+ let blendingMode: NSVisualEffectView.BlendingMode
+
+ func makeNSView(context _: Context) -> NSVisualEffectView {
+ let visualEffectView = NSVisualEffectView()
+ visualEffectView.material = material
+ visualEffectView.blendingMode = blendingMode
+ visualEffectView.state = NSVisualEffectView.State.active
+ visualEffectView.isEmphasized = true
+ return visualEffectView
+ }
+
+ func updateNSView(_ visualEffectView: NSVisualEffectView, context _: Context) {
+ visualEffectView.material = material
+ visualEffectView.blendingMode = blendingMode
+ }
+}
diff --git a/DynamicIsland/components/Settings/ListItemPopover.swift b/DynamicIsland/components/Settings/ListItemPopover.swift
new file mode 100644
index 0000000..0a32422
--- /dev/null
+++ b/DynamicIsland/components/Settings/ListItemPopover.swift
@@ -0,0 +1,28 @@
+//
+// ListItemPopover.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 15/09/2024.
+//
+
+import SwiftUI
+
+struct ListItemPopover: View {
+ let content: () -> Content
+
+ @State private var isPresented: Bool = false
+ var body: some View {
+ Button {
+ isPresented.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundStyle(.secondary)
+ }
+ .controlSize(.regular)
+ .popover(isPresented: $isPresented, attachmentAnchor: .rect(.bounds), arrowEdge: .trailing, content: {
+ content()
+ .padding()
+ })
+ }
+}
+
diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift
new file mode 100644
index 0000000..efe1cd5
--- /dev/null
+++ b/DynamicIsland/components/Settings/SettingsView.swift
@@ -0,0 +1,1172 @@
+//
+// SettingsView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 07/08/2024.
+//
+
+import AVFoundation
+import Defaults
+import EventKit
+import KeyboardShortcuts
+import LaunchAtLogin
+import LottieUI
+import Sparkle
+import SwiftUI
+import SwiftUIIntrospect
+
+struct SettingsView: View {
+ @Environment(\.scenePhase) private var scenePhase
+ @StateObject var extensionManager = DynamicIslandExtensionManager()
+ let updaterController: SPUStandardUpdaterController
+ @StateObject private var calendarManager = CalendarManager()
+
+ @State private var selectedTab = "General"
+
+ var body: some View {
+ NavigationSplitView {
+ List(selection: $selectedTab) {
+ NavigationLink(destination: GeneralSettings()) {
+ Label("General", systemImage: "gear")
+ }
+ NavigationLink(destination: Appearance()) {
+ Label("Appearance", systemImage: "eye")
+ }
+ NavigationLink(destination: Media()) {
+ Label("Media", systemImage: "play.laptopcomputer")
+ }
+ NavigationLink(destination: CalendarSettings()) {
+ Label("Calendar", systemImage: "calendar")
+ }
+ if extensionManager.installedExtensions
+ .contains(
+ where: { $0.bundleIdentifier == hudExtension
+ })
+ {
+ NavigationLink(destination: HUD()) {
+ Label("HUDs", systemImage: "dial.medium.fill")
+ }
+ }
+ NavigationLink(destination: Charge()) {
+ Label("Battery", systemImage: "battery.100.bolt")
+ }
+ if extensionManager.installedExtensions
+ .contains(
+ where: { $0.bundleIdentifier == downloadManagerExtension
+ })
+ {
+ NavigationLink(destination: Downloads()) {
+ Label("Downloads", systemImage: "square.and.arrow.down")
+ }
+ }
+ NavigationLink(destination: Shelf()) {
+ Label("Shelf", systemImage: "books.vertical")
+ }
+ NavigationLink(destination: Shortcuts()) {
+ Label("Shortcuts", systemImage: "keyboard")
+ }
+ NavigationLink(destination: Extensions()) {
+ Label("Extensions", systemImage: "puzzlepiece.extension")
+ }
+ NavigationLink(
+ destination: About(updaterController: updaterController)
+ ) {
+ Label("About", systemImage: "info.circle")
+ }
+ }
+ .tint(Defaults[.accentColor])
+ } detail: {
+ GeneralSettings()
+ }
+ .environmentObject(extensionManager)
+ .environmentObject(calendarManager)
+ .formStyle(.grouped)
+ .onChange(of: scenePhase) {
+ if scenePhase == .active {
+ NSApp.setActivationPolicy(.regular)
+ NSApp.activate(ignoringOtherApps: true)
+ NSApp.keyWindow?.makeKeyAndOrderFront(nil)
+ }
+ }
+ .onReceive(NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification)) { _ in
+ NSApp.setActivationPolicy(.accessory)
+ NSApp.deactivate()
+ }
+ .introspect(.window, on: .macOS(.v14, .v15)) { window in
+ window.toolbarStyle = .unified
+ window.styleMask.update(with: .resizable)
+ }
+ }
+}
+
+struct GeneralSettings: View {
+ @State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName }
+ let accentColors: [Color] = [.blue, .purple, .pink, .red, .orange, .yellow, .green, .gray]
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @Default(.accentColor) var accentColor
+ @Default(.mirrorShape) var mirrorShape
+ @Default(.showEmojis) var showEmojis
+ @Default(.gestureSensitivity) var gestureSensitivity
+ @Default(.minimumHoverDuration) var minimumHoverDuration
+ @Default(.nonNotchHeight) var nonNotchHeight
+ @Default(.nonNotchHeightMode) var nonNotchHeightMode
+ @Default(.notchHeight) var notchHeight
+ @Default(.notchHeightMode) var notchHeightMode
+ @Default(.showOnAllDisplays) var showOnAllDisplays
+ @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay
+ @Default(.enableGestures) var enableGestures
+ @Default(.openNotchOnHover) var openNotchOnHover
+ @Default(.alwaysHideInFullscreen) var alwaysHideInFullscreen
+
+ var body: some View {
+ Form {
+ Section {
+ HStack {
+ ForEach(accentColors, id: \.self) { color in
+ Button(action: {
+ withAnimation {
+ Defaults[.accentColor] = color
+ }
+ }) {
+ Circle()
+ .fill(color)
+ .frame(width: 20, height: 20)
+ .overlay(
+ Circle()
+ .stroke(Color.white, lineWidth: Defaults[.accentColor] == color ? 2 : 0)
+ .overlay {
+ Circle()
+ .fill(.white)
+ .frame(width: 7, height: 7)
+ .opacity(Defaults[.accentColor] == color ? 1 : 0)
+ }
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ Spacer()
+ ColorPicker("Custom color", selection: $accentColor)
+ .labelsHidden()
+ }
+ } header: {
+ Text("Accent color")
+ }
+
+ Section {
+ Defaults.Toggle("Menubar icon", key: .menubarIcon)
+ LaunchAtLogin.Toggle("Launch at login")
+ Defaults.Toggle(key: .showOnAllDisplays) {
+ Text("Show on all displays")
+ }
+ .onChange(of: showOnAllDisplays) {
+ NotificationCenter.default.post(name: Notification.Name.showOnAllDisplaysChanged, object: nil)
+ }
+ Picker("Show on a specific display", selection: $coordinator.preferredScreen) {
+ ForEach(screens, id: \.self) { screen in
+ Text(screen)
+ }
+ }
+ .onChange(of: NSScreen.screens) {
+ screens = NSScreen.screens.compactMap({$0.localizedName})
+ }
+ .disabled(showOnAllDisplays)
+ Defaults.Toggle("Automatically switch displays", key: .automaticallySwitchDisplay)
+ .onChange(of: automaticallySwitchDisplay) {
+ NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil)
+ }
+ .disabled(showOnAllDisplays)
+ } header: {
+ Text("System features")
+ }
+
+ Section {
+ Picker(selection: $notchHeightMode, label:
+ Text("Notch display height")) {
+ Text("Match real notch size")
+ .tag(WindowHeightMode.matchRealNotchSize)
+ Text("Match menubar height")
+ .tag(WindowHeightMode.matchMenuBar)
+ Text("Custom height")
+ .tag(WindowHeightMode.custom)
+ }
+ .onChange(of: notchHeightMode) {
+ switch notchHeightMode {
+ case .matchRealNotchSize:
+ notchHeight = 38
+ case .matchMenuBar:
+ notchHeight = 44
+ case .custom:
+ notchHeight = 38
+ }
+ NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
+ }
+ if notchHeightMode == .custom {
+ Slider(value: $notchHeight, in: 15...45, step: 1) {
+ Text("Custom notch size - \(notchHeight, specifier: "%.0f")")
+ }
+ .onChange(of: notchHeight) {
+ NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
+ }
+ }
+ Picker("Non-notch display height", selection: $nonNotchHeightMode) {
+ Text("Match menubar height")
+ .tag(WindowHeightMode.matchMenuBar)
+ Text("Match real notch size")
+ .tag(WindowHeightMode.matchRealNotchSize)
+ Text("Custom height")
+ .tag(WindowHeightMode.custom)
+ }
+ .onChange(of: nonNotchHeightMode) {
+ switch nonNotchHeightMode {
+ case .matchMenuBar:
+ nonNotchHeight = 24
+ case .matchRealNotchSize:
+ nonNotchHeight = 32
+ case .custom:
+ nonNotchHeight = 32
+ }
+ NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
+ }
+ if nonNotchHeightMode == .custom {
+ Slider(value: $nonNotchHeight, in: 0...40, step: 1) {
+ Text("Custom notch size - \(nonNotchHeight, specifier: "%.0f")")
+ }
+ .onChange(of: nonNotchHeight) {
+ NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
+ }
+ }
+ } header: {
+ Text("Notch Height")
+ }
+
+ NotchBehaviour()
+
+ gestureControls()
+ }
+ .tint(Defaults[.accentColor])
+ .toolbar {
+ Button("Quit app") {
+ NSApp.terminate(self)
+ }
+ .controlSize(.extraLarge)
+ }
+ .navigationTitle("General")
+ .onChange(of: openNotchOnHover) {
+ if !openNotchOnHover {
+ enableGestures = true
+ }
+ }
+ }
+
+ @ViewBuilder
+ func gestureControls() -> some View {
+ Section {
+ Defaults.Toggle("Enable gestures", key: .enableGestures)
+ .disabled(!openNotchOnHover)
+ if enableGestures {
+ Toggle("Media change with horizontal gestures", isOn: .constant(false))
+ .disabled(true)
+ Defaults.Toggle("Close gesture", key: .closeGestureEnabled)
+ Slider(value: $gestureSensitivity, in: 100...300, step: 100) {
+ HStack {
+ Text("Gesture sensitivity")
+ Spacer()
+ Text(Defaults[.gestureSensitivity] == 100 ? "High" : Defaults[.gestureSensitivity] == 200 ? "Medium" : "Low")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ } header: {
+ HStack {
+ Text("Gesture control")
+ customBadge(text: "Beta")
+ }
+ } footer: {
+ Text("Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled")
+ .multilineTextAlignment(.trailing)
+ .foregroundStyle(.secondary)
+ .font(.caption)
+ }
+ }
+
+ @ViewBuilder
+ func NotchBehaviour() -> some View {
+ Section {
+ Defaults.Toggle("Extend hover area", key: .extendHoverArea)
+ Defaults.Toggle("Enable haptics", key: .enableHaptics)
+ Defaults.Toggle("Open notch on hover", key: .openNotchOnHover)
+ Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault)
+ if openNotchOnHover {
+ Slider(value: $minimumHoverDuration, in: 0...1, step: 0.1) {
+ HStack {
+ Text("Minimum hover duration")
+ Spacer()
+ Text("\(minimumHoverDuration, specifier: "%.1f")s")
+ .foregroundStyle(.secondary)
+ }
+ }
+ .onChange(of: minimumHoverDuration) {
+ NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil)
+ }
+ }
+ } header: {
+ Text("Notch behavior")
+ }
+ }
+}
+
+struct Charge: View {
+ var body: some View {
+ Form {
+ Section {
+ Defaults.Toggle("Show battery indicator", key: .showBatteryIndicator)
+ Defaults.Toggle("Show power status notifications", key: .showPowerStatusNotifications)
+ } header: {
+ Text("General")
+ }
+ Section {
+ Defaults.Toggle("Show battery percentage", key: .showBatteryPercentage)
+ Defaults.Toggle("Show power status icons", key: .showPowerStatusIcons)
+ } header: {
+ Text("Battery Information")
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Battery")
+ }
+}
+
+struct Downloads: View {
+ @Default(.selectedDownloadIndicatorStyle) var selectedDownloadIndicatorStyle
+ @Default(.selectedDownloadIconStyle) var selectedDownloadIconStyle
+ var body: some View {
+ Form {
+ warningBadge("We don't support downloads yet", "It will be supported later on.")
+ Section {
+ Defaults.Toggle("Show download progress", key: .enableDownloadListener)
+ .disabled(true)
+ Defaults.Toggle("Enable Safari Downloads", key: .enableSafariDownloads)
+ .disabled(!Defaults[.enableDownloadListener])
+ Picker("Download indicator style", selection: $selectedDownloadIndicatorStyle) {
+ Text("Progress bar")
+ .tag(DownloadIndicatorStyle.progress)
+ Text("Percentage")
+ .tag(DownloadIndicatorStyle.percentage)
+ }
+ Picker("Download icon style", selection: $selectedDownloadIconStyle) {
+ Text("Only app icon")
+ .tag(DownloadIconStyle.onlyAppIcon)
+ Text("Only download icon")
+ .tag(DownloadIconStyle.onlyIcon)
+ Text("Both")
+ .tag(DownloadIconStyle.iconAndAppIcon)
+ }
+
+ } header: {
+ HStack {
+ Text("Download indicators")
+ comingSoonTag()
+ }
+ }
+ Section {
+ List {
+ ForEach([].indices, id: \.self) { index in
+ Text("\(index)")
+ }
+ }
+ .frame(minHeight: 96)
+ .overlay {
+ if true {
+ Text("No excluded apps")
+ .foregroundStyle(Color(.secondaryLabelColor))
+ }
+ }
+ .actionBar(padding: 0) {
+ Group {
+ Button {} label: {
+ Image(systemName: "plus")
+ .frame(width: 25, height: 16, alignment: .center)
+ .contentShape(Rectangle())
+ .foregroundStyle(.secondary)
+ }
+
+ Divider()
+ Button {} label: {
+ Image(systemName: "minus")
+ .frame(width: 20, height: 16, alignment: .center)
+ .contentShape(Rectangle())
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ } header: {
+ HStack(spacing: 4) {
+ Text("Exclude apps")
+ comingSoonTag()
+ }
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Downloads")
+ }
+}
+
+struct HUD: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @Default(.inlineHUD) var inlineHUD
+ @Default(.enableGradient) var enableGradient
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ var body: some View {
+ Form {
+ Section {
+ Toggle("Enable HUD replacement", isOn: $coordinator.hudReplacement)
+ } header: {
+ Text("General")
+ }
+ Section {
+ Picker("HUD style", selection: $inlineHUD) {
+ Text("Default")
+ .tag(false)
+ Text("Inline")
+ .tag(true)
+ }
+ .onChange(of: Defaults[.inlineHUD]) {
+ if Defaults[.inlineHUD] {
+ withAnimation {
+ Defaults[.systemEventIndicatorShadow] = false
+ Defaults[.enableGradient] = false
+ }
+ }
+ }
+ Picker("Progressbar style", selection: $enableGradient) {
+ Text("Hierarchical")
+ .tag(false)
+ Text("Gradient")
+ .tag(true)
+ }
+ Defaults.Toggle("Enable glowing effect", key: .systemEventIndicatorShadow)
+ Defaults.Toggle("Use accent color", key: .systemEventIndicatorUseAccent)
+ } header: {
+ HStack {
+ Text("Appearance")
+ }
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("HUDs")
+ }
+}
+
+struct Media: View {
+ @Default(.waitInterval) var waitInterval
+ @Default(.mediaController) var mediaController
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @Default(.hideNotchOption) var hideNotchOption
+ @Default(.enableSneakPeek) private var enableSneakPeek
+ @Default(.sneakPeekStyles) var sneakPeekStyles
+
+ var body: some View {
+ Form {
+ Section {
+ Picker("Music Source", selection: $mediaController) {
+ ForEach(availableMediaControllers) { controller in
+ Text(controller.rawValue).tag(controller)
+ }
+ }
+ .onChange(of: mediaController) { _, _ in
+ NotificationCenter.default.post(
+ name: Notification.Name.mediaControllerChanged,
+ object: nil
+ )
+ }
+ } header: {
+ Text("Media Source")
+ } footer: {
+ if MusicManager.shared.isNowPlayingDeprecated {
+ HStack {
+ Text("YouTube Music requires this third-party app to be installed: ")
+ .foregroundStyle(.secondary)
+ .font(.caption)
+ Link("https://github.com/th-ch/youtube-music", destination: URL(string: "https://github.com/th-ch/youtube-music")!)
+ .font(.caption)
+ .foregroundColor(.blue) // Ensures it's visibly a link
+ }
+ } else {
+ Text("'Now Playing' was the only option on previous versions and works with all media apps.")
+ .foregroundStyle(.secondary)
+ .font(.caption)
+ }
+ }
+ Section {
+ Toggle(
+ "Enable music live activity",
+ isOn: $coordinator.musicLiveActivityEnabled.animation()
+ )
+ Toggle("Enable sneak peek", isOn: $enableSneakPeek)
+ Picker("Sneak Peek Style", selection: $sneakPeekStyles){
+ ForEach(SneakPeekStyle.allCases) { style in
+ Text(style.rawValue).tag(style)
+ }
+ }.disabled(!enableSneakPeek)
+ HStack {
+ Stepper(value: $waitInterval, in: 0...10, step: 1) {
+ HStack {
+ Text("Media inactivity timeout")
+ Spacer()
+ Text("\(Defaults[.waitInterval], specifier: "%.0f") seconds")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ } header: {
+ Text("Media playback live activity")
+ }
+
+ Picker(selection: $hideNotchOption, label:
+ HStack {
+ Text("Hide DynamicIsland Options")
+ customBadge(text: "Beta")
+ }) {
+ Text("Always hide in fullscreen").tag(HideNotchOption.always)
+ Text("Hide only when NowPlaying app is in fullscreen").tag(HideNotchOption.nowPlayingOnly)
+ Text("Never hide").tag(HideNotchOption.never)
+ }
+ .onChange(of: hideNotchOption) { _, newValue in
+ Defaults[.alwaysHideInFullscreen] = newValue == .always
+ Defaults[.enableFullscreenMediaDetection] = newValue != .never
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Media")
+ }
+
+ // Only show controller options that are available on this macOS version
+ private var availableMediaControllers: [MediaControllerType] {
+ if MusicManager.shared.isNowPlayingDeprecated {
+ return MediaControllerType.allCases.filter { $0 != .nowPlaying }
+ } else {
+ return MediaControllerType.allCases
+ }
+ }
+}
+
+struct CalendarSettings: View {
+ @ObservedObject private var calendarManager = CalendarManager()
+ @Default(.showCalendar) var showCalendar: Bool
+
+ var body: some View {
+ Form {
+ if calendarManager.authorizationStatus != .fullAccess {
+ Text("Calendar access is denied. Please enable it in System Settings.")
+ .foregroundColor(.red)
+ .multilineTextAlignment(.center)
+ .padding()
+ Button("Open System Settings") {
+ if let settingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") {
+ NSWorkspace.shared.open(settingsURL)
+ }
+ }
+ } else {
+ Toggle("Show calendar", isOn: $showCalendar)
+ Section(header: Text("Select Calendars")) {
+ List {
+ ForEach(calendarManager.allCalendars, id: \.calendarIdentifier) { calendar in
+ Toggle(isOn: Binding(
+ get: { calendarManager.getCalendarSelected(calendar) },
+ set: { isSelected in
+ calendarManager.setCalendarSelected(calendar, isSelected: isSelected)
+ }
+ )) {
+ Text(calendar.title)
+ }
+ }
+ }
+ }
+ }
+ }
+ .onAppear {
+ calendarManager.checkCalendarAuthorization()
+ }
+ }
+}
+
+struct About: View {
+ @State private var showBuildNumber: Bool = false
+ let updaterController: SPUStandardUpdaterController
+ @Environment(\.openWindow) var openWindow
+ var body: some View {
+ VStack {
+ Form {
+ Section {
+ HStack {
+ Text("Release name")
+ Spacer()
+ Text(Defaults[.releaseName])
+ .foregroundStyle(.secondary)
+ }
+ HStack {
+ Text("Version")
+ Spacer()
+ if showBuildNumber {
+ Text("(\(Bundle.main.buildVersionNumber ?? ""))")
+ .foregroundStyle(.secondary)
+ }
+ Text(Bundle.main.releaseVersionNumber ?? "unkown")
+ .foregroundStyle(.secondary)
+ }
+ .onTapGesture {
+ withAnimation {
+ showBuildNumber.toggle()
+ }
+ }
+ } header: {
+ Text("Version info")
+ }
+
+ UpdaterSettingsView(updater: updaterController.updater)
+
+ HStack(spacing: 30) {
+ Spacer(minLength: 0)
+ Button {
+ NSWorkspace.shared.open(sponsorPage)
+ } label: {
+ VStack(spacing: 5) {
+ Image(systemName: "cup.and.saucer.fill")
+ .imageScale(.large)
+ Text("Support Us")
+ .foregroundStyle(.white)
+ }
+ .contentShape(Rectangle())
+ }
+ Spacer(minLength: 0)
+ Button {
+ NSWorkspace.shared.open(productPage)
+ } label: {
+ VStack(spacing: 5) {
+ Image("Github")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 18)
+ Text("GitHub")
+ .foregroundStyle(.white)
+ }
+ .contentShape(Rectangle())
+ }
+ Spacer(minLength: 0)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ VStack(spacing: 0) {
+ Divider()
+ Text("Made with π«Άπ» by the DynamicIsland team")
+ .foregroundStyle(.secondary)
+ .padding(.top, 5)
+ .padding(.bottom, 7)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 10)
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+ .toolbar {
+ Button("Welcome window") {
+ openWindow(id: "onboarding")
+ }
+ .controlSize(.extraLarge)
+ CheckForUpdatesView(updater: updaterController.updater)
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("About")
+ }
+}
+
+struct Shelf: View {
+ var body: some View {
+ Form {
+ Section {
+ Defaults.Toggle("Enable shelf", key: .dynamicShelf)
+ Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault)
+ } header: {
+ HStack {
+ Text("General")
+ }
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Shelf")
+ }
+}
+
+struct Extensions: View {
+ @EnvironmentObject var extensionManager: DynamicIslandExtensionManager
+ @State private var effectTrigger: Bool = false
+ var body: some View {
+ Form {
+ //warningBadge("We don't support extensions yet") // Uhhhh You do? <><><> Oori.S
+ Section {
+ List {
+ ForEach(extensionManager.installedExtensions.indices, id: \.self) { index in
+ let item = extensionManager.installedExtensions[index]
+ HStack {
+ AppIcon(for: item.bundleIdentifier)
+ .resizable()
+ .frame(width: 24, height: 24)
+ Text(item.name)
+ ListItemPopover {
+ Text("Description")
+ }
+ Spacer(minLength: 0)
+ HStack(spacing: 6) {
+ Circle()
+ .frame(width: 6, height: 6)
+ .foregroundColor(isExtensionRunning(item.bundleIdentifier) ? .green : item.status == .disabled ? .gray : .red)
+ .conditionalModifier(isExtensionRunning(item.bundleIdentifier)) { view in
+ view
+ .shadow(color: .green, radius: 3)
+ }
+ Text(isExtensionRunning(item.bundleIdentifier) ? "Running" : item.status == .disabled ? "Disabled" : "Stopped")
+ .contentTransition(.numericText())
+ .foregroundStyle(.secondary)
+ .font(.footnote)
+ }
+ .frame(width: 60, alignment: .leading)
+
+ Menu(content: {
+ Button("Restart") {
+ let ws = NSWorkspace.shared
+
+ if let ext = ws.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) {
+ ext.terminate()
+ }
+
+ if let appURL = ws.urlForApplication(withBundleIdentifier: item.bundleIdentifier) {
+ ws.openApplication(at: appURL, configuration: .init(), completionHandler: nil)
+ }
+ }
+ .keyboardShortcut("R", modifiers: .command)
+ Button("Disable") {
+ if let ext = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) {
+ ext.terminate()
+ }
+ extensionManager.installedExtensions[index].status = .disabled
+ }
+ .keyboardShortcut("D", modifiers: .command)
+ Divider()
+ Button("Uninstall", role: .destructive) {
+ //
+ }
+ }, label: {
+ Image(systemName: "ellipsis.circle")
+ .foregroundStyle(.secondary)
+ })
+ .controlSize(.regular)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .padding(.vertical, 5)
+ }
+ }
+ .frame(minHeight: 120)
+ .actionBar {
+ Button {} label: {
+ HStack(spacing: 3) {
+ Image(systemName: "plus")
+ Text("Add manually")
+ }
+ .foregroundStyle(.secondary)
+ }
+ .disabled(true)
+ Spacer()
+ Button {
+ withAnimation(.linear(duration: 1)) {
+ effectTrigger.toggle()
+ } completion: {
+ effectTrigger.toggle()
+ }
+ extensionManager.checkIfExtensionsAreInstalled()
+ } label: {
+ HStack(spacing: 3) {
+ Image(systemName: "arrow.triangle.2.circlepath")
+ .rotationEffect(effectTrigger ? .degrees(360) : .zero)
+ }
+ .foregroundStyle(.secondary)
+ }
+ }
+ .controlSize(.small)
+ .buttonStyle(PlainButtonStyle())
+ .overlay {
+ if extensionManager.installedExtensions.isEmpty {
+ Text("No extension installed")
+ .foregroundStyle(Color(.secondaryLabelColor))
+ .padding(.bottom, 22)
+ }
+ }
+ } header: {
+ HStack(spacing: 0) {
+ Text("Installed extensions")
+ if !extensionManager.installedExtensions.isEmpty {
+ Text(" β \(extensionManager.installedExtensions.count)")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Extensions")
+ // TipsView()
+ // .padding(.horizontal, 19)
+ }
+}
+
+struct Appearance: View {
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @Default(.mirrorShape) var mirrorShape
+ @Default(.sliderColor) var sliderColor
+ @Default(.useMusicVisualizer) var useMusicVisualizer
+ @Default(.customVisualizers) var customVisualizers
+ @Default(.selectedVisualizer) var selectedVisualizer
+ let icons: [String] = ["logo2"]
+ @State private var selectedIcon: String = "logo2"
+ @State private var selectedListVisualizer: CustomVisualizer? = nil
+
+ @State private var isPresented: Bool = false
+ @State private var name: String = ""
+ @State private var url: String = ""
+ @State private var speed: CGFloat = 1.0
+ var body: some View {
+ Form {
+ Section {
+ Toggle("Always show tabs", isOn: $coordinator.alwaysShowTabs)
+ Defaults.Toggle("Settings icon in notch", key: .settingsIconInNotch)
+ Defaults.Toggle("Enable window shadow", key: .enableShadow)
+ Defaults.Toggle("Corner radius scaling", key: .cornerRadiusScaling)
+ Defaults.Toggle("Use simpler close animation", key: .useModernCloseAnimation)
+ } header: {
+ Text("General")
+ }
+
+ Section {
+ Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram)
+ Defaults
+ .Toggle("Player tinting", key: .playerColorTinting)
+ Defaults.Toggle("Enable blur effect behind album art", key: .lightingEffect)
+ Picker("Slider color", selection: $sliderColor) {
+ ForEach(SliderColorEnum.allCases, id: \.self) { option in
+ Text(option.rawValue)
+ }
+ }
+ } header: {
+ Text("Media")
+ }
+
+ Section {
+ Toggle(
+ "Use music visualizer spectrogram",
+ isOn: $useMusicVisualizer.animation()
+ )
+ .disabled(true)
+ if !useMusicVisualizer {
+ if customVisualizers.count > 0 {
+ Picker(
+ "Selected animation",
+ selection: $selectedVisualizer
+ ) {
+ ForEach(
+ customVisualizers,
+ id: \.self
+ ) { visualizer in
+ Text(visualizer.name)
+ .tag(visualizer)
+ }
+ }
+ } else {
+ HStack {
+ Text("Selected animation")
+ Spacer()
+ Text("No custom animation available")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ } header: {
+ HStack {
+ Text("Custom music live activity animation")
+ customBadge(text: "Coming soon")
+ }
+ }
+
+ Section {
+ List {
+ ForEach(customVisualizers, id: \.self) { visualizer in
+ HStack {
+ LottieView(state: LUStateData(type: .loadedFrom(visualizer.url), speed: visualizer.speed, loopMode: .loop))
+ .frame(width: 30, height: 30, alignment: .center)
+ Text(visualizer.name)
+ Spacer(minLength: 0)
+ if selectedVisualizer == visualizer {
+ Text("selected")
+ .font(.caption)
+ .fontWeight(.medium)
+ .foregroundStyle(.secondary)
+ .padding(.trailing, 8)
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ .padding(.vertical, 2)
+ .background(
+ selectedListVisualizer != nil ? selectedListVisualizer == visualizer ? Defaults[.accentColor] : Color.clear : Color.clear,
+ in: RoundedRectangle(cornerRadius: 5)
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ if selectedListVisualizer == visualizer {
+ selectedListVisualizer = nil
+ return
+ }
+ selectedListVisualizer = visualizer
+ }
+ }
+ }
+ .safeAreaPadding(
+ EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0)
+ )
+ .frame(minHeight: 120)
+ .actionBar {
+ HStack(spacing: 5) {
+ Button {
+ name = ""
+ url = ""
+ speed = 1.0
+ isPresented.toggle()
+ } label: {
+ Image(systemName: "plus")
+ .foregroundStyle(.secondary)
+ .contentShape(Rectangle())
+ }
+ Divider()
+ Button {
+ if selectedListVisualizer != nil {
+ let visualizer = selectedListVisualizer!
+ selectedListVisualizer = nil
+ customVisualizers.remove(at: customVisualizers.firstIndex(of: visualizer)!)
+ if visualizer == selectedVisualizer && customVisualizers.count > 0 {
+ selectedVisualizer = customVisualizers[0]
+ }
+ }
+ } label: {
+ Image(systemName: "minus")
+ .foregroundStyle(.secondary)
+ .contentShape(Rectangle())
+ }
+ }
+ }
+ .controlSize(.small)
+ .buttonStyle(PlainButtonStyle())
+ .overlay {
+ if customVisualizers.isEmpty {
+ Text("No custom visualizer")
+ .foregroundStyle(Color(.secondaryLabelColor))
+ .padding(.bottom, 22)
+ }
+ }
+ .sheet(isPresented: $isPresented) {
+ VStack(alignment: .leading) {
+ Text("Add new visualizer")
+ .font(.largeTitle.bold())
+ .padding(.vertical)
+ TextField("Name", text: $name)
+ TextField("Lottie JSON URL", text: $url)
+ HStack {
+ Text("Speed")
+ Spacer(minLength: 80)
+ Text("\(speed, specifier: "%.1f")s")
+ .multilineTextAlignment(.trailing)
+ .foregroundStyle(.secondary)
+ Slider(value: $speed, in: 0...2, step: 0.1)
+ }
+ .padding(.vertical)
+ HStack {
+ Button {
+ isPresented.toggle()
+ } label: {
+ Text("Cancel")
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+
+ Button {
+ let visualizer: CustomVisualizer = .init(
+ UUID: UUID(),
+ name: name,
+ url: URL(string: url)!,
+ speed: speed
+ )
+
+ if !customVisualizers.contains(visualizer) {
+ customVisualizers.append(visualizer)
+ }
+
+ isPresented.toggle()
+ } label: {
+ Text("Add")
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+ .buttonStyle(BorderedProminentButtonStyle())
+ }
+ }
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .controlSize(.extraLarge)
+ .padding()
+ }
+ } header: {
+ HStack(spacing: 0) {
+ Text("Custom vizualizers (Lottie)")
+ if !Defaults[.customVisualizers].isEmpty {
+ Text(" β \(Defaults[.customVisualizers].count)")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+
+ Section {
+ Defaults.Toggle("Enable dynamic mirror", key: .showMirror)
+ .disabled(!checkVideoInput())
+ Picker("Mirror shape", selection: $mirrorShape) {
+ Text("Circle")
+ .tag(MirrorShapeEnum.circle)
+ Text("Square")
+ .tag(MirrorShapeEnum.rectangle)
+ }
+ Defaults.Toggle("Show cool face animation while inactivity", key: .showNotHumanFace)
+ } header: {
+ HStack {
+ Text("Additional features")
+ }
+ }
+
+ Section {
+ HStack {
+ ForEach(icons, id: \.self) { icon in
+ Spacer()
+ VStack {
+ Image(icon)
+ .resizable()
+ .frame(width: 80, height: 80)
+ .background(
+ RoundedRectangle(cornerRadius: 20, style: .circular)
+ .strokeBorder(
+ icon == selectedIcon ? Defaults[.accentColor] : .clear,
+ lineWidth: 2.5
+ )
+ )
+
+ Text("Default")
+ .fontWeight(.medium)
+ .font(.caption)
+ .foregroundStyle(icon == selectedIcon ? .white : .secondary)
+ .padding(.horizontal, 10)
+ .padding(.vertical, 3)
+ .background(
+ Capsule()
+ .fill(icon == selectedIcon ? Defaults[.accentColor] : .clear)
+ )
+ }
+ .onTapGesture {
+ withAnimation {
+ selectedIcon = icon
+ }
+ NSApp.applicationIconImage = NSImage(named: icon)
+ }
+ Spacer()
+ }
+ }
+ .disabled(true)
+ } header: {
+ HStack {
+ Text("App icon")
+ customBadge(text: "Coming soon")
+ }
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Appearance")
+ }
+
+ func checkVideoInput() -> Bool {
+ if let _ = AVCaptureDevice.default(for: .video) {
+ return true
+ }
+
+ return false
+ }
+}
+
+struct Shortcuts: View {
+ var body: some View {
+ Form {
+ Section {
+ KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek)
+ } header: {
+ Text("Media")
+ } footer: {
+ Text("Sneak Peek shows the media title and artist under the notch for a few seconds.")
+ .multilineTextAlignment(.trailing)
+ .foregroundStyle(.secondary)
+ .font(.caption)
+ }
+ Section {
+ KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen)
+ }
+ }
+ .tint(Defaults[.accentColor])
+ .navigationTitle("Shortcuts")
+ }
+}
+
+func proFeatureBadge() -> some View {
+ Text("Upgrade to Pro")
+ .foregroundStyle(Color(red: 0.545, green: 0.196, blue: 0.98))
+ .font(.footnote.bold())
+ .padding(.vertical, 3)
+ .padding(.horizontal, 6)
+ .background(RoundedRectangle(cornerRadius: 4).stroke(Color(red: 0.545, green: 0.196, blue: 0.98), lineWidth: 1))
+}
+
+func comingSoonTag() -> some View {
+ Text("Coming soon")
+ .foregroundStyle(.secondary)
+ .font(.footnote.bold())
+ .padding(.vertical, 3)
+ .padding(.horizontal, 6)
+ .background(Color(nsColor: .secondarySystemFill))
+ .clipShape(.capsule)
+}
+
+func customBadge(text: String) -> some View {
+ Text(text)
+ .foregroundStyle(.secondary)
+ .font(.footnote.bold())
+ .padding(.vertical, 3)
+ .padding(.horizontal, 6)
+ .background(Color(nsColor: .secondarySystemFill))
+ .clipShape(.capsule)
+}
+
+func warningBadge(_ text: String, _ description: String) -> some View {
+ Section {
+ HStack(spacing: 12) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 22))
+ .foregroundStyle(.yellow)
+ VStack(alignment: .leading) {
+ Text(text)
+ .font(.headline)
+ Text(description)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ }
+}
+
+#Preview {
+ HUD()
+}
diff --git a/DynamicIsland/components/Settings/SoftwareUpdater.swift b/DynamicIsland/components/Settings/SoftwareUpdater.swift
new file mode 100644
index 0000000..f9db680
--- /dev/null
+++ b/DynamicIsland/components/Settings/SoftwareUpdater.swift
@@ -0,0 +1,67 @@
+//
+// SoftwareUpdater.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 09/08/2024.
+//
+
+import SwiftUI
+import Sparkle
+
+final class CheckForUpdatesViewModel: ObservableObject {
+ @Published var canCheckForUpdates = false
+
+ init(updater: SPUUpdater) {
+ updater.publisher(for: \.canCheckForUpdates)
+ .assign(to: &$canCheckForUpdates)
+ }
+}
+
+struct CheckForUpdatesView: View {
+ @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel
+ private let updater: SPUUpdater
+
+ init(updater: SPUUpdater) {
+ self.updater = updater
+
+ // Create our view model for our CheckForUpdatesView
+ self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater)
+ }
+
+ var body: some View {
+ Button("Check for Updatesβ¦", action: updater.checkForUpdates)
+ .disabled(!checkForUpdatesViewModel.canCheckForUpdates)
+ }
+}
+
+struct UpdaterSettingsView: View {
+ private let updater: SPUUpdater
+
+ @State private var automaticallyChecksForUpdates: Bool
+ @State private var automaticallyDownloadsUpdates: Bool
+
+ init(updater: SPUUpdater) {
+ self.updater = updater
+ self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
+ self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates
+ }
+
+ var body: some View {
+ Section {
+ Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates)
+ .onChange(of: automaticallyChecksForUpdates) { _, newValue in
+ updater.automaticallyChecksForUpdates = newValue
+ }
+
+ Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates)
+ .disabled(!automaticallyChecksForUpdates)
+ .onChange(of: automaticallyDownloadsUpdates) { _, newValue in
+ updater.automaticallyDownloadsUpdates = newValue
+ }
+ } header: {
+ HStack {
+ Text("Software updates")
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/components/Shelf/AirDrop.swift b/DynamicIsland/components/Shelf/AirDrop.swift
new file mode 100644
index 0000000..a46f7c5
--- /dev/null
+++ b/DynamicIsland/components/Shelf/AirDrop.swift
@@ -0,0 +1,40 @@
+//
+// AirDrop.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/7.
+//
+
+import Cocoa
+
+class AirDrop: NSObject, NSSharingServiceDelegate {
+ let files: [URL]
+
+ init(files: [URL]) {
+ self.files = files
+ super.init()
+ }
+
+ func begin() {
+ do {
+ try sendEx(files)
+ } catch {
+ NSAlert.popError(error)
+ }
+ }
+
+ private func sendEx(_ files: [URL]) throws {
+ guard let service = NSSharingService(named: .sendViaAirDrop) else {
+ throw NSError(domain: "AirDrop", code: 1, userInfo: [
+ NSLocalizedDescriptionKey: NSLocalizedString("AirDrop service not available", comment: ""),
+ ])
+ }
+ guard service.canPerform(withItems: files) else {
+ throw NSError(domain: "AirDrop", code: 2, userInfo: [
+ NSLocalizedDescriptionKey: NSLocalizedString("AirDrop service not available", comment: ""),
+ ])
+ }
+ service.delegate = self
+ service.perform(withItems: files)
+ }
+}
diff --git a/DynamicIsland/components/Shelf/AirDropView.swift b/DynamicIsland/components/Shelf/AirDropView.swift
new file mode 100644
index 0000000..d6e3b2d
--- /dev/null
+++ b/DynamicIsland/components/Shelf/AirDropView.swift
@@ -0,0 +1,70 @@
+//
+// AirDrop+View.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/8.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct AirDropView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+
+ @State var trigger: UUID = .init()
+ @State var targeting = false
+
+ var body: some View {
+ dropArea
+ .onDrop(of: [.data], isTargeted: $vm.dropZoneTargeting) { providers in
+ trigger = .init()
+ vm.dropEvent = true
+ DispatchQueue.global().async { beginDrop(providers) }
+ return true
+ }
+ }
+
+ var dropArea: some View {
+ Rectangle()
+ .fill(.white.opacity(0.1))
+ .opacity(0.5)
+ .clipShape(RoundedRectangle(cornerRadius: 10))
+ .overlay { dropLabel }
+ .aspectRatio(1, contentMode: .fit)
+ .contentShape(Rectangle())
+ }
+
+ var dropLabel: some View {
+ VStack(spacing: 8) {
+ Image(systemName: "airplayaudio")
+ Text("AirDrop")
+ }
+ .foregroundStyle(.gray)
+ .font(.system(.headline, design: .rounded))
+ .contentShape(Rectangle())
+ .onTapGesture {
+ trigger = .init()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ let picker = NSOpenPanel()
+ picker.allowsMultipleSelection = true
+ picker.canChooseDirectories = true
+ picker.canChooseFiles = true
+ picker.begin { response in
+ if response == .OK {
+ let drop = AirDrop(files: picker.urls)
+ drop.begin()
+ }
+ }
+ }
+ }
+ }
+
+ func beginDrop(_ providers: [NSItemProvider]) {
+ assert(!Thread.isMainThread)
+ guard let urls = providers.interfaceConvert() else { return }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ let drop = AirDrop(files: urls)
+ drop.begin()
+ }
+ }
+}
diff --git a/DynamicIsland/components/Shelf/DragDropView.swift b/DynamicIsland/components/Shelf/DragDropView.swift
new file mode 100644
index 0000000..fa2a395
--- /dev/null
+++ b/DynamicIsland/components/Shelf/DragDropView.swift
@@ -0,0 +1,56 @@
+//
+// DragDropView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 2024. 10. 19..
+//
+
+import SwiftUI
+import AppKit
+
+class DragDropView: NSView {
+ var onDragEntered: () -> Void = {}
+ var onDragExited: () -> Void = {}
+ var onDrop: () -> Void = {}
+
+ override init(frame frameRect: NSRect) {
+ super.init(frame: frameRect)
+ registerForDraggedTypes([.fileURL])
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
+ onDragEntered()
+ return .copy
+ }
+
+ override func draggingExited(_ sender: NSDraggingInfo?) {
+ onDragExited()
+ }
+
+ override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ onDrop()
+ return true
+ }
+}
+
+struct DragDropViewRepresentable: NSViewRepresentable {
+ @Binding var isTargeted: Bool
+ var onDrop: () -> Void
+
+ func makeNSView(context: Context) -> DragDropView {
+ let view = DragDropView()
+ view.onDragEntered = { isTargeted = true }
+ view.onDragExited = { isTargeted = false }
+ view.onDrop = onDrop
+
+ view.autoresizingMask = [.width, .height]
+
+ return view
+ }
+
+ func updateNSView(_ nsView: DragDropView, context: Context) {}
+}
diff --git a/DynamicIsland/components/Shelf/DropItem.swift b/DynamicIsland/components/Shelf/DropItem.swift
new file mode 100644
index 0000000..0748cdd
--- /dev/null
+++ b/DynamicIsland/components/Shelf/DropItem.swift
@@ -0,0 +1,58 @@
+//
+// TrayDrop+DropItem.swift
+// TrayDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/8.
+//
+
+import Cocoa
+import Foundation
+import QuickLook
+
+extension TrayDrop {
+ struct DropItem: Identifiable, Codable, Equatable, Hashable {
+ let id: UUID
+
+ let fileName: String
+ let size: Int
+
+ let copiedDate: Date
+ let workspacePreviewImageData: Data
+
+ init(url: URL) throws {
+ assert(!Thread.isMainThread)
+
+ id = UUID()
+ fileName = url.lastPathComponent
+
+ size = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0
+ copiedDate = Date()
+ workspacePreviewImageData = url.snapshotPreview().pngRepresentation
+
+ try FileManager.default.createDirectory(
+ at: storageURL.deletingLastPathComponent(),
+ withIntermediateDirectories: true
+ )
+ try FileManager.default.copyItem(at: url, to: storageURL)
+ }
+ }
+}
+
+extension TrayDrop.DropItem {
+ static let mainDir = "CopiedItems"
+
+ var storageURL: URL {
+ documentsDirectory
+ .appendingPathComponent(Self.mainDir)
+ .appendingPathComponent(id.uuidString)
+ .appendingPathComponent(fileName)
+ }
+
+ var workspacePreviewImage: NSImage {
+ .init(data: workspacePreviewImageData) ?? .init()
+ }
+
+ var shouldClean: Bool { // TODO: In the future clean if old
+ return false
+ }
+}
diff --git a/DynamicIsland/components/Shelf/DropItemView.swift b/DynamicIsland/components/Shelf/DropItemView.swift
new file mode 100644
index 0000000..131d70c
--- /dev/null
+++ b/DynamicIsland/components/Shelf/DropItemView.swift
@@ -0,0 +1,79 @@
+//
+// TrayDrop+DropItemView.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/8.
+//
+
+import Foundation
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct DropItemView: View {
+ let item: TrayDrop.DropItem
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @StateObject var tvm = TrayDrop.shared
+
+ @State var hover = false
+
+ var body: some View {
+ ZStack(alignment: .topTrailing) {
+ VStack {
+ RoundedRectangle(cornerRadius: 6)
+ .fill(.clear)
+ .background {
+ Image(nsImage: item.workspacePreviewImage)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .clipShape(RoundedRectangle(cornerRadius: 6))
+ }
+
+ Text(item.fileName)
+ .multilineTextAlignment(.center)
+ .font(.footnote)
+ .foregroundStyle(hover ? .white : .gray)
+ .lineLimit(2)
+ .allowsTightening(true)
+ }
+ .contentShape(Rectangle())
+ .onDrag {
+ handleOnDrag(for: item)
+ }
+ .frame(width: 64, height: 64)
+
+ if hover {
+ Circle()
+ .fill(.white)
+ .overlay(Image(systemName: "xmark").foregroundStyle(.black).font(.system(size: 7)).fontWeight(.semibold))
+ .frame(width: spacing, height: spacing)
+ .opacity(hover ? 1 : 0) // TODO: Use option key pressed to show delete
+ .scaleEffect(coordinator.optionKeyPressed ? 1 : 0.5)
+ .transition(.blurReplace.combined(with: .scale))
+ .offset(x: spacing / 2, y: -spacing / 2)
+ .onTapGesture { tvm.delete(item.id) }
+ .shadow(color: .black, radius: 3)
+ }
+ }
+ .onHover { hovering in
+ withAnimation(.smooth) {
+ if hovering {
+ hover.toggle()
+ } else {
+ hover.toggle()
+ }
+ }
+ }
+ }
+
+ private func handleOnDrag(for item: TrayDrop.DropItem) -> NSItemProvider {
+ guard let itemProvider = NSItemProvider(contentsOf: item.storageURL) else {
+ return NSItemProvider()
+ }
+
+ let nameWithoutExtension = (item.fileName as NSString).deletingPathExtension
+ itemProvider.suggestedName = nameWithoutExtension
+
+ return itemProvider
+ }
+}
diff --git a/DynamicIsland/components/Shelf/Ext+FileProvider.swift b/DynamicIsland/components/Shelf/Ext+FileProvider.swift
new file mode 100644
index 0000000..c755079
--- /dev/null
+++ b/DynamicIsland/components/Shelf/Ext+FileProvider.swift
@@ -0,0 +1,61 @@
+//
+// Ext+FileProvider.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/8.
+//
+
+import Cocoa
+import Foundation
+import UniformTypeIdentifiers
+
+extension NSItemProvider {
+ private func duplicateToOurStorage(_ url: URL?) throws -> URL? {
+ guard let url else { return nil }
+ let temp = temporaryDirectory
+ .appendingPathComponent("TemporaryDrop")
+ .appendingPathComponent(UUID().uuidString)
+ .appendingPathComponent(url.lastPathComponent)
+ try? FileManager.default.createDirectory(
+ at: temp.deletingLastPathComponent(),
+ withIntermediateDirectories: true
+ )
+ try FileManager.default.copyItem(at: url, to: temp)
+ return temp
+ }
+
+ func convertToFilePathThatIsWhatWeThinkItWillWorkWithNotchDrop() -> URL? {
+ var url: URL?
+ let sem = DispatchSemaphore(value: 0)
+ _ = loadObject(ofClass: URL.self) { item, _ in
+ url = try? self.duplicateToOurStorage(item)
+ sem.signal()
+ }
+ sem.wait()
+ if url == nil {
+ loadInPlaceFileRepresentation(
+ forTypeIdentifier: UTType.data.identifier
+ ) { input, _, _ in
+ defer { sem.signal() }
+ url = try? self.duplicateToOurStorage(input)
+ }
+ sem.wait()
+ }
+ return url
+ }
+}
+
+extension [NSItemProvider] {
+ func interfaceConvert() -> [URL]? {
+ let urls = compactMap { provider -> URL? in
+ provider.convertToFilePathThatIsWhatWeThinkItWillWorkWithNotchDrop()
+ }
+ guard urls.count == count else {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ NSAlert.popError(NSLocalizedString("One or more files failed to load", comment: ""))
+ }
+ return nil
+ }
+ return urls
+ }
+}
diff --git a/DynamicIsland/components/Shelf/Ext+NSAlert.swift b/DynamicIsland/components/Shelf/Ext+NSAlert.swift
new file mode 100644
index 0000000..4c72ab7
--- /dev/null
+++ b/DynamicIsland/components/Shelf/Ext+NSAlert.swift
@@ -0,0 +1,36 @@
+//
+// Ext+NSAlert.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/9.
+//
+
+import Cocoa
+
+extension NSAlert {
+ static func popError(_ error: String) {
+ let alert = NSAlert()
+ alert.messageText = NSLocalizedString("Error", comment: "")
+ alert.alertStyle = .critical
+ alert.informativeText = error
+ alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
+ alert.runModal()
+ }
+
+ static func popRestart(_ error: String, completion: @escaping () -> Void) {
+ let alert = NSAlert()
+ alert.messageText = NSLocalizedString("Need Restart", comment: "")
+ alert.alertStyle = .critical
+ alert.informativeText = error
+ alert.addButton(withTitle: NSLocalizedString("Exit", comment: ""))
+ alert.addButton(withTitle: NSLocalizedString("Later", comment: ""))
+ let response = alert.runModal()
+ if response == .alertFirstButtonReturn {
+ completion()
+ }
+ }
+
+ static func popError(_ error: Error) {
+ popError(error.localizedDescription)
+ }
+}
diff --git a/DynamicIsland/components/Shelf/Ext+NSImage.swift b/DynamicIsland/components/Shelf/Ext+NSImage.swift
new file mode 100644
index 0000000..32d357a
--- /dev/null
+++ b/DynamicIsland/components/Shelf/Ext+NSImage.swift
@@ -0,0 +1,19 @@
+//
+// Ext+NSImage.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/8.
+//
+
+import Cocoa
+
+extension NSImage {
+ var pngRepresentation: Data {
+ guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else {
+ return .init()
+ }
+ let imageRep = NSBitmapImageRep(cgImage: cgImage)
+ imageRep.size = size
+ return imageRep.representation(using: .png, properties: [:]) ?? .init()
+ }
+}
diff --git a/DynamicIsland/components/Shelf/Ext+URL.swift b/DynamicIsland/components/Shelf/Ext+URL.swift
new file mode 100644
index 0000000..0eaf442
--- /dev/null
+++ b/DynamicIsland/components/Shelf/Ext+URL.swift
@@ -0,0 +1,24 @@
+//
+// Ext+URL.swift
+// NotchDrop
+//
+// Created by η§ζζ‘₯ on 2024/7/8.
+//
+
+import Cocoa
+import Foundation
+import QuickLook
+
+extension URL {
+ func snapshotPreview() -> NSImage {
+ if let preview = QLThumbnailImageCreate(
+ kCFAllocatorDefault,
+ self as CFURL,
+ CGSize(width: 128, height: 128),
+ nil
+ )?.takeRetainedValue() {
+ return NSImage(cgImage: preview, size: .zero)
+ }
+ return NSWorkspace.shared.icon(forFile: path)
+ }
+}
diff --git a/DynamicIsland/components/Shelf/TrayDrop.swift b/DynamicIsland/components/Shelf/TrayDrop.swift
new file mode 100644
index 0000000..b701b6a
--- /dev/null
+++ b/DynamicIsland/components/Shelf/TrayDrop.swift
@@ -0,0 +1,77 @@
+import Cocoa
+import Combine
+import Foundation
+import OrderedCollections
+
+class TrayDrop: ObservableObject {
+ static let shared = TrayDrop()
+
+ @Published var items: OrderedSet
+ var isEmpty: Bool { items.isEmpty }
+ @Published var isLoading: Int = 0
+
+ init(items: OrderedSet = .init(), isLoading: Int = 0) {
+ self.items = items
+ self.isLoading = isLoading
+ }
+
+ func load(_ providers: [NSItemProvider]) {
+ assert(!Thread.isMainThread)
+ DispatchQueue.main.asyncAndWait { isLoading += 1 }
+
+ guard let urls = providers.interfaceConvert() else {
+ DispatchQueue.main.asyncAndWait { isLoading -= 1 }
+ print("Faield to load items")
+ return
+ }
+ let dropItems = urls.map { url in
+ try? DropItem(url: url)
+ }.compactMap { $0 }
+
+ DispatchQueue.main.async {
+ dropItems.forEach { self.items.updateOrInsert($0, at: 0) }
+ self.isLoading -= 1
+ }
+ print("DONE")
+ }
+
+ func cleanExpiredFiles() {
+ var inEdit = items
+ let shouldCleanItems = items.filter(\.shouldClean)
+ for item in shouldCleanItems {
+ inEdit.remove(item)
+ }
+ items = inEdit
+ }
+
+ func delete(_ item: DropItem.ID) {
+ guard let item = items.first(where: { $0.id == item }) else { return }
+ delete(item: item)
+ }
+
+ private func delete(item: DropItem) {
+ var inEdit = items
+
+ var url = item.storageURL
+ try? FileManager.default.removeItem(at: url)
+
+ do {
+ // loops up to the main directory
+ url = url.deletingLastPathComponent()
+ while url.lastPathComponent != DropItem.mainDir, url != documentsDirectory {
+ let contents = try FileManager.default.contentsOfDirectory(atPath: url.path)
+ guard contents.isEmpty else { break }
+ try FileManager.default.removeItem(at: url)
+ url = url.deletingLastPathComponent()
+ }
+ } catch {}
+
+ inEdit.remove(item)
+ items = inEdit
+ }
+
+ func removeAll() {
+ items.forEach { delete(item: $0) }
+ }
+}
+
diff --git a/DynamicIsland/components/Tabs/TabButton.swift b/DynamicIsland/components/Tabs/TabButton.swift
new file mode 100644
index 0000000..e6eb6b4
--- /dev/null
+++ b/DynamicIsland/components/Tabs/TabButton.swift
@@ -0,0 +1,30 @@
+//
+// TabButton.swift
+// boringNotch
+//
+// Created by Hugo Persson on 2024-08-24.
+//
+
+import SwiftUI
+
+struct TabButton: View {
+ let label: String
+ let icon: String
+ let selected: Bool
+ let onClick: () -> Void
+
+ var body: some View {
+ Button(action: onClick) {
+ Image(systemName: icon)
+ .padding(.horizontal, 15)
+ .contentShape(Capsule())
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+#Preview {
+ TabButton(label: "Home", icon: "tray.fill", selected: true) {
+ print("Tapped")
+ }
+}
diff --git a/DynamicIsland/components/Tabs/TabSelectionView.swift b/DynamicIsland/components/Tabs/TabSelectionView.swift
new file mode 100644
index 0000000..5832c30
--- /dev/null
+++ b/DynamicIsland/components/Tabs/TabSelectionView.swift
@@ -0,0 +1,55 @@
+//
+// TabSelectionView.swift
+// boringNotch
+//
+// Created by Hugo Persson on 2024-08-25.
+//
+
+import SwiftUI
+
+struct TabModel: Identifiable {
+ let id = UUID()
+ let label: String
+ let icon: String
+ let view: NotchViews
+}
+
+let tabs = [
+ TabModel(label: "Home", icon: "house.fill", view: .home),
+ TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)
+]
+
+struct TabSelectionView: View {
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @Namespace var animation
+ var body: some View {
+ HStack(spacing: 0) {
+ ForEach(tabs) { tab in
+ TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) {
+ withAnimation(.smooth) {
+ coordinator.currentView = tab.view
+ }
+ }
+ .frame(height: 26)
+ .foregroundStyle(tab.view == coordinator.currentView ? .white : .gray)
+ .background {
+ if tab.view == coordinator.currentView {
+ Capsule()
+ .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear)
+ .matchedGeometryEffect(id: "capsule", in: animation)
+ } else {
+ Capsule()
+ .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear)
+ .matchedGeometryEffect(id: "capsule", in: animation)
+ .hidden()
+ }
+ }
+ }
+ }
+ .clipShape(Capsule())
+ }
+}
+
+#Preview {
+ DynamicIslandHeader().environmentObject(DynamicIslandViewModel())
+}
diff --git a/DynamicIsland/components/TestView.swift b/DynamicIsland/components/TestView.swift
new file mode 100644
index 0000000..fbee749
--- /dev/null
+++ b/DynamicIsland/components/TestView.swift
@@ -0,0 +1,92 @@
+//
+// TestView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 14/08/2024.
+//
+
+import SwiftUI
+
+struct FluidSlider: View {
+ private let color: Color = Color.white
+ @State private var offset: CGFloat = 0
+ var rectSize = CGSize(width: 300, height: 50)
+ var rectSize2 = CGSize(width: 200, height: 18)
+ var circleSize: CGFloat = 35
+ @GestureState var isDragging: Bool = false
+ @State var previousOffset: CGFloat = 0
+ @State private var isBeating: Bool = false
+
+ var body: some View {
+ HStack {
+ slider
+ .frame(width: rectSize2.width, height: circleSize)
+ }
+ .padding()
+ .background(.black)
+ }
+
+ private var slider: some View {
+ ZStack {
+ Canvas { context, size in
+ context.addFilter(.alphaThreshold(min: 0.5, max: 1, color: color))
+ context.addFilter(.blur(radius: 10))
+
+ context.drawLayer { ctx in
+ if let rectangle = ctx.resolveSymbol(id: "Capsule") {
+ ctx.draw(rectangle, at: CGPoint(x: size.width/2, y: size.height/2))
+ }
+ if let circle = ctx.resolveSymbol(id: "Circle") {
+ ctx.draw(circle, at: CGPoint(x: size.width/2 - rectSize2.width/2 + circleSize/2, y: size.height/2))
+ }
+ }
+ } symbols: {
+ Capsule()
+ .frame(width: rectSize2.width, height: rectSize2.height, alignment: .center)
+ .tag("Capsule")
+
+ Circle()
+ .frame(width: circleSize, height: circleSize, alignment: .center)
+ .offset(x: offset)
+ .animation(.spring(), value: isDragging)
+ .tag("Circle")
+ }
+ .simultaneousGesture(
+ DragGesture(minimumDistance: 0)
+ .updating($isDragging, body: { _, state, _ in
+ state = true
+ })
+ .onChanged({ value in
+ self.offset = min(max(self.previousOffset + value.translation.width, 0), rectSize2.width - circleSize)
+ })
+ .onEnded({ value in
+ self.previousOffset = self.offset
+ })
+ )
+ Circle()
+ .fill(Color.black)
+ .frame(width: circleSize * 0.6)
+ .overlay {
+ Image(systemName: "speaker.wave.2.fill")
+ .imageScale(.small)
+ }
+ .offset(x: (-rectSize2.width/2) + (circleSize/2))
+ .offset(x: offset)
+ .animation(.spring(), value: isDragging)
+ .allowsHitTesting(false)
+ }
+ }
+
+
+ private var animation: Animation {
+ .spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5)
+ }
+
+ private var percentage: Int {
+ Int((offset) / (rectSize.width - circleSize) * 100)
+ }
+}
+
+#Preview {
+ FluidSlider()
+}
diff --git a/DynamicIsland/components/Tips/TipStore.swift b/DynamicIsland/components/Tips/TipStore.swift
new file mode 100644
index 0000000..51b5d45
--- /dev/null
+++ b/DynamicIsland/components/Tips/TipStore.swift
@@ -0,0 +1,74 @@
+//
+// TipStore.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 15/09/2024.
+//
+
+import SwiftUI
+import TipKit
+
+struct HUDsTip: Tip {
+ var title: Text {
+ Text("Enhance your experience with HUDs")
+ }
+
+
+ var message: Text? {
+ Text("Unlock advanced features and improve your experience. Upgrade now for more customizations!")
+ }
+
+
+ var image: Image? {
+ AppIcon(for: "dynamicisland.DynamicIsland")
+ }
+
+ var actions: [Action] {
+ Action {
+ Text("More")
+ }
+ }
+}
+
+struct CBTip: Tip {
+ var title: Text {
+ Text("Boost your productivity with Clipboard Manager")
+ }
+
+
+ var message: Text? {
+ Text("Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!")
+ }
+
+
+ var image: Image? {
+ AppIcon(for: "dynamicisland.DynamicIsland")
+ }
+
+ var actions: [Action] {
+ Action {
+ Text("More")
+ }
+ }
+}
+
+struct TipsView: View {
+ var hudTip = HUDsTip()
+ var cbTip = CBTip()
+ var body: some View {
+ VStack {
+ TipView(hudTip)
+ TipView(cbTip)
+ }
+ .task {
+ try? Tips.configure([
+ .displayFrequency(.immediate),
+ .datastoreLocation(.applicationDefault)
+ ])
+ }
+ }
+}
+
+#Preview {
+ TipsView()
+}
diff --git a/DynamicIsland/components/Webcam/WebcamView.swift b/DynamicIsland/components/Webcam/WebcamView.swift
new file mode 100644
index 0000000..e77f9c0
--- /dev/null
+++ b/DynamicIsland/components/Webcam/WebcamView.swift
@@ -0,0 +1,122 @@
+//
+// WebcamView.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 19/08/24.
+//
+
+import AVFoundation
+import Defaults
+import SwiftUI
+
+struct CameraPreviewView: View {
+ @EnvironmentObject var vm: DynamicIslandViewModel
+ @ObservedObject var webcamManager: WebcamManager
+
+ // Track if authorization request is in progress to avoid multiple requests
+ @State private var isRequestingAuthorization: Bool = false
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack {
+ if let previewLayer = webcamManager.previewLayer {
+ CameraPreviewLayerView(previewLayer: previewLayer)
+ .scaleEffect(x: -1, y: 1)
+ .clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? !Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.closed : MusicPlayerImageSizes.cornerRadiusInset.opened : 100))
+ .frame(width: geometry.size.width, height: geometry.size.width)
+ .opacity(webcamManager.isSessionRunning ? 1 : 0)
+ }
+
+ if !webcamManager.isSessionRunning {
+ ZStack {
+ RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? !Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.closed : 12 : 100)
+ .fill(Color(red: 20/255, green: 20/255, blue: 20/255))
+ .strokeBorder(.white.opacity(0.04), lineWidth: 1)
+ .frame(width: geometry.size.width, height: geometry.size.width)
+ VStack(spacing: 8) {
+ Image(systemName: webcamManager.authorizationStatus == .denied ? "exclamationmark.triangle" : "web.camera")
+ .foregroundStyle(.gray)
+ .font(.system(size: geometry.size.width/3.5))
+ Text(webcamManager.authorizationStatus == .denied ? "Access Denied" : "Mirror")
+ .font(.caption2)
+ .foregroundColor(.gray)
+ }
+ }
+ }
+ }
+ .onTapGesture {
+ handleCameraTap()
+ }
+ .onDisappear {
+ webcamManager.stopSession()
+ }
+ }
+ .aspectRatio(1, contentMode: .fit)
+ }
+
+ private func handleCameraTap() {
+ if isRequestingAuthorization {
+ return // Prevent multiple authorization requests
+ }
+
+ switch webcamManager.authorizationStatus {
+ case .authorized:
+ if webcamManager.isSessionRunning {
+ webcamManager.stopSession()
+ } else if webcamManager.cameraAvailable {
+ webcamManager.startSession()
+ }
+ case .denied, .restricted:
+ DispatchQueue.main.async {
+ NSApp.setActivationPolicy(.regular)
+ NSApp.activate(ignoringOtherApps: true)
+ let alert = NSAlert()
+ alert.messageText = "Camera Access Required"
+ alert.informativeText = "Please allow camera access in System Settings to use the mirror feature."
+ alert.addButton(withTitle: "Open Settings")
+ alert.addButton(withTitle: "Cancel")
+
+ if alert.runModal() == .alertFirstButtonReturn {
+ if let settingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") {
+ NSWorkspace.shared.open(settingsURL)
+ }
+ }
+ NSApp.setActivationPolicy(.accessory)
+ NSApp.deactivate()
+ }
+ case .notDetermined:
+ isRequestingAuthorization = true
+ webcamManager.checkAndRequestVideoAuthorization()
+ // Reset the request flag after a reasonable delay
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+ isRequestingAuthorization = false
+ }
+ @unknown default:
+ break
+ }
+ }
+}
+
+struct CameraPreviewLayerView: NSViewRepresentable {
+ let previewLayer: AVCaptureVideoPreviewLayer
+
+ func makeNSView(context: Context) -> NSView {
+ let view = NSView(frame: .zero)
+ previewLayer.frame = view.bounds
+ previewLayer.videoGravity = .resizeAspectFill
+ view.layer = previewLayer
+ view.wantsLayer = true
+ return view
+ }
+
+ func updateNSView(_ nsView: NSView, context: Context) {
+ CATransaction.begin()
+ CATransaction.setDisableActions(true)
+ previewLayer.frame = nsView.bounds
+ CATransaction.commit()
+ }
+}
+
+#Preview {
+ CameraPreviewView(webcamManager: WebcamManager())
+}
diff --git a/DynamicIsland/components/WhatsNewView.swift b/DynamicIsland/components/WhatsNewView.swift
new file mode 100644
index 0000000..154558a
--- /dev/null
+++ b/DynamicIsland/components/WhatsNewView.swift
@@ -0,0 +1,35 @@
+//
+// WhatsNewView.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 09/08/2024.
+//
+
+import SwiftUI
+
+struct WhatsNewView: View {
+ @Binding var isPresented: Bool
+
+ var body: some View {
+ VStack(spacing: 20) {
+ Text("What's New")
+ .font(.largeTitle)
+
+ VStack(alignment: .leading, spacing: 10) {
+ Text("β’ New feature 1")
+ Text("β’ Improved performance")
+ Text("β’ Bug fixes")
+ }
+
+ Button("Got it!") {
+ isPresented = false
+ }
+ }
+ .frame(width: 300, height: 200)
+ .padding()
+ }
+}
+
+#Preview {
+ WhatsNewView(isPresented: .constant(true))
+}
diff --git a/DynamicIsland/boring.m4a b/DynamicIsland/dynamic.m4a
similarity index 100%
rename from DynamicIsland/boring.m4a
rename to DynamicIsland/dynamic.m4a
diff --git a/DynamicIsland/enums/generic.swift b/DynamicIsland/enums/generic.swift
new file mode 100644
index 0000000..e70b459
--- /dev/null
+++ b/DynamicIsland/enums/generic.swift
@@ -0,0 +1,69 @@
+//
+// generic.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+//
+
+import Foundation
+import Defaults
+
+public enum Style {
+ case notch
+ case floating
+}
+
+public enum ContentType: Int, Codable, Hashable, Equatable {
+ case normal
+ case menu
+ case settings
+}
+
+public enum NotchState {
+ case closed
+ case open
+}
+
+public enum NotchViews {
+ case home
+ case shelf
+}
+
+enum SettingsEnum {
+ case general
+ case about
+ case charge
+ case download
+ case mediaPlayback
+ case hud
+ case shelf
+ case extensions
+}
+
+enum DownloadIndicatorStyle: String, Defaults.Serializable {
+ case progress = "Progress"
+ case percentage = "Percentage"
+}
+
+enum DownloadIconStyle: String, Defaults.Serializable {
+ case onlyAppIcon = "Only app icon"
+ case onlyIcon = "Only download icon"
+ case iconAndAppIcon = "Icon and app icon"
+}
+
+enum MirrorShapeEnum: String, Defaults.Serializable {
+ case rectangle = "Rectangular"
+ case circle = "Circular"
+}
+
+enum WindowHeightMode: String, Defaults.Serializable {
+ case matchMenuBar = "Match menubar height"
+ case matchRealNotchSize = "Match real notch height"
+ case custom = "Custom height"
+}
+
+enum SliderColorEnum: String, CaseIterable, Defaults.Serializable {
+ case white = "White"
+ case albumArt = "Match album art"
+ case accent = "Accent color"
+}
diff --git a/DynamicIsland/extensions/ActionBar.swift b/DynamicIsland/extensions/ActionBar.swift
new file mode 100644
index 0000000..28d0881
--- /dev/null
+++ b/DynamicIsland/extensions/ActionBar.swift
@@ -0,0 +1,30 @@
+//
+// ActionBar.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 15/09/2024.
+//
+
+import SwiftUI
+
+extension View {
+ func actionBar(padding: CGFloat = 10, @ViewBuilder content: () -> Content) -> some View {
+ self
+ .padding(.bottom, 24)
+ .overlay(alignment: .bottom) {
+ VStack(spacing: -1) {
+ Divider()
+ HStack(spacing: 0) {
+ content()
+ .buttonStyle(PlainButtonStyle())
+ }
+ .frame(height: 16)
+ .padding(.vertical, 4)
+ .padding(.horizontal, padding)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .frame(height: 24)
+ .background(.separator)
+ }
+ }
+}
diff --git a/DynamicIsland/extensions/BundleInfos.swift b/DynamicIsland/extensions/BundleInfos.swift
new file mode 100644
index 0000000..02de5d1
--- /dev/null
+++ b/DynamicIsland/extensions/BundleInfos.swift
@@ -0,0 +1,57 @@
+//
+// BundleInfos.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 08/08/2024.
+//
+
+import SwiftUI
+
+extension Bundle {
+ var releaseVersionNumber: String? {
+ return infoDictionary?["CFBundleShortVersionString"] as? String
+ }
+ var buildVersionNumber: String? {
+ return infoDictionary?["CFBundleVersion"] as? String
+ }
+ var releaseVersionNumberPretty: String {
+ return "v\(releaseVersionNumber ?? "1.0.0")"
+ }
+
+ var iconFileName: String? {
+ guard let icons = infoDictionary?["CFBundleIcons"] as? [String: Any],
+ let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
+ let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
+ let iconFileName = iconFiles.last
+ else { return nil }
+ return iconFileName
+ }
+}
+
+struct BundleAppIcon: View {
+ var body: some View {
+ Bundle.main.iconFileName
+ .flatMap { NSImage(named: $0) }
+ .map { Image(nsImage: $0) }
+ }
+}
+
+func isNewVersion() -> Bool {
+ let defaults = UserDefaults.standard
+ let currentVersion = Bundle.main.releaseVersionNumber ?? "1.0"
+ let savedVersion = defaults.string(forKey: "LastVersionRun") ?? ""
+
+ if currentVersion != savedVersion {
+ defaults.set(currentVersion, forKey: "LastVersionRun")
+ return true
+ }
+ return false
+}
+
+func isExtensionRunning(_ bundleID: String) -> Bool {
+ if let _ = NSWorkspace.shared.runningApplications.first(where: {$0.bundleIdentifier == bundleID}) {
+ return true
+ }
+
+ return false
+}
diff --git a/DynamicIsland/extensions/Button+Bouncing.swift b/DynamicIsland/extensions/Button+Bouncing.swift
new file mode 100644
index 0000000..17ca476
--- /dev/null
+++ b/DynamicIsland/extensions/Button+Bouncing.swift
@@ -0,0 +1,35 @@
+//
+// Button+Bouncing.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 19/08/24.
+//
+import SwiftUI
+import Defaults
+
+struct BouncingButtonStyle: ButtonStyle {
+ let vm: DynamicIslandViewModel
+ @State private var isPressed = false
+
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .padding(12)
+ .background(
+ RoundedRectangle(cornerRadius: Defaults[.cornerRadiusScaling] ? 10 : MusicPlayerImageSizes.cornerRadiusInset.closed)
+ .fill(Color(red: 20/255, green: 20/255, blue: 20/255))
+ .strokeBorder(.white.opacity(0.04), lineWidth: 1)
+ )
+ .scaleEffect(isPressed ? 0.9 : 1.0)
+ .onChange(of: configuration.isPressed) { _, _ in
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) {
+ isPressed.toggle()
+ }
+ }
+ }
+}
+
+extension Button {
+ func bouncingStyle(vm: DynamicIslandViewModel) -> some View {
+ self.buttonStyle(BouncingButtonStyle(vm: vm))
+ }
+}
diff --git a/DynamicIsland/extensions/ConditionalModifier.swift b/DynamicIsland/extensions/ConditionalModifier.swift
new file mode 100644
index 0000000..9903e74
--- /dev/null
+++ b/DynamicIsland/extensions/ConditionalModifier.swift
@@ -0,0 +1,18 @@
+//
+// ConditionalModifier.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 20/08/2024.
+//
+
+import SwiftUI
+
+extension View {
+ @ViewBuilder func conditionalModifier(_ condition: Bool, transform: (Self) -> Content) -> some View {
+ if condition {
+ transform(self)
+ } else {
+ self
+ }
+ }
+}
diff --git a/DynamicIsland/extensions/DataTypes+Extensions.swift b/DynamicIsland/extensions/DataTypes+Extensions.swift
new file mode 100644
index 0000000..1b662e2
--- /dev/null
+++ b/DynamicIsland/extensions/DataTypes+Extensions.swift
@@ -0,0 +1,94 @@
+ //
+ // DataTypes+Extensions.swift
+ // boringNotch
+ //
+ // Created by Harsh Vardhan Goswami on 27/08/24.
+ //
+
+import Foundation
+
+
+
+extension Date {
+ static var yesterday: Date { return Date().dayBefore }
+ static var tomorrow: Date { return Date().dayAfter }
+ var dayBefore: Date {
+ return Calendar.current.date(byAdding: .day, value: -1, to: noon)!
+ }
+ var dayAfter: Date {
+ return Calendar.current.date(byAdding: .day, value: 1, to: noon)!
+ }
+ var noon: Date {
+ return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
+ }
+
+ var date: String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "dd"
+ return dateFormatter.string(from: self)
+ }
+
+ var month: String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "MMMM"
+ return dateFormatter.string(from: self)
+ }
+
+ func dayOfTheWeek(dayOfWeek: Int) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "EEE"
+ let date = Calendar.current.date(bySetting: .weekday, value: dayOfWeek, of: self) ?? self
+ return dateFormatter.string(from: date)
+ }
+}
+
+extension NSSize {
+ var s: String { "\(width.i)Γ\(height.i)" }
+
+ var aspectRatio: Double {
+ width / height
+ }
+ func scaled(by factor: Double) -> CGSize {
+ CGSize(width: (width * factor).evenInt, height: (height * factor).evenInt)
+ }
+
+}
+
+extension Int {
+ var s: String {
+ String(self)
+ }
+ var d: Double {
+ Double(self)
+ }
+}
+
+extension Double {
+ @inline(__always) @inlinable var intround: Int {
+ rounded().i
+ }
+
+ @inline(__always) @inlinable var i: Int {
+ Int(self)
+ }
+
+ var evenInt: Int {
+ let x = intround
+ return x + x % 2
+ }
+}
+
+extension CGFloat {
+ @inline(__always) @inlinable var intround: Int {
+ rounded().i
+ }
+
+ @inline(__always) @inlinable var i: Int {
+ Int(self)
+ }
+
+ var evenInt: Int {
+ let x = intround
+ return x + x % 2
+ }
+}
diff --git a/DynamicIsland/extensions/KeyboardShortcutsHelper.swift b/DynamicIsland/extensions/KeyboardShortcutsHelper.swift
new file mode 100644
index 0000000..1e6548e
--- /dev/null
+++ b/DynamicIsland/extensions/KeyboardShortcutsHelper.swift
@@ -0,0 +1,88 @@
+//
+// KeyboardShortcutsHelper.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 16/08/2024.
+//
+
+import KeyboardShortcuts
+import SwiftUI
+import Carbon
+
+
+extension View {
+
+ public func keyboardShortcut(_ shortcut: KeyboardShortcuts.Name) -> some View {
+ if let shortcut = shortcut.shortcut {
+ if let keyEquivalent = shortcut.toKeyEquivalent() {
+ return AnyView(self.keyboardShortcut(keyEquivalent, modifiers: shortcut.toEventModifiers()))
+ }
+ }
+
+ return AnyView(self)
+ }
+
+}
+
+extension KeyboardShortcuts.Shortcut {
+
+ func toKeyEquivalent() -> KeyEquivalent? {
+ let carbonKeyCode = UInt16(self.carbonKeyCode)
+ let maxNameLength = 4
+ var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
+ var nameLength = 0
+
+ let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
+ var deadKeys: UInt32 = 0
+ let keyboardType = UInt32(LMGetKbdType())
+
+ let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
+ guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
+ NSLog("Could not get keyboard layout data")
+ return nil
+ }
+ let layoutData = Unmanaged.fromOpaque(ptr).takeUnretainedValue() as Data
+ let osStatus = layoutData.withUnsafeBytes {
+ UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, carbonKeyCode, UInt16(kUCKeyActionDown),
+ modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
+ &deadKeys, maxNameLength, &nameLength, &nameBuffer)
+ }
+ guard osStatus == noErr else {
+ NSLog("Code: 0x%04X Status: %+i", carbonKeyCode, osStatus);
+ return nil
+ }
+
+ return KeyEquivalent(Character(String(utf16CodeUnits: nameBuffer, count: nameLength)))
+ }
+
+ func toEventModifiers() -> SwiftUI.EventModifiers {
+ var modifiers: SwiftUI.EventModifiers = []
+
+ if self.modifiers.contains(NSEvent.ModifierFlags.command) {
+ modifiers.update(with: EventModifiers.command)
+ }
+
+ if self.modifiers.contains(NSEvent.ModifierFlags.control) {
+ modifiers.update(with: EventModifiers.control)
+ }
+
+ if self.modifiers.contains(NSEvent.ModifierFlags.option) {
+ modifiers.update(with: EventModifiers.option)
+ }
+
+ if self.modifiers.contains(NSEvent.ModifierFlags.shift) {
+ modifiers.update(with: EventModifiers.shift)
+ }
+
+ if self.modifiers.contains(NSEvent.ModifierFlags.capsLock) {
+ modifiers.update(with: EventModifiers.capsLock)
+ }
+
+ if self.modifiers.contains(NSEvent.ModifierFlags.numericPad) {
+ modifiers.update(with: EventModifiers.numericPad)
+ }
+
+ return modifiers
+ }
+
+}
diff --git a/DynamicIsland/extensions/MouseTracker.swift b/DynamicIsland/extensions/MouseTracker.swift
new file mode 100644
index 0000000..6b8eb7f
--- /dev/null
+++ b/DynamicIsland/extensions/MouseTracker.swift
@@ -0,0 +1,18 @@
+//
+// MouseTracker.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 12/08/2024.
+//
+
+import SwiftUI
+
+extension NSScreen {
+ static var screenWithMouse: NSScreen? {
+ let mouseLocation = NSEvent.mouseLocation
+ let screens = NSScreen.screens
+ let screenWithMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) })
+
+ return screenWithMouse
+ }
+}
diff --git a/DynamicIsland/extensions/NSImage+Extensions.swift b/DynamicIsland/extensions/NSImage+Extensions.swift
new file mode 100644
index 0000000..c47a98b
--- /dev/null
+++ b/DynamicIsland/extensions/NSImage+Extensions.swift
@@ -0,0 +1,169 @@
+//
+// Image2Color.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 07/08/2024.
+//
+
+import SwiftUI
+import AppKit
+import Cocoa
+import Foundation
+import CoreImage
+import CoreGraphics
+import CoreImage.CIFilterBuiltins
+
+extension NSImage {
+
+
+ func averageColor(completion: @escaping (NSColor?) -> Void) {
+ DispatchQueue.global(qos: .userInitiated).async {
+ guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+ return
+ }
+
+ let width = cgImage.width
+ let height = cgImage.height
+ let totalPixels = width * height
+
+ guard let context = CGContext(data: nil,
+ width: width,
+ height: height,
+ bitsPerComponent: 8,
+ bytesPerRow: width * 4,
+ space: CGColorSpaceCreateDeviceRGB(),
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+ return
+ }
+
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
+
+ guard let data = context.data else {
+ DispatchQueue.main.async {
+ completion(nil)
+ }
+ return
+ }
+
+ let pointer = data.bindMemory(to: UInt32.self, capacity: totalPixels)
+
+ var totalRed: UInt64 = 0
+ var totalGreen: UInt64 = 0
+ var totalBlue: UInt64 = 0
+
+ for i in 0..> 8) & 0xFF)
+ totalBlue += UInt64((color >> 16) & 0xFF)
+ }
+
+ let averageRed = CGFloat(totalRed) / CGFloat(totalPixels) / 255.0
+ let averageGreen = CGFloat(totalGreen) / CGFloat(totalPixels) / 255.0
+ let averageBlue = CGFloat(totalBlue) / CGFloat(totalPixels) / 255.0
+
+ let minBrightness: CGFloat = 0.5
+ let isNearBlack = averageRed < 0.03 && averageGreen < 0.03 && averageBlue < 0.03
+
+ var finalColor: NSColor
+
+ if isNearBlack {
+ // If it's near black, just return a gray color with the minimum brightness
+ finalColor = NSColor(white: minBrightness, alpha: 1.0)
+ } else {
+ var color = NSColor(red: averageRed, green: averageGreen, blue: averageBlue, alpha: 1.0)
+
+ var hue: CGFloat = 0
+ var saturation: CGFloat = 0
+ var brightness: CGFloat = 0
+ var alpha: CGFloat = 0
+
+ color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
+
+ if brightness < minBrightness {
+ // Increase brightness while maintaining hue and reducing saturation
+ let saturationScale = brightness / minBrightness
+ color = NSColor(hue: hue,
+ saturation: saturation * saturationScale,
+ brightness: minBrightness,
+ alpha: alpha)
+ }
+
+ finalColor = color
+ }
+
+ DispatchQueue.main.async {
+ completion(finalColor)
+ }
+ }
+
+ }
+
+ func getBrightness() -> CGFloat {
+ guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
+ return 0
+ }
+
+ let inputImage = CIImage(cgImage: cgImage)
+
+ let filter = CIFilter.areaAverage()
+ filter.inputImage = inputImage
+ filter.extent = inputImage.extent
+
+ guard let outputImage = filter.outputImage else {
+ return 0
+ }
+
+ let context = CIContext(options: nil)
+
+ var bitmap = [UInt8](repeating: 0, count: 4)
+ context.render(outputImage,
+ toBitmap: &bitmap,
+ rowBytes: 4,
+ bounds: CGRect(x: 0, y: 0, width: 1, height: 1),
+ format: .RGBA8,
+ colorSpace: CGColorSpaceCreateDeviceRGB())
+
+ let brightness = (0.2126 * CGFloat(bitmap[0]) + 0.7152 * CGFloat(bitmap[1]) + 0.0722 * CGFloat(bitmap[2])) / 255.0
+
+ return brightness
+ }
+}
+
+extension Color {
+ func ensureMinimumBrightness(factor: CGFloat) -> Color {
+ guard factor >= 0 && factor <= 1 else {
+ return self // Return original color if factor is out of bounds
+ }
+
+ let nsColor = NSColor(self)
+
+ // Convert to RGB color space
+ guard let rgbColor = nsColor.usingColorSpace(.sRGB) else {
+ return self // Return original color if conversion fails
+ }
+
+ var red: CGFloat = 0
+ var green: CGFloat = 0
+ var blue: CGFloat = 0
+ var alpha: CGFloat = 0
+
+ rgbColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
+
+ // Calculate perceived brightness using the formula: (0.299*R + 0.587*G + 0.114*B)
+ let perceivedBrightness = (0.2126 * red + 0.7152 * green + 0.0722 * blue)
+
+ let scale = factor / perceivedBrightness
+ red = min(red * scale, 1.0)
+ green = min(green * scale, 1.0)
+ blue = min(blue * scale, 1.0)
+
+ return Color(red: Double(red), green: Double(green), blue: Double(blue), opacity: Double(alpha))
+ }
+}
diff --git a/DynamicIsland/extensions/PanGesture.swift b/DynamicIsland/extensions/PanGesture.swift
new file mode 100644
index 0000000..4915a23
--- /dev/null
+++ b/DynamicIsland/extensions/PanGesture.swift
@@ -0,0 +1,101 @@
+//
+// PanGesture.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 21/08/2024.
+//
+
+import SwiftUI
+import AppKit
+
+extension View {
+ func panGesture(direction: PanDirection, action: @escaping (CGFloat, NSEvent.Phase) -> Void) -> some View {
+ background(
+ PanGestureView(direction: direction, action: action)
+ .frame(maxWidth: 0, maxHeight: 0)
+ )
+ }
+}
+
+struct PanGestureView: NSViewRepresentable {
+ let direction: PanDirection
+ let action: (CGFloat, NSEvent.Phase) -> Void
+
+ func makeNSView(context: Context) -> NSView {
+ let view = NSView()
+ NSEvent.addLocalMonitorForEvents(matching: [.scrollWheel]) { event in
+ if event.window == view.window {
+ context.coordinator.handleEvent(event)
+ }
+ return event
+ }
+ return view
+ }
+
+ func updateNSView(_ nsView: NSView, context: Context) {
+
+ }
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(direction: direction, action: action)
+ }
+
+ class Coordinator: NSObject {
+ let direction: PanDirection
+ let action: (CGFloat, NSEvent.Phase) -> Void
+
+ var accumulatedScrollDeltaX: CGFloat = 0
+ var accumulatedScrollDeltaY: CGFloat = 0
+
+ init(direction: PanDirection, action: @escaping (CGFloat, NSEvent.Phase) -> Void) {
+ self.direction = direction
+ self.action = action
+ }
+
+ @objc func handleEvent(_ event: NSEvent) {
+ if event.type == .scrollWheel {
+ accumulatedScrollDeltaX += event.scrollingDeltaX
+ accumulatedScrollDeltaY += event.scrollingDeltaY
+
+ switch direction {
+ case .down:
+ if accumulatedScrollDeltaY > 0 {
+ handle()
+ }
+ case .up:
+ if accumulatedScrollDeltaY < 0 {
+ handle()
+ }
+ case .left:
+ if accumulatedScrollDeltaX < 0 {
+ handle()
+ }
+ case .right:
+ if accumulatedScrollDeltaX > 0 {
+ handle()
+ }
+ }
+
+ func handle() {
+ if (direction == .left || direction == .right) {
+ action(abs(accumulatedScrollDeltaX), event.phase)
+ } else {
+ action(abs(accumulatedScrollDeltaY), event.phase)
+ }
+ }
+
+ if event.phase == .ended {
+ accumulatedScrollDeltaY = 0
+ accumulatedScrollDeltaX = 0
+ }
+ }
+ }
+ }
+}
+
+enum PanDirection {
+ case left
+ case right
+ case up
+ case down
+}
diff --git a/DynamicIsland/helpers/AppIcons.swift b/DynamicIsland/helpers/AppIcons.swift
new file mode 100644
index 0000000..ecb58b6
--- /dev/null
+++ b/DynamicIsland/helpers/AppIcons.swift
@@ -0,0 +1,67 @@
+//
+// AppIcons.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 16/08/24.
+//
+
+import SwiftUI
+import AppKit
+
+struct AppIcons {
+
+ func getIcon(file path: String) -> NSImage? {
+ guard FileManager.default.fileExists(atPath: path)
+ else { return nil }
+
+ return NSWorkspace.shared.icon(forFile: path)
+ }
+
+ func getIcon(bundleID: String) -> NSImage? {
+ guard let path = NSWorkspace.shared.urlForApplication(
+ withBundleIdentifier: bundleID
+ )?.absoluteString
+ else { return nil }
+
+ return getIcon(file: path)
+ }
+
+ func getIcon(application: String) -> NSImage? {
+ guard let path = NSWorkspace.shared.fullPath(forApplication: application)
+ else { return nil }
+
+ return getIcon(file: path)
+ }
+
+ /// Easily read Info.plist as a Dictionary from any bundle by accessing .infoDictionary on Bundle
+ func bundle(forBundleID: String) -> Bundle? {
+ guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: forBundleID)
+ else { return nil }
+
+ return Bundle(url: url)
+ }
+
+}
+
+func AppIcon(for bundleID: String) -> Image {
+ let workspace = NSWorkspace.shared
+
+ if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleID) {
+ let appIcon = workspace.icon(forFile: appURL.path)
+ return Image(nsImage: appIcon)
+ }
+
+ return Image(nsImage: workspace.icon(for: .applicationBundle))
+}
+
+
+func AppIconAsNSImage(for bundleID: String) -> NSImage? {
+ let workspace = NSWorkspace.shared
+
+ if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleID) {
+ let appIcon = workspace.icon(forFile: appURL.path)
+ return appIcon
+ }
+ return nil
+}
+
diff --git a/DynamicIsland/helpers/AppleScriptHelper.swift b/DynamicIsland/helpers/AppleScriptHelper.swift
new file mode 100644
index 0000000..966b84a
--- /dev/null
+++ b/DynamicIsland/helpers/AppleScriptHelper.swift
@@ -0,0 +1,31 @@
+//
+// AppleScriptHelper.swift
+// boringNotch
+//
+// Created by Alexander Greco on 2025-03-29.
+//
+
+import Foundation
+
+class AppleScriptHelper {
+ @discardableResult
+ class func execute(_ scriptText: String) async throws -> NSAppleEventDescriptor? {
+ try await withCheckedThrowingContinuation { continuation in
+ Task.detached(priority: .userInitiated) {
+ let script = NSAppleScript(source: scriptText)
+ var error: NSDictionary?
+ if let descriptor = script?.executeAndReturnError(&error) {
+ continuation.resume(returning: descriptor)
+ } else if let error = error {
+ continuation.resume(throwing: NSError(domain: "AppleScriptError", code: 1, userInfo: error as? [String: Any]))
+ } else {
+ continuation.resume(throwing: NSError(domain: "AppleScriptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown error"]))
+ }
+ }
+ }
+ }
+
+ class func executeVoid(_ scriptText: String) async throws {
+ _ = try await execute(scriptText)
+ }
+}
diff --git a/DynamicIsland/helpers/AudioPlayer.swift b/DynamicIsland/helpers/AudioPlayer.swift
new file mode 100644
index 0000000..c202873
--- /dev/null
+++ b/DynamicIsland/helpers/AudioPlayer.swift
@@ -0,0 +1,15 @@
+//
+// AudioPlayer.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 09/08/24.
+//
+
+import Foundation
+import AppKit
+
+class AudioPlayer {
+ func play(fileName: String, fileExtension: String) {
+ NSSound(contentsOf:Bundle.main.url(forResource: fileName, withExtension: fileExtension)!, byReference: false)?.play()
+ }
+}
diff --git a/DynamicIsland/helpers/Clipboard+Content.swift b/DynamicIsland/helpers/Clipboard+Content.swift
new file mode 100644
index 0000000..8b7d02e
--- /dev/null
+++ b/DynamicIsland/helpers/Clipboard+Content.swift
@@ -0,0 +1,25 @@
+import AppKit
+
+func getAttributedString(content: Any, type: NSPasteboard.PasteboardType) -> NSAttributedString? {
+ if let stringContent = content as? String {
+ return NSAttributedString(string: stringContent)
+ }
+ if type == .rtf, let data = content as? Data {
+ return NSAttributedString(rtf: data, documentAttributes: nil)
+ } else if type == .html, let data = content as? Data {
+ return try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
+ } else if type.rawValue == "public.utf8-plain-text", let data = content as? Data {
+ return try? NSAttributedString(data: data, documentAttributes: nil)
+ } else if type == .string {
+ return NSAttributedString(string: content as? String ?? "")
+ } else if type == .fileURL {
+ return NSAttributedString(string: content as? String ?? "")
+ } else if type == NSPasteboard.PasteboardType("com.apple.webarchive"), let data = content as? Data {
+ return try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.webArchive], documentAttributes: nil)
+ }
+ return nil
+}
+
+func isText(type: NSPasteboard.PasteboardType) -> Bool {
+ return type == .string || type == .html || type == .rtf || type == .html || type == .string || type.rawValue == "public.utf8-plain-text" || type.rawValue == "public.utf16-external-plain-text" || type.rawValue == "com.apple.webarchive"
+}
diff --git a/DynamicIsland/managers/BatteryActivityManager.swift b/DynamicIsland/managers/BatteryActivityManager.swift
new file mode 100644
index 0000000..61b1c66
--- /dev/null
+++ b/DynamicIsland/managers/BatteryActivityManager.swift
@@ -0,0 +1,322 @@
+import Foundation
+import IOKit.ps
+
+/// Manages and monitors battery status changes on the device
+/// - Note: This class uses the IOKit framework to monitor battery status
+class BatteryActivityManager {
+
+ static let shared = BatteryActivityManager()
+
+ var onBatteryLevelChange: ((Float) -> Void)?
+ var onMaxCapacityChange: ((Float) -> Void)?
+ var onPowerModeChange: ((Bool) -> Void)?
+ var onPowerSourceChange: ((Bool) -> Void)?
+ var onChargingChange: ((Bool) -> Void)?
+ var onTimeToFullChargeChange: ((Int) -> Void)?
+
+ private var batterySource: CFRunLoopSource?
+ private var observers: [(BatteryEvent) -> Void] = []
+ private var previousBatteryInfo: BatteryInfo?
+ private var notificationQueue: [BatteryEvent] = []
+ private var isProcessingNotifications = false
+
+ enum BatteryEvent {
+ case powerSourceChanged(isPluggedIn: Bool)
+ case batteryLevelChanged(level: Float)
+ case lowPowerModeChanged(isEnabled: Bool)
+ case isChargingChanged(isCharging: Bool)
+ case timeToFullChargeChanged(time: Int)
+ case maxCapacityChanged(capacity: Float)
+ case error(description: String)
+ }
+
+ enum BatteryError: Error {
+ case powerSourceUnavailable
+ case batteryInfoUnavailable(String)
+ case batteryParameterMissing(String)
+ }
+
+ private let defaultBatteryInfo = BatteryInfo(
+ isPluggedIn: false,
+ isCharging: false,
+ currentCapacity: 0,
+ maxCapacity: 0,
+ isInLowPowerMode: false,
+ timeToFullCharge: 0
+ )
+
+ private init() {
+ startMonitoring()
+ setupLowPowerModeObserver()
+ }
+
+ /// Setup observer for low power mode changes
+ private func setupLowPowerModeObserver() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(lowPowerModeChanged),
+ name: NSNotification.Name.NSProcessInfoPowerStateDidChange,
+ object: nil
+ )
+ }
+
+ /// Called when low power mode is enabled or disabled
+ @objc private func lowPowerModeChanged() {
+ notifyBatteryChanges()
+ }
+
+ /// Starts monitoring battery changes
+ private func startMonitoring() {
+ guard let powerSource = IOPSNotificationCreateRunLoopSource({ context in
+ guard let context = context else { return }
+ let manager = Unmanaged.fromOpaque(context).takeUnretainedValue()
+ manager.notifyBatteryChanges()
+ }, Unmanaged.passUnretained(self).toOpaque())?.takeRetainedValue() else {
+ return
+ }
+ batterySource = powerSource
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), powerSource, .defaultMode)
+ }
+
+ /// Stops monitoring battery changes
+ private func stopMonitoring() {
+ if let powerSource = batterySource {
+ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), powerSource, .defaultMode)
+ batterySource = nil
+ }
+ }
+
+ /// Checks for changes in a property and notifies observers
+ private func checkAndNotify(
+ previous: T,
+ current: T,
+ eventGenerator: (T) -> BatteryEvent
+ ) {
+ if previous != current {
+ enqueueNotification(eventGenerator(current))
+ }
+ }
+
+ /// Notifies the observers of battery changes
+ /// Checks for changes in battery status and notifies observers
+ private func notifyBatteryChanges() {
+ let batteryInfo = getBatteryInfo()
+
+ // Check for changes
+ if let previousInfo = previousBatteryInfo {
+ // Usar la funciΓ³n auxiliar para cada propiedad
+ checkAndNotify(
+ previous: previousInfo.isPluggedIn,
+ current: batteryInfo.isPluggedIn,
+ eventGenerator: { .powerSourceChanged(isPluggedIn: $0) }
+ )
+
+ checkAndNotify(
+ previous: previousInfo.currentCapacity,
+ current: batteryInfo.currentCapacity,
+ eventGenerator: { .batteryLevelChanged(level: $0) }
+ )
+
+ checkAndNotify(
+ previous: previousInfo.isCharging,
+ current: batteryInfo.isCharging,
+ eventGenerator: { .isChargingChanged(isCharging: $0) }
+ )
+
+ checkAndNotify(
+ previous: previousInfo.isInLowPowerMode,
+ current: batteryInfo.isInLowPowerMode,
+ eventGenerator: { .lowPowerModeChanged(isEnabled: $0) }
+ )
+
+ checkAndNotify(
+ previous: previousInfo.timeToFullCharge,
+ current: batteryInfo.timeToFullCharge,
+ eventGenerator: { .timeToFullChargeChanged(time: $0) }
+ )
+
+ checkAndNotify(
+ previous: previousInfo.maxCapacity,
+ current: batteryInfo.maxCapacity,
+ eventGenerator: { .maxCapacityChanged(capacity: $0) }
+ )
+ } else {
+ // First time notification
+ enqueueNotification(.powerSourceChanged(isPluggedIn: batteryInfo.isPluggedIn))
+ enqueueNotification(.batteryLevelChanged(level: batteryInfo.currentCapacity))
+ enqueueNotification(.isChargingChanged(isCharging: batteryInfo.isCharging))
+ enqueueNotification(.lowPowerModeChanged(isEnabled: batteryInfo.isInLowPowerMode))
+ enqueueNotification(.timeToFullChargeChanged(time: batteryInfo.timeToFullCharge))
+ enqueueNotification(.maxCapacityChanged(capacity: batteryInfo.maxCapacity))
+ }
+
+ // Update previous battery info
+ previousBatteryInfo = batteryInfo
+
+ // Trigger optional callbacks
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.onBatteryLevelChange?(batteryInfo.currentCapacity)
+ self.onPowerSourceChange?(batteryInfo.isPluggedIn)
+ self.onChargingChange?(batteryInfo.isCharging)
+ self.onPowerModeChange?(batteryInfo.isInLowPowerMode)
+ self.onTimeToFullChargeChange?(batteryInfo.timeToFullCharge)
+ self.onMaxCapacityChange?(batteryInfo.maxCapacity)
+ }
+ }
+
+ /// Enqueues a notification to be processed
+ /// - Parameter event: The battery event
+ private func enqueueNotification(_ event: BatteryEvent) {
+ notificationQueue.append(event)
+ processNextNotification()
+ }
+
+ /// Processes the next notification in the queue
+ /// If there are no more notifications, the queue is cleared
+ /// and the processing flag is set to false
+ private func processNextNotification() {
+ guard !isProcessingNotifications, !notificationQueue.isEmpty else { return }
+ isProcessingNotifications = true
+
+ let event = notificationQueue.removeFirst()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ guard let self = self else { return }
+ self.notifyObservers(event: event)
+ self.isProcessingNotifications = false
+
+ // Check if there are more items in the queue
+ if !self.notificationQueue.isEmpty {
+ self.processNextNotification()
+ }
+ }
+ }
+
+ /// Initializes the battery information when the manager starts
+ /// - Returns: Current battery information
+ func initializeBatteryInfo() -> BatteryInfo {
+ previousBatteryInfo = getBatteryInfo()
+ guard let batteryInfo = previousBatteryInfo else {
+ return BatteryInfo(
+ isPluggedIn: false,
+ isCharging: false,
+ currentCapacity: 0,
+ maxCapacity: 0,
+ isInLowPowerMode: false,
+ timeToFullCharge: 0
+ )
+ }
+ return batteryInfo
+ }
+
+ /// Get the current battery information
+ /// - Returns: The current battery information
+ private func getBatteryInfo() -> BatteryInfo {
+ do {
+ // Get power source information
+ guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue() else {
+ throw BatteryError.powerSourceUnavailable
+ }
+
+ guard let sources = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue() as? [CFTypeRef],
+ !sources.isEmpty else {
+ throw BatteryError.batteryInfoUnavailable("No power sources available")
+ }
+
+ let source = sources.first!
+
+ guard let description = IOPSGetPowerSourceDescription(snapshot, source)?.takeUnretainedValue() as? [String: Any] else {
+ throw BatteryError.batteryInfoUnavailable("Could not get power source description")
+ }
+
+ // Extract required battery parameters with error handling
+ guard let currentCapacity = description[kIOPSCurrentCapacityKey] as? Float else {
+ throw BatteryError.batteryParameterMissing("Current capacity")
+ }
+
+ guard let maxCapacity = description[kIOPSMaxCapacityKey] as? Float else {
+ throw BatteryError.batteryParameterMissing("Max capacity")
+ }
+
+ guard let isCharging = description["Is Charging"] as? Bool else {
+ throw BatteryError.batteryParameterMissing("Charging state")
+ }
+
+ guard let powerSource = description[kIOPSPowerSourceStateKey] as? String else {
+ throw BatteryError.batteryParameterMissing("Power source state")
+ }
+
+ // Create battery info with the extracted parameters
+ var batteryInfo = BatteryInfo(
+ isPluggedIn: powerSource == kIOPSACPowerValue,
+ isCharging: isCharging,
+ currentCapacity: currentCapacity,
+ maxCapacity: maxCapacity,
+ isInLowPowerMode: ProcessInfo.processInfo.isLowPowerModeEnabled,
+ timeToFullCharge: 0
+ )
+
+ // Optional parameters
+ if let timeToFullCharge = description[kIOPSTimeToFullChargeKey] as? Int {
+ batteryInfo.timeToFullCharge = timeToFullCharge
+ }
+
+ return batteryInfo
+
+ } catch BatteryError.powerSourceUnavailable {
+ print("β οΈ Error: Power source information unavailable")
+ return defaultBatteryInfo
+ } catch BatteryError.batteryInfoUnavailable(let reason) {
+ print("β οΈ Error: Battery information unavailable - \(reason)")
+ return defaultBatteryInfo
+ } catch BatteryError.batteryParameterMissing(let parameter) {
+ print("β οΈ Error: Battery parameter missing - \(parameter)")
+ return defaultBatteryInfo
+ } catch {
+ print("β οΈ Error: Unexpected error getting battery info - \(error.localizedDescription)")
+ return defaultBatteryInfo
+ }
+ }
+
+ /// Adds an observer to listen to battery changes
+ /// - Parameter observer: The observer closure to be called on battery events
+ /// - Returns: The ID of the observer for later removal
+ func addObserver(_ observer: @escaping (BatteryEvent) -> Void) -> Int {
+ observers.append(observer)
+ return observers.count - 1
+ }
+
+ /// Removes an observer by its ID
+ /// - Parameter id: The ID of the observer to be removed
+ func removeObserver(byId id: Int) {
+ guard id >= 0 && id < observers.count else { return }
+ observers.remove(at: id)
+ }
+
+ /// Notifies all observers of a battery event
+ /// - Parameter event: The battery event to notify
+ private func notifyObservers(event: BatteryEvent) {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ for observer in self.observers {
+ observer(event)
+ }
+ }
+ }
+
+ deinit {
+ stopMonitoring()
+ NotificationCenter.default.removeObserver(self)
+ }
+
+}
+
+/// Struct to hold battery information
+struct BatteryInfo {
+ var isPluggedIn: Bool
+ var isCharging: Bool
+ var currentCapacity: Float
+ var maxCapacity: Float
+ var isInLowPowerMode: Bool
+ var timeToFullCharge: Int
+}
diff --git a/DynamicIsland/managers/CalendarManager.swift b/DynamicIsland/managers/CalendarManager.swift
new file mode 100644
index 0000000..9922659
--- /dev/null
+++ b/DynamicIsland/managers/CalendarManager.swift
@@ -0,0 +1,136 @@
+//
+// CalenderManager.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 08/09/24.
+//
+
+import EventKit
+import SwiftUI
+import Defaults
+
+// MARK: - CalendarManager
+
+class CalendarManager: ObservableObject {
+ @Published var currentWeekStartDate: Date
+ @Published var events: [EKEvent] = []
+ @Published var allCalendars: [EKCalendar] = []
+ private var selectedCalendars: [EKCalendar] = []
+ private let eventStore = EKEventStore()
+ @Published var authorizationStatus: EKAuthorizationStatus = .notDetermined
+
+ init() {
+ self.currentWeekStartDate = CalendarManager.startOfDay(Date())
+ if(Defaults[.showCalendar]) {
+ checkCalendarAuthorization()
+ }
+ }
+
+ func checkCalendarAuthorization() {
+ let status = EKEventStore.authorizationStatus(for: .event)
+ DispatchQueue.main.async {
+ print("π
Current calendar authorization status: \(status)")
+ self.authorizationStatus = status
+ }
+
+ switch status {
+ case .notDetermined:
+ requestCalendarAccess()
+ case .restricted, .denied:
+ // Handle the case where the user has denied or restricted access
+ NSLog("Calendar access denied or restricted")
+ case .fullAccess:
+ NSLog("Full access")
+ self.allCalendars = eventStore.calendars(for: .event)
+ updateSelectedCalendars()
+ fetchEvents()
+ case .writeOnly:
+ NSLog("Write only")
+ @unknown default:
+ print("Unknown authorization status")
+ }
+ }
+
+ func requestCalendarAccess() {
+ eventStore.requestFullAccessToEvents { [weak self] granted, error in
+ DispatchQueue.main.async {
+ if let error = error {
+ print("π
Calendar access error: \(error.localizedDescription)")
+ }
+
+ self?.authorizationStatus = granted ? .fullAccess : .denied
+ if granted {
+ print("π
Calendar access granted")
+ self?.fetchEvents()
+ } else {
+ print("π
Calendar access denied")
+ }
+ }
+ }
+ }
+
+ func updateSelectedCalendars() {
+ selectedCalendars = allCalendars.filter { getCalendarSelected($0) }
+ }
+
+ func getCalendarSelected(_ calendar: EKCalendar) -> Bool {
+ switch Defaults[.calendarSelectionState] {
+ case .all:
+ return true
+ case .selected(let identifiers):
+ return identifiers.contains(calendar.calendarIdentifier)
+ }
+ }
+
+ func setCalendarSelected(_ calendar: EKCalendar, isSelected: Bool) {
+ var selectionState = Defaults[.calendarSelectionState]
+
+ switch selectionState {
+ case .all:
+ if !isSelected {
+ let identifiers = Set(allCalendars.map { $0.calendarIdentifier }).subtracting([calendar.calendarIdentifier])
+ selectionState = .selected(identifiers)
+ }
+
+ case .selected(var identifiers):
+ if isSelected {
+ identifiers.insert(calendar.calendarIdentifier)
+ } else {
+ identifiers.remove(calendar.calendarIdentifier)
+ }
+
+ selectionState = identifiers.count == allCalendars.count ? .all : .selected(identifiers)
+ }
+
+ Defaults[.calendarSelectionState] = selectionState
+ fetchEvents()
+ }
+
+ func fetchEvents() {
+ guard !self.selectedCalendars.isEmpty else {
+ DispatchQueue.main.async {
+ self.events = []
+ }
+ return
+ }
+
+ let endOfWeek = Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!
+ let predicate = eventStore.predicateForEvents(withStart: currentWeekStartDate, end: endOfWeek, calendars: self.selectedCalendars)
+ let fetchedEvents = eventStore.events(matching: predicate)
+
+ DispatchQueue.main.async {
+ self.events = fetchedEvents.sorted { $0.startDate < $1.startDate }
+ print("π
Fetched \(self.events.count) calendar events")
+ }
+ }
+
+ static func startOfDay(_ date: Date) -> Date {
+ return Calendar.current.startOfDay(for: date)
+ }
+
+ func updateCurrentDate(_ date: Date) {
+ print("π
Updating current date to: \(date)")
+ currentWeekStartDate = Calendar.current.startOfDay(for: date)
+ fetchEvents()
+ }
+}
diff --git a/DynamicIsland/managers/DynamicIslandExtensionManager.swift b/DynamicIsland/managers/DynamicIslandExtensionManager.swift
new file mode 100644
index 0000000..881ae41
--- /dev/null
+++ b/DynamicIsland/managers/DynamicIslandExtensionManager.swift
@@ -0,0 +1,59 @@
+//
+// DynamicIslandExtensionManager.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+// BoringExtensionManager.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 07/09/24.
+//
+
+import Foundation
+import SwiftUI
+
+var clipboardExtension: String = "dynamicisland.TheDynamicClipboard"
+var hudExtension: String = "dynamicisland.TheDynamicHUDs"
+var downloadManagerExtension: String = "dynamicisland.TheDynamicDownloadManager"
+
+struct Extension: Identifiable, Hashable {
+ var id = UUID()
+ var name: String
+ var bundleIdentifier: String
+ var status: StatusModel = .enabled
+}
+
+enum StatusModel {
+ case disabled
+ case enabled
+}
+
+class DynamicIslandExtensionManager: ObservableObject {
+
+ @Published var installedExtensions: [Extension] = [] {
+ didSet {
+ print("Extensions installed: \(installedExtensions)")
+ }
+ }
+
+ var extensions = [
+ clipboardExtension,
+ hudExtension
+ ]
+
+ init() {
+ checkIfExtensionsAreInstalled()
+
+ DistributedNotificationCenter.default().addObserver(self, selector: #selector(checkIfExtensionsAreInstalled), name: NSNotification.Name("NSWorkspaceDidLaunchApplicationNotification"), object: nil)
+ }
+
+ @objc func checkIfExtensionsAreInstalled() {
+ installedExtensions = []
+ for extensionName in extensions {
+ if NSWorkspace.shared.urlForApplication(withBundleIdentifier: extensionName) != nil {
+ let ext = Extension(name: extensionName.components(separatedBy: ".").last ?? extensionName, bundleIdentifier: extensionName)
+ installedExtensions.append(ext)
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift
new file mode 100644
index 0000000..a72ef83
--- /dev/null
+++ b/DynamicIsland/managers/MusicManager.swift
@@ -0,0 +1,397 @@
+//
+// MusicManager.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 03/08/24.
+//
+import AppKit
+import Combine
+import Defaults
+import SwiftUI
+
+let defaultImage: NSImage = .init(
+ systemSymbolName: "heart.fill",
+ accessibilityDescription: "Album Art"
+)!
+
+class MusicManager: ObservableObject {
+ // MARK: - Properties
+ static let shared = MusicManager()
+ private var cancellables = Set()
+ private var controllerCancellables = Set()
+ private var debounceToggle: DispatchWorkItem?
+
+ // Helper to check if macOS is too new for NowPlayingController
+ public var isNowPlayingDeprecated: Bool {
+ if #available(macOS 15.4, *) {
+ return true
+ }
+ return false
+ }
+
+ // Active controller
+ private var activeController: (any MediaControllerProtocol)?
+
+ // Published properties for UI
+ @Published var songTitle: String = "I'm Handsome"
+ @Published var artistName: String = "Me"
+ @Published var albumArt: NSImage = defaultImage
+ @Published var isPlaying = false
+ @Published var album: String = "Self Love"
+ @Published var lastUpdated: Date = .distantPast
+ @Published var ignoreLastUpdated = true
+ @Published var isPlayerIdle: Bool = true
+ @Published var animations: BoringAnimations = .init()
+ @Published var avgColor: NSColor = .white
+ @Published var bundleIdentifier: String? = nil
+ @Published var songDuration: TimeInterval = 0
+ @Published var elapsedTime: TimeInterval = 0
+ @Published var timestampDate: Date = .init()
+ @Published var playbackRate: Double = 1
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @Published var usingAppIconForArtwork: Bool = false
+
+ private var artworkData: Data? = nil
+
+ @Published var isFlipping: Bool = false
+ private var flipWorkItem: DispatchWorkItem?
+
+ @Published var isTransitioning: Bool = false
+ private var transitionWorkItem: DispatchWorkItem?
+
+ // MARK: - Initialization
+ init() {
+ // Listen for changes to the default controller preference
+ NotificationCenter.default.publisher(for: Notification.Name.mediaControllerChanged)
+ .sink { [weak self] _ in
+ self?.setActiveControllerBasedOnPreference()
+ }
+ .store(in: &cancellables)
+
+ // Initialize the active controller
+ setActiveControllerBasedOnPreference()
+ }
+
+ deinit {
+ debounceToggle?.cancel()
+ cancellables.removeAll()
+ controllerCancellables.removeAll()
+ flipWorkItem?.cancel()
+ transitionWorkItem?.cancel()
+
+ // Release active controller
+ activeController = nil
+ }
+
+ // MARK: - Setup Methods
+ private func createController(for type: MediaControllerType) -> (any MediaControllerProtocol)? {
+ // Cleanup previous controller
+ if let _ = activeController {
+ controllerCancellables.removeAll()
+ activeController = nil
+ }
+
+ let newController: (any MediaControllerProtocol)?
+
+ switch type {
+ case .nowPlaying:
+ // Only create NowPlayingController if not deprecated on this macOS version
+ if !self.isNowPlayingDeprecated {
+ ignoreLastUpdated = false
+ newController = NowPlayingController()
+ } else {
+ return nil
+ }
+ case .appleMusic:
+ ignoreLastUpdated = true
+ newController = AppleMusicController()
+ case .spotify:
+ ignoreLastUpdated = true
+ newController = SpotifyController()
+ case .youtubeMusic:
+ ignoreLastUpdated = true
+ newController = YouTubeMusicController()
+ }
+
+ // Set up state observation for the new controller
+ if let controller = newController {
+ controller.playbackStatePublisher
+ .sink { [weak self] state in
+ guard let self = self,
+ self.activeController === controller else { return }
+ self.updateFromPlaybackState(state)
+ }
+ .store(in: &controllerCancellables)
+ }
+
+ return newController
+ }
+
+ private func setActiveControllerBasedOnPreference() {
+ let preferredType = Defaults[.mediaController]
+ print("Preferred Media Controller: \(preferredType)")
+
+ // If NowPlaying is deprecated but that's the preference, use Apple Music instead
+ let controllerType = (self.isNowPlayingDeprecated && preferredType == .nowPlaying)
+ ? .appleMusic
+ : preferredType
+
+ if let controller = createController(for: controllerType) {
+ setActiveController(controller)
+ } else if controllerType != .appleMusic, let fallbackController = createController(for: .appleMusic) {
+ // Fallback to Apple Music if preferred controller couldn't be created
+ setActiveController(fallbackController)
+ }
+ }
+
+ private func setActiveController(_ controller: any MediaControllerProtocol) {
+ // Transition animation when changing controllers
+ transitionWorkItem?.cancel()
+ let workItem = DispatchWorkItem { [weak self] in
+ self?.isTransitioning = true
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ self?.isTransitioning = false
+ }
+ }
+ transitionWorkItem = workItem
+ DispatchQueue.main.async(execute: workItem)
+
+ // Set new active controller
+ activeController = controller
+
+ // Get current state from active controller
+ if let state = Mirror(reflecting: controller).children.first(where: { $0.label == "playbackState" })?.value as? PlaybackState {
+ updateFromPlaybackState(state)
+ }
+ }
+
+ // MARK: - Update Methods
+ private func updateFromPlaybackState(_ state: PlaybackState) {
+ // Create a batch of updates to apply together
+ let updateBatch = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+
+ // Check for playback state changes (playing/paused)
+ if state.isPlaying != self.isPlaying {
+ self.lastUpdated = Date()
+ withAnimation(.smooth) {
+ self.isPlaying = state.isPlaying
+ self.updateIdleState(state: state.isPlaying)
+ }
+
+ if state.isPlaying && !state.title.isEmpty && !state.artist.isEmpty {
+ self.updateSneakPeek()
+ }
+ }
+
+ // Check for changes in track metadata
+ let titleChanged = state.title != self.songTitle
+ let artistChanged = state.artist != self.artistName
+ let albumChanged = state.album != self.album
+
+ // Check for artwork changes
+ let artworkChanged = state.artwork != nil && state.artwork != self.artworkData
+ let hasContentChange = titleChanged || artistChanged || albumChanged || artworkChanged
+
+ // Handle artwork and visual transitions for changed content
+ if hasContentChange {
+ self.triggerFlipAnimation()
+
+ if artworkChanged, let artwork = state.artwork {
+ self.updateArtwork(artwork)
+ } else if hasContentChange && state.artwork == nil {
+ // Try to use app icon if no artwork but track changed
+ if let appIconImage = AppIconAsNSImage(for: state.bundleIdentifier) {
+ self.usingAppIconForArtwork = true
+ self.updateAlbumArt(newAlbumArt: appIconImage)
+ }
+ }
+ self.artworkData = state.artwork
+
+ // Only update sneak peek if there's actual content and something changed
+ if !state.title.isEmpty && !state.artist.isEmpty && state.isPlaying {
+ self.updateSneakPeek()
+ }
+ }
+
+ let timeChanged = state.currentTime != self.elapsedTime
+ let durationChanged = state.duration != self.songDuration
+ let playbackRateChanged = state.playbackRate != self.playbackRate
+
+ if titleChanged {
+ self.songTitle = state.title
+ }
+
+ if artistChanged {
+ self.artistName = state.artist
+ }
+
+ if albumChanged {
+ self.album = state.album
+ }
+
+ if timeChanged {
+ self.elapsedTime = state.currentTime
+ }
+
+ if durationChanged {
+ self.songDuration = state.duration
+ }
+
+ if playbackRateChanged {
+ self.playbackRate = state.playbackRate
+ }
+
+ if state.bundleIdentifier != self.bundleIdentifier {
+ self.bundleIdentifier = state.bundleIdentifier
+ }
+
+ self.timestampDate = state.lastUpdated
+ }
+
+ // Execute the batch update on the main thread
+ DispatchQueue.main.async(execute: updateBatch)
+ }
+
+ 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
+ }
+ }
+
+ flipWorkItem = workItem
+ DispatchQueue.main.async(execute: workItem)
+ }
+
+ private func updateArtwork(_ artworkData: Data) {
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ guard let self = self else { return }
+
+ if let artworkImage = NSImage(data: artworkData) {
+ DispatchQueue.main.async {
+ self.usingAppIconForArtwork = false
+ self.updateAlbumArt(newAlbumArt: artworkImage)
+ }
+ }
+ }
+ }
+
+ private func updateIdleState(state: Bool) {
+ if state {
+ isPlayerIdle = false
+ debounceToggle?.cancel()
+ } else {
+ debounceToggle = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ if self.lastUpdated.timeIntervalSinceNow < -Defaults[.waitInterval] {
+ withAnimation {
+ self.isPlayerIdle = !self.isPlaying
+ }
+ }
+ }
+
+ DispatchQueue.main.asyncAfter(
+ deadline: .now() + Defaults[.waitInterval], execute: debounceToggle!)
+ }
+ }
+
+ private var workItem: DispatchWorkItem?
+
+ func updateAlbumArt(newAlbumArt: NSImage) {
+ workItem?.cancel()
+ workItem = DispatchWorkItem { [weak self] in
+ withAnimation(.smooth) {
+ self?.albumArt = newAlbumArt
+ if Defaults[.coloredSpectrogram] {
+ self?.calculateAverageColor()
+ }
+ }
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: workItem!)
+ }
+
+ func calculateAverageColor() {
+ albumArt.averageColor { [weak self] color in
+ DispatchQueue.main.async {
+ withAnimation(.smooth) {
+ self?.avgColor = color ?? .white
+ }
+ }
+ }
+ }
+
+ private func updateSneakPeek() {
+ if isPlaying && Defaults[.enableSneakPeek] {
+ if Defaults[.sneakPeekStyles] == .standard {
+ coordinator.toggleSneakPeek(status: true, type: .music)
+ } else {
+ coordinator.toggleExpandingView(status: true, type: .music)
+ }
+ }
+ }
+
+ // MARK: - Public Methods for controlling playback
+ func playPause() {
+ activeController?.togglePlay()
+ }
+
+ func play() {
+ activeController?.play()
+ }
+
+ func pause() {
+ activeController?.pause()
+ }
+
+ func togglePlay() {
+ activeController?.togglePlay()
+ }
+
+ func nextTrack() {
+ activeController?.nextTrack()
+ }
+
+ func previousTrack() {
+ activeController?.previousTrack()
+ }
+
+ func seek(to position: TimeInterval) {
+ activeController?.seek(to: position)
+ }
+
+ func openMusicApp() {
+ guard let bundleID = bundleIdentifier else {
+ print("Error: appBundleIdentifier is nil")
+ return
+ }
+
+ let workspace = NSWorkspace.shared
+ if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleID) {
+ let configuration = NSWorkspace.OpenConfiguration()
+ workspace.openApplication(at: appURL, configuration: configuration) { (app, error) in
+ if let error = error {
+ print("Failed to launch app with bundle ID: \(bundleID), error: \(error)")
+ } else {
+ print("Launched app with bundle ID: \(bundleID)")
+ }
+ }
+ } else {
+ print("Failed to find app with bundle ID: \(bundleID)")
+ }
+ }
+
+ func forceUpdate() {
+ // Request immediate update from the active controller
+ DispatchQueue.main.async { [weak self] in
+ if self?.activeController?.isActive() == true {
+ self?.activeController?.updatePlaybackInfo()
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/managers/NotchSpaceManager.swift b/DynamicIsland/managers/NotchSpaceManager.swift
new file mode 100644
index 0000000..5e45495
--- /dev/null
+++ b/DynamicIsland/managers/NotchSpaceManager.swift
@@ -0,0 +1,19 @@
+//
+// NotchSpaceManager.swift
+// boringNotch
+//
+// Created by Alexander Greco on 2024-10-27.
+//
+
+import Foundation
+
+class NotchSpaceManager {
+ static let shared = NotchSpaceManager()
+ let notchSpace: CGSSpace
+ private var eventTap: CFMachPort?
+ private var runLoopSource: CFRunLoopSource?
+
+ private init() {
+ notchSpace = CGSSpace(level: 2147483647) // Max level
+ }
+}
diff --git a/DynamicIsland/managers/WebcamManager.swift b/DynamicIsland/managers/WebcamManager.swift
new file mode 100644
index 0000000..6a92286
--- /dev/null
+++ b/DynamicIsland/managers/WebcamManager.swift
@@ -0,0 +1,312 @@
+//
+// WebcamManager.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 19/08/24.
+//
+import AVFoundation
+import SwiftUI
+
+class WebcamManager: NSObject, ObservableObject {
+ @Published var previewLayer: AVCaptureVideoPreviewLayer? {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ private var captureSession: AVCaptureSession?
+ @Published var isSessionRunning: Bool = false {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ @Published var authorizationStatus: AVAuthorizationStatus = .notDetermined {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ @Published var cameraAvailable: Bool = false {
+ didSet {
+ objectWillChange.send()
+ }
+ }
+
+ private let sessionQueue = DispatchQueue(label: "BoringNotch.WebcamManager.SessionQueue", qos: .userInitiated)
+
+ private var isCleaningUp: Bool = false
+
+ // MARK: - Constants
+
+ enum WebcamError: Error, LocalizedError {
+ case deviceUnavailable
+ case accessDenied
+ case configurationFailed(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .deviceUnavailable:
+ return "No camera devices available"
+ case .accessDenied:
+ return "Camera access denied"
+ case .configurationFailed(let message):
+ return "Camera configuration failed: \(message)"
+ }
+ }
+ }
+
+ // MARK: - Properties
+
+ override init() {
+ super.init()
+ checkAndRequestVideoAuthorization()
+ NotificationCenter.default.addObserver(self, selector: #selector(deviceWasDisconnected), name: .AVCaptureDeviceWasDisconnected, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(deviceWasConnected), name: .AVCaptureDeviceWasConnected, object: nil)
+ checkCameraAvailability()
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+
+ if let session = captureSession {
+ if session.isRunning {
+ session.stopRunning()
+ }
+ }
+ captureSession = nil
+
+ previewLayer = nil
+ }
+
+ // MARK: - Camera Management
+
+ /// Checks current authorization status and requests access if needed
+ func checkAndRequestVideoAuthorization() {
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
+ DispatchQueue.main.async {
+ self.authorizationStatus = status
+ }
+
+ switch status {
+ case .authorized:
+ checkCameraAvailability() // Check availability if authorized
+ case .notDetermined:
+ requestVideoAccess()
+ case .denied, .restricted:
+ NSLog("Camera access denied or restricted")
+ @unknown default:
+ NSLog("Unknown authorization status")
+ }
+ }
+
+ /// Requests access to the camera
+ private func requestVideoAccess() {
+ AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
+ DispatchQueue.main.async {
+ self?.authorizationStatus = granted ? .authorized : .denied
+ if granted {
+ self?.checkCameraAvailability() // Check availability if access granted
+ }
+ }
+ }
+ }
+
+ /// Checks if any camera devices are available and sets up capture session if needed
+ func checkCameraAvailability() {
+ let availableDevices = AVCaptureDevice.DiscoverySession(
+ deviceTypes: [.external, .builtInWideAngleCamera],
+ mediaType: .video,
+ position: .unspecified
+ ).devices
+
+ let hasAvailableDevices = !availableDevices.isEmpty
+
+ DispatchQueue.main.async {
+ self.cameraAvailable = hasAvailableDevices
+ }
+ }
+
+ /// Sets up the capture session with a completion handler
+ private func setupCaptureSession(completion: @escaping (Bool) -> Void) {
+ sessionQueue.async { [weak self] in
+ guard let self = self else {
+ completion(false)
+ return
+ }
+
+ // Clean up any existing session before creating a new one
+ self.cleanupExistingSession()
+
+ let session = AVCaptureSession()
+
+ do {
+ // Get available devices and prefer external camera if available
+ let discoverySession = AVCaptureDevice.DiscoverySession(
+ deviceTypes: [.external, .builtInWideAngleCamera],
+ mediaType: .video,
+ position: .unspecified
+ )
+
+ guard let videoDevice = discoverySession.devices.first else {
+ NSLog("No video devices available")
+ DispatchQueue.main.async {
+ self.isSessionRunning = false
+ self.cameraAvailable = false
+ }
+ completion(false)
+ return
+ }
+
+ NSLog("Using camera: \(videoDevice.localizedName)")
+
+ // Lock device for configuration
+ try videoDevice.lockForConfiguration()
+ defer { videoDevice.unlockForConfiguration() }
+
+ let videoInput = try AVCaptureDeviceInput(device: videoDevice)
+ guard session.canAddInput(videoInput) else {
+ throw NSError(domain: "BoringNotch.WebcamManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Cannot add video input"])
+ }
+
+ session.beginConfiguration()
+ session.sessionPreset = .high
+ session.addInput(videoInput)
+
+ let videoOutput = AVCaptureVideoDataOutput()
+ videoOutput.setSampleBufferDelegate(nil, queue: nil)
+ if session.canAddOutput(videoOutput) {
+ session.addOutput(videoOutput)
+ }
+ session.commitConfiguration()
+
+ self.captureSession = session
+
+ // Create and set up preview layer on main thread
+ DispatchQueue.main.async {
+ self.cameraAvailable = true
+ let previewLayer = AVCaptureVideoPreviewLayer(session: session)
+ previewLayer.videoGravity = .resizeAspectFill
+ self.previewLayer = previewLayer
+
+ // Setup is complete, let the caller know
+ completion(true)
+ }
+
+ NSLog("Capture session setup completed successfully")
+ } catch {
+ NSLog("Failed to setup capture session: \(error.localizedDescription)")
+ DispatchQueue.main.async {
+ self.isSessionRunning = false
+ self.cameraAvailable = false
+ self.previewLayer = nil
+ }
+ completion(false)
+ }
+ }
+ }
+
+ /// Cleans up an existing capture session, removing all inputs and outputs
+ private func cleanupExistingSession() {
+ if let existingSession = self.captureSession {
+ // First stop the session if running
+ if existingSession.isRunning {
+ existingSession.stopRunning()
+ }
+
+ // Then perform configuration cleanup
+ existingSession.beginConfiguration()
+
+ // Remove all inputs and outputs
+ for input in existingSession.inputs {
+ existingSession.removeInput(input)
+ }
+ for output in existingSession.outputs {
+ existingSession.removeOutput(output)
+ }
+
+ existingSession.commitConfiguration()
+ self.captureSession = nil
+
+ // Clear preview layer on main thread
+ DispatchQueue.main.async {
+ self.previewLayer = nil
+ }
+ }
+ }
+
+ @objc private func deviceWasDisconnected(notification: Notification) {
+ NSLog("Camera device was disconnected")
+ sessionQueue.async { [weak self] in
+ guard let self = self else { return }
+ self.stopSession()
+ DispatchQueue.main.async {
+ self.cameraAvailable = false
+ }
+ }
+ }
+
+ @objc private func deviceWasConnected(notification: Notification) {
+ NSLog("Camera device was connected")
+ sessionQueue.async { [weak self] in
+ guard let self = self else { return }
+ self.checkCameraAvailability()
+ }
+ }
+
+ private func updateSessionState() {
+ let isRunning = self.captureSession?.isRunning ?? false
+ DispatchQueue.main.async {
+ self.isSessionRunning = isRunning
+ }
+ }
+
+ func startSession() {
+ sessionQueue.async { [weak self] in
+ guard let self = self else { return }
+
+ // If no session exists, create new session
+ if self.captureSession == nil {
+ self.setupCaptureSession { success in
+ if success {
+ // Only start the session if setup was successful
+ self.startRunningCaptureSession()
+ }
+ }
+ } else {
+ // Session already exists, just start it
+ self.startRunningCaptureSession()
+ }
+ }
+ }
+
+ private func startRunningCaptureSession() {
+ sessionQueue.async { [weak self] in
+ guard let self = self, let session = self.captureSession, !session.isRunning else {
+ return
+ }
+
+ session.startRunning()
+
+ // Update state on main thread
+ self.updateSessionState()
+
+ NSLog("Capture session started successfully")
+ }
+ }
+
+ func stopSession() {
+ sessionQueue.async { [weak self] in
+ guard let self = self else { return }
+
+ // Update state to indicate we're stopping
+ DispatchQueue.main.async {
+ self.isSessionRunning = false
+ }
+
+ self.cleanupExistingSession()
+
+ NSLog("Capture session stopped and cleaned up")
+ }
+ }
+}
diff --git a/DynamicIsland/menu/StatusBarMenu.swift b/DynamicIsland/menu/StatusBarMenu.swift
new file mode 100644
index 0000000..2bcbae2
--- /dev/null
+++ b/DynamicIsland/menu/StatusBarMenu.swift
@@ -0,0 +1,24 @@
+import Cocoa
+
+class BoringStatusMenu: NSMenu {
+
+ var statusItem: NSStatusItem!
+
+ override init() {
+ super.init()
+
+ // Initialize the status item
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
+
+ if let button = statusItem.button {
+ button.image = NSImage(systemSymbolName: "music.note", accessibilityDescription: "BoringNotch")
+ button.action = #selector(showMenu)
+ }
+
+ // Set up the menu
+ let menu = NSMenu()
+ menu.addItem(NSMenuItem(title: "Quit", action: #selector(quitAction), keyEquivalent: "q"))
+ statusItem.menu = menu
+ }
+
+}
diff --git a/DynamicIsland/metal/visualizer.metal b/DynamicIsland/metal/visualizer.metal
new file mode 100644
index 0000000..9bd88b0
--- /dev/null
+++ b/DynamicIsland/metal/visualizer.metal
@@ -0,0 +1,18 @@
+//
+// visualizer.metal
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 28/08/24.
+//
+
+#include
+using namespace metal;
+
+vertex float4 vertexShader(uint vertexID [[vertex_id]],
+ constant float2 *vertices [[buffer(0)]]) {
+ return float4(vertices[vertexID], 0, 1);
+}
+
+fragment float4 fragmentShader(constant float4 &color [[buffer(0)]]) {
+ return color;
+}
diff --git a/DynamicIsland/models/BatteryStatusViewModel.swift b/DynamicIsland/models/BatteryStatusViewModel.swift
new file mode 100644
index 0000000..f439248
--- /dev/null
+++ b/DynamicIsland/models/BatteryStatusViewModel.swift
@@ -0,0 +1,147 @@
+import Foundation
+import Cocoa
+import SwiftUI
+import IOKit.ps
+import Defaults
+
+/// A view model that manages and monitors the battery status of the device
+class BatteryStatusViewModel: ObservableObject {
+
+ private var wasCharging: Bool = false
+ private var powerSourceChangedCallback: IOPowerSourceCallbackType?
+ private var runLoopSource: Unmanaged?
+ var animations: DynamicIslandAnimations = DynamicIslandAnimations()
+
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+
+ @Published private(set) var levelBattery: Float = 0.0
+ @Published private(set) var maxCapacity: Float = 0.0
+ @Published private(set) var isPluggedIn: Bool = false
+ @Published private(set) var isCharging: Bool = false
+ @Published private(set) var isInLowPowerMode: Bool = false
+ @Published private(set) var isInitial: Bool = false
+ @Published private(set) var timeToFullCharge: Int = 0
+ @Published private(set) var statusText: String = ""
+
+ private let managerBattery = BatteryActivityManager.shared
+ private var managerBatteryId: Int?
+
+ static let shared = BatteryStatusViewModel()
+
+ /// Initializes the view model with a given DynamicIslandViewModel instance
+ /// - Parameter vm: The DynamicIslandViewModel instance
+ private init() {
+ setupPowerStatus()
+ setupMonitor()
+ }
+
+ /// Sets up the initial power status by fetching battery information
+ private func setupPowerStatus() {
+ let batteryInfo = managerBattery.initializeBatteryInfo()
+ updateBatteryInfo(batteryInfo)
+ }
+
+ /// Sets up the monitor to observe battery events
+ private func setupMonitor() {
+ managerBatteryId = managerBattery.addObserver { [weak self] event in
+ guard let self = self else { return }
+ self.handleBatteryEvent(event)
+ }
+ }
+
+ /// Handles battery events and updates the corresponding properties
+ /// - Parameter event: The battery event to handle
+ private func handleBatteryEvent(_ event: BatteryActivityManager.BatteryEvent) {
+ switch event {
+ case .powerSourceChanged(let isPluggedIn):
+ print("π Power source: \(isPluggedIn ? "Connected" : "Disconnected")")
+ withAnimation {
+ self.isPluggedIn = isPluggedIn
+ self.statusText = isPluggedIn ? "Plugged In" : "Unplugged"
+ self.notifyImportanChangeStatus()
+ }
+
+ case .batteryLevelChanged(let level):
+ print("π Battery level: \(Int(level))%")
+ withAnimation {
+ self.levelBattery = level
+ }
+
+ case .lowPowerModeChanged(let isEnabled):
+ print("β‘ Low power mode: \(isEnabled ? "Enabled" : "Disabled")")
+ self.notifyImportanChangeStatus()
+ withAnimation {
+ self.isInLowPowerMode = isEnabled
+ self.statusText = "Low Power: \(self.isInLowPowerMode ? "On" : "Off")"
+ }
+
+ case .isChargingChanged(let isCharging):
+ print("π Charging: \(isCharging ? "Yes" : "No")")
+ print("maxCapacity: \(self.maxCapacity)")
+ print("levelBattery: \(self.levelBattery)")
+ self.notifyImportanChangeStatus()
+ withAnimation {
+ self.isCharging = isCharging
+ self.statusText = isCharging ? "Charging battery" : (self.levelBattery < self.maxCapacity ? "Not charging" : "Full charge")
+ }
+
+ case .timeToFullChargeChanged(let time):
+ print("π Time to full charge: \(time) minutes")
+ withAnimation {
+ self.timeToFullCharge = time
+ }
+
+ case .maxCapacityChanged(let capacity):
+ print("π Max capacity: \(capacity)")
+ withAnimation {
+ self.maxCapacity = capacity
+ }
+
+ case .error(let description):
+ print("β οΈ Error: \(description)")
+ }
+ }
+
+ /// Updates the battery information with the given BatteryInfo instance
+ /// - Parameter batteryInfo: The BatteryInfo instance containing the battery data
+ private func updateBatteryInfo(_ batteryInfo: BatteryInfo) {
+ withAnimation {
+ self.levelBattery = batteryInfo.currentCapacity
+ self.isPluggedIn = batteryInfo.isPluggedIn
+ self.isCharging = batteryInfo.isCharging
+ self.isInLowPowerMode = batteryInfo.isInLowPowerMode
+ self.timeToFullCharge = batteryInfo.timeToFullCharge
+ self.maxCapacity = batteryInfo.maxCapacity
+ self.statusText = batteryInfo.isPluggedIn ? "Plugged In" : "Unplugged"
+ }
+ notifyImportanChangeStatus(delay: coordinator.firstLaunch ? 6 : 0.0)
+ withAnimation {
+ if self.isCharging {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ self.statusText = "Charging: Yes"
+ }
+ }
+ if self.isInLowPowerMode {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ self.statusText = "Low Power: On"
+ }
+ }
+ }
+ }
+
+ /// Notifies important changes in the battery status with an optional delay
+ /// - Parameter delay: The delay before notifying the change, default is 0.0
+ private func notifyImportanChangeStatus(delay: Double = 0.0) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ self.coordinator.toggleExpandingView(status: true, type: .battery)
+ }
+ }
+
+ deinit {
+ print("π Cleaning up battery monitoring...")
+ if let managerBatteryId: Int = managerBatteryId {
+ managerBattery.removeObserver(byId: managerBatteryId)
+ }
+ }
+
+}
diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift
new file mode 100644
index 0000000..150db79
--- /dev/null
+++ b/DynamicIsland/models/Constants.swift
@@ -0,0 +1,165 @@
+//
+// Constants.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 2024. 10. 17..
+//
+
+import SwiftUI
+import Defaults
+
+private let availableDirectories = FileManager
+ .default
+ .urls(for: .documentDirectory, in: .userDomainMask)
+let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+let bundleIdentifier = Bundle.main.bundleIdentifier!
+let appVersion = "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""))"
+
+let temporaryDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+let spacing: CGFloat = 16
+
+struct CustomVisualizer: Codable, Hashable, Equatable, Defaults.Serializable {
+ let UUID: UUID
+ var name: String
+ var url: URL
+ var speed: CGFloat = 1.0
+}
+
+enum CalendarSelectionState: Codable, Defaults.Serializable {
+ case all
+ case selected(Set)
+}
+
+enum HideNotchOption: String, Defaults.Serializable {
+ case always
+ case nowPlayingOnly
+ case never
+}
+
+// Define notification names at file scope
+extension Notification.Name {
+ static let mediaControllerChanged = Notification.Name("mediaControllerChanged")
+}
+
+// Media controller types for selection in settings
+enum MediaControllerType: String, CaseIterable, Identifiable, Defaults.Serializable {
+ case nowPlaying = "Now Playing"
+ case appleMusic = "Apple Music"
+ case spotify = "Spotify"
+ case youtubeMusic = "Youtube Music"
+
+ var id: String { self.rawValue }
+}
+
+// Sneak peek styles for selection in settings
+enum SneakPeekStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
+ case standard = "Default"
+ case inline = "Inline"
+
+ var id: String { self.rawValue }
+}
+
+extension Defaults.Keys {
+ // MARK: General
+ static let menubarIcon = Key("menubarIcon", default: true)
+ static let showOnAllDisplays = Key("showOnAllDisplays", default: false)
+ static let automaticallySwitchDisplay = Key("automaticallySwitchDisplay", default: true)
+ static let releaseName = Key("releaseName", default: "Wolf Painting πΊπ¨")
+
+ // MARK: Behavior
+ static let minimumHoverDuration = Key("minimumHoverDuration", default: 0.3)
+ static let enableHaptics = Key("enableHaptics", default: true)
+ static let openNotchOnHover = Key("openNotchOnHover", default: true)
+ static let extendHoverArea = Key("extendHoverArea", default: false)
+ static let notchHeightMode = Key(
+ "notchHeightMode",
+ default: WindowHeightMode.matchRealNotchSize
+ )
+ static let nonNotchHeightMode = Key(
+ "nonNotchHeightMode",
+ default: WindowHeightMode.matchMenuBar
+ )
+ static let nonNotchHeight = Key("nonNotchHeight", default: 32)
+ static let notchHeight = Key("notchHeight", default: 32)
+ //static let openLastTabByDefault = Key("openLastTabByDefault", default: false)
+
+ // MARK: Appearance
+ static let showEmojis = Key("showEmojis", default: false)
+ //static let alwaysShowTabs = Key("alwaysShowTabs", default: true)
+ static let showMirror = Key("showMirror", default: false)
+ static let mirrorShape = Key("mirrorShape", default: MirrorShapeEnum.rectangle)
+ static let settingsIconInNotch = Key("settingsIconInNotch", default: true)
+ static let lightingEffect = Key("lightingEffect", default: true)
+ static let accentColor = Key("accentColor", default: Color.blue)
+ static let enableShadow = Key("enableShadow", default: true)
+ static let cornerRadiusScaling = Key("cornerRadiusScaling", default: true)
+ static let useModernCloseAnimation = Key("useModernCloseAnimation", default: true)
+ static let showNotHumanFace = Key("showNotHumanFace", default: false)
+ static let tileShowLabels = Key("tileShowLabels", default: false)
+ static let showCalendar = Key("showCalendar", default: false)
+ static let sliderColor = Key(
+ "sliderUseAlbumArtColor",
+ default: SliderColorEnum.white
+ )
+ static let playerColorTinting = Key("playerColorTinting", default: true)
+ static let useMusicVisualizer = Key("useMusicVisualizer", default: true)
+ static let customVisualizers = Key<[CustomVisualizer]>("customVisualizers", default: [])
+ static let selectedVisualizer = Key("selectedVisualizer", default: nil)
+
+ // MARK: Gestures
+ static let enableGestures = Key("enableGestures", default: true)
+ static let closeGestureEnabled = Key("closeGestureEnabled", default: true)
+ static let gestureSensitivity = Key("gestureSensitivity", default: 200.0)
+
+ // MARK: Media playback
+ static let coloredSpectrogram = Key("coloredSpectrogram", default: true)
+ static let enableSneakPeek = Key("enableSneakPeek", default: false)
+ static let sneakPeekStyles = Key("sneakPeekStyles", default: .standard)
+ static let enableFullscreenMediaDetection = Key("enableFullscreenMediaDetection", default: true)
+ static let waitInterval = Key("waitInterval", default: 3)
+
+ // MARK: Battery
+ static let showPowerStatusNotifications = Key("showPowerStatusNotifications", default: true)
+ static let showBatteryIndicator = Key("showBatteryIndicator", default: true)
+ static let showBatteryPercentage = Key("showBatteryPercentage", default: true)
+ static let showPowerStatusIcons = Key("showPowerStatusIcons", default: true)
+
+ // MARK: Downloads
+ static let enableDownloadListener = Key("enableDownloadListener", default: true)
+ static let enableSafariDownloads = Key("enableSafariDownloads", default: true)
+ static let selectedDownloadIndicatorStyle = Key("selectedDownloadIndicatorStyle", default: DownloadIndicatorStyle.progress)
+ static let selectedDownloadIconStyle = Key("selectedDownloadIconStyle", default: DownloadIconStyle.onlyAppIcon)
+
+ // MARK: HUD
+ static let inlineHUD = Key("inlineHUD", default: false)
+ static let enableGradient = Key("enableGradient", default: false)
+ static let systemEventIndicatorShadow = Key("systemEventIndicatorShadow", default: false)
+ static let systemEventIndicatorUseAccent = Key("systemEventIndicatorUseAccent", default: false)
+
+ // MARK: Shelf
+ static let dynamicShelf = Key("dynamicShelf", default: true)
+ static let openShelfByDefault = Key("openShelfByDefault", default: true)
+
+ // MARK: Calendar
+ static let calendarSelectionState = Key("calendarSelectionState", default: .all)
+
+ // MARK: Fullscreen Media Detection
+ static let alwaysHideInFullscreen = Key("alwaysHideInFullscreen", default: false)
+
+ static let hideNotchOption = Key("hideNotchOption", default: .nowPlayingOnly)
+
+ // MARK: Wobble Animation
+ static let enableWobbleAnimation = Key("enableWobbleAnimation", default: false)
+
+ // MARK: Media Controller
+ static let mediaController = Key("mediaController", default: defaultMediaController)
+
+ // Helper to determine the default media controller based on macOS version
+ static var defaultMediaController: MediaControllerType {
+ if #available(macOS 15.4, *) {
+ return .appleMusic
+ } else {
+ return .nowPlaying
+ }
+ }
+}
diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift
new file mode 100644
index 0000000..a7efe00
--- /dev/null
+++ b/DynamicIsland/models/DynamicIslandViewModel.swift
@@ -0,0 +1,151 @@
+//
+// DynamicIslandViewModel.swift
+// DynamicIsland
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+//
+
+import Combine
+import Defaults
+import SwiftUI
+import TheBoringWorkerNotifier
+
+class DynamicIslandViewModel: NSObject, ObservableObject {
+ @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
+ @ObservedObject var detector = FullscreenMediaDetector.shared
+
+ let animationLibrary: DynamicIslandAnimations = .init()
+ let animation: Animation?
+
+ @Published var contentType: ContentType = .normal
+ @Published private(set) var notchState: NotchState = .closed
+
+ @Published var dragDetectorTargeting: Bool = false
+ @Published var dropZoneTargeting: Bool = false
+ @Published var dropEvent: Bool = false
+ @Published var anyDropZoneTargeting: Bool = false
+ var cancellables: Set = []
+
+ @Published var hideOnClosed: Bool = true
+ @Published var isHoveringCalendar: Bool = false
+ @Published var isBatteryPopoverActive: Bool = false
+
+ @Published var screen: String?
+
+ @Published var notchSize: CGSize = getClosedNotchSize()
+ @Published var closedNotchSize: CGSize = getClosedNotchSize()
+
+ deinit {
+ destroy()
+ }
+
+ func destroy() {
+ cancellables.forEach { $0.cancel() }
+ cancellables.removeAll()
+ }
+
+ init(screen: String? = nil) {
+ animation = animationLibrary.animation
+
+ super.init()
+
+ self.screen = screen
+ notchSize = getClosedNotchSize(screen: screen)
+ closedNotchSize = notchSize
+
+ Publishers.CombineLatest($dropZoneTargeting, $dragDetectorTargeting)
+ .map { value1, value2 in
+ value1 || value2
+ }
+ .assign(to: \.anyDropZoneTargeting, on: self)
+ .store(in: &cancellables)
+
+ setupDetectorObserver()
+ }
+
+ private func setupDetectorObserver() {
+ // 1) Publisher for the userβs fullscreen detection setting
+ let enabledPublisher = Defaults
+ .publisher(.enableFullscreenMediaDetection)
+ .map(\.newValue)
+
+ // 2) For each nonβnil screen name, map to a Bool publisher for that screen's status
+ let statusPublisher = $screen
+ .compactMap { $0 }
+ .removeDuplicates()
+ .map { screenName in
+ self.detector.$fullscreenStatus
+ .map { $0[screenName] ?? false }
+ .removeDuplicates()
+ }
+ .switchToLatest()
+
+ // 3) Combine enabled & status, animate only on changes
+ Publishers.CombineLatest(statusPublisher, enabledPublisher)
+ .map { status, enabled in enabled && status }
+ .removeDuplicates()
+ .receive(on: RunLoop.main)
+ .sink { [weak self] shouldHide in
+ withAnimation(.smooth) {
+ self?.hideOnClosed = shouldHide
+ }
+ }
+ .store(in: &cancellables)
+ }
+
+ // Computed property for effective notch height
+ var effectiveClosedNotchHeight: CGFloat {
+ let currentScreen = NSScreen.screens.first { $0.localizedName == screen }
+ let noNotchAndFullscreen = hideOnClosed && (currentScreen?.safeAreaInsets.top ?? 0 <= 0 || currentScreen == nil)
+ return noNotchAndFullscreen ? 0 : closedNotchSize.height
+ }
+
+ func isMouseHovering(position: NSPoint = NSEvent.mouseLocation) -> Bool {
+ let screenFrame = getScreenFrame(screen)
+ if let frame = screenFrame {
+
+ let baseY = frame.maxY - notchSize.height
+ let baseX = frame.midX - notchSize.width / 2
+
+ return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width
+ }
+
+ return false
+ }
+
+ func open() {
+ withAnimation(.bouncy) {
+ self.notchSize = openNotchSize
+ self.notchState = .open
+ }
+
+ // Force music information update when notch is opened
+ MusicManager.shared.forceUpdate()
+ }
+
+ func close() {
+ withAnimation(.smooth) { [weak self] in
+ guard let self = self else { return }
+ self.notchSize = getClosedNotchSize(screen: self.screen)
+ self.closedNotchSize = self.notchSize
+ self.notchState = .closed
+ }
+
+ // Set the current view to shelf if it contains files and the user enables openShelfByDefault
+ // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home
+ if !TrayDrop.shared.isEmpty && Defaults[.openShelfByDefault] {
+ coordinator.currentView = .shelf
+ } else if !coordinator.openLastTabByDefault {
+ coordinator.currentView = .home
+ }
+ }
+
+ func closeHello() {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in
+ self?.coordinator.firstLaunch = false
+ withAnimation(self?.animationLibrary.animation) {
+ self?.close()
+ }
+ }
+ }
+}
diff --git a/DynamicIsland/models/PlaybackState.swift b/DynamicIsland/models/PlaybackState.swift
new file mode 100644
index 0000000..b14e17a
--- /dev/null
+++ b/DynamicIsland/models/PlaybackState.swift
@@ -0,0 +1,23 @@
+//
+// PlaybackState.swift
+// boringNotch
+//
+// Created by Alexander Greco on 2025-03-29.
+//
+
+import Foundation
+
+struct PlaybackState {
+ var bundleIdentifier: String
+ var isPlaying: Bool = false
+ var title: String = "I'm Handsome"
+ var artist: String = "Me"
+ var album: String = "Self Love"
+ var currentTime: Double = 0
+ var duration: Double = 0
+ var playbackRate: Double = 1
+ var isShuffled: Bool? = nil
+ var isRepeating: Bool? = nil
+ var lastUpdated: Date = Date.distantPast
+ var artwork: Data?
+}
diff --git a/DynamicIsland/observers/FullscreenMediaDetection.swift b/DynamicIsland/observers/FullscreenMediaDetection.swift
new file mode 100644
index 0000000..b7e4d25
--- /dev/null
+++ b/DynamicIsland/observers/FullscreenMediaDetection.swift
@@ -0,0 +1,88 @@
+//
+// FullscreenMediaDetection.swift
+// boringNotch
+//
+// Created by Richard Kunkli on 06/09/2024.
+//
+
+import Accessibility
+import Cocoa
+import CoreAudio
+import Defaults
+import MacroVisionKit
+import SwiftUI
+
+@MainActor
+class FullscreenMediaDetector: ObservableObject {
+ static let shared = FullscreenMediaDetector()
+ private let detector: MacroVisionKit
+
+ // changed from [NSScreen:Bool] to [screenName:Bool]
+ @Published private(set) var fullscreenStatus: [String: Bool] = [:]
+
+ private init() {
+ self.detector = MacroVisionKit.shared
+ detector.configuration.includeSystemApps = true
+ setupNotificationObservers()
+ updateFullScreenStatus() // now on MainActor
+ }
+
+ private func setupNotificationObservers() {
+ let nc = NSWorkspace.shared.notificationCenter
+ nc.addObserver(self, selector: #selector(handleChange),
+ name: NSWorkspace.activeSpaceDidChangeNotification, object: nil)
+ nc.addObserver(self, selector: #selector(handleChange),
+ name: NSApplication.didChangeScreenParametersNotification, object: nil)
+ }
+
+ @objc private func handleChange() {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self.updateFullScreenStatus()
+ }
+ }
+
+ private func updateFullScreenStatus() {
+ NSLog("π Fullscreen status update triggered")
+ guard Defaults[.enableFullscreenMediaDetection] else {
+ NSLog("β Fullscreen media detection disabled")
+ let reset = Dictionary(uniqueKeysWithValues: NSScreen.screens.map { ($0.localizedName, false) }
+ )
+ if reset != fullscreenStatus {
+ fullscreenStatus = reset
+ NSLog("βΈ Fullscreen disabled β reset all screens")
+ }
+ return
+ }
+
+ NSLog("π Fullscreen media detection enabled")
+
+ let apps = detector.detectFullscreenApps(debug: false)
+ let names = NSScreen.screens.map { $0.localizedName }
+ var newStatus: [String: Bool] = [:]
+ for name in names {
+ newStatus[name] = apps.contains { $0.screen.localizedName == name && $0.bundleIdentifier != "com.apple.finder" }
+ }
+
+ if newStatus != fullscreenStatus {
+ fullscreenStatus = newStatus
+ NSLog("β
Fullscreen status: \(newStatus)")
+ }
+ }
+
+ func isScreenInFullScreen(_ screen: NSScreen) -> Bool {
+ let screenName = screen.localizedName
+ return fullscreenStatus[screenName] ?? false
+ }
+
+ func isAnyScreenInFullScreen() -> Bool {
+ return fullscreenStatus.values.contains(true)
+ }
+
+ private func cleanupNotificationObservers() {
+ NSWorkspace.shared.notificationCenter.removeObserver(self)
+ }
+
+ deinit {
+ NSWorkspace.shared.notificationCenter.removeObserver(self)
+ }
+}
diff --git a/DynamicIsland/private/CGSSpace.swift b/DynamicIsland/private/CGSSpace.swift
new file mode 100644
index 0000000..ccbe25a
--- /dev/null
+++ b/DynamicIsland/private/CGSSpace.swift
@@ -0,0 +1,76 @@
+//
+// CGSSpace.swift
+// boringNotch
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Original source: https://github.com/avaidyam/Parrot/
+// Modified by Alexander Greco on 2024-10-27
+
+import AppKit
+
+/// Small Spaces API wrapper.
+public final class CGSSpace {
+ private let identifier: CGSSpaceID
+ private let createdByInit: Bool
+
+ public var windows: Set = [] {
+ didSet {
+ let remove = oldValue.subtracting(self.windows)
+ let add = self.windows.subtracting(oldValue)
+
+ CGSRemoveWindowsFromSpaces(_CGSDefaultConnection(),
+ remove.map { $0.windowNumber } as NSArray,
+ [self.identifier])
+ CGSAddWindowsToSpaces(_CGSDefaultConnection(),
+ add.map { $0.windowNumber } as NSArray,
+ [self.identifier])
+ }
+ }
+
+ /// Initialized `CGSSpace`s *MUST* be de-initialized upon app exit!
+ public init(level: Int = 0) {
+ let flag = 0x1 // this value MUST be 1, otherwise, Finder decides to draw desktop icons
+ self.identifier = CGSSpaceCreate(_CGSDefaultConnection(), flag, nil)
+ CGSSpaceSetAbsoluteLevel(_CGSDefaultConnection(), self.identifier, level)
+ CGSShowSpaces(_CGSDefaultConnection(), [self.identifier])
+ self.createdByInit = true // Mark as created by the first init
+ }
+
+ public init(id: UInt64) {
+ let flag = 0x1 // this value MUST be 1, otherwise, Finder decides to draw desktop icons
+ self.identifier = id
+ CGSShowSpaces(_CGSDefaultConnection(), [self.identifier])
+ self.createdByInit = false // Mark as created externally
+ }
+
+ deinit {
+ CGSHideSpaces(_CGSDefaultConnection(), [self.identifier])
+ // Only call CGSSpaceDestroy if the space was created by the first init
+ if createdByInit {
+ CGSSpaceDestroy(_CGSDefaultConnection(), self.identifier)
+ }
+ }
+}
+
+// CGSSpace stuff:
+fileprivate typealias CGSConnectionID = UInt
+fileprivate typealias CGSSpaceID = UInt64
+@_silgen_name("_CGSDefaultConnection")
+fileprivate func _CGSDefaultConnection() -> CGSConnectionID
+@_silgen_name("CGSSpaceCreate")
+fileprivate func CGSSpaceCreate(_ cid: CGSConnectionID, _ unknown: Int, _ options: NSDictionary?) -> CGSSpaceID
+@_silgen_name("CGSSpaceDestroy")
+fileprivate func CGSSpaceDestroy(_ cid: CGSConnectionID, _ space: CGSSpaceID)
+@_silgen_name("CGSSpaceSetAbsoluteLevel")
+fileprivate func CGSSpaceSetAbsoluteLevel(_ cid: CGSConnectionID, _ space: CGSSpaceID, _ level: Int)
+@_silgen_name("CGSAddWindowsToSpaces")
+fileprivate func CGSAddWindowsToSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray)
+@_silgen_name("CGSRemoveWindowsFromSpaces")
+fileprivate func CGSRemoveWindowsFromSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray)
+@_silgen_name("CGSHideSpaces")
+fileprivate func CGSHideSpaces(_ cid: CGSConnectionID, _ spaces: NSArray)
+@_silgen_name("CGSShowSpaces")
+fileprivate func CGSShowSpaces(_ cid: CGSConnectionID, _ spaces: NSArray)
diff --git a/DynamicIsland/sizing/matters.swift b/DynamicIsland/sizing/matters.swift
new file mode 100644
index 0000000..506749b
--- /dev/null
+++ b/DynamicIsland/sizing/matters.swift
@@ -0,0 +1,76 @@
+//
+// sizeMatters.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 05/08/24.
+//
+
+import Defaults
+import Foundation
+import SwiftUI
+
+let downloadSneakSize: CGSize = .init(width: 65, height: 1)
+let batterySneakSize: CGSize = .init(width: 160, height: 1)
+
+let openNotchSize: CGSize = .init(width: 610, height: 200)
+let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
+
+enum MusicPlayerImageSizes {
+ static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
+ static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20))
+}
+
+func getScreenFrame(_ screen: String? = nil) -> CGRect? {
+ var selectedScreen = NSScreen.main
+
+ if let customScreen = screen {
+ selectedScreen = NSScreen.screens.first(where: { $0.localizedName == customScreen })
+ }
+
+ if let screen = selectedScreen {
+ return screen.frame
+ }
+
+ return nil
+}
+
+func getClosedNotchSize(screen: String? = nil) -> CGSize {
+ // Default notch size, to avoid using optionals
+ var notchHeight: CGFloat = Defaults[.nonNotchHeight]
+ var notchWidth: CGFloat = 185
+
+ var selectedScreen = NSScreen.main
+
+ if let customScreen = screen {
+ selectedScreen = NSScreen.screens.first(where: { $0.localizedName == customScreen })
+ }
+
+ // Check if the screen is available
+ if let screen = selectedScreen {
+ // Calculate and set the exact width of the notch
+ if let topLeftNotchpadding: CGFloat = screen.auxiliaryTopLeftArea?.width,
+ let topRightNotchpadding: CGFloat = screen.auxiliaryTopRightArea?.width
+ {
+ notchWidth = screen.frame.width - topLeftNotchpadding - topRightNotchpadding + 4
+ }
+
+ // Check if the Mac has a notch
+ if screen.safeAreaInsets.top > 0 {
+ // This is a display WITH a notch - use notch height settings
+ notchHeight = Defaults[.notchHeight]
+ if Defaults[.notchHeightMode] == .matchRealNotchSize {
+ notchHeight = screen.safeAreaInsets.top
+ } else if Defaults[.notchHeightMode] == .matchMenuBar {
+ notchHeight = screen.frame.maxY - screen.visibleFrame.maxY
+ }
+ } else {
+ // This is a display WITHOUT a notch - use non-notch height settings
+ notchHeight = Defaults[.nonNotchHeight]
+ if Defaults[.nonNotchHeightMode] == .matchMenuBar {
+ notchHeight = screen.frame.maxY - screen.visibleFrame.maxY
+ }
+ }
+ }
+
+ return .init(width: notchWidth, height: notchHeight)
+}
diff --git a/DynamicIsland/strings/constants.swift b/DynamicIsland/strings/constants.swift
new file mode 100644
index 0000000..5272416
--- /dev/null
+++ b/DynamicIsland/strings/constants.swift
@@ -0,0 +1,13 @@
+//
+// constants.swift
+// boringNotch
+//
+// Created by Harsh Vardhan Goswami on 04/08/24.
+//
+
+import Foundation
+
+let productPage = URL(string: "https://github.com/TheBoredTeam/boring.notch")!
+let sponsorPage = URL(string: "https://buymeacoffee.com/jfxh67wvfxq")!
+
+
diff --git a/DynamicIsland/utils/Logger.swift b/DynamicIsland/utils/Logger.swift
new file mode 100644
index 0000000..e95cdba
--- /dev/null
+++ b/DynamicIsland/utils/Logger.swift
@@ -0,0 +1,77 @@
+import Foundation
+import SwiftUI
+
+enum LogCategory: String {
+ case lifecycle = "π"
+ case memory = "πΎ"
+ case performance = "β‘οΈ"
+ case ui = "π¨"
+ case network = "π"
+ case error = "β"
+ case warning = "β οΈ"
+ case success = "β
"
+ case debug = "π"
+}
+
+struct Logger {
+ static func log(
+ _ message: String,
+ category: LogCategory,
+ file: String = #file,
+ function: String = #function,
+ line: Int = #line
+ ) {
+ let fileName = (file as NSString).lastPathComponent
+ let timestamp = ISO8601DateFormatter().string(from: Date())
+ print("\(category.rawValue) [\(timestamp)] [\(fileName):\(line)] \(function) - \(message)")
+ }
+
+ static func trackMemory(
+ file: String = #file,
+ function: String = #function,
+ line: Int = #line
+ ) {
+ var info = mach_task_basic_info()
+ var count = mach_msg_type_number_t(MemoryLayout.size)/4
+
+ let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
+ $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
+ task_info(mach_task_self_,
+ task_flavor_t(MACH_TASK_BASIC_INFO),
+ $0,
+ &count)
+ }
+ }
+
+ if kerr == KERN_SUCCESS {
+ let usedMB = Double(info.resident_size) / 1024.0 / 1024.0
+ log(String(format: "Memory used: %.2f MB", usedMB),
+ category: .memory,
+ file: file,
+ function: function,
+ line: line)
+ }
+ }
+}
+
+extension View {
+ func trackLifecycle(_ identifier: String) -> some View {
+ self.modifier(ViewLifecycleTracker(identifier: identifier))
+ }
+}
+
+struct ViewLifecycleTracker: ViewModifier {
+ let identifier: String
+
+ func body(content: Content) -> some View {
+ content
+ .onAppear {
+ Logger.log("\(identifier) appeared", category: .lifecycle)
+ Logger.trackMemory()
+ }
+ .onDisappear {
+ Logger.log("\(identifier) disappeared", category: .lifecycle)
+ Logger.trackMemory()
+ }
+ }
+}
\ No newline at end of file