1
1
mirror of https://github.com/danbee/persephone synced 2025-03-04 08:39:11 +00:00

Add a button to albums that plays the album

This works by clearing the queue, adding all the tracks from the album
to the queue and playing the first track.

Fix bug where resize might leave a play button visible

Had to refactor the QueueView somewhat as there was a bug that only
surfaced on clearing and refilling the playlist. The bug was due to the
way NSOutlineView reuses subviews.

Add screenshot with album play button

Move queue datasource and refactor view controller

Move album datasource out of the view controller
This commit is contained in:
Daniel Barber 2019-02-17 12:23:55 -05:00
parent 5d0e1c4719
commit 2d6aa478a7
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
20 changed files with 440 additions and 226 deletions

View File

@ -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 = "<group>"; };
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawRepresentable.swift; sourceTree = "<group>"; };
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewController.swift; sourceTree = "<group>"; };
E408D3C8220E341D0006D9BE /* AlbumItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = "<group>"; };
E408D3C9220E341D0006D9BE /* AlbumItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumItem.xib; sourceTree = "<group>"; };
E40F41F2221EDE27004B6CB8 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
E40FE718221B48E300A4223F /* AlbumViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewLayout.swift; sourceTree = "<group>"; };
E40FE71A221B904300A4223F /* NSEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEvent.swift; sourceTree = "<group>"; };
E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libmpdclient.2.dylib; path = libmpdclient/output/libmpdclient.2.dylib; sourceTree = "<group>"; };
@ -131,12 +135,13 @@
E41B22EA21FB966C00D544F6 /* queue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = queue.h; sourceTree = "<group>"; };
E41B22EB21FB966C00D544F6 /* playlist.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = playlist.h; sourceTree = "<group>"; };
E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; };
E41EA46E221715910068EF46 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
E42A8F3922176D6400A13ED9 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
E42A8F3A22176D6400A13ED9 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
E465049921E94DF500A70F4C /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = "<group>"; };
E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
E47E2FD222205D2500F747E6 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = "<group>"; };
E47E2FD4222071FD00F747E6 /* AlbumItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = "<group>"; };
E47E2FD62220720300F747E6 /* AlbumItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumItemView.swift; sourceTree = "<group>"; };
E4928E0A2218D62A001D4BEA /* CGColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGColor.swift; sourceTree = "<group>"; };
E4A642D922090CBE00067D21 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = "<group>"; };
@ -145,6 +150,9 @@
E4E8CC9922075D370024217A /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = "<group>"; };
E4EB2378220F10B8008C70C0 /* Pair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pair.swift; sourceTree = "<group>"; };
E4EB237A220F7CF1008C70C0 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueDataSource.swift; sourceTree = "<group>"; };
E4F6B462221E125900ACF42A /* SongItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongItem.swift; sourceTree = "<group>"; };
E4F6B466221E233200ACF42A /* AlbumDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDataSource.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
E41EA46D221715820068EF46 /* Models */ = {
isa = PBXGroup;
children = (
E41EA46E221715910068EF46 /* Preferences.swift */,
);
path = Models;
sourceTree = "<group>";
};
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 = "<group>";
};
E4F6B45E221E117600ACF42A /* DataSources */ = {
isa = PBXGroup;
children = (
E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */,
E4F6B466221E233200ACF42A /* AlbumDataSource.swift */,
);
path = DataSources;
sourceTree = "<group>";
};
E4F6B461221E124700ACF42A /* Models */ = {
isa = PBXGroup;
children = (
E40F41F2221EDE27004B6CB8 /* Preferences.swift */,
E4F6B462221E125900ACF42A /* SongItem.swift */,
);
path = Models;
sourceTree = "<group>";
};
/* 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 */,

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

View File

@ -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!

View File

@ -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!
}

View File

@ -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!
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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 }

View File

@ -41,6 +41,10 @@ extension MPDClient {
mpd_song_free(mpdSong)
}
var uri: UnsafePointer<Int8> {
return mpd_song_get_uri(mpdSong)
}
func getTag(_ tagType: TagType) -> String {
let mpdTagType = mpd_tag_type(rawValue: Int32(tagType.rawValue))

View File

@ -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
}

View File

