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