@ -8,21 +8,43 @@ struct ContentView: View {
var audio : AudioService
var playlist : PlaylistViewModel
var shazam : ShazamService
var db : DatabaseService
@ Binding var showNewPlaylistAlert : Bool
@ State private var showRenameAlert = false
@ State private var showEditQueryAlert = false
@ State private var playlistNameInput = " "
@ State private var playlistToRename : Playlist ?
@ State private var editQueryInput = " "
@ State private var itemToRename : ( any PlaylistRepresentable ) ?
@ State private var smartPlaylistToEdit : SmartPlaylist ?
@ State private var scrollToPlayingTrigger = UUID ( )
@ State private var searchText = " "
@ 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 ] = [ ]
var body : some View {
VStack ( spacing : 0 ) {
SearchBarView (
trackCount : playlist . selectedPlaylist != nil ? playlist . playlistTracks . count : library . trackCount ,
searchText : $ searchText ,
trackCount : playlist . selectedItem != nil ? playlist . playlistTracks . count : library . trackCount ,
onSearch : { text in
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 )
}
} ,
onSaveSearch : { query in
try ? playlist . createSmartPlaylist ( searchQuery : query )
} ,
isShazamListening : shazam . isListening ,
onShazam : { shazam . isListening ? shazam . stopListening ( ) : shazam . startListening ( ) }
)
@ -38,9 +60,13 @@ struct ContentView: View {
. padding ( . vertical , 4 )
}
if let selected = playlist . selectedPlaylist {
if showHome || playlist . selectedItem != nil {
HStack ( spacing : 4 ) {
Button ( action : { playlist . deselectPlaylist ( ) } ) {
Button ( action : {
playlist . deselectPlaylist ( )
searchText = " "
showHome = false
} ) {
HStack ( spacing : 2 ) {
Image ( systemName : " chevron.left " )
. font ( . system ( size : 10 ) )
@ -55,7 +81,7 @@ struct ContentView: View {
. font ( . system ( size : 12 ) )
. foregroundStyle ( . quaternary )
Text ( selected . name )
Text ( showHome ? " Home " : ( playlist . s electedItem ? . name ? ? " " ) )
. font ( . system ( size : 12 , weight : . medium ) )
}
. padding ( . horizontal , 12 )
@ -63,71 +89,114 @@ struct ContentView: View {
. frame ( maxWidth : . infinity , alignment : . leading )
}
TrackTableView (
tracks : playlist . selectedPlaylist != nil ? playlist . playlistTracks : library . tracks ,
playingTrackId : player . currentTrack ? . id ,
onSort : { column in
if playlist . selectedPlaylist = = nil {
library . sort ( by : column )
if showHome && playlist . selectedItem = = nil {
HomeView (
recentTracks : recentTracks ,
trackCount : library . trackCount ,
totalDuration : totalDuration ,
monthlyAdditions : monthlyAdditions ,
onTrackDoubleClick : { track in
player . setQueue ( recentTracks )
player . play ( track )
} ,
onShowAll : {
showHome = false
}
)
. onAppear { loadHomeData ( ) }
} else {
TrackTableView (
tracks : playlist . selectedItem != nil ? playlist . playlistTracks : library . tracks ,
playingTrackId : player . currentTrack ? . id ,
sortColumn : playlist . selectedSmartPlaylist != nil ? playlist . sortColumn : library . sortColumn ,
sortAscending : playlist . selectedSmartPlaylist != nil ? playlist . sortAscending : library . sortAscending ,
onSort : { column in
if playlist . selectedSmartPlaylist != nil {
playlist . sort ( by : column )
} else if playlist . selectedItem = = nil {
library . sort ( by : column )
}
} ,
onDoubleClick : { track in
let trackList = playlist . selectedItem != nil ? playlist . playlistTracks : library . tracks
player . setQueue ( trackList )
player . play ( track )
} ,
playlists : playlist . playlists ,
lastUsedPlaylistName : playlist . lastUsedPlaylistName ,
selectedPlaylist : playlist . selectedPlaylist ,
onAddToPlaylist : { track , targetPlaylist in
try ? playlist . addTrack ( track , to : targetPlaylist )
} ,
onAddToLastPlaylist : { track in
try ? playlist . addTrackToLastUsedPlaylist ( track )
} ,
onRemoveFromPlaylist : playlist . selectedPlaylist != nil ? { track in
if let selected = playlist . selectedPlaylist {
try ? playlist . removeTrack ( track , from : selected )
}
} : nil ,
onReorder : playlist . selectedPlaylist != nil ? { from , to in
if let selected = playlist . selectedPlaylist {
try ? playlist . moveTrack ( in : selected , from : from , to : to )
}
} : nil ,
scrollToPlayingTrigger : scrollToPlayingTrigger
)
}
PlaylistBarView (
playlists : playlist . allPlaylists ,
selectedItem : showHome ? nil : playlist . selectedItem ,
isHomeSelected : showHome ,
onHomeSelect : {
if showHome {
showHome = false
} else {
playlist . deselectPlaylist ( )
searchText = " "
showHome = true
}
} ,
onDoubleClick : { track in
let trackList = playlist . selectedPlaylist != nil ? playlist . playlistTracks : library . tracks
player . setQueue ( trackList )
player . play ( track )
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
library . search ( smart . searchQuery )
}
} ,
onPlayPause : { audio . togglePlayPause ( ) } ,
playlists : playlist . playlists ,
lastUsedPlaylistName : playlist . lastUsedPlaylistName ,
selectedPlaylist : playlist . selectedPlaylist ,
onAddToPlaylist : { track , targetPlaylist in
try ? playlist . addTrack ( track , to : targetPlaylist )
onDeselect : {
playlist . deselectPlaylist ( )
searchText = " "
} ,
onAddToLastPlaylist : { track in
try ? playlist . addTrackToLastUsedPlaylist ( track )
onRename : { item in
itemToRename = item
playlistNameInput = item . name
showRenameAlert = true
} ,
onRemoveFromPlaylist : playlist . selectedPlaylist != nil ? { track in
if let selected = playlist . selectedPlaylist {
try ? playlist . removeTrack ( track , from : selected )
onDelete : { item in
if let p = item as ? Playlist {
try ? playlist . deletePlaylist ( p )
} else if let sp = item as ? SmartPlaylist {
try ? playlist . deleteSmartPlaylist ( sp )
}
} : nil ,
onReorder : playlist . selectedPlaylist != nil ? { from , to in
if let selected = playlist . selectedPlaylist {
try ? playlist . moveTrack ( in : selected , from : from , to : to )
}
} : nil
)
PlaylistBarView (
playlists : playlist . playlists ,
selectedPlaylist : playlist . selectedPlaylist ,
onSelect : { playlist . selectPlaylist ( $0 ) } ,
onDeselect : { playlist . deselectPlaylist ( ) } ,
onRename : { p in
playlistToRename = p
playlistNameInput = p . name
showRenameAlert = true
} ,
onDelete : { p in
try ? playlist . deletePlaylist ( p )
onEditQuery : { smart in
smartPlaylistToEdit = smart
editQueryInput = smart . searchQuery
showEditQueryAlert = true
}
)
PlayerControlsView (
currentTrack : player . currentTrack ,
isPlaying : audio . isPlaying ,
currentTime : audio . currentTime ,
duration : audio . duration ,
volume : audio . volume ,
isShuffled : player . isShuffled ,
onPlayPause : { audio . togglePlayPause ( ) } ,
onNext : { player . next ( ) } ,
onPrevious : { player . previous ( ) } ,
onSeek : { audio . seek ( to : $0 ) } ,
onVolumeChange : { audio . volume = $0 } ,
onShuffleToggle : { player . toggleShuffle ( ) }
)
playerControls
}
. onAppear { installKeyboardMonitor ( ) }
. onDisappear { removeKeyboardMonitor ( ) }
. onDrop ( of : [ . fileURL ] , isTargeted : nil ) { providers in
handleDrop ( providers )
return true
@ -135,6 +204,9 @@ struct ContentView: View {
. onChange ( of : audio . currentTime ) { _ , _ in
player . checkHalfway ( )
}
. onChange ( of : library . trackCount ) { _ , _ in
if showHome { loadHomeData ( ) }
}
. alert ( " New Playlist " , isPresented : $ showNewPlaylistAlert ) {
TextField ( " Playlist name " , text : $ playlistNameInput )
Button ( " Cancel " , role : . cancel ) { playlistNameInput = " " }
@ -146,16 +218,36 @@ struct ContentView: View {
playlistNameInput = " "
}
}
. alert ( " Rename Playlist " , isPresented : $ showRenameAlert ) {
TextField ( " Playlist n ame" , text : $ playlistNameInput )
. alert ( " Rename " , isPresented : $ showRenameAlert ) {
TextField ( " N ame" , text : $ playlistNameInput )
Button ( " Cancel " , role : . cancel ) { playlistNameInput = " " }
Button ( " Rename " ) {
let name = playlistNameInput . trimmingCharacters ( in : . whitespaces )
if ! name . isEmpty , let p = playlistToRename {
try ? playlist . renamePlaylist ( p , to : name )
if ! name . isEmpty , let item = itemToRename {
if let p = item as ? Playlist {
try ? playlist . renamePlaylist ( p , to : name )
} else if let sp = item as ? SmartPlaylist {
try ? playlist . renameSmartPlaylist ( sp , to : name )
}
}
playlistNameInput = " "
playlistToRename = nil
itemToRename = nil
}
}
. alert ( " Edit Search Query " , isPresented : $ showEditQueryAlert ) {
TextField ( " Search query " , text : $ editQueryInput )
Button ( " Cancel " , role : . cancel ) { editQueryInput = " " }
Button ( " Save " ) {
let query = editQueryInput . trimmingCharacters ( in : . whitespaces )
if ! query . isEmpty , let sp = smartPlaylistToEdit {
try ? playlist . updateSmartPlaylistQuery ( sp , to : query )
if playlist . selectedSmartPlaylist ? . id = = sp . id {
searchText = query
library . search ( query )
}
}
editQueryInput = " "
smartPlaylistToEdit = nil
}
}
. alert ( " Song Identified " , isPresented : Binding (
@ -180,6 +272,65 @@ struct ContentView: View {
}
}
private var playerControls : some View {
PlayerControlsView (
currentTrack : player . currentTrack ,
isPlaying : audio . isPlaying ,
currentTime : audio . currentTime ,
duration : audio . duration ,
volume : audio . volume ,
isShuffled : player . isShuffled ,
onPlayPause : { audio . togglePlayPause ( ) } ,
onNext : { player . next ( ) } ,
onPrevious : { player . previous ( ) } ,
onSeek : { audio . seek ( to : $0 ) } ,
onScrubStart : { audio . beginScrubbing ( ) } ,
onScrub : { audio . scrub ( to : $0 ) } ,
onScrubEnd : { audio . endScrubbing ( at : $0 ) } ,
onVolumeChange : { audio . volume = $0 } ,
onShuffleToggle : { player . toggleShuffle ( ) } ,
onNowPlayingTap : { scrollToPlayingTrigger = UUID ( ) }
)
}
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 : // s p a c e
audio . togglePlayPause ( )
return nil
case 123 : // l e f t a r r o w
player . previous ( )
return nil
case 124 : // r i g h t a r r o w
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
monthlyAdditions = ( try ? db . fetchMonthlyAdditions ( months : 12 ) ) ? ? [ ]
}
private func handleDrop ( _ providers : [ NSItemProvider ] ) {
for provider in providers {
provider . loadItem ( forTypeIdentifier : " public.file-url " ) { data , _ in