diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index cbde219..b05f28a 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -22,6 +22,10 @@ E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (Required, ); }; }; E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41B22C521FB932700D544F6 /* MPDClient.swift */; }; + E41EA46C221636AF0068EF46 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */; }; + E41EA46F221715910068EF46 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41EA46E221715910068EF46 /* Preferences.swift */; }; + E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = E42A8F3922176D6400A13ED9 /* LICENSE.md */; }; + E42A8F3C22176D6400A13ED9 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = E42A8F3A22176D6400A13ED9 /* README.md */; }; E465049A21E94DF500A70F4C /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E465049921E94DF500A70F4C /* WindowController.swift */; }; E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4928E0A2218D62A001D4BEA /* CGColor.swift */; }; E4A642DA22090CBE00067D21 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* Status.swift */; }; @@ -122,6 +126,10 @@ E41B22E921FB966C00D544F6 /* capabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = capabilities.h; sourceTree = ""; }; E41B22EA21FB966C00D544F6 /* queue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = queue.h; sourceTree = ""; }; E41B22EB21FB966C00D544F6 /* playlist.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = playlist.h; sourceTree = ""; }; + E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; + E41EA46E221715910068EF46 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + E42A8F3922176D6400A13ED9 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; + E42A8F3A22176D6400A13ED9 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; E465049921E94DF500A70F4C /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; E4928E0A2218D62A001D4BEA /* CGColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGColor.swift; sourceTree = ""; }; E4A642D922090CBE00067D21 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; @@ -167,6 +175,8 @@ E40786382110CE70006887B1 /* PersephoneUITests */, E40786192110CE6E006887B1 /* Products */, E41B22BE21FB6B3300D544F6 /* Frameworks */, + E42A8F3922176D6400A13ED9 /* LICENSE.md */, + E42A8F3A22176D6400A13ED9 /* README.md */, ); sourceTree = ""; }; @@ -184,6 +194,7 @@ isa = PBXGroup; children = ( E40FE717221B48CE00A4223F /* Layouts */, + E41EA46D221715820068EF46 /* Models */, E407861F2110CE70006887B1 /* Assets.xcassets */, E408D3B7220DE8CC0006D9BE /* Extensions */, E4D1B598220BA3C90026F233 /* Resources */, @@ -309,6 +320,14 @@ path = mpd; sourceTree = ""; }; + E41EA46D221715820068EF46 /* Models */ = { + isa = PBXGroup; + children = ( + E41EA46E221715910068EF46 /* Preferences.swift */, + ); + path = Models; + sourceTree = ""; + }; E4A642DB220912FA00067D21 /* MPDClient */ = { isa = PBXGroup; children = ( @@ -343,6 +362,7 @@ isa = PBXGroup; children = ( E408D3C1220E134F0006D9BE /* AlbumViewController.swift */, + E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */, E4E8CC932206097F0024217A /* NotificationsController.swift */, E4E8CC912204F4B80024217A /* QueueViewController.swift */, E465049921E94DF500A70F4C /* WindowController.swift */, @@ -472,7 +492,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */, E40786202110CE70006887B1 /* Assets.xcassets in Resources */, + E42A8F3C22176D6400A13ED9 /* README.md in Resources */, E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */, E40786232110CE70006887B1 /* Main.storyboard in Resources */, ); @@ -507,11 +529,13 @@ E408D3B6220DD8970006D9BE /* Notification.swift in Sources */, E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */, E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */, + E41EA46F221715910068EF46 /* Preferences.swift in Sources */, E465049A21E94DF500A70F4C /* WindowController.swift in Sources */, E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */, E407861C2110CE6E006887B1 /* AppDelegate.swift in Sources */, E4E8CC9A22075D370024217A /* Song.swift in Sources */, E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */, + E41EA46C221636AF0068EF46 /* PreferencesViewController.swift in Sources */, E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */, diff --git a/Persephone/AppDelegate.swift b/Persephone/AppDelegate.swift index f1aee74..6a52a54 100644 --- a/Persephone/AppDelegate.swift +++ b/Persephone/AppDelegate.swift @@ -10,15 +10,46 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { + var preferences = Preferences() + static let mpdClient = MPDClient( withDelegate: NotificationsController() ) func applicationDidFinishLaunching(_ aNotification: Notification) { - AppDelegate.mpdClient.connect() + connect() + + preferences.addObserver(self, forKeyPath: "mpdHost") + preferences.addObserver(self, forKeyPath: "mpdPort") } func applicationWillTerminate(_ aNotification: Notification) { + disconnect() + } + + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer? + ) { + switch keyPath { + case "mpdHost", "mpdPort": + disconnect() + connect() + default: + break + } + } + + func connect() { + AppDelegate.mpdClient.connect( + host: preferences.mpdHostOrDefault, + port: preferences.mpdPortOrDefault + ) + } + + func disconnect() { AppDelegate.mpdClient.disconnect() } } diff --git a/Persephone/Controllers/AlbumViewController.swift b/Persephone/Controllers/AlbumViewController.swift index 173ab02..f58896c 100644 --- a/Persephone/Controllers/AlbumViewController.swift +++ b/Persephone/Controllers/AlbumViewController.swift @@ -25,6 +25,13 @@ class AlbumViewController: NSViewController, name: Notification.loadedAlbums, object: AppDelegate.mpdClient ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(clearAlbums(_:)), + name: Notification.willDisconnect, + object: AppDelegate.mpdClient + ) } override func viewWillLayout() { @@ -42,6 +49,12 @@ class AlbumViewController: NSViewController, albumCollectionView.reloadData() } + @objc func clearAlbums(_ notification: Notification) { + self.albums = [] + + albumCollectionView.reloadData() + } + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return albums.count } diff --git a/Persephone/Controllers/NotificationsController.swift b/Persephone/Controllers/NotificationsController.swift index 74a9d20..1d004bf 100644 --- a/Persephone/Controllers/NotificationsController.swift +++ b/Persephone/Controllers/NotificationsController.swift @@ -10,7 +10,15 @@ import Foundation class NotificationsController: MPDClientDelegate { let notificationQueue = DispatchQueue.main - + + func didConnect(mpdClient: MPDClient) { + sendNotification(name: Notification.didConnect) + } + + func willDisconnect(mpdClient: MPDClient) { + sendNotification(name: Notification.willDisconnect) + } + func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State) { sendNotification( name: Notification.stateChanged, @@ -39,7 +47,7 @@ class NotificationsController: MPDClientDelegate { ) } - private func sendNotification(name: Notification.Name, userInfo: [AnyHashable : Any]) { + private func sendNotification(name: Notification.Name, userInfo: [AnyHashable : Any] = [:]) { self.notificationQueue.async { NotificationCenter.default.post( name: name, diff --git a/Persephone/Controllers/PreferencesViewController.swift b/Persephone/Controllers/PreferencesViewController.swift new file mode 100644 index 0000000..b751c93 --- /dev/null +++ b/Persephone/Controllers/PreferencesViewController.swift @@ -0,0 +1,36 @@ +// +// PreferencesViewController.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/14. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class PreferencesViewController: NSViewController { + var preferences = Preferences() + + override func viewDidLoad() { + super.viewDidLoad() + + if let mpdHost = preferences.mpdHost { + mpdHostField.stringValue = mpdHost + } + + if let mpdPort = preferences.mpdPort { + mpdPortField.stringValue = "\(mpdPort)" + } + } + + @IBAction func updateMpdHost(_ sender: NSTextField) { + preferences.mpdHost = sender.stringValue + } + + @IBAction func updateMpdPort(_ sender: NSTextField) { + preferences.mpdPort = sender.integerValue + } + + @IBOutlet var mpdHostField: NSTextField! + @IBOutlet var mpdPortField: NSTextField! +} diff --git a/Persephone/Controllers/QueueViewController.swift b/Persephone/Controllers/QueueViewController.swift index cd09b36..17fedbf 100644 --- a/Persephone/Controllers/QueueViewController.swift +++ b/Persephone/Controllers/QueueViewController.swift @@ -50,6 +50,13 @@ class QueueViewController: NSViewController, NSOutlineViewDataSource, NSOutlineV name: Notification.queuePosChanged, object: AppDelegate.mpdClient ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(clearQueue(_:)), + name: Notification.willDisconnect, + object: AppDelegate.mpdClient + ) } @IBAction func playTrack(_ sender: Any) { @@ -95,6 +102,12 @@ class QueueViewController: NSViewController, NSOutlineViewDataSource, NSOutlineV ) } + @objc func clearQueue(_ notification: Notification) { + self.queue = [] + + queueView.reloadData() + } + func setQueueIcon(_ state: MPDClient.Status.State) { switch state { case .playing: diff --git a/Persephone/Extensions/Notification.swift b/Persephone/Extensions/Notification.swift index 903f39a..462bec8 100644 --- a/Persephone/Extensions/Notification.swift +++ b/Persephone/Extensions/Notification.swift @@ -9,6 +9,9 @@ import Foundation extension Notification { + static let didConnect = Notification.Name("MPDClientDidConnect") + static let willDisconnect = Notification.Name("MPDClientWillDisconnect") + static let stateChanged = Notification.Name("MPDClientStateChanged") static let queueChanged = Notification.Name("MPDClientQueueChanged") static let queuePosChanged = Notification.Name("MPDClientQueuePosChanged") diff --git a/Persephone/MPDClient/MPDClient.swift b/Persephone/MPDClient/MPDClient.swift index 35cc66c..2219783 100644 --- a/Persephone/MPDClient/MPDClient.swift +++ b/Persephone/MPDClient/MPDClient.swift @@ -12,10 +12,8 @@ import mpdclient class MPDClient { var delegate: MPDClientDelegate? - let HOST = "localhost" - let PORT: UInt32 = 6600 - private var connection: OpaquePointer? + private var isConnected: Bool = false private var status: Status? private var queue: [Song] = [] @@ -46,30 +44,40 @@ class MPDClient { self.delegate = delegate } - func connect() { - guard let connection = mpd_connection_new(HOST, PORT, 0) - else { return } + func connect(host: String, port: Int) { + commandQueue.async { [unowned self] in + guard let connection = mpd_connection_new(host, UInt32(port), 10000), + mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS + else { return } - guard let status = mpd_run_status(connection) - else { return } + self.isConnected = true - self.connection = connection - self.status = Status(status) + guard let status = mpd_run_status(connection) + else { return } - fetchQueue() + self.connection = connection + self.status = Status(status) - fetchAllAlbums() + self.fetchQueue() + self.fetchAllAlbums() + self.idle() - self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state) - self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue) - self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) - idle() + self.delegate?.didConnect(mpdClient: self) + self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state) + self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue) + self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) + } } func disconnect() { + guard isConnected else { return } + noIdle() commandQueue.async { [unowned self] in + self.delegate?.willDisconnect(mpdClient: self) + mpd_connection_free(self.connection) + self.isConnected = false } } @@ -102,6 +110,8 @@ class MPDClient { } func playTrack(queuePos: Int) { + guard isConnected else { return } + noIdle() commandQueue.async { [unowned self] in mpd_run_play_pos(self.connection, UInt32(queuePos)) @@ -110,6 +120,8 @@ class MPDClient { } func queueCommand(command: Command) { + guard isConnected else { return } + noIdle() commandQueue.async { [unowned self] in self.sendCommand(command: command) @@ -236,15 +248,15 @@ class MPDClient { } } - func getLastErrorMessage() -> String! { + func getLastErrorMessage() -> String? { if mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS { - return "no error" + return nil } if let errorMessage = mpd_connection_get_error_message(connection) { return String(cString: errorMessage) } - return "no error" + return nil } } diff --git a/Persephone/MPDClient/Protocols/Delegate.swift b/Persephone/MPDClient/Protocols/Delegate.swift index 5db155e..c98edf0 100644 --- a/Persephone/MPDClient/Protocols/Delegate.swift +++ b/Persephone/MPDClient/Protocols/Delegate.swift @@ -9,6 +9,9 @@ import Foundation protocol MPDClientDelegate { + func didConnect(mpdClient: MPDClient) + func willDisconnect(mpdClient: MPDClient) + func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State) func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song]) func didUpdateQueuePos(mpdClient: MPDClient, song: Int) diff --git a/Persephone/Models/Preferences.swift b/Persephone/Models/Preferences.swift new file mode 100644 index 0000000..acb630f --- /dev/null +++ b/Persephone/Models/Preferences.swift @@ -0,0 +1,47 @@ +// +// Preferences.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/15. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation + +struct Preferences { + let preferences = UserDefaults.standard + + var mpdHost: String? { + get { + return preferences.string(forKey: "mpdHost") + } + set { + preferences.set(newValue, forKey: "mpdHost") + } + } + + var mpdPort: Int? { + get { + return preferences.value(forKey: "mpdPort") as? Int + } + set { + if (newValue.map { $0 > 0 } ?? false) { + preferences.set(newValue, forKey: "mpdPort") + } else { + preferences.removeObject(forKey: "mpdPort") + } + } + } + + var mpdHostOrDefault: String { + return mpdHost ?? "127.0.0.1" + } + + var mpdPortOrDefault: Int { + return mpdPort ?? 6600 + } + + func addObserver(_ observer: NSObject, forKeyPath keyPath: String) { + preferences.addObserver(observer, forKeyPath: keyPath, options: .new, context: nil) + } +} diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Resources/Base.lproj/Main.storyboard index dd49a48..2db03f7 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Resources/Base.lproj/Main.storyboard @@ -24,7 +24,12 @@ - + + + + + + @@ -680,7 +685,7 @@ - + @@ -703,10 +708,10 @@ - - - - + + + + @@ -758,6 +763,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +