| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- import AppKit
- import Foundation
- import MediaPlayer
- /// Integrates with macOS media keys (play/pause, next, previous) and Now Playing info center.
- /// This makes MixBoard respond to keyboard media keys and appear in the Now Playing widget.
- @MainActor
- final class MediaKeyHandler {
- static let shared = MediaKeyHandler()
- private let commandCenter = MPRemoteCommandCenter.shared()
- private let infoCenter = MPNowPlayingInfoCenter.default()
- private var keyMonitor: Any?
- private var doubleClickMonitor: Any?
- weak var playerVM: PlayerViewModel?
- private init() {}
- /// Start handling media key events. Call once at app launch.
- func register(playerVM: PlayerViewModel) {
- self.playerVM = playerVM
- // Play
- commandCenter.playCommand.isEnabled = true
- commandCenter.playCommand.addTarget { [weak self] _ in
- guard let self, let vm = self.playerVM else { return .commandFailed }
- if !vm.isPlaying {
- vm.togglePlayPause()
- }
- return .success
- }
- // Pause
- commandCenter.pauseCommand.isEnabled = true
- commandCenter.pauseCommand.addTarget { [weak self] _ in
- guard let self, let vm = self.playerVM else { return .commandFailed }
- if vm.isPlaying {
- vm.togglePlayPause()
- }
- return .success
- }
- // Toggle play/pause
- commandCenter.togglePlayPauseCommand.isEnabled = true
- commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
- guard let self, let vm = self.playerVM else { return .commandFailed }
- vm.togglePlayPause()
- return .success
- }
- // Next track
- commandCenter.nextTrackCommand.isEnabled = true
- commandCenter.nextTrackCommand.addTarget { [weak self] _ in
- guard let self, let vm = self.playerVM else { return .commandFailed }
- vm.playNext()
- return .success
- }
- // Previous track
- commandCenter.previousTrackCommand.isEnabled = true
- commandCenter.previousTrackCommand.addTarget { [weak self] _ in
- guard let self, let vm = self.playerVM else { return .commandFailed }
- vm.playPrevious()
- return .success
- }
- // Seek (for scrubbing via Touch Bar or control center)
- commandCenter.changePlaybackPositionCommand.isEnabled = true
- commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
- guard let self, let vm = self.playerVM,
- let posEvent = event as? MPChangePlaybackPositionCommandEvent else {
- return .commandFailed
- }
- vm.seek(to: posEvent.positionTime)
- return .success
- }
- // Spacebar → play/pause (when no text field is focused)
- keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
- // Only handle bare spacebar (keyCode 49), no modifiers
- guard event.keyCode == 49,
- event.modifierFlags.intersection(.deviceIndependentFlagsMask) == [] else {
- return event
- }
- // Don't intercept if a text field / search field is the first responder
- if let responder = NSApp.keyWindow?.firstResponder,
- responder is NSTextView || responder is NSTextField {
- return event
- }
- self?.playerVM?.togglePlayPause()
- return nil // consume the event
- }
- // Double-click on playlist row → play that track (like foobar2000)
- doubleClickMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
- guard event.clickCount == 2 else { return event }
- // Post notification — PlaylistEntryList will handle it
- NotificationCenter.default.post(name: .doubleClickPlayTrack, object: nil)
- return event
- }
- }
- /// Update the Now Playing info with current track metadata.
- func updateNowPlaying(track: Track?, isPlaying: Bool, currentTime: TimeInterval, duration: TimeInterval) {
- guard let track else {
- infoCenter.nowPlayingInfo = nil
- return
- }
- var info = [String: Any]()
- info[MPMediaItemPropertyTitle] = track.title
- info[MPMediaItemPropertyArtist] = track.artist
- info[MPMediaItemPropertyAlbumTitle] = track.album
- info[MPMediaItemPropertyPlaybackDuration] = duration
- info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
- info[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
- if let bpm = track.bpm {
- info[MPMediaItemPropertyBeatsPerMinute] = Int(bpm)
- }
- infoCenter.nowPlayingInfo = info
- infoCenter.playbackState = isPlaying ? .playing : .paused
- }
- }
|