// // PlaylistView.swift // Bremer // // Created by yhornisse on 2023/07/17. // import SwiftUI struct PlaylistTopView : View { @State var filterText = "" @State var showedLoginView = false @State var playlistNewName = "" @State var playlistTargetSlug = "" @State var showedNewPlaylistDialog = false @State var showedUpdatePlaylistDialog = false @State var showedErrorAlert = false @State var showedDeletePlaylistDialog = false @State private var errorAlertTitle = "" @EnvironmentObject private var settingViewModel : SettingViewModel @ObservedObject var allPlaylistViewModel = AllPlaylistViewModel() var filterResults: [Playlist] { if filterText.isEmpty { return allPlaylistViewModel.playlists } else { return allPlaylistViewModel.playlists.filter { $0.name.contains(filterText) } } } var body : some View { NavigationView { VStack { if settingViewModel.getSetting().baseUrl == "" { Text("Base URLを設定してください") } else if !allPlaylistViewModel.messageText.isEmpty { if allPlaylistViewModel.hasError { Text(allPlaylistViewModel.messageText) .foregroundColor(.red) if allPlaylistViewModel.messageText == "ログインしてください" { LoginButton(action: { self.showedLoginView = true }) } } else { Text(allPlaylistViewModel.messageText) } } List { ForEach(filterResults, id:\.slug) { playlist in NavigationLink(destination: PlaylistDescView(slug: playlist.slug)) { Text(playlist.name) .buttonStyle(.bordered) .font(.system(size: 16)) .aspectRatio(contentMode: .fit) } .swipeActions(edge: .trailing) { Button { self.playlistTargetSlug = playlist.slug self.showedDeletePlaylistDialog = true } label: { Text("削除") } .tint(.red) Button { self.playlistTargetSlug = playlist.slug self.playlistNewName = playlist.name self.showedUpdatePlaylistDialog = true } label: { Text("変更") } .tint(.green) } } } .listStyle(.plain) .searchable(text: $filterText, prompt: "検索キーワード") .keyboardType(.default) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: { self.playlistNewName = "" self.showedNewPlaylistDialog = true }) { Image(systemName: "plus.app") } } } .alert("新規登録", isPresented: $showedNewPlaylistDialog) { NameAlertView(label: "プレイリスト", act: "追加", onOk: allPlaylistViewModel.createPlaylist) {(message: String) in self.showedErrorAlert = true self.errorAlertTitle = message } } message: { Text("プレイリスト名を入力してください") } .alert("更新", isPresented: $showedUpdatePlaylistDialog) { NameAlertView(label: "プレイリスト", act: "変更", input: playlistNewName) {(name: String) in allPlaylistViewModel.updatePlaylist(playlistTargetSlug, name) } onError : {(message: String) in self.showedErrorAlert = true self.errorAlertTitle = message } } message: { Text("プレイリスト名を入力してください") } .confirmationDialog( "プレイリスト削除", isPresented: $showedDeletePlaylistDialog, actions: { Button("OK", role: .destructive){ allPlaylistViewModel.deletePlaylist(playlistTargetSlug) } Button("キャンセル", role: .cancel){ self.playlistNewName = "" } }, message: { Text( "本当にプレイリストを削除しますか?") }) .alert(errorAlertTitle, isPresented: $showedErrorAlert, actions: { Button(action: { // nop }, label: { Text("確認") }) }) } .sheet(isPresented: $showedLoginView) { LoginWebView() .onDisappear { allPlaylistViewModel.getAllPlaylist() } } .scrollDismissesKeyboard(.immediately) .onAppear { allPlaylistViewModel.bremerApiBaseUrl = settingViewModel.getSetting().baseApiUrl() allPlaylistViewModel.getAllPlaylist() } } } struct PlaylistDescView : View { @EnvironmentObject private var playlistAudioViewModel : PlaylistAudioViewModel @EnvironmentObject private var audioPlayerViewModel : AudioPlayerViewModel @EnvironmentObject private var configViewModel : SettingViewModel var slug : String var body : some View { VStack { if !playlistAudioViewModel.messageText.isEmpty { if (playlistAudioViewModel.hasError) { VStack { Text(playlistAudioViewModel.messageText) .foregroundColor(.red) } } else { Text(playlistAudioViewModel.messageText) } } List { ForEach(playlistAudioViewModel.result, id:\.slug) { audio in Button(action: { audioPlayerViewModel.playAudioList( audioList: playlistAudioViewModel.result, audio: audio, usedBy: "playlist") }) { Text("\(audio.name) - \(audio.album ?? "")") .font(.system(size: 14)) .frame(height: 35, alignment: .center) .aspectRatio(contentMode: .fit) } .swipeActions(edge: .trailing) { Button { // TODO 再生中の時は削除する必要ある self.playlistAudioViewModel.deleteAudioInPlaylist( playlistSlug: slug, audioSlug: audio.slug) } label: { Text("削除") } .tint(.red) } } .onMove {src, dst in self.audioPlayerViewModel.reorderAudioList(from: src, to: dst, usedBy: "playlist") self.playlistAudioViewModel.reorderPlaylist(from: src, to: dst) self.playlistAudioViewModel.save(slug: slug) } } .listStyle(.plain) .keyboardType(.default) Spacer() Divider() AudioPlayerView(audioPlayerViewModel: audioPlayerViewModel) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { NavigationLink(destination: PlaylistAddView(slug: slug)){ Image(systemName: "plus.app") } } } .onAppear { self.playlistAudioViewModel.bremerApiBaseUrl = configViewModel.getSetting().baseApiUrl() self.playlistAudioViewModel.getAllAudio(slug: slug) } } } struct PlaylistAddView : View { @State var slug : String @State private var searchText : String = "" @State private var showedLoginView = false @EnvironmentObject private var playlistAudioViewModel : PlaylistAudioViewModel @EnvironmentObject private var audioPlayerViewModel : AudioPlayerViewModel @EnvironmentObject private var searchAudioViewModel : SearchAudioViewModel @EnvironmentObject private var configViewModel : SettingViewModel @State private var audioTargetSlug = "" @State private var showedErrorAlert = false @State private var errorAlertTitle = "" @State private var audioInfo = AudioInfo() @State private var showedToast = false @State private var toastMessage = "" @State var showedSearchAudioDialog = false @State var showedUpdateView = false var body : some View { NavigationView { ZStack { VStack { if !searchAudioViewModel.messageText.isEmpty { if (searchAudioViewModel.hasError) { VStack { Text(searchAudioViewModel.messageText) .foregroundColor(.red) LoginButton(action: { self.showedLoginView = true }) } } else { Text(searchAudioViewModel.messageText) } } List { ForEach(searchAudioViewModel.result, id:\.slug) { audio in HStack { Button(action: { audioPlayerViewModel.playAudioList( audioList: [audio], audio: audio, usedBy: "playlist-add") }) { Text("\(audio.name) - \(audio.album ?? "")") .font(.system(size: 14)) .frame(height: 35, alignment: .center) .aspectRatio(contentMode: .fit) } .buttonStyle(PlainButtonStyle()) Spacer() Button(action: { playlistAudioViewModel.addAudio(audio: audio) audioPlayerViewModel.addAudio(audio: audio, usedBy: "playlist-\(slug)") playlistAudioViewModel.save(slug: self.slug) self.toastMessage = "\(audio.name) を追加" self.showedToast = true }) { Image(systemName: "plus.app") .resizable() .scaledToFill() .frame(width: 33, height: 33) } .buttonStyle(PlainButtonStyle()) } .swipeActions(edge: .trailing) { Button(action: { self.audioTargetSlug = audio.slug self.audioInfo.audioName = audio.name self.audioInfo.artistName = audio.artist ?? "" self.audioInfo.albumName = audio.album ?? "" self.audioInfo.localPath = audio.localPath self.showedUpdateView = true }) { Text("変更") } .tint(.green) } } } .listStyle(.plain) .searchable(text: $searchText, prompt: "検索キーワード") .onSubmit(of: .search) { searchAudioViewModel.searchAudio(keyword: searchText) } } if showedToast { VStack { Spacer() ToastView(message: toastMessage) } .onAppear(perform: { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { withAnimation { self.showedToast = false } } }) } } } .sheet(isPresented: $showedLoginView) { LoginWebView() } .sheet(isPresented: $showedUpdateView) { RenameAudioView(audioInfo: self.audioInfo){(audioInfo : AudioInfo) in searchAudioViewModel.updateAudio( slug: self.audioTargetSlug, audioName: audioInfo.audioName, artistName: audioInfo.artistName, albumName: audioInfo.albumName) } } .scrollDismissesKeyboard(.immediately) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: { self.showedSearchAudioDialog = true }) { Image(systemName: "text.magnifyingglass") } } } .alert("詳細検索", isPresented: $showedSearchAudioDialog) { SearchDetailAlertView(searchAction: searchAudioViewModel.searchAudio) } message: { Text("新しい名前を入力してください") } .onAppear { searchAudioViewModel.bremerApiBaseUrl = configViewModel.getSetting().baseApiUrl() searchAudioViewModel.result = [] } } }