diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index e6c6510..7566c97 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -401,6 +401,8 @@ ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DynamicIsland/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Atoll; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Dynamic Island uses AppleScripts to control Spotify and Apple Music"; INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode"; @@ -412,7 +414,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = "1.0.1-beta"; + MARKETING_VERSION = "1.0.2-beta"; PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -449,6 +451,8 @@ ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DynamicIsland/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Atoll; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Dynamic Island uses AppleScripts to control Spotify and Apple Music"; INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode"; @@ -460,7 +464,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = "1.0.1-beta"; + MARKETING_VERSION = "1.0.2-beta"; PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/DynamicIsland/managers/SystemMediaControllers.swift b/DynamicIsland/managers/SystemMediaControllers.swift index a5add30..31d1b48 100644 --- a/DynamicIsland/managers/SystemMediaControllers.swift +++ b/DynamicIsland/managers/SystemMediaControllers.swift @@ -46,6 +46,10 @@ final class SystemVolumeController { } func adjust(by delta: Float) { + guard delta != 0 else { return } + if isMuted { + setMuted(false) + } var newValue = currentVolume + delta newValue = max(0, min(1, newValue)) setVolume(newValue) @@ -64,18 +68,53 @@ final class SystemVolumeController { } func setVolume(_ value: Float) { - var volume = max(0, min(1, value)) - let status = setData(selector: kAudioDevicePropertyVolumeScalar, data: &volume) - if status != noErr { - NSLog("⚠️ Failed to set volume: \(status)") + let clamped = max(0, min(1, value)) + if isMuted && clamped > 0 { + setMuted(false) } + + let elements = volumeElements() + + if elements.isEmpty { + var volume = clamped + let status = setData(selector: kAudioDevicePropertyVolumeScalar, data: &volume) + if status != noErr { + NSLog("⚠️ Failed to set volume: \(status)") + } + } else { + for element in elements { + var volume = clamped + let status = setData(selector: kAudioDevicePropertyVolumeScalar, element: element, data: &volume) + if status != noErr { + NSLog("⚠️ Failed to set volume for element \(element): \(status)") + } else { + cache(element: element, for: kAudioDevicePropertyVolumeScalar) + } + } + } + notifyCurrentState() } func setMuted(_ muted: Bool) { var muteFlag: UInt32 = muted ? 1 : 0 - let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag) - if status != noErr { - NSLog("⚠️ Failed to set mute state: \(status)") + let elements = muteElements() + + if elements.isEmpty { + let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag) + if status != noErr { + NSLog("⚠️ Failed to set mute state: \(status)") + } + return + } + + for element in elements { + var value = muteFlag + let status = setData(selector: kAudioDevicePropertyMute, element: element, data: &value) + if status != noErr { + NSLog("⚠️ Failed to set mute state for element \(element): \(status)") + } else { + cache(element: element, for: kAudioDevicePropertyMute) + } } } @@ -159,22 +198,85 @@ final class SystemVolumeController { } private func getVolume() -> Float { - var volume = Float32(0) - let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume) - if status != noErr { - NSLog("⚠️ Unable to fetch volume: \(status)") + let elements = volumeElements() + + if elements.isEmpty { + var volume = Float32(0) + let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume) + if status != noErr { + NSLog("⚠️ Unable to fetch volume: \(status)") + } + return volume } - return volume + + var masterVolume: Float? + var accumulator: Float = 0 + var count: Float = 0 + + for element in elements { + var value = Float32(0) + let status = getData(selector: kAudioDevicePropertyVolumeScalar, element: element, data: &value) + if status == noErr { + if element == kAudioObjectPropertyElementMaster { + masterVolume = value + } + accumulator += value + count += 1 + } + } + + if let masterVolume { + return masterVolume + } + + if count > 0 { + return accumulator / count + } + + var fallback = Float32(0) + let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &fallback) + if status != noErr { + NSLog("⚠️ Unable to fetch fallback volume: \(status)") + } + return fallback } private func getMuteState() -> Bool { - var mute: UInt32 = 0 - let status = getData(selector: kAudioDevicePropertyMute, data: &mute) + let elements = muteElements() + + if elements.isEmpty { + var mute: UInt32 = 0 + let status = getData(selector: kAudioDevicePropertyMute, data: &mute) + if status != noErr { + return false + } + return mute != 0 + } + + var retrieved = false + var allMuted = true + + for element in elements { + var value: UInt32 = 0 + let status = getData(selector: kAudioDevicePropertyMute, element: element, data: &value) + if status == noErr { + retrieved = true + if value == 0 { + allMuted = false + } + } + } + + if retrieved { + return allMuted + } + + var fallback: UInt32 = 0 + let status = getData(selector: kAudioDevicePropertyMute, data: &fallback) if status != noErr { - // Some devices do not support mute; treat as not muted return false } - return mute != 0 + return fallback != 0 } private func refreshPropertyElements() { @@ -264,6 +366,38 @@ final class SystemVolumeController { } return lastStatus } + + private func getData(selector: AudioObjectPropertySelector, element: AudioObjectPropertyElement, data: inout T) -> OSStatus { + var address = makeAddress(selector: selector, element: element) + guard propertyExists(deviceID: currentDeviceID, address: &address) else { + return kAudioHardwareUnknownPropertyError + } + var size = UInt32(MemoryLayout.size) + return AudioObjectGetPropertyData(currentDeviceID, &address, 0, nil, &size, &data) + } + + private func setData(selector: AudioObjectPropertySelector, element: AudioObjectPropertyElement, data: inout T) -> OSStatus { + var address = makeAddress(selector: selector, element: element) + guard propertyExists(deviceID: currentDeviceID, address: &address) else { + return kAudioHardwareUnknownPropertyError + } + let size = UInt32(MemoryLayout.size) + return AudioObjectSetPropertyData(currentDeviceID, &address, 0, nil, size, &data) + } + + private func volumeElements() -> [AudioObjectPropertyElement] { + candidateElements.filter { element in + var address = makeAddress(selector: kAudioDevicePropertyVolumeScalar, element: element) + return propertyExists(deviceID: currentDeviceID, address: &address) + } + } + + private func muteElements() -> [AudioObjectPropertyElement] { + candidateElements.filter { element in + var address = makeAddress(selector: kAudioDevicePropertyMute, element: element) + return propertyExists(deviceID: currentDeviceID, address: &address) + } + } } final class SystemBrightnessController { @@ -275,9 +409,17 @@ final class SystemBrightnessController { private var observers: [NSObjectProtocol] = [] private var notificationsInstalled = false private var displayID: CGDirectDisplayID = CGMainDisplayID() + private var brightnessAnimationTimer: Timer? + private var brightnessAnimationStart: Float = 0 + private var brightnessAnimationTarget: Float = 0 + private var brightnessAnimationStartDate: Date? + private let brightnessAnimationDuration: TimeInterval = 0.18 + private let brightnessAnimationSteps = 10 + private var lastEmittedBrightness: Float = 0.5 private init() { registerExternalNotifications() + lastEmittedBrightness = currentBrightness } func start() { @@ -286,6 +428,8 @@ final class SystemBrightnessController { func stop() { onBrightnessChange = nil + brightnessAnimationTimer?.invalidate() + brightnessAnimationTimer = nil } func adjust(by delta: Float) { @@ -294,17 +438,8 @@ final class SystemBrightnessController { func setBrightness(_ value: Float) { let clamped = max(0, min(1, value)) - if setBrightnessViaDisplayServices(clamped) { - notifyCurrentBrightness() - return - } - guard let service = displayService() else { return } - let status = IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, clamped) - IOObjectRelease(service) - if status == kIOReturnSuccess { - notifyCurrentBrightness() - } else { - NSLog("⚠️ Failed to set brightness via IODisplay: \(status)") + DispatchQueue.main.async { + self.beginBrightnessAnimation(to: clamped) } } @@ -324,10 +459,77 @@ final class SystemBrightnessController { private func notifyCurrentBrightness() { let brightness = currentBrightness - DispatchQueue.main.async { - self.onBrightnessChange?(brightness) - self.notificationCenter.post(name: .systemBrightnessDidChange, object: nil, userInfo: ["value": brightness]) + emitBrightnessChange(value: brightness) + } + + private func beginBrightnessAnimation(to target: Float) { + brightnessAnimationTimer?.invalidate() + + let start = lastEmittedBrightness + if abs(start - target) <= 0.0005 { + applyBrightness(target) + emitBrightnessChange(value: target) + return } + + brightnessAnimationStart = start + brightnessAnimationTarget = target + brightnessAnimationStartDate = Date() + + let interval = brightnessAnimationDuration / Double(brightnessAnimationSteps) + let timer = Timer(timeInterval: interval, repeats: true) { [weak self] timer in + guard let self else { return } + guard let startDate = self.brightnessAnimationStartDate else { + timer.invalidate() + self.brightnessAnimationTimer = nil + return + } + let elapsed = Date().timeIntervalSince(startDate) + let progress = min(elapsed / self.brightnessAnimationDuration, 1) + let eased = self.ease(progress) + let value = self.brightnessAnimationStart + (self.brightnessAnimationTarget - self.brightnessAnimationStart) * Float(eased) + self.applyBrightness(value) + self.emitBrightnessChange(value: value) + if progress >= 1 { + timer.invalidate() + self.brightnessAnimationTimer = nil + } + } + brightnessAnimationTimer = timer + RunLoop.main.add(timer, forMode: .common) + timer.fire() + } + + private func applyBrightness(_ value: Float) { + let clamped = max(0, min(1, value)) + if setBrightnessViaDisplayServices(clamped) { + return + } + guard let service = displayService() else { return } + let status = IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, clamped) + IOObjectRelease(service) + if status != kIOReturnSuccess { + NSLog("⚠️ Failed to set brightness via IODisplay: \(status)") + } + } + + private func emitBrightnessChange(value: Float) { + let clamped = max(0, min(1, value)) + lastEmittedBrightness = clamped + let dispatchBlock = { + self.onBrightnessChange?(clamped) + self.notificationCenter.post(name: .systemBrightnessDidChange, object: nil, userInfo: ["value": clamped]) + } + if Thread.isMainThread { + dispatchBlock() + } else { + DispatchQueue.main.async(execute: dispatchBlock) + } + } + + private func ease(_ t: Double) -> Double { + let clamped = min(max(t, 0), 1) + return 1 - pow(1 - clamped, 3) } private func displayService() -> io_service_t? { @@ -386,6 +588,7 @@ final class SystemBrightnessController { } deinit { + brightnessAnimationTimer?.invalidate() observers.forEach { DistributedNotificationCenter.default().removeObserver($0) } } }