parent
c5b468103a
commit
c374cfa9eb
@ -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…
Reference in new issue