feat: improve player UX — scrubbing, keyboard shortcuts, now-playing indicator, Home button

Add chase-seek scrubbing for smooth slider interaction, global keyboard
monitor for space/arrows, now-playing speaker column in track table, Home
button in playlist bar, smart playlist sorting, and UI polish (label colors,
track selection, app icon 512@2x). Bump version to 10.
feat/music-streaming
Laurent 1 month ago
parent a657075ef9
commit 11d5f91a86
  1. 4
      Music.xcodeproj/project.pbxproj
  2. 2
      Music/Assets.xcassets/AppIcon.appiconset/Contents.json
  3. BIN
      Music/Assets.xcassets/AppIcon.appiconset/icon_mu.png
  4. 88
      Music/ContentView.swift
  5. 71
      Music/Services/AudioService.swift
  6. 12
      Music/ViewModels/PlaylistViewModel.swift
  7. 12
      Music/Views/HomeView.swift
  8. 37
      Music/Views/PlayerControlsView.swift
  9. 81
      Music/Views/PlaylistBarView.swift
  10. 92
      Music/Views/TrackTableView.swift

@ -426,7 +426,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = 526E96RFNP;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@ -464,7 +464,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = 526E96RFNP;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;

@ -36,7 +36,6 @@
"size" : "256x256"
},
{
"filename" : "icon_mu.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
@ -47,6 +46,7 @@
"size" : "512x512"
},
{
"filename" : "icon_mu.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -18,7 +18,8 @@ struct ContentView: View {
@State private var smartPlaylistToEdit: SmartPlaylist?
@State private var scrollToPlayingTrigger = UUID()
@State private var searchText = ""
@State private var showHome = true
@State private var keyMonitor: Any?
@State private var showHome = false
@State private var recentTracks: [Track] = []
@State private var totalDuration: Double = 0
@State private var monthlyAdditions: [MonthlyCount] = []
@ -29,11 +30,13 @@ struct ContentView: View {
searchText: $searchText,
trackCount: playlist.selectedItem != nil ? playlist.playlistTracks.count : library.trackCount,
onSearch: { text in
if text.isEmpty {
showHome = true
} else {
if !text.isEmpty {
showHome = false
}
if !text.isEmpty && library.searchText.isEmpty {
library.sortColumn = "album"
library.sortAscending = true
}
library.search(text)
if playlist.selectedPlaylist != nil {
playlist.search(text)
@ -57,12 +60,12 @@ struct ContentView: View {
.padding(.vertical, 4)
}
if let selected = playlist.selectedItem {
if showHome || playlist.selectedItem != nil {
HStack(spacing: 4) {
Button(action: {
playlist.deselectPlaylist()
searchText = ""
showHome = true
showHome = false
}) {
HStack(spacing: 2) {
Image(systemName: "chevron.left")
@ -78,7 +81,7 @@ struct ContentView: View {
.font(.system(size: 12))
.foregroundStyle(.quaternary)
Text(selected.name)
Text(showHome ? "Home" : (playlist.selectedItem?.name ?? ""))
.font(.system(size: 12, weight: .medium))
}
.padding(.horizontal, 12)
@ -86,7 +89,7 @@ struct ContentView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
if showHome && playlist.selectedItem == nil && searchText.isEmpty {
if showHome && playlist.selectedItem == nil {
HomeView(
recentTracks: recentTracks,
trackCount: library.trackCount,
@ -105,10 +108,12 @@ struct ContentView: View {
TrackTableView(
tracks: playlist.selectedItem != nil ? playlist.playlistTracks : library.tracks,
playingTrackId: player.currentTrack?.id,
sortColumn: library.sortColumn,
sortAscending: library.sortAscending,
sortColumn: playlist.selectedSmartPlaylist != nil ? playlist.sortColumn : library.sortColumn,
sortAscending: playlist.selectedSmartPlaylist != nil ? playlist.sortAscending : library.sortAscending,
onSort: { column in
if playlist.selectedItem == nil {
if playlist.selectedSmartPlaylist != nil {
playlist.sort(by: column)
} else if playlist.selectedItem == nil {
library.sort(by: column)
}
},
@ -117,7 +122,6 @@ struct ContentView: View {
player.setQueue(trackList)
player.play(track)
},
onPlayPause: { audio.togglePlayPause() },
playlists: playlist.playlists,
lastUsedPlaylistName: playlist.lastUsedPlaylistName,
selectedPlaylist: playlist.selectedPlaylist,
@ -143,8 +147,23 @@ struct ContentView: View {
PlaylistBarView(
playlists: playlist.allPlaylists,
selectedItem: playlist.selectedItem,
selectedItem: showHome ? nil : playlist.selectedItem,
isHomeSelected: showHome,
onHomeSelect: {
if showHome {
showHome = false
} else {
playlist.deselectPlaylist()
searchText = ""
showHome = true
}
},
onSelect: { item in
showHome = false
if item is SmartPlaylist {
playlist.sortColumn = "album"
playlist.sortAscending = true
}
playlist.selectItem(item)
if let smart = item as? SmartPlaylist {
searchText = smart.searchQuery
@ -154,7 +173,6 @@ struct ContentView: View {
onDeselect: {
playlist.deselectPlaylist()
searchText = ""
showHome = true
},
onRename: { item in
itemToRename = item
@ -177,14 +195,8 @@ struct ContentView: View {
playerControls
}
.onKeyPress(.leftArrow) {
player.previous()
return .handled
}
.onKeyPress(.rightArrow) {
player.next()
return .handled
}
.onAppear { installKeyboardMonitor() }
.onDisappear { removeKeyboardMonitor() }
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
handleDrop(providers)
return true
@ -281,6 +293,38 @@ struct ContentView: View {
)
}
private func installKeyboardMonitor() {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [audio, player] event in
guard event.modifierFlags.intersection([.command, .control, .option, .shift]).isEmpty else {
return event
}
guard let responder = NSApp.keyWindow?.firstResponder,
!(responder is NSTextView) else {
return event
}
switch event.keyCode {
case 49: // space
audio.togglePlayPause()
return nil
case 123: // left arrow
player.previous()
return nil
case 124: // right arrow
player.next()
return nil
default:
return event
}
}
}
private func removeKeyboardMonitor() {
if let monitor = keyMonitor {
NSEvent.removeMonitor(monitor)
keyMonitor = nil
}
}
private func loadHomeData() {
recentTracks = (try? db.fetchRecentlyAdded(limit: 50)) ?? []
totalDuration = (try? db.totalDuration()) ?? 0

@ -20,6 +20,10 @@ final class AudioService {
private var timeObserver: Any?
private var endObserver: NSObjectProtocol?
private(set) var isScrubbing = false
private var seekInProgress = false
private var pendingSeekTime: Double?
var onTrackFinished: (() -> Void)?
func play(url: URL) {
@ -33,7 +37,7 @@ final class AudioService {
forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
queue: .main
) { [weak self] time in
guard let self else { return }
guard let self, !self.isScrubbing else { return }
self.currentTime = time.seconds
if let dur = self.player?.currentItem?.duration, dur.isValid, !dur.isIndefinite {
self.duration = dur.seconds
@ -69,7 +73,70 @@ final class AudioService {
}
func seek(to time: Double) {
player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
let clamped = clampedTime(time)
currentTime = clamped
player?.seek(
to: CMTime(seconds: clamped, preferredTimescale: 600),
toleranceBefore: .zero,
toleranceAfter: .zero
)
}
func beginScrubbing() {
isScrubbing = true
}
func scrub(to time: Double) {
let clamped = clampedTime(time)
currentTime = clamped
chaseSeek(to: clamped)
}
func endScrubbing(at time: Double) {
let clamped = clampedTime(time)
currentTime = clamped
pendingSeekTime = nil
seekInProgress = false
player?.seek(
to: CMTime(seconds: clamped, preferredTimescale: 600),
toleranceBefore: .zero,
toleranceAfter: .zero
) { [weak self] _ in
DispatchQueue.main.async {
self?.isScrubbing = false
}
}
}
private func chaseSeek(to time: Double) {
pendingSeekTime = time
guard !seekInProgress else { return }
performPendingSeek()
}
private func performPendingSeek() {
guard let time = pendingSeekTime else { return }
pendingSeekTime = nil
seekInProgress = true
player?.seek(
to: CMTime(seconds: time, preferredTimescale: 600),
toleranceBefore: CMTime(seconds: 0.1, preferredTimescale: 600),
toleranceAfter: CMTime(seconds: 0.1, preferredTimescale: 600)
) { [weak self] _ in
DispatchQueue.main.async {
guard let self else { return }
self.seekInProgress = false
if self.pendingSeekTime != nil {
self.performPendingSeek()
}
}
}
}
private func clampedTime(_ time: Double) -> Double {
max(0, min(time, duration))
}
func stop() {

@ -142,6 +142,18 @@ final class PlaylistViewModel {
searchText = ""
}
func sort(by column: String) {
if sortColumn == column {
sortAscending.toggle()
} else {
sortColumn = column
sortAscending = true
}
if let smart = selectedSmartPlaylist {
observeSmartPlaylistTracks(searchQuery: smart.searchQuery)
}
}
func search(_ text: String) {
searchText = text
searchTask?.cancel()

@ -9,6 +9,8 @@ struct HomeView: View {
let onTrackDoubleClick: (Track) -> Void
let onShowAll: () -> Void
@State private var selectedTrack: Track?
var body: some View {
HStack(alignment: .top, spacing: 0) {
recentlyAddedPanel
@ -19,6 +21,7 @@ struct HomeView: View {
statsPanel
.frame(minWidth: 300, maxWidth: 300, maxHeight: .infinity)
}
.background(.white)
}
private var recentlyAddedPanel: some View {
@ -50,10 +53,19 @@ struct HomeView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
.padding(.horizontal, 16)
.background(
selectedTrack == track
? Color.accentColor.opacity(0.2)
: Color.clear
)
.clipShape(RoundedRectangle(cornerRadius: 4))
.contentShape(Rectangle())
.onTapGesture(count: 2) {
onTrackDoubleClick(track)
}
.simultaneousGesture(TapGesture().onEnded {
selectedTrack = track
})
}
}
}

@ -11,8 +11,15 @@ struct PlayerControlsView: View {
let onNext: () -> Void
let onPrevious: () -> Void
let onSeek: (Double) -> Void
let onScrubStart: () -> Void
let onScrub: (Double) -> Void
let onScrubEnd: (Double) -> Void
let onVolumeChange: (Float) -> Void
let onShuffleToggle: () -> Void
let onNowPlayingTap: () -> Void
@State private var isDragging = false
@State private var dragValue: Double = 0
var body: some View {
HStack(spacing: 0) {
@ -60,6 +67,12 @@ struct PlayerControlsView: View {
}
}
}
.contentShape(Rectangle())
.onTapGesture {
if currentTrack != nil {
onNowPlayingTap()
}
}
}
private var transportSection: some View {
@ -81,6 +94,7 @@ struct PlayerControlsView: View {
Button(action: onPlayPause) {
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.font(.system(size: 22))
.frame(width: 24, height: 24)
}
.buttonStyle(.plain)
@ -95,17 +109,32 @@ struct PlayerControlsView: View {
}
HStack(spacing: 8) {
Text(Self.formatTime(currentTime))
Text(Self.formatTime(isDragging ? dragValue : currentTime))
.font(.system(size: 10).monospacedDigit())
.foregroundStyle(.secondary)
.frame(width: 45, alignment: .trailing)
Slider(
value: Binding(
get: { currentTime },
set: { onSeek($0) }
get: { isDragging ? dragValue : currentTime },
set: { newValue in
dragValue = newValue
if isDragging {
onScrub(newValue)
}
}
),
in: 0...max(duration, 1)
in: 0...max(duration, 1),
onEditingChanged: { editing in
if editing {
isDragging = true
dragValue = currentTime
onScrubStart()
} else {
onScrubEnd(dragValue)
isDragging = false
}
}
)
.controlSize(.small)

@ -3,6 +3,8 @@ import SwiftUI
struct PlaylistBarView: View {
var playlists: [any PlaylistRepresentable]
var selectedItem: (any PlaylistRepresentable)?
var isHomeSelected: Bool
var onHomeSelect: () -> Void
var onSelect: (any PlaylistRepresentable) -> Void
var onDeselect: () -> Void
var onRename: (any PlaylistRepresentable) -> Void
@ -10,33 +12,39 @@ struct PlaylistBarView: View {
var onEditQuery: (SmartPlaylist) -> Void
var body: some View {
if !playlists.isEmpty {
FlowLayout(spacing: 6) {
ForEach(playlists, id: \.id) { item in
PlaylistButton(
name: item.name,
isSelected: selectedItem?.id == item.id,
isSmart: item.isSmartPlaylist,
action: {
if selectedItem?.id == item.id {
onDeselect()
} else {
onSelect(item)
}
}
)
.contextMenu {
Button("Rename...") { onRename(item) }
if let smart = item as? SmartPlaylist {
Button("Edit Search Query...") { onEditQuery(smart) }
FlowLayout(spacing: 6) {
PlaylistButton(
name: "Home",
isSelected: isHomeSelected,
isSmart: false,
icon: "house.fill",
action: onHomeSelect
)
ForEach(playlists, id: \.id) { item in
PlaylistButton(
name: item.name,
isSelected: selectedItem?.id == item.id,
isSmart: item.isSmartPlaylist,
action: {
if selectedItem?.id == item.id {
onDeselect()
} else {
onSelect(item)
}
Button("Delete") { onDelete(item) }
}
)
.contextMenu {
Button("Rename...") { onRename(item) }
if let smart = item as? SmartPlaylist {
Button("Edit Search Query...") { onEditQuery(smart) }
}
Button("Delete") { onDelete(item) }
}
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
}
@ -44,6 +52,7 @@ private struct PlaylistButton: View {
let name: String
let isSelected: Bool
let isSmart: Bool
var icon: String? = nil
let action: () -> Void
private var tintColor: Color {
@ -56,17 +65,23 @@ private struct PlaylistButton: View {
var body: some View {
Button(action: action) {
Text(name)
.font(.system(size: 11))
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(isSelected ? tintColor.opacity(0.2) : Color.secondary.opacity(0.1))
.foregroundStyle(isSelected ? tintColor : inactiveColor)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(isSelected ? tintColor : Color.secondary.opacity(0.3), lineWidth: 1)
)
.cornerRadius(4)
HStack(spacing: 4) {
if let icon {
Image(systemName: icon)
.font(.system(size: 10))
}
Text(name)
}
.font(.system(size: 11))
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(isSelected ? tintColor.opacity(0.2) : Color.secondary.opacity(0.1))
.foregroundStyle(isSelected ? tintColor : inactiveColor)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(isSelected ? tintColor : Color.secondary.opacity(0.3), lineWidth: 1)
)
.cornerRadius(4)
}
.buttonStyle(.plain)
}

@ -38,9 +38,10 @@ private func loadVisibleColumnIds() -> Set<String> {
struct TrackTableView: NSViewRepresentable {
let tracks: [Track]
let playingTrackId: Int64?
let sortColumn: String
let sortAscending: Bool
let onSort: (String) -> Void
let onDoubleClick: (Track) -> Void
let onPlayPause: () -> Void
var playlists: [Playlist]
var lastUsedPlaylistName: String?
var selectedPlaylist: Playlist?
@ -48,6 +49,7 @@ struct TrackTableView: NSViewRepresentable {
var onAddToLastPlaylist: ((Track) -> Void)?
var onRemoveFromPlaylist: ((Track) -> Void)?
var onReorder: ((Int, Int) -> Void)?
var scrollToPlayingTrigger: UUID = UUID()
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
@ -63,6 +65,13 @@ struct TrackTableView: NSViewRepresentable {
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 {
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(col.id))
column.title = col.title
@ -76,12 +85,13 @@ struct TrackTableView: NSViewRepresentable {
tableView.addTableColumn(column)
}
tableView.sortDescriptors = [NSSortDescriptor(key: sortColumn, ascending: sortAscending)]
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.doubleAction = #selector(Coordinator.handleDoubleClick(_:))
tableView.target = context.coordinator
tableView.enterAction = #selector(Coordinator.handleEnterKey(_:))
tableView.spaceAction = #selector(Coordinator.handleSpaceKey(_:))
context.coordinator.tableView = tableView
@ -116,9 +126,15 @@ struct TrackTableView: NSViewRepresentable {
let tracksChanged = context.coordinator.tracks != tracks
let playingChanged = context.coordinator.playingTrackId != playingTrackId
let scrollTriggered = context.coordinator.lastScrollTrigger != scrollToPlayingTrigger
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 tableView.registeredDraggedTypes.isEmpty || !tableView.registeredDraggedTypes.contains(.string) {
tableView.registerForDraggedTypes([.string])
@ -128,6 +144,15 @@ struct TrackTableView: NSViewRepresentable {
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 }
let selectedIds = Set(tableView.selectedRowIndexes.compactMap { idx -> Int64? in
@ -156,6 +181,7 @@ struct TrackTableView: NSViewRepresentable {
var parent: TrackTableView
var tracks: [Track] = []
var playingTrackId: Int64?
var lastScrollTrigger: UUID = UUID()
weak var tableView: NSTableView?
init(_ parent: TrackTableView) {
@ -171,6 +197,36 @@ struct TrackTableView: NSViewRepresentable {
let track = tracks[row]
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 cellView: NSTableCellView
if let existing = tableView.makeView(withIdentifier: cellId, owner: nil) as? NSTableCellView {
@ -193,76 +249,59 @@ struct TrackTableView: NSViewRepresentable {
let cell = cellView.textField!
let isPlaying = track.id == parent.playingTrackId
cell.font = isPlaying ? .boldSystemFont(ofSize: 12) : .systemFont(ofSize: 12)
cell.textColor = .secondaryLabelColor
cell.textColor = .labelColor
cell.alignment = .left
switch colId {
case "title":
cell.stringValue = track.title
cell.textColor = .labelColor
case "artist":
cell.stringValue = track.artist
case "albumArtist":
cell.stringValue = track.albumArtist
case "album":
cell.stringValue = track.album
cell.textColor = .tertiaryLabelColor
case "composer":
cell.stringValue = track.composer
case "genre":
cell.stringValue = track.genre
cell.textColor = .tertiaryLabelColor
case "year":
cell.stringValue = track.year.map { String($0) } ?? ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "bpm":
cell.stringValue = track.bpm.map { String($0) } ?? ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "trackNumber":
cell.stringValue = track.trackNumber.map { String($0) } ?? ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "discNumber":
cell.stringValue = track.discNumber.map { String($0) } ?? ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "duration":
cell.stringValue = Self.formatDuration(track.duration)
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "playCount":
cell.stringValue = track.playCount > 0 ? "\(track.playCount)" : ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "lastPlayedAt":
cell.stringValue = track.lastPlayedAt.map { Self.formatDate($0) } ?? ""
cell.textColor = .tertiaryLabelColor
case "rating":
cell.stringValue = track.rating > 0 ? String(repeating: "", count: track.rating) : ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "dateAdded":
cell.stringValue = Self.formatDate(track.dateAdded)
cell.textColor = .tertiaryLabelColor
case "dateModified":
cell.stringValue = Self.formatDate(track.dateModified)
cell.textColor = .tertiaryLabelColor
case "fileFormat":
cell.stringValue = track.fileFormat.uppercased()
cell.textColor = .tertiaryLabelColor
case "bitrate":
cell.stringValue = track.bitrate.map { "\($0) kbps" } ?? ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "sampleRate":
cell.stringValue = track.sampleRate.map { Self.formatSampleRate($0) } ?? ""
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
case "fileSize":
cell.stringValue = Self.formatFileSize(track.fileSize)
cell.textColor = .tertiaryLabelColor
cell.alignment = .right
default:
cell.stringValue = ""
@ -272,9 +311,9 @@ struct TrackTableView: NSViewRepresentable {
}
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
if let sort = tableView.sortDescriptors.first, let key = sort.key {
parent.onSort(key)
}
guard let sort = tableView.sortDescriptors.first, let key = sort.key else { return }
guard key != parent.sortColumn || sort.ascending != parent.sortAscending else { return }
parent.onSort(key)
}
@objc func handleDoubleClick(_ sender: NSTableView) {
@ -289,10 +328,6 @@ struct TrackTableView: NSViewRepresentable {
parent.onDoubleClick(tracks[row])
}
@objc func handleSpaceKey(_ sender: NSTableView) {
parent.onPlayPause()
}
// MARK: - Context Menu
func menuNeedsUpdate(_ menu: NSMenu) {
@ -422,13 +457,10 @@ struct TrackTableView: NSViewRepresentable {
private final class PlayableTableView: NSTableView {
var enterAction: Selector?
var spaceAction: Selector?
override func keyDown(with event: NSEvent) {
if event.keyCode == 36 || event.keyCode == 76, let enterAction, let target {
NSApp.sendAction(enterAction, to: target, from: self)
} else if event.keyCode == 49, let spaceAction, let target {
NSApp.sendAction(spaceAction, to: target, from: self)
} else {
super.keyDown(with: event)
}

Loading…
Cancel
Save