diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 0f2dfbc..0e6aec4 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ E408D3B6220DD8970006D9BE /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B5220DD8970006D9BE /* Notification.swift */; }; E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */; }; E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */; }; + E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C1220E134F0006D9BE /* AlbumViewController.swift */; }; + E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C8220E341D0006D9BE /* AlbumItem.swift */; }; + E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = E408D3C9220E341D0006D9BE /* AlbumItem.xib */; }; 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 */; }; @@ -24,6 +27,8 @@ E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; }; E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC932206097F0024217A /* NotificationsController.swift */; }; E4E8CC9A22075D370024217A /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC9922075D370024217A /* Song.swift */; }; + E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EB2378220F10B8008C70C0 /* Pair.swift */; }; + E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EB237A220F7CF1008C70C0 /* Album.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -73,6 +78,9 @@ E408D3B5220DD8970006D9BE /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSUserInterfaceItemIdentifier.swift; sourceTree = ""; }; E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawRepresentable.swift; sourceTree = ""; }; + E408D3C1220E134F0006D9BE /* AlbumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewController.swift; sourceTree = ""; }; + E408D3C8220E341D0006D9BE /* AlbumItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = ""; }; + E408D3C9220E341D0006D9BE /* AlbumItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumItem.xib; sourceTree = ""; }; E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libmpdclient.2.dylib; path = libmpdclient/output/libmpdclient.2.dylib; sourceTree = ""; }; E41B22C421FB715A00D544F6 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; E41B22C521FB932700D544F6 /* MPDClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDClient.swift; sourceTree = ""; }; @@ -117,6 +125,8 @@ E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = ""; }; E4E8CC932206097F0024217A /* NotificationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsController.swift; sourceTree = ""; }; E4E8CC9922075D370024217A /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = ""; }; + E4EB2378220F10B8008C70C0 /* Pair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pair.swift; sourceTree = ""; }; + E4EB237A220F7CF1008C70C0 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -172,6 +182,7 @@ E408D3B7220DE8CC0006D9BE /* Extensions */, E4D1B598220BA3C90026F233 /* Resources */, E4D1B597220BA3A20026F233 /* Controllers */, + E408D3C3220E138B0006D9BE /* Views */, E41B22C721FB966C00D544F6 /* include */, E407861B2110CE6E006887B1 /* AppDelegate.swift */, E40786242110CE70006887B1 /* Info.plist */, @@ -216,6 +227,16 @@ path = Extensions; sourceTree = ""; }; + E408D3C3220E138B0006D9BE /* Views */ = { + isa = PBXGroup; + children = ( + E40786212110CE70006887B1 /* Main.storyboard */, + E408D3C8220E341D0006D9BE /* AlbumItem.swift */, + E408D3C9220E341D0006D9BE /* AlbumItem.xib */, + ); + path = Views; + sourceTree = ""; + }; E41B22BE21FB6B3300D544F6 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -291,6 +312,8 @@ children = ( E4E8CC9922075D370024217A /* Song.swift */, E4A642D922090CBE00067D21 /* Status.swift */, + E4EB2378220F10B8008C70C0 /* Pair.swift */, + E4EB237A220F7CF1008C70C0 /* Album.swift */, ); path = Models; sourceTree = ""; @@ -306,6 +329,7 @@ E4D1B597220BA3A20026F233 /* Controllers */ = { isa = PBXGroup; children = ( + E408D3C1220E134F0006D9BE /* AlbumViewController.swift */, E4E8CC932206097F0024217A /* NotificationsController.swift */, E4E8CC912204F4B80024217A /* QueueViewController.swift */, E465049921E94DF500A70F4C /* WindowController.swift */, @@ -317,7 +341,6 @@ isa = PBXGroup; children = ( E407861F2110CE70006887B1 /* Assets.xcassets */, - E40786212110CE70006887B1 /* Main.storyboard */, ); path = Resources; sourceTree = ""; @@ -433,6 +456,7 @@ buildActionMask = 2147483647; files = ( E40786202110CE70006887B1 /* Assets.xcassets in Resources */, + E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */, E40786232110CE70006887B1 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -458,17 +482,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */, E4A642DA22090CBE00067D21 /* Status.swift in Sources */, E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */, E408D3B6220DD8970006D9BE /* Notification.swift in Sources */, E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */, + E4EB2379220F10B8008C70C0 /* Pair.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 */, E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */, + E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Persephone/Controllers/AlbumViewController.swift b/Persephone/Controllers/AlbumViewController.swift new file mode 100644 index 0000000..8e67518 --- /dev/null +++ b/Persephone/Controllers/AlbumViewController.swift @@ -0,0 +1,56 @@ +// +// AlbumViewController.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/08. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class AlbumViewController: NSViewController, + NSCollectionViewDataSource, + NSCollectionViewDelegate { + var albums: [MPDClient.Album] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + NotificationCenter.default.addObserver( + self, + selector: #selector(updateAlbums(_:)), + name: Notification.loadedAlbums, + object: AppDelegate.mpdClient + ) + } + + @objc func updateAlbums(_ notification: Notification) { + guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album] + else { return } + + print("Loaded albums") + self.albums = albums + + albumCollectionView.reloadData() + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return albums.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let item = collectionView.makeItem( + withIdentifier: NSUserInterfaceItemIdentifier("AlbumItem"), + for: indexPath + ) + guard let albumItem = item as? AlbumItem else { return item } + + albumItem.view.wantsLayer = true + albumItem.setAlbumTitle(albums[indexPath.item].title) + albumItem.setAlbumArtist(albums[indexPath.item].artist) + + return albumItem + } + + @IBOutlet var albumCollectionView: NSCollectionView! +} diff --git a/Persephone/Controllers/NotificationsController.swift b/Persephone/Controllers/NotificationsController.swift index 1087b0a..74a9d20 100644 --- a/Persephone/Controllers/NotificationsController.swift +++ b/Persephone/Controllers/NotificationsController.swift @@ -32,6 +32,13 @@ class NotificationsController: MPDClientDelegate { ) } + func didLoadAlbums(mpdClient: MPDClient, albums: [MPDClient.Album]) { + sendNotification( + name: Notification.loadedAlbums, + userInfo: [Notification.albumsKey: albums] + ) + } + private func sendNotification(name: Notification.Name, userInfo: [AnyHashable : Any]) { self.notificationQueue.async { NotificationCenter.default.post( diff --git a/Persephone/Extensions/Notification.swift b/Persephone/Extensions/Notification.swift index ad8e275..903f39a 100644 --- a/Persephone/Extensions/Notification.swift +++ b/Persephone/Extensions/Notification.swift @@ -12,8 +12,10 @@ extension Notification { static let stateChanged = Notification.Name("MPDClientStateChanged") static let queueChanged = Notification.Name("MPDClientQueueChanged") static let queuePosChanged = Notification.Name("MPDClientQueuePosChanged") + static let loadedAlbums = Notification.Name("MPDClientLoadedAlbums") static let stateKey = "state" static let queueKey = "queue" static let queuePosKey = "song" + static let albumsKey = "albums" } diff --git a/Persephone/MPDClient/MPDClient.swift b/Persephone/MPDClient/MPDClient.swift index 19d1c39..5a434bf 100644 --- a/Persephone/MPDClient/MPDClient.swift +++ b/Persephone/MPDClient/MPDClient.swift @@ -22,7 +22,8 @@ class MPDClient { private let commandQueue = DispatchQueue(label: "commandQueue") enum Command { - case prevTrack, nextTrack, playPause, stop, fetchStatus, fetchQueue + case prevTrack, nextTrack, playPause, stop, fetchStatus, fetchQueue, + fetchAllAlbums } struct Idle: OptionSet { @@ -57,6 +58,8 @@ class MPDClient { fetchQueue() + fetchAllAlbums() + 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) @@ -78,6 +81,10 @@ class MPDClient { sendCommand(command: .fetchQueue) } + func fetchAllAlbums() { + sendCommand(command: .fetchAllAlbums) + } + func playPause() { queueCommand(command: .playPause) } @@ -103,6 +110,7 @@ class MPDClient { } func sendCommand(command: Command) { + print("Command:", command) switch command { // Transport commands @@ -114,19 +122,12 @@ class MPDClient { sendStop() case .playPause: sendPlay() - case .fetchStatus: - guard let status = mpd_run_status(connection) else { break } - self.status = Status(status) - + sendRunStatus() case .fetchQueue: - self.queue = [] - mpd_send_list_queue_meta(connection) - - while let mpdSong = mpd_recv_song(connection) { - let song = Song(mpdSong) - self.queue.append(song) - } + sendFetchQueue() + case .fetchAllAlbums: + allAlbums() } } @@ -158,6 +159,84 @@ class MPDClient { } } + func sendRunStatus() { + guard let status = mpd_run_status(connection) else { return } + self.status = Status(status) + } + + func sendFetchQueue() { + self.queue = [] + mpd_send_list_queue_meta(connection) + + while let mpdSong = mpd_recv_song(connection) { + let song = Song(mpdSong) + self.queue.append(song) + } + } + + func allAlbums() { + var albums: [Album] = [] + var artist: String = "" + + mpd_search_db_tags(self.connection, MPD_TAG_ALBUM) + mpd_search_add_group_tag(self.connection, MPD_TAG_ALBUM_ARTIST) + mpd_search_commit(self.connection) + while let mpdPair = mpd_recv_pair(self.connection) { + let name = String(cString: mpdPair.pointee.name) + let value = String(cString: mpdPair.pointee.value) + + switch name { + case "AlbumArtist": + artist = value + case "Album": + albums.append(Album(title: value, artist: artist)) + default: + break + } + + mpd_return_pair(self.connection, mpdPair) + } + + delegate?.didLoadAlbums(mpdClient: self, albums: albums) + } + + func albumsForArtist(_ artist: String) -> [Album] { + var albums: [Album] = [] + + mpd_search_db_tags(self.connection, MPD_TAG_ALBUM) + mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ARTIST, artist) + mpd_search_commit(self.connection) + while let mpdAlbumPair = mpd_recv_pair_tag(self.connection, MPD_TAG_ALBUM) { + print(artist, "-", String(cString: mpdAlbumPair.pointee.value)) + albums.append(Album(title: String(cString: mpdAlbumPair.pointee.value), artist: artist)) + mpd_return_pair(self.connection, mpdAlbumPair) + } + + return albums + } + + func allArtists() -> [String] { + var artists: [String] = [] + + mpd_search_db_tags(self.connection, MPD_TAG_ARTIST) + mpd_search_commit(self.connection) + while let mpdArtistPair = mpd_recv_pair_tag(self.connection, MPD_TAG_ARTIST) { + artists.append(String(cString: mpdArtistPair.pointee.value)) + mpd_return_pair(self.connection, mpdArtistPair) + } + + return artists + } + + func sendSearchDbTags(_ tagType: mpd_tag_type) { + mpd_search_db_tags(self.connection, tagType) + mpd_search_commit(self.connection) + while let mpdPair = mpd_recv_pair_tag(self.connection, tagType) { + print(String(cString: mpdPair.pointee.value)) + mpd_return_pair(self.connection, mpdPair) + } + } + func noIdle() { mpd_send_noidle(connection) } @@ -184,6 +263,7 @@ class MPDClient { self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song) } if !mpdIdle.isEmpty { + print("Status") self.idle() } } diff --git a/Persephone/MPDClient/Models/Album.swift b/Persephone/MPDClient/Models/Album.swift new file mode 100644 index 0000000..2cb6f6c --- /dev/null +++ b/Persephone/MPDClient/Models/Album.swift @@ -0,0 +1,21 @@ +// +// Album.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/09. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation + +extension MPDClient { + class Album { + let title: String + let artist: String + + init(title: String, artist: String) { + self.title = title + self.artist = artist + } + } +} diff --git a/Persephone/MPDClient/Models/Pair.swift b/Persephone/MPDClient/Models/Pair.swift new file mode 100644 index 0000000..012ba1c --- /dev/null +++ b/Persephone/MPDClient/Models/Pair.swift @@ -0,0 +1,18 @@ +// +// Pair.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/09. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation +import mpdclient + +class Pair { + let mpdPair: UnsafeMutablePointer + + init(_ mpdPair: UnsafeMutablePointer) { + self.mpdPair = mpdPair + } +} diff --git a/Persephone/MPDClient/Protocols/Delegate.swift b/Persephone/MPDClient/Protocols/Delegate.swift index 1871f0e..5db155e 100644 --- a/Persephone/MPDClient/Protocols/Delegate.swift +++ b/Persephone/MPDClient/Protocols/Delegate.swift @@ -12,4 +12,5 @@ protocol MPDClientDelegate { func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State) 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/Assets.xcassets/blankAlbum.imageset/Contents.json b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/Contents.json new file mode 100644 index 0000000..f6c870a --- /dev/null +++ b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "blankAlbum.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/blankAlbum.pdf b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/blankAlbum.pdf new file mode 100644 index 0000000..4c51e9d Binary files /dev/null and b/Persephone/Resources/Assets.xcassets/blankAlbum.imageset/blankAlbum.pdf differ diff --git a/Persephone/Views/AlbumItem.swift b/Persephone/Views/AlbumItem.swift new file mode 100644 index 0000000..9b61d61 --- /dev/null +++ b/Persephone/Views/AlbumItem.swift @@ -0,0 +1,27 @@ +// +// AlbumItem.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/08. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class AlbumItem: NSCollectionViewItem { + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + } + + func setAlbumTitle(_ title: String) { + albumTitle.stringValue = title + } + + func setAlbumArtist(_ artist: String) { + albumArtist.stringValue = artist + } + + @IBOutlet var albumTitle: NSTextField! + @IBOutlet var albumArtist: NSTextField! +} diff --git a/Persephone/Views/AlbumItem.xib b/Persephone/Views/AlbumItem.xib new file mode 100644 index 0000000..fc10c0a --- /dev/null +++ b/Persephone/Views/AlbumItem.xib @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Views/Base.lproj/Main.storyboard similarity index 98% rename from Persephone/Resources/Base.lproj/Main.storyboard rename to Persephone/Views/Base.lproj/Main.storyboard index 634fbc1..2d1dc43 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Views/Base.lproj/Main.storyboard @@ -908,47 +908,55 @@ - + - + - + - + - + - - + + + + + + + - - - - - + + + + + + + diff --git a/Resources/export/blankAlbum.pdf b/Resources/export/blankAlbum.pdf new file mode 100644 index 0000000..4c51e9d Binary files /dev/null and b/Resources/export/blankAlbum.pdf differ diff --git a/Resources/icons.sketch b/Resources/icons.sketch index 1975b3e..6580d22 100644 Binary files a/Resources/icons.sketch and b/Resources/icons.sketch differ