@ -9,7 +9,7 @@
<customObject id="-2" userLabel="File's Owner" customClass="AlbumItem" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="albumArtist" destination="5Uu-j1-qyT" id="2Et-tX-InT"/>
<outlet property="albumCoverView" destination="Kfb-8f-ean" id="ZAL-jD-lHj"/>
<outlet property="albumCoverView" destination="Kfb-8f-ean" id="CXx-gB-gz8"/>
<outlet property="albumTitle" destination="KEh-NL-c2W" id="SI3-hm-H2B"/>
<outlet property="imageView" destination="Kfb-8f-ean" id="Ur0-hX-wJm"/>
<outlet property="view" destination="Hz6-mo-xeY" id="v7W-XA-Emc"/>
@ -17,7 +17,7 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="Hz6-mo-xeY">
<customView id="Hz6-mo-xeY" customClass="AlbumItemView" customModule="Persephone" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="128" height="167"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
@ -41,24 +41,45 @@
<rect key="frame" x="0.0" y="39" width="128" height="128"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="blankAlbum" id="FsA-JX-BFh"/>
</imageView>
<button hidden="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n8W-do-HyG">
<rect key="frame" x="43" y="81" width="42" height="43"/>
<constraints>
<constraint firstAttribute="height" constant="42" id="XXC-YE-Ego"/>
<constraint firstAttribute="width" constant="42" id="zcR-GT-zym"/>
</constraints>
<buttonCell key="cell" type="inline" bezelStyle="inline" image="playButtonLarge" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" inset="2" id="T1p-LZ-RpJ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystemBold"/>
</buttonCell>
<connections>
<action selector="playAlbum:" target="-2" id="gNt-Rn-kte"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="5Uu-j1-qyT" firstAttribute="trailing" secondItem="KEh-NL-c2W" secondAttribute="trailing" id="64z-uz-4nY"/>
<constraint firstAttribute="bottom" secondItem="KEh-NL-c2W" secondAttribute="bottom" constant="18" id="8Kg-1r-wNp"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="JMi-4i-dgs"/>
<constraint firstAttribute="trailing" secondItem="Kfb-8f-ean" secondAttribute="trailing" id="KQC-Wz-Bsg"/>
<constraint firstItem="n8W-do-HyG" firstAttribute="centerX" secondItem="KEh-NL-c2W" secondAttribute="centerX" id="Kf1-ws-d4q"/>
<constraint firstItem="5Uu-j1-qyT" firstAttribute="leading" secondItem="KEh-NL-c2W" secondAttribute="leading" id="MUo-0i-fX9"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="Qbk-jx-zAi"/>
<constraint firstItem="KEh-NL-c2W" firstAttribute="trailing" secondItem="Kfb-8f-ean" secondAttribute="trailing" id="U0w-G4-ggX"/>
<constraint firstItem="KEh-NL-c2W" firstAttribute="leading" secondItem="Kfb-8f-ean" secondAttribute="leading" id="V8r-Rc-Dx7"/>
<constraint firstAttribute="bottom" secondItem="5Uu-j1-qyT" secondAttribute="bottom" id="gci-4h-pDZ"/>
<constraint firstItem="n8W-do-HyG" firstAttribute="centerY" secondItem="Kfb-8f-ean" secondAttribute="centerY" id="pgP-oA-Nxa"/>
<constraint firstAttribute="bottom" secondItem="Kfb-8f-ean" secondAttribute="bottom" constant="39" id="sid-zJ-YMA"/>
</constraints>
<connections>
<outlet property="imageView" destination="Kfb-8f-ean" id="T7Z-En-dU3"/>
<outlet property="playButton" destination="n8W-do-HyG" id="Xw0-iI-svx"/>
</connections>
<point key="canvasLocation" x="-22" y="125.5"/>
</customView>
<collectionViewItem id="Qgu-aI-55A" customClass="AlbumItem" customModule="Persephone" customModuleProvider="target"/>
</objects>
<resources>
<image name="blankAlbum" width="128" height="128"/>
<image name="playButtonLarge" width="22" height="22"/>
</resources>
</document>

View File

@ -932,7 +932,7 @@
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="songArtistColumn" width="144" minWidth="128" maxWidth="1000" id="SPM-QP-DX8">
<tableColumn identifier="songArtistColumn" width="144" minWidth="64" maxWidth="1000" id="SPM-QP-DX8">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Artist">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@ -949,9 +949,8 @@
<rect key="frame" x="204" y="1" width="144" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tBe-Q9-3Rw">
<rect key="frame" x="0.0" y="0.0" width="222" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="tBe-Q9-3Rw">
<rect key="frame" x="0.0" y="0.0" width="149" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="Ceb-ec-ydU">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -959,6 +958,11 @@
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="tBe-Q9-3Rw" firstAttribute="centerY" secondItem="JSk-Vc-Y7e" secondAttribute="centerY" id="Tkg-cb-Bg6"/>
<constraint firstAttribute="trailing" secondItem="tBe-Q9-3Rw" secondAttribute="trailing" constant="-3" id="VhZ-ua-QQX"/>
<constraint firstItem="tBe-Q9-3Rw" firstAttribute="leading" secondItem="JSk-Vc-Y7e" secondAttribute="leading" constant="2" id="cTy-tR-Grg"/>
</constraints>
<connections>
<outlet property="textField" destination="tBe-Q9-3Rw" id="2e6-zi-tKj"/>
</connections>
@ -968,7 +972,6 @@
</tableColumns>
<connections>
<action trigger="doubleAction" selector="playTrack:" target="KIP-rq-4dM" id="opa-6G-OW0"/>
<outlet property="dataSource" destination="KIP-rq-4dM" id="K1Q-7o-xXW"/>
<outlet property="delegate" destination="KIP-rq-4dM" id="60F-6x-bUE"/>
</connections>
</outlineView>
@ -1024,7 +1027,6 @@
<collectionViewLayout key="collectionViewLayout" id="YE8-sD-l5P" customClass="AlbumViewLayout" customModule="Persephone" customModuleProvider="target"/>
<color key="primaryBackgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<connections>
<outlet property="dataSource" destination="gPn-fP-LFc" id="2VB-5V-ltv"/>
<outlet property="delegate" destination="gPn-fP-LFc" id="LQ2-Vl-r08"/>
</connections>
</collectionView>
@ -1049,6 +1051,7 @@
</view>
<connections>
<outlet property="albumCollectionView" destination="lfq-AB-epE" id="p69-Fs-hCN"/>
<outlet property="albumScrollView" destination="i5f-35-7x8" id="jmd-Sa-Bxt"/>
</connections>
</viewController>
<customObject id="uex-Ws-5X4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>

View File

@ -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!
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 179 KiB