feat: add TrackTableView with NSTableView for high-performance track list

feat/music-streaming
Laurent 1 month ago
parent c5b468103a
commit c374cfa9eb
  1. 163
      Music/Views/TrackTableView.swift

@ -0,0 +1,163 @@
import SwiftUI
import AppKit
struct TrackTableView: NSViewRepresentable {
let tracks: [Track]
let playingTrackId: Int64?
let onSort: (String) -> Void
let onDoubleClick: (Track) -> Void
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = true
let tableView = NSTableView()
tableView.style = .plain
tableView.usesAlternatingRowBackgroundColors = true
tableView.allowsMultipleSelection = false
tableView.rowHeight = 24
tableView.intercellSpacing = NSSize(width: 10, height: 0)
let columns: [(id: String, title: String, width: CGFloat)] = [
("title", "Title", 300),
("artist", "Artist", 200),
("album", "Album", 200),
("genre", "Genre", 100),
("duration", "Duration", 70),
]
for col in columns {
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(col.id))
column.title = col.title
column.width = col.width
column.minWidth = 50
column.sortDescriptorPrototype = NSSortDescriptor(key: col.id, ascending: true)
if col.id == "duration" {
column.headerCell.alignment = .right
}
tableView.addTableColumn(column)
}
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.doubleAction = #selector(Coordinator.handleDoubleClick(_:))
tableView.target = context.coordinator
scrollView.documentView = tableView
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let tableView = scrollView.documentView as? NSTableView else { return }
let tracksChanged = context.coordinator.tracks != tracks
let playingChanged = context.coordinator.playingTrackId != playingTrackId
context.coordinator.parent = self
guard tracksChanged || playingChanged else { return }
let selectedIds = Set(tableView.selectedRowIndexes.compactMap { idx -> Int64? in
guard idx < context.coordinator.tracks.count else { return nil }
return context.coordinator.tracks[idx].id
})
context.coordinator.tracks = tracks
context.coordinator.playingTrackId = playingTrackId
tableView.reloadData()
if !selectedIds.isEmpty {
let newSelection = IndexSet(tracks.enumerated().compactMap { index, track in
selectedIds.contains(track.id ?? -1) ? index : nil
})
tableView.selectRowIndexes(newSelection, byExtendingSelection: false)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
var parent: TrackTableView
var tracks: [Track] = []
var playingTrackId: Int64?
init(_ parent: TrackTableView) {
self.parent = parent
}
func numberOfRows(in tableView: NSTableView) -> Int {
tracks.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard row < tracks.count else { return nil }
let track = tracks[row]
let colId = tableColumn?.identifier.rawValue ?? ""
let cellId = NSUserInterfaceItemIdentifier("Cell_\(colId)")
let cell: NSTextField
if let existing = tableView.makeView(withIdentifier: cellId, owner: nil) as? NSTextField {
cell = existing
} else {
cell = NSTextField(labelWithString: "")
cell.identifier = cellId
cell.lineBreakMode = .byTruncatingTail
cell.font = .systemFont(ofSize: 12)
}
switch colId {
case "title":
let isPlaying = track.id == parent.playingTrackId
if isPlaying {
cell.stringValue = "\(track.title)"
cell.textColor = .systemBlue
cell.font = .systemFont(ofSize: 12, weight: .medium)
} else {
cell.stringValue = track.title
cell.textColor = .labelColor
cell.font = .systemFont(ofSize: 12)
}
case "artist":
cell.stringValue = track.artist
cell.textColor = .secondaryLabelColor
case "album":
cell.stringValue = track.album
cell.textColor = .tertiaryLabelColor
case "genre":
cell.stringValue = track.genre
cell.textColor = .tertiaryLabelColor
case "duration":
cell.stringValue = Self.formatDuration(track.duration)
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
default:
cell.stringValue = ""
}
return cell
}
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
if let sort = tableView.sortDescriptors.first, let key = sort.key {
parent.onSort(key)
}
}
@objc func handleDoubleClick(_ sender: NSTableView) {
let row = sender.clickedRow
guard row >= 0, row < tracks.count else { return }
parent.onDoubleClick(tracks[row])
}
static func formatDuration(_ seconds: Double) -> String {
guard seconds.isFinite, seconds > 0 else { return "0:00" }
let mins = Int(seconds) / 60
let secs = Int(seconds) % 60
return "\(mins):\(String(format: "%02d", secs))"
}
}
}
Loading…
Cancel
Save