diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 884ae8f..cf6d187 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -39,6 +39,9 @@ E435E3E2221CD4E200184CFC /* NSFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = E435E3E1221CD4E200184CFC /* NSFont.swift */; }; E435E3E4221CD75D00184CFC /* NSImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E435E3E3221CD75D00184CFC /* NSImage.swift */; }; E439109822640213002982E9 /* SongNotifierService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E439109722640213002982E9 /* SongNotifierService.swift */; }; + E43B67AA22909793007DCF55 /* AlbumDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43B67A822909793007DCF55 /* AlbumDetailView.swift */; }; + E43B67AB22909793007DCF55 /* AlbumDetailView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E43B67A922909793007DCF55 /* AlbumDetailView.xib */; }; + E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */; }; E4405192227644340090CD6F /* MPDServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4405191227644340090CD6F /* MPDServerController.swift */; }; E44051942278765A0090CD6F /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44051932278765A0090CD6F /* App.swift */; }; E4405196227879960090CD6F /* MPDActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4405195227879960090CD6F /* MPDActions.swift */; }; @@ -245,6 +248,9 @@ E435E3E1221CD4E200184CFC /* NSFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFont.swift; sourceTree = ""; }; E435E3E3221CD75D00184CFC /* NSImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImage.swift; sourceTree = ""; }; E439109722640213002982E9 /* SongNotifierService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongNotifierService.swift; sourceTree = ""; }; + E43B67A822909793007DCF55 /* AlbumDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailView.swift; sourceTree = ""; }; + E43B67A922909793007DCF55 /* AlbumDetailView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumDetailView.xib; sourceTree = ""; }; + E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTracksDataSource.swift; sourceTree = ""; }; E4405191227644340090CD6F /* MPDServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDServerController.swift; sourceTree = ""; }; E44051932278765A0090CD6F /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; E4405195227879960090CD6F /* MPDActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDActions.swift; sourceTree = ""; }; @@ -670,6 +676,7 @@ E4D1B597220BA3A20026F233 /* Controllers */ = { isa = PBXGroup; children = ( + E43B67A822909793007DCF55 /* AlbumDetailView.swift */, E408D3C1220E134F0006D9BE /* AlbumViewController.swift */, E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */, E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */, @@ -685,6 +692,7 @@ isa = PBXGroup; children = ( E40786212110CE70006887B1 /* Main.storyboard */, + E43B67A922909793007DCF55 /* AlbumDetailView.xib */, E408D3C9220E341D0006D9BE /* AlbumViewItem.xib */, ); path = Resources; @@ -695,6 +703,7 @@ children = ( E4F6B466221E233200ACF42A /* AlbumDataSource.swift */, E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */, + E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */, ); path = DataSources; sourceTree = ""; @@ -835,6 +844,7 @@ E450AD9122262C780091BED3 /* SwiftyJSON.framework.dSYM in Resources */, E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */, E45E4FDA22515D87004B537F /* CHANGELOG.md in Resources */, + E43B67AB22909793007DCF55 /* AlbumDetailView.xib in Resources */, E45E4FDC22515D87004B537F /* Cartfile in Resources */, E450AD98222633920091BED3 /* Alamofire.framework.dSYM in Resources */, E40786202110CE70006887B1 /* Assets.xcassets in Resources */, @@ -915,6 +925,7 @@ E4B11BC02275EE150075461B /* QueueActions.swift in Sources */, E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */, E450AD9522262DF10091BED3 /* CoverArtQueue.swift in Sources */, + E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */, E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */, E450AD7E222620A10091BED3 /* Album.swift in Sources */, E408D3B6220DD8970006D9BE /* Notification.swift in Sources */, @@ -922,6 +933,7 @@ E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */, E4EB2379220F10B8008C70C0 /* MPDPair.swift in Sources */, E440519E227BB0720090CD6F /* UIReducer.swift in Sources */, + E43B67AA22909793007DCF55 /* AlbumDetailView.swift in Sources */, E4FF7190227601B400D4C412 /* PreferencesReducer.swift in Sources */, E4F6B463221E125900ACF42A /* QueueItem.swift in Sources */, E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */, diff --git a/Persephone/Controllers/AlbumDetailView.swift b/Persephone/Controllers/AlbumDetailView.swift new file mode 100644 index 0000000..e174c43 --- /dev/null +++ b/Persephone/Controllers/AlbumDetailView.swift @@ -0,0 +1,109 @@ +// +// AlbumDetailView.swift +// Persephone +// +// Created by Daniel Barber on 2019/5/18. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class AlbumDetailView: NSViewController { + var album: Album? + var dataSource = AlbumTracksDataSource() + + @IBOutlet var albumTracksView: NSTableView! + @IBOutlet var albumTitle: NSTextField! + @IBOutlet var albumArtist: NSTextField! + @IBOutlet var albumCoverView: NSImageView! + + override func viewDidLoad() { + super.viewDidLoad() + + albumTracksView.dataSource = dataSource + albumTracksView.delegate = self + + albumCoverView.wantsLayer = true + albumCoverView.layer?.cornerRadius = 5 + albumCoverView.layer?.borderWidth = 1 + setAppearance() + + guard let album = album else { return } + + getAlbumSongs(for: album) + + albumTitle.stringValue = album.title + albumArtist.stringValue = album.artist + + switch album.coverArt { + case .loaded(let coverArt): + albumCoverView.image = coverArt ?? .defaultCoverArt + default: + albumCoverView.image = .defaultCoverArt + } + } + + func getAlbumSongs(for album: Album) { + App.mpdClient.getAlbumSongs(for: album.mpdAlbum) { (mpdSongs: [MPDClient.MPDSong]) in + self.dataSource.albumTracks = mpdSongs.map { + return Song(mpdSong: $0) + } + + DispatchQueue.main.async { + self.albumTracksView.reloadData() + } + } + } + + func setAppearance() { + if #available(OSX 10.14, *) { + let darkMode = NSApp.effectiveAppearance.bestMatch(from: + [.darkAqua, .aqua]) == .darkAqua + + albumCoverView.layer?.borderColor = darkMode ? .albumBorderColorDark : .albumBorderColorLight + } else { + albumCoverView.layer?.borderColor = .albumBorderColorLight + } + } + + func setAlbum(_ album: Album) { + self.album = album + } +} + +extension AlbumDetailView: NSTableViewDelegate { + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let song = dataSource.albumTracks[row] + + switch tableColumn?.identifier.rawValue { + case "trackNumberColumn": + return cellForTrackNumber(tableView, with: song) + case "trackTitleColumn": + return cellForSongTitle(tableView, with: song) + default: + return nil + } + } + + func cellForTrackNumber(_ tableView: NSTableView, with song: Song) -> NSView { + let cellView = tableView.makeView( + withIdentifier: .trackNumber, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = "\(song.trackNumber)." + + return cellView + } + + func cellForSongTitle(_ tableView: NSTableView, with song: Song) -> NSView { + let cellView = tableView.makeView( + withIdentifier: .songTitle, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = song.title + + return cellView + } +} diff --git a/Persephone/Controllers/AlbumViewItem.swift b/Persephone/Controllers/AlbumViewItem.swift index 5728c38..bb3af6e 100644 --- a/Persephone/Controllers/AlbumViewItem.swift +++ b/Persephone/Controllers/AlbumViewItem.swift @@ -51,13 +51,29 @@ class AlbumViewItem: NSCollectionViewItem { } } - @IBAction func playAlbum(_ sender: Any) { + @IBAction func playAlbum(_ sender: NSButton) { guard let album = album else { return } App.store.dispatch(MPDPlayAlbum(album: album.mpdAlbum)) } - @IBOutlet var albumCoverView: NSImageView! + @IBAction func showAlbumDetail(_ sender: NSButton) { + guard let album = album else { return } + let detailViewController = AlbumDetailView() + + detailViewController.setAlbum(album) + + let popoverView = NSPopover() + popoverView.contentViewController = detailViewController + popoverView.behavior = .transient + popoverView.show( + relativeTo: sender.bounds, + of: sender, + preferredEdge: .maxY + ) + } + + @IBOutlet var albumCoverView: NSButton! @IBOutlet var albumTitle: NSTextField! @IBOutlet var albumArtist: NSTextField! } diff --git a/Persephone/DataSources/AlbumTracksDataSource.swift b/Persephone/DataSources/AlbumTracksDataSource.swift new file mode 100644 index 0000000..678dfe6 --- /dev/null +++ b/Persephone/DataSources/AlbumTracksDataSource.swift @@ -0,0 +1,17 @@ +// +// AlbumTracksDataSource.swift +// Persephone +// +// Created by Daniel Barber on 2019/5/19. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import AppKit + +class AlbumTracksDataSource: NSObject, NSTableViewDataSource { + var albumTracks: [Song] = [] + + func numberOfRows(in tableView: NSTableView) -> Int { + return albumTracks.count + } +} diff --git a/Persephone/Extensions/NSUserInterfaceItemIdentifier.swift b/Persephone/Extensions/NSUserInterfaceItemIdentifier.swift index 44a2bdf..47251d1 100644 --- a/Persephone/Extensions/NSUserInterfaceItemIdentifier.swift +++ b/Persephone/Extensions/NSUserInterfaceItemIdentifier.swift @@ -17,4 +17,8 @@ extension NSUserInterfaceItemIdentifier { static let queueSongTitle = NSUserInterfaceItemIdentifier("songTitleCell") static let albumViewItem = NSUserInterfaceItemIdentifier("AlbumViewItem") + + static let trackNumber = NSUserInterfaceItemIdentifier("trackNumberCell") + static let songTitle = NSUserInterfaceItemIdentifier("songTitleCell") + static let songDuration = NSUserInterfaceItemIdentifier("durationCell") } diff --git a/Persephone/MPDClient/Extensions/MPDClient+Album.swift b/Persephone/MPDClient/Extensions/MPDClient+Album.swift index ff27a09..b5ca72c 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Album.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Album.swift @@ -26,21 +26,27 @@ extension MPDClient { ) } - func sendPlayAlbum(_ album: MPDAlbum) { - var songs: [MPDSong] = [] + func getAlbumSongs(for album: MPDAlbum, callback: @escaping ([MPDSong]) -> Void) { + enqueueCommand( + command: .getAlbumSongs, + priority: .normal, + userData: ["album": album, "callback": callback] + ) + } - mpd_run_clear(self.connection) - mpd_search_db_songs(self.connection, true) - mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title) - mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist) - mpd_search_commit(self.connection) - while let song = mpd_recv_song(self.connection) { - songs.append(MPDSong(song)) + func sendPlayAlbum(_ album: MPDAlbum) { + getAlbumSongs(for: album) { songs in + self.enqueueCommand( + command: .replaceQueue, + priority: .normal, + userData: ["songs": songs] + ) + self.enqueueCommand( + command: .playTrack, + priority: .normal, + userData: ["queuePos": 0] + ) } - for song in songs { - mpd_run_add(self.connection, song.uri) - } - mpd_run_play_pos(self.connection, 0) } func allAlbums() { @@ -91,4 +97,21 @@ extension MPDClient { callback(firstSong) } + + func albumSongs(for album: MPDAlbum, callback: ([MPDSong]) -> Void) { + guard isConnected else { return } + + var songs: [MPDSong] = [] + + mpd_search_db_songs(self.connection, true) + mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title) + mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist) + mpd_search_commit(self.connection) + + while let song = mpd_recv_song(self.connection) { + songs.append(MPDSong(song)) + } + + callback(songs) + } } diff --git a/Persephone/MPDClient/Extensions/MPDClient+Command.swift b/Persephone/MPDClient/Extensions/MPDClient+Command.swift index f5b0cca..7163b59 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Command.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Command.swift @@ -54,6 +54,10 @@ extension MPDClient { guard let queuePos = userData["queuePos"] as? Int else { return } sendPlayTrack(at: queuePos) + case .replaceQueue: + guard let songs = userData["songs"] as? [MPDSong] + else { return } + sendReplaceQueue(songs) // Album commands case .fetchAllAlbums: @@ -67,6 +71,13 @@ extension MPDClient { else { return } albumFirstSong(for: album, callback: callback) + + case .getAlbumSongs: + guard let album = userData["album"] as? MPDAlbum, + let callback = userData["callback"] as? ([MPDSong]) -> Void + else { return } + + albumSongs(for: album, callback: callback) } } diff --git a/Persephone/MPDClient/Extensions/MPDClient+Queue.swift b/Persephone/MPDClient/Extensions/MPDClient+Queue.swift index 162586b..e8b7829 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Queue.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Queue.swift @@ -31,4 +31,13 @@ extension MPDClient { self.queue.append(song) } } + + func sendReplaceQueue(_ songs: [MPDSong]) { + mpd_run_clear(self.connection) + + for song in songs { + mpd_run_add(self.connection, song.uri) + } + mpd_run_play_pos(self.connection, 0) + } } diff --git a/Persephone/MPDClient/Models/MPDCommand.swift b/Persephone/MPDClient/Models/MPDCommand.swift index b938056..0bf5330 100644 --- a/Persephone/MPDClient/Models/MPDCommand.swift +++ b/Persephone/MPDClient/Models/MPDCommand.swift @@ -29,10 +29,12 @@ extension MPDClient { // Queue commands case fetchQueue case playTrack + case replaceQueue // Album commands case fetchAllAlbums case playAlbum case getAlbumFirstSong + case getAlbumSongs } } diff --git a/Persephone/Models/Song.swift b/Persephone/Models/Song.swift index 867c027..99db96e 100644 --- a/Persephone/Models/Song.swift +++ b/Persephone/Models/Song.swift @@ -11,6 +11,10 @@ import Foundation struct Song { var mpdSong: MPDClient.MPDSong + var trackNumber: String { + return mpdSong.getTag(.track) + } + var title: String { return mpdSong.getTag(.title) } diff --git a/Persephone/Resources/AlbumDetailView.xib b/Persephone/Resources/AlbumDetailView.xib new file mode 100644 index 0000000..edf6d4e --- /dev/null +++ b/Persephone/Resources/AlbumDetailView.xib @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Persephone/Resources/AlbumViewItem.xib b/Persephone/Resources/AlbumViewItem.xib index 32ca98f..c802cf7 100644 --- a/Persephone/Resources/AlbumViewItem.xib +++ b/Persephone/Resources/AlbumViewItem.xib @@ -37,10 +37,20 @@ - + + + + - + + +