diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 4e55b25..c51894e 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -356,6 +356,7 @@ struct GeneralSettings: View { Section { Defaults.Toggle("Enable Lock Screen Live Activity", key: .enableLockScreenLiveActivity) + Defaults.Toggle("Play lock/unlock sounds", key: .enableLockSounds) if #available(macOS 26.0, *) { Picker("Lock screen material", selection: $lockScreenGlassStyle) { ForEach(LockScreenGlassStyle.allCases) { style in diff --git a/DynamicIsland/lock.mp3 b/DynamicIsland/lock.mp3 new file mode 100644 index 0000000..5d873fe Binary files /dev/null and b/DynamicIsland/lock.mp3 differ diff --git a/DynamicIsland/managers/LockScreenManager.swift b/DynamicIsland/managers/LockScreenManager.swift index ca38307..e36adcf 100644 --- a/DynamicIsland/managers/LockScreenManager.swift +++ b/DynamicIsland/managers/LockScreenManager.swift @@ -11,6 +11,7 @@ import Combine import AppKit import Defaults import SwiftUI +import AVFoundation enum LockScreenAnimationTimings { static let lockExpand: TimeInterval = 0.45 @@ -80,6 +81,7 @@ class LockScreenManager: ObservableObject { @objc private func screenLocked() { print("[\(timestamp())] LockScreenManager: 🔒 Screen LOCKED event received") + LockSoundPlayer.shared.playLockChime() // Update state SYNCHRONOUSLY without Task/await to avoid any delay lastUpdated = Date() @@ -120,6 +122,7 @@ class LockScreenManager: ObservableObject { @objc private func screenUnlocked() { print("[\(timestamp())] LockScreenManager: 🔓 Screen UNLOCKED event received") + LockSoundPlayer.shared.playUnlockChime() // Hide panel window immediately and synchronously print("[\(timestamp())] LockScreenManager: 🚪 Hiding panel window") @@ -190,3 +193,66 @@ extension LockScreenManager { return true // Always available on macOS } } + +// MARK: - Lock Sound Playback + +@MainActor +final class LockSoundPlayer { + static let shared = LockSoundPlayer() + private let throttleInterval: TimeInterval = 0.25 + private var players: [SoundType: AVAudioPlayer] = [:] + private var lastPlaybackDates: [SoundType: Date] = [:] + + private init() {} + + func playLockChime() { + play(.lock) + } + + func playUnlockChime() { + play(.unlock) + } + + private func play(_ type: SoundType) { + guard Defaults[.enableLockSounds] else { return } + guard shouldPlay(type) else { return } + guard let player = resolvePlayer(for: type) else { return } + + player.currentTime = 0 + player.play() + lastPlaybackDates[type] = Date() + } + + private func shouldPlay(_ type: SoundType) -> Bool { + guard let last = lastPlaybackDates[type] else { return true } + return Date().timeIntervalSince(last) >= throttleInterval + } + + private func resolvePlayer(for type: SoundType) -> AVAudioPlayer? { + if let cached = players[type] { + return cached + } + + guard let url = Bundle.main.url(forResource: type.resourceName, withExtension: "mp3") else { + Logger.log("Missing \(type.resourceName).mp3 in bundle", category: .warning) + return nil + } + + do { + let player = try AVAudioPlayer(contentsOf: url) + player.prepareToPlay() + players[type] = player + return player + } catch { + Logger.log("Failed to initialize lock sound player for \(type.resourceName): \(error.localizedDescription)", category: .error) + return nil + } + } + + private enum SoundType: String { + case lock + case unlock + + var resourceName: String { rawValue } + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index c2677ef..9d7dc3c 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -601,6 +601,7 @@ extension Defaults.Keys { // MARK: Lock Screen Features static let enableLockScreenLiveActivity = Key("enableLockScreenLiveActivity", default: true) + static let enableLockSounds = Key("enableLockSounds", default: true) // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false) diff --git a/DynamicIsland/unlock.mp3 b/DynamicIsland/unlock.mp3 new file mode 100644 index 0000000..3b1b06d Binary files /dev/null and b/DynamicIsland/unlock.mp3 differ