diff --git a/Persephone/Controllers/NotificationsController.swift b/Persephone/Controllers/NotificationsController.swift index 74a9d20..78b11ed 100644 --- a/Persephone/Controllers/NotificationsController.swift +++ b/Persephone/Controllers/NotificationsController.swift @@ -18,6 +18,16 @@ class NotificationsController: MPDClientDelegate { ) } + func didUpdateTime(mpdClient: MPDClient, total: UInt, elapsedMs: UInt) { + sendNotification( + name: Notification.timeChanged, + userInfo: [ + Notification.totalTimeKey: total, + Notification.elapsedTimeMsKey: elapsedMs + ] + ) + } + func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song]) { sendNotification( name: Notification.queueChanged, diff --git a/Persephone/Controllers/WindowController.swift b/Persephone/Controllers/WindowController.swift index e95e853..86ba0ed 100644 --- a/Persephone/Controllers/WindowController.swift +++ b/Persephone/Controllers/WindowController.swift @@ -13,6 +13,11 @@ class WindowController: NSWindowController { case prevTrack, playPause, stop, nextTrack } + var state: MPDClient.Status.State? + var totalTime: UInt? + var elapsedTimeMs: UInt? + var trackTimer: Timer? + let playIcon = NSImage(named: "playButton") let pauseIcon = NSImage(named: "pauseButton") @@ -26,16 +31,38 @@ class WindowController: NSWindowController { name: Notification.stateChanged, object: AppDelegate.mpdClient ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(timeChanged(_:)), + name: Notification.timeChanged, + object: AppDelegate.mpdClient + ) } @objc func stateChanged(_ notification: Notification) { guard let state = notification.userInfo?[Notification.stateKey] as? MPDClient.Status.State else { return } - setTransportControlState(state) + self.state = state + + setTransportControlState() } - func setTransportControlState(_ state: MPDClient.Status.State) { + @objc func timeChanged(_ notification: Notification) { + guard let totalTime = notification.userInfo?[Notification.totalTimeKey] as? UInt, + let elapsedTimeMs = notification.userInfo?[Notification.elapsedTimeMsKey] as? UInt + else { return } + + self.totalTime = totalTime + self.elapsedTimeMs = elapsedTimeMs + + setTrackProgressControls() + } + + func setTransportControlState() { + guard let state = state else { return } + transportControls.setEnabled(state.isOneOf([.playing, .paused]), forSegment: 0) transportControls.setEnabled(state.isOneOf([.playing, .paused, .stopped]), forSegment: 1) transportControls.setEnabled(state.isOneOf([.playing, .paused]), forSegment: 2) @@ -48,6 +75,43 @@ class WindowController: NSWindowController { } } + func setTrackProgressControls() { + guard let totalTime = totalTime, + let elapsedTimeMs = elapsedTimeMs + else { return } + + trackProgressBar.maxValue = Double(totalTime * 1000) + trackProgressBar.integerValue = Int(elapsedTimeMs) + + if [.playing, .paused].contains(state) { + trackProgressBar.isEnabled = true + } else { + trackProgressBar.isEnabled = false + } + + if state == .playing { + trackTimer?.invalidate() + + trackTimer = Timer.scheduledTimer( + timeInterval: 0.25, + target: self, + selector: #selector(updateProgress(_:)), + userInfo: nil, + repeats: true + ) + } else { + trackTimer?.invalidate() + } + } + + @objc func updateProgress(_ timer: Timer) { + guard let currentProgress = elapsedTimeMs else { return } + + elapsedTimeMs = currentProgress + 250 + + trackProgressBar.integerValue = Int(elapsedTimeMs!) + } + @IBAction func handleTransportControl(_ sender: NSSegmentedControl) { guard let transportAction = TransportAction(rawValue: sender.selectedSegment) else { return } @@ -65,4 +129,8 @@ class WindowController: NSWindowController { } @IBOutlet var transportControls: NSSegmentedCell! + + @IBOutlet var trackProgress: NSTextField! + @IBOutlet var trackProgressBar: NSSlider! + @IBOutlet var trackRemaining: NSTextField! } diff --git a/Persephone/Extensions/Notification.swift b/Persephone/Extensions/Notification.swift index 903f39a..72d901b 100644 --- a/Persephone/Extensions/Notification.swift +++ b/Persephone/Extensions/Notification.swift @@ -10,6 +10,7 @@ import Foundation extension Notification { static let stateChanged = Notification.Name("MPDClientStateChanged") + static let timeChanged = Notification.Name("MPDClientTimeChanged") static let queueChanged = Notification.Name("MPDClientQueueChanged") static let queuePosChanged = Notification.Name("MPDClientQueuePosChanged") static let loadedAlbums = Notification.Name("MPDClientLoadedAlbums") @@ -18,4 +19,6 @@ extension Notification { static let queueKey = "queue" static let queuePosKey = "song" static let albumsKey = "albums" + static let totalTimeKey = "totalTime" + static let elapsedTimeMsKey = "elapsedTimeMs" } diff --git a/Persephone/MPDClient/MPDClient.swift b/Persephone/MPDClient/MPDClient.swift index 35cc66c..fb8ea4f 100644 --- a/Persephone/MPDClient/MPDClient.swift +++ b/Persephone/MPDClient/MPDClient.swift @@ -213,8 +213,8 @@ class MPDClient { func idle() { commandQueue.async { [unowned self] in mpd_send_idle(self.connection) - let result = mpd_recv_idle(self.connection, true) + let result = mpd_recv_idle(self.connection, true) self.handleIdleResult(result) } } @@ -228,8 +228,12 @@ class MPDClient { } if mpdIdle.contains(.player) { self.fetchStatus() - self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state) - self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) + + if let status = self.status { + self.delegate?.didUpdateState(mpdClient: self, state: status.state) + self.delegate?.didUpdateTime(mpdClient: self, total: status.totalTime, elapsedMs: status.elapsedTimeMs) + self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song) + } } if !mpdIdle.isEmpty { self.idle() diff --git a/Persephone/MPDClient/Models/Status.swift b/Persephone/MPDClient/Models/Status.swift index bbd30ce..e43d140 100644 --- a/Persephone/MPDClient/Models/Status.swift +++ b/Persephone/MPDClient/Models/Status.swift @@ -34,9 +34,20 @@ extension MPDClient { return State(rawValue: UInt(mpdState.rawValue))! } + var totalTime: UInt { + let mpdTotalTime = mpd_status_get_total_time(mpdStatus) + + return UInt(mpdTotalTime) + } + + var elapsedTimeMs: UInt { + let mpdElapsedTimeMs = mpd_status_get_elapsed_ms(mpdStatus) + + return UInt(mpdElapsedTimeMs) + } + var song: Int { return Int(mpd_status_get_song_pos(mpdStatus)) } - } } diff --git a/Persephone/MPDClient/Protocols/Delegate.swift b/Persephone/MPDClient/Protocols/Delegate.swift index 5db155e..f8199d3 100644 --- a/Persephone/MPDClient/Protocols/Delegate.swift +++ b/Persephone/MPDClient/Protocols/Delegate.swift @@ -10,7 +10,10 @@ import Foundation protocol MPDClientDelegate { func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State) + func didUpdateTime(mpdClient: MPDClient, total: UInt, elapsedMs: UInt) + func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song]) func didUpdateQueuePos(mpdClient: MPDClient, song: Int) + func didLoadAlbums(mpdClient: MPDClient, albums: [MPDClient.Album]) } diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Resources/Base.lproj/Main.storyboard index 1b54af7..cae16af 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Resources/Base.lproj/Main.storyboard @@ -57,582 +57,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -680,7 +104,7 @@ - + @@ -715,9 +139,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -725,6 +188,9 @@ + + +