Restored files from old Mac, force pushing

This commit is contained in:
Hariharan Mudaliar 2025-07-13 20:26:03 +05:30
commit 5040381840
126 changed files with 10431 additions and 67 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

64
.gitignore vendored Normal file
View file

@ -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

View file

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View file

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View file

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "bolt.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,20 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "TheBoringTeam.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "256-mac 1.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "BoringNotch icon.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "plug.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "sparkle.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,18 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_95_55)">
<circle cx="13" cy="13" r="3" fill="white"/>
</g>
<defs>
<filter id="filter0_d_95_55" x="0.8" y="0.8" width="24.4" height="24.4" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_95_55"/>
<feOffset/>
<feGaussianBlur stdDeviation="2.6"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_95_55"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_95_55" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "spotlight.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,35 @@
<svg width="689" height="1005" viewBox="0 0 689 1005" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.27" filter="url(#filter0_f_95_52)">
<ellipse cx="344.5" cy="925.5" rx="339.5" ry="74.5" fill="url(#paint0_linear_95_52)"/>
</g>
<g opacity="0.3" filter="url(#filter1_f_95_52)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 923.5C85.0575 575.896 218.5 -4 218.5 -4H481.5L684 923.5H683.772C677.163 883.632 527.795 851.757 344.5 851.757C161.205 851.757 11.8371 883.632 5.22814 923.5H5Z" fill="url(#paint1_linear_95_52)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 923.5C85.0575 575.896 218.5 -4 218.5 -4H481.5L684 923.5H683.772C677.163 883.632 527.795 851.757 344.5 851.757C161.205 851.757 11.8371 883.632 5.22814 923.5H5Z" fill="url(#paint2_linear_95_52)"/>
</g>
<defs>
<filter id="filter0_f_95_52" x="0" y="846" width="689" height="159" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.5" result="effect1_foregroundBlur_95_52"/>
</filter>
<filter id="filter1_f_95_52" x="0" y="-9" width="689" height="937.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.5" result="effect1_foregroundBlur_95_52"/>
</filter>
<linearGradient id="paint0_linear_95_52" x1="-579.5" y1="1205.5" x2="-232.344" y2="1580.76" gradientUnits="userSpaceOnUse">
<stop stop-color="#E1E1E1"/>
<stop offset="1" stop-color="#3B3B3B"/>
</linearGradient>
<linearGradient id="paint1_linear_95_52" x1="342.5" y1="-4" x2="342.5" y2="924.669" gradientUnits="userSpaceOnUse">
<stop stop-color="#D7D7D7"/>
<stop offset="1" stop-color="#252525"/>
</linearGradient>
<linearGradient id="paint2_linear_95_52" x1="143" y1="338.48" x2="567.46" y2="361.604" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="0.24" stop-color="white" stop-opacity="0.21045"/>
<stop offset="0.53" stop-color="#262626" stop-opacity="0.3657"/>
<stop offset="1" stop-color="#2E2E2E" stop-opacity="0.69"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -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
}
}

View file

@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.spotify.client</string>
<string>com.apple.Music</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.automation.apple-events</key>

View file

@ -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 {
var body: some Scene {
WindowGroup {
ContentView()
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 {
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
}
}

View file

@ -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
}
}

View file

@ -7,6 +7,8 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSAppleEventsUsageDescription</key>
<string>Dynamic Island uses AppleScripts to control Spotify and Apple Music</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Show calender events inside the notch</string>
<key>SUEnableDownloaderService</key>
@ -16,7 +18,7 @@
<key>SUFeedURL</key>
<string>https://TheBoredTeam.github.io/boring.notch/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>LlPWpDWaN5DLDCuB45z9xwxVXkdIqJhOT0eP5CiMsto=</string>
<string>B1Y47t8C/v8ImurYA+9arEsuCrpxwJSviekiflMElbI=</string>
<key>UTImportedTypeDeclarations</key>
<array>
<dict/>

View file

@ -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"

View file

@ -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<PlaybackState>.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
}
}

View file

@ -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<PlaybackState>.Publisher { get }
func play()
func pause()
func seek(to time: Double)
func nextTrack()
func previousTrack()
func togglePlay()
func isActive() -> Bool
func updatePlaybackInfo()
}

View file

@ -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<PlaybackState>.Publisher { $playbackState }
private var cancellables = Set<AnyCancellable>()
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
}
}
}
}
}
}

View file

@ -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<PlaybackState>.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
}
}

View file

@ -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<PlaybackState>.Publisher { $playbackState }
private let baseURL = "http://localhost:26538"
private var accessToken: String?
private var refreshTimer: Timer?
private var cancellables = Set<AnyCancellable>()
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?
}

View file

@ -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]))
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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 + 2 * spacerNum + 1, id: \.self) { index in
if(index < spacerNum || index > 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..<todayIndex+config.future:
selectedDate = dateForIndex(targetDateIndex, offset: offset)
haptics.toggle()
default:
return
}
}
private func scrollToToday(config: Config) {
let today = Date()
let todayIndex = indexForDate(today, offset: -config.offset - config.past)
byClick = true
scrollPosition = todayIndex - config.offset
selectedDate = today
}
private func indexForDate(_ date: Date, offset: Int) -> 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())
}

View file

@ -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()
}

View file

@ -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")
}

View file

@ -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
}
}
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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)
}

View file

@ -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())
}

View file

@ -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<Left: View, Right: View>: 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<Left: View, Right: View>(
for activityId: ActivityType,
@ViewBuilder left: @escaping () -> Left,
@ViewBuilder right: @escaping () -> Right
) -> some View {
self.modifier(LiveActivityModifier(for: activityId, leftContent: left, rightContent: right))
}
}

View file

@ -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<String>, 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)
}
}

View file

@ -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))
}
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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())
}

View file

@ -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())
}

View file

@ -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
}
}

View file

@ -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<Double>
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())
}

View file

@ -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<CGFloat, CGFloat> {
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)
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}

View file

@ -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) {}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -0,0 +1,28 @@
//
// ListItemPopover.swift
// boringNotch
//
// Created by Richard Kunkli on 15/09/2024.
//
import SwiftUI
struct ListItemPopover<Content: View>: 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()
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -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")
}
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}

View file

@ -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) {}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,77 @@
import Cocoa
import Combine
import Foundation
import OrderedCollections
class TrayDrop: ObservableObject {
static let shared = TrayDrop()
@Published var items: OrderedSet<DropItem>
var isEmpty: Bool { items.isEmpty }
@Published var isLoading: Int = 0
init(items: OrderedSet<DropItem> = .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) }
}
}

View file

@ -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")
}
}

View file

@ -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())
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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())
}

View file

@ -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))
}

View file

@ -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"
}

View file

@ -0,0 +1,30 @@
//
// ActionBar.swift
// boringNotch
//
// Created by Richard Kunkli on 15/09/2024.
//
import SwiftUI
extension View {
func actionBar<Content: View>(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)
}
}
}

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -0,0 +1,18 @@
//
// ConditionalModifier.swift
// boringNotch
//
// Created by Richard Kunkli on 20/08/2024.
//
import SwiftUI
extension View {
@ViewBuilder func conditionalModifier<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}

Some files were not shown because too many files have changed in this diff Show more