diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index c050135..122f776 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -16,20 +16,21 @@ 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 */; }; + E40F41F3221EDE27004B6CB8 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40F41F2221EDE27004B6CB8 /* Preferences.swift */; }; E40FE719221B48E300A4223F /* AlbumViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40FE718221B48E300A4223F /* AlbumViewLayout.swift */; }; E40FE71B221B904300A4223F /* NSEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40FE71A221B904300A4223F /* NSEvent.swift */; }; 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 */; }; E47E2FD122205C4600F747E6 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */; }; E47E2FD322205D2500F747E6 /* MainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD222205D2500F747E6 /* MainWindow.swift */; }; + E47E2FD5222071FD00F747E6 /* AlbumItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD4222071FD00F747E6 /* AlbumItem.swift */; }; + E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD62220720300F747E6 /* AlbumItemView.swift */; }; E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4928E0A2218D62A001D4BEA /* CGColor.swift */; }; E4A642DA22090CBE00067D21 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* Status.swift */; }; E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; }; @@ -38,6 +39,9 @@ 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 */; }; + E4F6B460221E119B00ACF42A /* QueueDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */; }; + E4F6B463221E125900ACF42A /* SongItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F6B462221E125900ACF42A /* SongItem.swift */; }; + E4F6B467221E233200ACF42A /* AlbumDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F6B466221E233200ACF42A /* AlbumDataSource.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -88,8 +92,8 @@ 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 = ""; }; + E40F41F2221EDE27004B6CB8 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; E40FE718221B48E300A4223F /* AlbumViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewLayout.swift; sourceTree = ""; }; E40FE71A221B904300A4223F /* NSEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEvent.swift; sourceTree = ""; }; E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libmpdclient.2.dylib; path = libmpdclient/output/libmpdclient.2.dylib; sourceTree = ""; }; @@ -131,12 +135,13 @@ 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 = ""; }; E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = ""; }; E47E2FD222205D2500F747E6 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = ""; }; + E47E2FD4222071FD00F747E6 /* AlbumItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = ""; }; + E47E2FD62220720300F747E6 /* AlbumItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumItemView.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 = ""; }; E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; @@ -145,6 +150,9 @@ 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 = ""; }; + E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueDataSource.swift; sourceTree = ""; }; + E4F6B462221E125900ACF42A /* SongItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongItem.swift; sourceTree = ""; }; + E4F6B466221E233200ACF42A /* AlbumDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDataSource.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -200,7 +208,8 @@ isa = PBXGroup; children = ( E40FE717221B48CE00A4223F /* Layouts */, - E41EA46D221715820068EF46 /* Models */, + E4F6B461221E124700ACF42A /* Models */, + E4F6B45E221E117600ACF42A /* DataSources */, E407861F2110CE70006887B1 /* Assets.xcassets */, E408D3B7220DE8CC0006D9BE /* Extensions */, E4D1B598220BA3C90026F233 /* Resources */, @@ -255,7 +264,7 @@ E408D3C3220E138B0006D9BE /* Views */ = { isa = PBXGroup; children = ( - E408D3C8220E341D0006D9BE /* AlbumItem.swift */, + E47E2FD62220720300F747E6 /* AlbumItemView.swift */, E47E2FD222205D2500F747E6 /* MainWindow.swift */, ); path = Views; @@ -328,14 +337,6 @@ path = mpd; sourceTree = ""; }; - E41EA46D221715820068EF46 /* Models */ = { - isa = PBXGroup; - children = ( - E41EA46E221715910068EF46 /* Preferences.swift */, - ); - path = Models; - sourceTree = ""; - }; E4A642DB220912FA00067D21 /* MPDClient */ = { isa = PBXGroup; children = ( @@ -369,6 +370,7 @@ E4D1B597220BA3A20026F233 /* Controllers */ = { isa = PBXGroup; children = ( + E47E2FD4222071FD00F747E6 /* AlbumItem.swift */, E408D3C1220E134F0006D9BE /* AlbumViewController.swift */, E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */, E4E8CC932206097F0024217A /* NotificationsController.swift */, @@ -388,6 +390,24 @@ path = Resources; sourceTree = ""; }; + E4F6B45E221E117600ACF42A /* DataSources */ = { + isa = PBXGroup; + children = ( + E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */, + E4F6B466221E233200ACF42A /* AlbumDataSource.swift */, + ); + path = DataSources; + sourceTree = ""; + }; + E4F6B461221E124700ACF42A /* Models */ = { + isa = PBXGroup; + children = ( + E40F41F2221EDE27004B6CB8 /* Preferences.swift */, + E4F6B462221E125900ACF42A /* SongItem.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -530,25 +550,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E4F6B467221E233200ACF42A /* AlbumDataSource.swift in Sources */, E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */, E40FE71B221B904300A4223F /* NSEvent.swift in Sources */, E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */, + E4F6B460221E119B00ACF42A /* QueueDataSource.swift in Sources */, E4A642DA22090CBE00067D21 /* Status.swift in Sources */, + E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */, E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */, E40FE719221B48E300A4223F /* AlbumViewLayout.swift in Sources */, E408D3B6220DD8970006D9BE /* Notification.swift in Sources */, E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */, E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */, - E41EA46F221715910068EF46 /* Preferences.swift in Sources */, + E4F6B463221E125900ACF42A /* SongItem.swift in Sources */, E465049A21E94DF500A70F4C /* WindowController.swift in Sources */, E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */, + E40F41F3221EDE27004B6CB8 /* Preferences.swift in Sources */, E407861C2110CE6E006887B1 /* AppDelegate.swift in Sources */, E47E2FD322205D2500F747E6 /* MainWindow.swift in Sources */, E47E2FD122205C4600F747E6 /* MainSplitViewController.swift in Sources */, E4E8CC9A22075D370024217A /* Song.swift in Sources */, - E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */, E41EA46C221636AF0068EF46 /* PreferencesViewController.swift in Sources */, E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */, + E47E2FD5222071FD00F747E6 /* AlbumItem.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */, E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */, diff --git a/Persephone/Assets.xcassets/playButtonLarge.imageset/Contents.json b/Persephone/Assets.xcassets/playButtonLarge.imageset/Contents.json new file mode 100644 index 0000000..15f9f85 --- /dev/null +++ b/Persephone/Assets.xcassets/playButtonLarge.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "playButtonLarge.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "playButtonLarge@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Persephone/Assets.xcassets/playButtonLarge.imageset/playButtonLarge.png b/Persephone/Assets.xcassets/playButtonLarge.imageset/playButtonLarge.png new file mode 100644 index 0000000..a489b7b Binary files /dev/null and b/Persephone/Assets.xcassets/playButtonLarge.imageset/playButtonLarge.png differ diff --git a/Persephone/Assets.xcassets/playButtonLarge.imageset/playButtonLarge@2x.png b/Persephone/Assets.xcassets/playButtonLarge.imageset/playButtonLarge@2x.png new file mode 100644 index 0000000..109a9ca Binary files /dev/null and b/Persephone/Assets.xcassets/playButtonLarge.imageset/playButtonLarge@2x.png differ diff --git a/Persephone/Views/AlbumItem.swift b/Persephone/Controllers/AlbumItem.swift similarity index 86% rename from Persephone/Views/AlbumItem.swift rename to Persephone/Controllers/AlbumItem.swift index e9a3be4..0bec18a 100644 --- a/Persephone/Views/AlbumItem.swift +++ b/Persephone/Controllers/AlbumItem.swift @@ -10,6 +10,7 @@ import Cocoa class AlbumItem: NSCollectionViewItem { var observer: NSKeyValueObservation? + var album: MPDClient.Album? override func viewDidLoad() { super.viewDidLoad() @@ -27,6 +28,7 @@ class AlbumItem: NSCollectionViewItem { } func setAlbum(_ album: MPDClient.Album) { + self.album = album albumTitle.stringValue = album.title albumArtist.stringValue = album.artist } @@ -42,6 +44,12 @@ class AlbumItem: NSCollectionViewItem { } } + @IBAction func playAlbum(_ sender: Any) { + guard let album = album else { return } + + AppDelegate.mpdClient.playAlbum(album) + } + @IBOutlet var albumCoverView: NSImageView! @IBOutlet var albumTitle: NSTextField! @IBOutlet var albumArtist: NSTextField! diff --git a/Persephone/Controllers/AlbumViewController.swift b/Persephone/Controllers/AlbumViewController.swift index f58896c..42bec54 100644 --- a/Persephone/Controllers/AlbumViewController.swift +++ b/Persephone/Controllers/AlbumViewController.swift @@ -9,16 +9,18 @@ import Cocoa class AlbumViewController: NSViewController, - NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout { - var albums: [MPDClient.Album] = [] let paddingWidth: CGFloat = 40 let gutterWidth: CGFloat = 20 + var dataSource = AlbumDataSource() + override func viewDidLoad() { super.viewDidLoad() + albumScrollView.postsBoundsChangedNotifications = true + NotificationCenter.default.addObserver( self, selector: #selector(updateAlbums(_:)), @@ -32,6 +34,8 @@ class AlbumViewController: NSViewController, name: Notification.willDisconnect, object: AppDelegate.mpdClient ) + + albumCollectionView.dataSource = dataSource } override func viewWillLayout() { @@ -44,30 +48,16 @@ class AlbumViewController: NSViewController, guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album] else { return } - self.albums = albums - + dataSource.albums = albums albumCollectionView.reloadData() } @objc func clearAlbums(_ notification: Notification) { - self.albums = [] + dataSource.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: .albumItem, for: indexPath) - guard let albumItem = item as? AlbumItem else { return item } - - albumItem.view.wantsLayer = true - albumItem.setAlbum(albums[indexPath.item]) - - return albumItem - } - + @IBOutlet var albumScrollView: NSScrollView! @IBOutlet var albumCollectionView: NSCollectionView! } diff --git a/Persephone/Controllers/QueueViewController.swift b/Persephone/Controllers/QueueViewController.swift index 9919625..7023ff0 100644 --- a/Persephone/Controllers/QueueViewController.swift +++ b/Persephone/Controllers/QueueViewController.swift @@ -8,28 +8,140 @@ import Cocoa -class QueueViewController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate { - var queue: [MPDClient.Song] = [] - var queuePos: Int = -1 - - var queueIcon: NSImage? = nil +class QueueViewController: NSViewController, + NSOutlineViewDelegate { + var dataSource = QueueDataSource() let systemFontRegular = NSFont.systemFont(ofSize: 13, weight: .regular) let systemFontBold = NSFont.systemFont(ofSize: 13, weight: .bold) - + let playIcon = NSImage(named: "playButton") let pauseIcon = NSImage(named: "pauseButton") - struct SongItem { - var song: MPDClient.Song - var queuePos: Int - } + @IBOutlet var queueView: NSOutlineView! override func viewDidLoad() { super.viewDidLoad() - queueView.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle + setupNotificationObservers() + queueView.dataSource = dataSource + queueView.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle + } + + override func keyDown(with event: NSEvent) { + switch event.keyCode { + case NSEvent.keyCodeSpace: + nextResponder?.keyDown(with: event) + default: + super.keyDown(with: event) + } + } + + @IBAction func playTrack(_ sender: Any) { + if dataSource.queuePos >= 0 { + AppDelegate.mpdClient.playTrack(queuePos: dataSource.queuePos) + } + } + + @objc func stateChanged(_ notification: Notification) { + guard let state = notification.userInfo?[Notification.stateKey] as? MPDClient.Status.State + else { return } + + dataSource.setQueueIcon(state) + } + + @objc func queueChanged(_ notification: Notification) { + guard let queue = notification.userInfo?[Notification.queueKey] as? [MPDClient.Song] + else { return } + + dataSource.updateQueue(queue) + queueView.reloadData() + } + + @objc func queuePosChanged(_ notification: Notification) { + guard let queuePos = notification.userInfo?[Notification.queuePosKey] as? Int + else { return } + + dataSource.setQueuePos(queuePos) + + queueView.reloadData() + } + + func outlineView( + _ outlineView: NSOutlineView, + selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet + ) -> IndexSet { + if proposedSelectionIndexes.contains(0) { + return IndexSet() + } else { + return proposedSelectionIndexes + } + } + + func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { + if let songItem = item as? SongItem { + switch tableColumn?.identifier.rawValue { + case "songTitleColumn": + return cellForSongTitle(outlineView, with: songItem) + case "songArtistColumn": + return cellForSongArtist(outlineView, with: songItem) + default: + return nil + } + } else if tableColumn?.identifier.rawValue == "songTitleColumn" { + return cellForQueueHeading(outlineView) + } else { + return nil + } + } + + func cellForSongTitle(_ outlineView: NSOutlineView, with songItem: SongItem) -> NSView { + let cellView = outlineView.makeView( + withIdentifier: .queueSongTitle, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = songItem.song.getTag(.title) + if songItem.isPlaying { + cellView.textField?.font = systemFontBold + cellView.imageView?.image = dataSource.queueIcon + } else { + cellView.textField?.font = systemFontRegular + cellView.imageView?.image = nil + } + + return cellView + } + + func cellForSongArtist(_ outlineView: NSOutlineView, with songItem: SongItem) -> NSView { + let cellView = outlineView.makeView( + withIdentifier: .queueSongArtist, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = songItem.song.getTag(.artist) + if songItem.isPlaying { + cellView.textField?.font = systemFontBold + } else { + cellView.textField?.font = systemFontRegular + } + + return cellView + } + + func cellForQueueHeading(_ outlineView: NSOutlineView) -> NSView { + let cellView = outlineView.makeView( + withIdentifier: .queueHeading, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = "QUEUE" + + return cellView + } + + func setupNotificationObservers() { NotificationCenter.default.addObserver( self, selector: #selector(stateChanged(_:)), @@ -50,177 +162,5 @@ class QueueViewController: NSViewController, NSOutlineViewDataSource, NSOutlineV name: Notification.queuePosChanged, object: AppDelegate.mpdClient ) - - NotificationCenter.default.addObserver( - self, - selector: #selector(clearQueue(_:)), - name: Notification.willDisconnect, - object: AppDelegate.mpdClient - ) } - - override func keyDown(with event: NSEvent) { - switch event.keyCode { - case NSEvent.keyCodeSpace: - nextResponder?.keyDown(with: event) - default: - super.keyDown(with: event) - } - } - - @IBAction func playTrack(_ sender: Any) { - guard let view = sender as? NSOutlineView - else { return } - - let queuePos = view.selectedRow - 1 - - if queuePos >= 0 { - AppDelegate.mpdClient.playTrack(queuePos: queuePos) - } - } - - @objc func stateChanged(_ notification: Notification) { - guard let state = notification.userInfo?[Notification.stateKey] as? MPDClient.Status.State - else { return } - - setQueueIcon(state) - } - - @objc func queueChanged(_ notification: Notification) { - guard let queue = notification.userInfo?[Notification.queueKey] as? [MPDClient.Song] - else { return } - - self.queue = queue - - queueView.reloadData() - } - - @objc func queuePosChanged(_ notification: Notification) { - guard let queuePos = notification.userInfo?[Notification.queuePosKey] as? Int - else { return } - - let oldSongRowPos = self.queuePos + 1 - let newSongRowPos = queuePos + 1 - self.queuePos = queuePos - - setQueuePos(oldSongRowPos: oldSongRowPos, newSongRowPos: newSongRowPos) - - queueView.reloadData( - forRowIndexes: [oldSongRowPos, newSongRowPos], - columnIndexes: [0, 1] - ) - } - - @objc func clearQueue(_ notification: Notification) { - self.queue = [] - - queueView.reloadData() - } - - func setQueueIcon(_ state: MPDClient.Status.State) { - switch state { - case .playing: - self.queueIcon = playIcon - case .paused: - self.queueIcon = pauseIcon - default: - self.queueIcon = nil - } - } - - func setQueuePos(oldSongRowPos: Int, newSongRowPos: Int) { - if oldSongRowPos > 0 { - guard let oldSongRow = queueView.rowView(atRow: oldSongRowPos, makeIfNecessary: true), - let oldSongTitleCell = oldSongRow.view(atColumn: 0) as? NSTableCellView - else { return } - - setRowFont(rowView: oldSongRow, font: systemFontRegular) - oldSongTitleCell.imageView?.image = nil - } - - guard let songRow = queueView.rowView(atRow: newSongRowPos, makeIfNecessary: true), - let newSongTitleCell = songRow.view(atColumn: 0) as? NSTableCellView - else { return } - - setRowFont(rowView: songRow, font: systemFontBold) - newSongTitleCell.imageView?.image = self.queueIcon - } - - func setRowFont(rowView: NSTableRowView, font: NSFont) { - guard let songTitleCell = rowView.view(atColumn: 0) as? NSTableCellView, - let songArtistCell = rowView.view(atColumn: 1) as? NSTableCellView - else { return } - - songTitleCell.textField?.font = font - songArtistCell.textField?.font = font - } - - func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { - return queue.count + 1 - } - - func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { - return false - } - - func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { - if index > 0 { - return SongItem(song: queue[index - 1], queuePos: index - 1) - } else { - return false - } - } - - func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { - if let songItem = item as? SongItem { - switch tableColumn?.identifier.rawValue { - case "songTitleColumn": - let cellView = outlineView.makeView( - withIdentifier: .queueSongTitle, - owner: self - ) as! NSTableCellView - - cellView.textField?.stringValue = songItem.song.getTag(.title) - - return cellView - case "songArtistColumn": - let cellView = outlineView.makeView( - withIdentifier: .queueSongArtist, - owner: self - ) as! NSTableCellView - - cellView.textField?.stringValue = songItem.song.getTag(.artist) - - return cellView - default: - return nil - } - } else { - if tableColumn?.identifier.rawValue == "songTitleColumn" { - let cellView = outlineView.makeView( - withIdentifier: .queueHeading, - owner: self - ) as! NSTableCellView - - cellView.textField?.stringValue = "QUEUE" - - return cellView - } else { - return nil - } - } - } - - func outlineView( - _ outlineView: NSOutlineView, - selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet - ) -> IndexSet { - if proposedSelectionIndexes.contains(0) { - return IndexSet() - } else { - return proposedSelectionIndexes - } - } - - @IBOutlet var queueView: NSOutlineView! } diff --git a/Persephone/DataSources/AlbumDataSource.swift b/Persephone/DataSources/AlbumDataSource.swift new file mode 100644 index 0000000..7d63005 --- /dev/null +++ b/Persephone/DataSources/AlbumDataSource.swift @@ -0,0 +1,27 @@ +// +// AlbumDataSource.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/20. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class AlbumDataSource: NSObject, NSCollectionViewDataSource { + var albums: [MPDClient.Album] = [] + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return albums.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let item = collectionView.makeItem(withIdentifier: .albumItem, for: indexPath) + guard let albumItem = item as? AlbumItem else { return item } + + albumItem.view.wantsLayer = true + albumItem.setAlbum(albums[indexPath.item]) + + return albumItem + } +} diff --git a/Persephone/DataSources/QueueDataSource.swift b/Persephone/DataSources/QueueDataSource.swift new file mode 100644 index 0000000..0158e15 --- /dev/null +++ b/Persephone/DataSources/QueueDataSource.swift @@ -0,0 +1,65 @@ +// +// QueueDataSource.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/20. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class QueueDataSource: NSObject, NSOutlineViewDataSource { + var queue: [SongItem] = [] + var queuePos: Int = -1 + + var queueIcon: NSImage? = nil + + let playIcon = NSImage(named: "playButton") + let pauseIcon = NSImage(named: "pauseButton") + + func updateQueue(_ queue: [MPDClient.Song]) { + self.queue = queue.enumerated().map { index, song in + SongItem(song: song, queuePos: index, isPlaying: index == queuePos) + } + } + + func setQueuePos(_ queuePos: Int) { + let oldSongRowPos = self.queuePos + let newSongRowPos = queuePos + self.queuePos = queuePos + + if oldSongRowPos >= 0 { + queue[oldSongRowPos].isPlaying = false + } + if newSongRowPos >= 0 { + queue[newSongRowPos].isPlaying = true + } + } + + func setQueueIcon(_ state: MPDClient.Status.State) { + switch state { + case .playing: + queueIcon = playIcon + case .paused: + queueIcon = pauseIcon + default: + queueIcon = nil + } + } + + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + return queue.count + 1 + } + + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { + return false + } + + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + if index > 0 { + return queue[index - 1] + } else { + return false + } + } +} diff --git a/Persephone/MPDClient/MPDClient.swift b/Persephone/MPDClient/MPDClient.swift index 2219783..ad63a74 100644 --- a/Persephone/MPDClient/MPDClient.swift +++ b/Persephone/MPDClient/MPDClient.swift @@ -119,6 +119,27 @@ class MPDClient { idle() } + func playAlbum(_ album: Album) { + noIdle() + commandQueue.async { [unowned self] in + var songs: [Song] = [] + + 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 mpdSong = mpd_recv_song(self.connection) { + songs.append(Song(mpdSong)) + } + for song in songs { + mpd_run_add(self.connection, song.uri) + } + mpd_run_play_pos(self.connection, 0) + } + idle() + } + func queueCommand(command: Command) { guard isConnected else { return } diff --git a/Persephone/MPDClient/Models/Song.swift b/Persephone/MPDClient/Models/Song.swift index 3b43d5b..902dd21 100644 --- a/Persephone/MPDClient/Models/Song.swift +++ b/Persephone/MPDClient/Models/Song.swift @@ -41,6 +41,10 @@ extension MPDClient { mpd_song_free(mpdSong) } + var uri: UnsafePointer { + return mpd_song_get_uri(mpdSong) + } + func getTag(_ tagType: TagType) -> String { let mpdTagType = mpd_tag_type(rawValue: Int32(tagType.rawValue)) diff --git a/Persephone/Models/SongItem.swift b/Persephone/Models/SongItem.swift new file mode 100644 index 0000000..f9ef949 --- /dev/null +++ b/Persephone/Models/SongItem.swift @@ -0,0 +1,15 @@ +// +// SongItem.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/20. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Foundation + +struct SongItem { + var song: MPDClient.Song + var queuePos: Int + var isPlaying: Bool +} diff --git a/Persephone/Resources/AlbumItem.xib b/Persephone/Resources/AlbumItem.xib index 8791f09..a58c678 100644 --- a/Persephone/Resources/AlbumItem.xib +++ b/Persephone/Resources/AlbumItem.xib @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@ - + @@ -41,24 +41,45 @@ + + + + + + + + diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Resources/Base.lproj/Main.storyboard index 5a59a1b..7266679 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Resources/Base.lproj/Main.storyboard @@ -932,7 +932,7 @@ - + @@ -949,9 +949,8 @@ - - - + + @@ -959,6 +958,11 @@ + + + + + @@ -968,7 +972,6 @@ - @@ -1024,7 +1027,6 @@ - @@ -1049,6 +1051,7 @@ + diff --git a/Persephone/Views/AlbumItemView.swift b/Persephone/Views/AlbumItemView.swift new file mode 100644 index 0000000..d7087d0 --- /dev/null +++ b/Persephone/Views/AlbumItemView.swift @@ -0,0 +1,71 @@ +// +// AlbumItemView.swift +// Persephone +// +// Created by Daniel Barber on 2019/2/17. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +class AlbumItemView: NSView { + var trackingArea: NSTrackingArea? + + override func updateTrackingAreas() { + super.updateTrackingAreas() + + guard let albumImageView = imageView else { return } + + if let trackingArea = self.trackingArea { + self.removeTrackingArea(trackingArea) + } + + let trackingArea = NSTrackingArea( + rect: albumImageView.frame, + options: [.mouseEnteredAndExited, .activeAlways], + owner: self, + userInfo: nil + ) + + self.trackingArea = trackingArea + addTrackingArea(trackingArea) + } + + required init?(coder decoder: NSCoder) { + super.init(coder: decoder) + + NotificationCenter.default.addObserver( + self, + selector: #selector(viewWillScroll(_:)), + name: NSScrollView.willStartLiveScrollNotification, + object: nil + ) + } + + @objc func viewWillScroll(_ notification: Notification) { + hidePlayButton() + } + + override func resize(withOldSuperviewSize oldSize: NSSize) { + hidePlayButton() + } + + override func mouseEntered(with event: NSEvent) { + showPlayButton() + } + + override func mouseExited(with event: NSEvent) { + hidePlayButton() + } + + func showPlayButton() { + playButton.isHidden = false + } + + func hidePlayButton() { + playButton.isHidden = true + } + + @IBOutlet var imageView: NSImageView! + @IBOutlet var playButton: NSButton! +} diff --git a/Resources/export/playButtonLarge.pdf b/Resources/export/playButtonLarge.pdf new file mode 100644 index 0000000..a04b172 Binary files /dev/null and b/Resources/export/playButtonLarge.pdf differ diff --git a/Resources/export/playButtonLarge.png b/Resources/export/playButtonLarge.png new file mode 100644 index 0000000..a489b7b Binary files /dev/null and b/Resources/export/playButtonLarge.png differ diff --git a/Resources/export/playButtonLarge@2x.png b/Resources/export/playButtonLarge@2x.png new file mode 100644 index 0000000..109a9ca Binary files /dev/null and b/Resources/export/playButtonLarge@2x.png differ diff --git a/Resources/icons.sketch b/Resources/icons.sketch index 70a35a3..1631adf 100644 Binary files a/Resources/icons.sketch and b/Resources/icons.sketch differ diff --git a/Resources/screenshot.png b/Resources/screenshot.png index ff26962..641f395 100644 Binary files a/Resources/screenshot.png and b/Resources/screenshot.png differ