@ -38,9 +38,10 @@ private func loadVisibleColumnIds() -> Set<String> {
struct TrackTableView : NSViewRepresentable {
struct TrackTableView : NSViewRepresentable {
let tracks : [ Track ]
let tracks : [ Track ]
let playingTrackId : Int64 ?
let playingTrackId : Int64 ?
let sortColumn : String
let sortAscending : Bool
let onSort : ( String ) -> Void
let onSort : ( String ) -> Void
let onDoubleClick : ( Track ) -> Void
let onDoubleClick : ( Track ) -> Void
let onPlayPause : ( ) -> Void
var playlists : [ Playlist ]
var playlists : [ Playlist ]
var lastUsedPlaylistName : String ?
var lastUsedPlaylistName : String ?
var selectedPlaylist : Playlist ?
var selectedPlaylist : Playlist ?
@ -48,6 +49,7 @@ struct TrackTableView: NSViewRepresentable {
var onAddToLastPlaylist : ( ( Track ) -> Void ) ?
var onAddToLastPlaylist : ( ( Track ) -> Void ) ?
var onRemoveFromPlaylist : ( ( Track ) -> Void ) ?
var onRemoveFromPlaylist : ( ( Track ) -> Void ) ?
var onReorder : ( ( Int , Int ) -> Void ) ?
var onReorder : ( ( Int , Int ) -> Void ) ?
var scrollToPlayingTrigger : UUID = UUID ( )
func makeNSView ( context : Context ) -> NSScrollView {
func makeNSView ( context : Context ) -> NSScrollView {
let scrollView = NSScrollView ( )
let scrollView = NSScrollView ( )
@ -63,6 +65,13 @@ struct TrackTableView: NSViewRepresentable {
let visibleIds = loadVisibleColumnIds ( )
let visibleIds = loadVisibleColumnIds ( )
let nowPlayingColumn = NSTableColumn ( identifier : NSUserInterfaceItemIdentifier ( " nowPlaying " ) )
nowPlayingColumn . title = " "
nowPlayingColumn . width = 20
nowPlayingColumn . minWidth = 20
nowPlayingColumn . maxWidth = 20
tableView . addTableColumn ( nowPlayingColumn )
for col in columnDefinitions {
for col in columnDefinitions {
let column = NSTableColumn ( identifier : NSUserInterfaceItemIdentifier ( col . id ) )
let column = NSTableColumn ( identifier : NSUserInterfaceItemIdentifier ( col . id ) )
column . title = col . title
column . title = col . title
@ -76,12 +85,13 @@ struct TrackTableView: NSViewRepresentable {
tableView . addTableColumn ( column )
tableView . addTableColumn ( column )
}
}
tableView . sortDescriptors = [ NSSortDescriptor ( key : sortColumn , ascending : sortAscending ) ]
tableView . delegate = context . coordinator
tableView . delegate = context . coordinator
tableView . dataSource = context . coordinator
tableView . dataSource = context . coordinator
tableView . doubleAction = #selector ( Coordinator . handleDoubleClick ( _ : ) )
tableView . doubleAction = #selector ( Coordinator . handleDoubleClick ( _ : ) )
tableView . target = context . coordinator
tableView . target = context . coordinator
tableView . enterAction = #selector ( Coordinator . handleEnterKey ( _ : ) )
tableView . enterAction = #selector ( Coordinator . handleEnterKey ( _ : ) )
tableView . spaceAction = #selector ( Coordinator . handleSpaceKey ( _ : ) )
context . coordinator . tableView = tableView
context . coordinator . tableView = tableView
@ -116,9 +126,15 @@ struct TrackTableView: NSViewRepresentable {
let tracksChanged = context . coordinator . tracks != tracks
let tracksChanged = context . coordinator . tracks != tracks
let playingChanged = context . coordinator . playingTrackId != playingTrackId
let playingChanged = context . coordinator . playingTrackId != playingTrackId
let scrollTriggered = context . coordinator . lastScrollTrigger != scrollToPlayingTrigger
context . coordinator . parent = self
context . coordinator . parent = self
let expectedDescriptor = NSSortDescriptor ( key : sortColumn , ascending : sortAscending )
if tableView . sortDescriptors . first != expectedDescriptor {
tableView . sortDescriptors = [ expectedDescriptor ]
}
if context . coordinator . parent . onReorder != nil {
if context . coordinator . parent . onReorder != nil {
if tableView . registeredDraggedTypes . isEmpty || ! tableView . registeredDraggedTypes . contains ( . string ) {
if tableView . registeredDraggedTypes . isEmpty || ! tableView . registeredDraggedTypes . contains ( . string ) {
tableView . registerForDraggedTypes ( [ . string ] )
tableView . registerForDraggedTypes ( [ . string ] )
@ -128,6 +144,15 @@ struct TrackTableView: NSViewRepresentable {
tableView . unregisterDraggedTypes ( )
tableView . unregisterDraggedTypes ( )
}
}
if scrollTriggered {
context . coordinator . lastScrollTrigger = scrollToPlayingTrigger
if let playingId = playingTrackId ,
let row = context . coordinator . tracks . firstIndex ( where : { $0 . id = = playingId } ) {
tableView . scrollRowToVisible ( row )
tableView . selectRowIndexes ( IndexSet ( integer : row ) , byExtendingSelection : false )
}
}
guard tracksChanged || playingChanged else { return }
guard tracksChanged || playingChanged else { return }
let selectedIds = Set ( tableView . selectedRowIndexes . compactMap { idx -> Int64 ? in
let selectedIds = Set ( tableView . selectedRowIndexes . compactMap { idx -> Int64 ? in
@ -156,6 +181,7 @@ struct TrackTableView: NSViewRepresentable {
var parent : TrackTableView
var parent : TrackTableView
var tracks : [ Track ] = [ ]
var tracks : [ Track ] = [ ]
var playingTrackId : Int64 ?
var playingTrackId : Int64 ?
var lastScrollTrigger : UUID = UUID ( )
weak var tableView : NSTableView ?
weak var tableView : NSTableView ?
init ( _ parent : TrackTableView ) {
init ( _ parent : TrackTableView ) {
@ -171,6 +197,36 @@ struct TrackTableView: NSViewRepresentable {
let track = tracks [ row ]
let track = tracks [ row ]
let colId = tableColumn ? . identifier . rawValue ? ? " "
let colId = tableColumn ? . identifier . rawValue ? ? " "
if colId = = " nowPlaying " {
let cellId = NSUserInterfaceItemIdentifier ( " Cell_nowPlaying " )
let cellView : NSTableCellView
if let existing = tableView . makeView ( withIdentifier : cellId , owner : nil ) as ? NSTableCellView {
cellView = existing
} else {
cellView = NSTableCellView ( )
cellView . identifier = cellId
let imageView = NSImageView ( )
imageView . translatesAutoresizingMaskIntoConstraints = false
imageView . imageScaling = . scaleProportionallyDown
cellView . addSubview ( imageView )
cellView . imageView = imageView
NSLayoutConstraint . activate ( [
imageView . centerXAnchor . constraint ( equalTo : cellView . centerXAnchor ) ,
imageView . centerYAnchor . constraint ( equalTo : cellView . centerYAnchor ) ,
imageView . widthAnchor . constraint ( equalToConstant : 12 ) ,
imageView . heightAnchor . constraint ( equalToConstant : 12 ) ,
] )
}
let isPlaying = track . id = = parent . playingTrackId
if isPlaying {
cellView . imageView ? . image = NSImage ( systemSymbolName : " speaker.fill " , accessibilityDescription : " Now Playing " )
cellView . imageView ? . contentTintColor = . secondaryLabelColor
} else {
cellView . imageView ? . image = nil
}
return cellView
}
let cellId = NSUserInterfaceItemIdentifier ( " Cell_ \( colId ) " )
let cellId = NSUserInterfaceItemIdentifier ( " Cell_ \( colId ) " )
let cellView : NSTableCellView
let cellView : NSTableCellView
if let existing = tableView . makeView ( withIdentifier : cellId , owner : nil ) as ? NSTableCellView {
if let existing = tableView . makeView ( withIdentifier : cellId , owner : nil ) as ? NSTableCellView {
@ -193,76 +249,59 @@ struct TrackTableView: NSViewRepresentable {
let cell = cellView . textField !
let cell = cellView . textField !
let isPlaying = track . id = = parent . playingTrackId
let isPlaying = track . id = = parent . playingTrackId
cell . font = isPlaying ? . boldSystemFont ( ofSize : 12 ) : . systemFont ( ofSize : 12 )
cell . font = isPlaying ? . boldSystemFont ( ofSize : 12 ) : . systemFont ( ofSize : 12 )
cell . textColor = . secondaryL abelColor
cell . textColor = . l abelColor
cell . alignment = . left
cell . alignment = . left
switch colId {
switch colId {
case " title " :
case " title " :
cell . stringValue = track . title
cell . stringValue = track . title
cell . textColor = . labelColor
case " artist " :
case " artist " :
cell . stringValue = track . artist
cell . stringValue = track . artist
case " albumArtist " :
case " albumArtist " :
cell . stringValue = track . albumArtist
cell . stringValue = track . albumArtist
case " album " :
case " album " :
cell . stringValue = track . album
cell . stringValue = track . album
cell . textColor = . tertiaryLabelColor
case " composer " :
case " composer " :
cell . stringValue = track . composer
cell . stringValue = track . composer
case " genre " :
case " genre " :
cell . stringValue = track . genre
cell . stringValue = track . genre
cell . textColor = . tertiaryLabelColor
case " year " :
case " year " :
cell . stringValue = track . year . map { String ( $0 ) } ? ? " "
cell . stringValue = track . year . map { String ( $0 ) } ? ? " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " bpm " :
case " bpm " :
cell . stringValue = track . bpm . map { String ( $0 ) } ? ? " "
cell . stringValue = track . bpm . map { String ( $0 ) } ? ? " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " trackNumber " :
case " trackNumber " :
cell . stringValue = track . trackNumber . map { String ( $0 ) } ? ? " "
cell . stringValue = track . trackNumber . map { String ( $0 ) } ? ? " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " discNumber " :
case " discNumber " :
cell . stringValue = track . discNumber . map { String ( $0 ) } ? ? " "
cell . stringValue = track . discNumber . map { String ( $0 ) } ? ? " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " duration " :
case " duration " :
cell . stringValue = Self . formatDuration ( track . duration )
cell . stringValue = Self . formatDuration ( track . duration )
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " playCount " :
case " playCount " :
cell . stringValue = track . playCount > 0 ? " \( track . playCount ) " : " "
cell . stringValue = track . playCount > 0 ? " \( track . playCount ) " : " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " lastPlayedAt " :
case " lastPlayedAt " :
cell . stringValue = track . lastPlayedAt . map { Self . formatDate ( $0 ) } ? ? " "
cell . stringValue = track . lastPlayedAt . map { Self . formatDate ( $0 ) } ? ? " "
cell . textColor = . tertiaryLabelColor
case " rating " :
case " rating " :
cell . stringValue = track . rating > 0 ? String ( repeating : " ★ " , count : track . rating ) : " "
cell . stringValue = track . rating > 0 ? String ( repeating : " ★ " , count : track . rating ) : " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " dateAdded " :
case " dateAdded " :
cell . stringValue = Self . formatDate ( track . dateAdded )
cell . stringValue = Self . formatDate ( track . dateAdded )
cell . textColor = . tertiaryLabelColor
case " dateModified " :
case " dateModified " :
cell . stringValue = Self . formatDate ( track . dateModified )
cell . stringValue = Self . formatDate ( track . dateModified )
cell . textColor = . tertiaryLabelColor
case " fileFormat " :
case " fileFormat " :
cell . stringValue = track . fileFormat . uppercased ( )
cell . stringValue = track . fileFormat . uppercased ( )
cell . textColor = . tertiaryLabelColor
case " bitrate " :
case " bitrate " :
cell . stringValue = track . bitrate . map { " \( $0 ) kbps " } ? ? " "
cell . stringValue = track . bitrate . map { " \( $0 ) kbps " } ? ? " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " sampleRate " :
case " sampleRate " :
cell . stringValue = track . sampleRate . map { Self . formatSampleRate ( $0 ) } ? ? " "
cell . stringValue = track . sampleRate . map { Self . formatSampleRate ( $0 ) } ? ? " "
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
case " fileSize " :
case " fileSize " :
cell . stringValue = Self . formatFileSize ( track . fileSize )
cell . stringValue = Self . formatFileSize ( track . fileSize )
cell . textColor = . tertiaryLabelColor
cell . alignment = . right
cell . alignment = . right
default :
default :
cell . stringValue = " "
cell . stringValue = " "
@ -272,9 +311,9 @@ struct TrackTableView: NSViewRepresentable {
}
}
func tableView ( _ tableView : NSTableView , sortDescriptorsDidChange oldDescriptors : [ NSSortDescriptor ] ) {
func tableView ( _ tableView : NSTableView , sortDescriptorsDidChange oldDescriptors : [ NSSortDescriptor ] ) {
if let sort = tableView . sortDescriptors . first , let key = sort . key {
guard let sort = tableView . sortDescriptors . first , let key = sort . key else { return }
parent . onSort ( key )
guard key != parent . sortColumn || sort . ascending != parent . sortAscending else { return }
}
parent . onSort ( key )
}
}
@objc func handleDoubleClick ( _ sender : NSTableView ) {
@objc func handleDoubleClick ( _ sender : NSTableView ) {
@ -289,10 +328,6 @@ struct TrackTableView: NSViewRepresentable {
parent . onDoubleClick ( tracks [ row ] )
parent . onDoubleClick ( tracks [ row ] )
}
}
@objc func handleSpaceKey ( _ sender : NSTableView ) {
parent . onPlayPause ( )
}
// MARK: - C o n t e x t M e n u
// MARK: - C o n t e x t M e n u
func menuNeedsUpdate ( _ menu : NSMenu ) {
func menuNeedsUpdate ( _ menu : NSMenu ) {
@ -422,13 +457,10 @@ struct TrackTableView: NSViewRepresentable {
private final class PlayableTableView : NSTableView {
private final class PlayableTableView : NSTableView {
var enterAction : Selector ?
var enterAction : Selector ?
var spaceAction : Selector ?
override func keyDown ( with event : NSEvent ) {
override func keyDown ( with event : NSEvent ) {
if event . keyCode = = 36 || event . keyCode = = 76 , let enterAction , let target {
if event . keyCode = = 36 || event . keyCode = = 76 , let enterAction , let target {
NSApp . sendAction ( enterAction , to : target , from : self )
NSApp . sendAction ( enterAction , to : target , from : self )
} else if event . keyCode = = 49 , let spaceAction , let target {
NSApp . sendAction ( spaceAction , to : target , from : self )
} else {
} else {
super . keyDown ( with : event )
super . keyDown ( with : event )
}
}