|
|
|
|
@ -10,19 +10,28 @@ struct ContentView: View { |
|
|
|
|
var shazam: ShazamService |
|
|
|
|
@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 = "" |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
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 +47,12 @@ struct ContentView: View { |
|
|
|
|
.padding(.vertical, 4) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let selected = playlist.selectedPlaylist { |
|
|
|
|
if let selected = playlist.selectedItem { |
|
|
|
|
HStack(spacing: 4) { |
|
|
|
|
Button(action: { playlist.deselectPlaylist() }) { |
|
|
|
|
Button(action: { |
|
|
|
|
playlist.deselectPlaylist() |
|
|
|
|
searchText = "" |
|
|
|
|
}) { |
|
|
|
|
HStack(spacing: 2) { |
|
|
|
|
Image(systemName: "chevron.left") |
|
|
|
|
.font(.system(size: 10)) |
|
|
|
|
@ -64,15 +76,17 @@ struct ContentView: View { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
TrackTableView( |
|
|
|
|
tracks: playlist.selectedPlaylist != nil ? playlist.playlistTracks : library.tracks, |
|
|
|
|
tracks: playlist.selectedItem != nil ? playlist.playlistTracks : library.tracks, |
|
|
|
|
playingTrackId: player.currentTrack?.id, |
|
|
|
|
sortColumn: library.sortColumn, |
|
|
|
|
sortAscending: library.sortAscending, |
|
|
|
|
onSort: { column in |
|
|
|
|
if playlist.selectedPlaylist == nil { |
|
|
|
|
if playlist.selectedItem == nil { |
|
|
|
|
library.sort(by: column) |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
onDoubleClick: { track in |
|
|
|
|
let trackList = playlist.selectedPlaylist != nil ? playlist.playlistTracks : library.tracks |
|
|
|
|
let trackList = playlist.selectedItem != nil ? playlist.playlistTracks : library.tracks |
|
|
|
|
player.setQueue(trackList) |
|
|
|
|
player.play(track) |
|
|
|
|
}, |
|
|
|
|
@ -95,38 +109,44 @@ struct ContentView: View { |
|
|
|
|
if let selected = playlist.selectedPlaylist { |
|
|
|
|
try? playlist.moveTrack(in: selected, from: from, to: to) |
|
|
|
|
} |
|
|
|
|
} : nil |
|
|
|
|
} : nil, |
|
|
|
|
scrollToPlayingTrigger: scrollToPlayingTrigger |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
PlaylistBarView( |
|
|
|
|
playlists: playlist.playlists, |
|
|
|
|
selectedPlaylist: playlist.selectedPlaylist, |
|
|
|
|
onSelect: { playlist.selectPlaylist($0) }, |
|
|
|
|
onDeselect: { playlist.deselectPlaylist() }, |
|
|
|
|
onRename: { p in |
|
|
|
|
playlistToRename = p |
|
|
|
|
playlistNameInput = p.name |
|
|
|
|
playlists: playlist.allPlaylists, |
|
|
|
|
selectedItem: playlist.selectedItem, |
|
|
|
|
onSelect: { item in |
|
|
|
|
playlist.selectItem(item) |
|
|
|
|
if let smart = item as? SmartPlaylist { |
|
|
|
|
searchText = smart.searchQuery |
|
|
|
|
library.search(smart.searchQuery) |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
onDeselect: { |
|
|
|
|
playlist.deselectPlaylist() |
|
|
|
|
searchText = "" |
|
|
|
|
}, |
|
|
|
|
onRename: { item in |
|
|
|
|
itemToRename = item |
|
|
|
|
playlistNameInput = item.name |
|
|
|
|
showRenameAlert = true |
|
|
|
|
}, |
|
|
|
|
onDelete: { p in |
|
|
|
|
try? playlist.deletePlaylist(p) |
|
|
|
|
onDelete: { item in |
|
|
|
|
if let p = item as? Playlist { |
|
|
|
|
try? playlist.deletePlaylist(p) |
|
|
|
|
} else if let sp = item as? SmartPlaylist { |
|
|
|
|
try? playlist.deleteSmartPlaylist(sp) |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
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 |
|
|
|
|
} |
|
|
|
|
.onDrop(of: [.fileURL], isTargeted: nil) { providers in |
|
|
|
|
handleDrop(providers) |
|
|
|
|
@ -146,16 +166,36 @@ struct ContentView: View { |
|
|
|
|
playlistNameInput = "" |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
.alert("Rename Playlist", isPresented: $showRenameAlert) { |
|
|
|
|
TextField("Playlist name", text: $playlistNameInput) |
|
|
|
|
.alert("Rename", isPresented: $showRenameAlert) { |
|
|
|
|
TextField("Name", 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 +220,27 @@ 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 handleDrop(_ providers: [NSItemProvider]) { |
|
|
|
|
for provider in providers { |
|
|
|
|
provider.loadItem(forTypeIdentifier: "public.file-url") { data, _ in |
|
|
|
|
|