/ flo / AlbumView.swift
AlbumView.swift
  1  //
  2  //  AlbumView.swift
  3  //  flo
  4  //
  5  //  Created by rizaldy on 08/06/24.
  6  //
  7  
  8  import NukeUI
  9  import SwiftUI
 10  
 11  struct AlbumView: View {
 12    @Environment(\.dismiss) private var dismiss
 13  
 14    @EnvironmentObject var playerViewModel: PlayerViewModel
 15    @EnvironmentObject var downloadViewModel: DownloadViewModel
 16  
 17    @ObservedObject var viewModel: AlbumViewModel
 18  
 19    @State private var showAlbumInfo: Bool = false
 20  
 21    @State private var shareDescription: String = ""
 22    @State private var generatedShareURL: String = ""
 23    @State private var showShareAlert: Bool = false
 24    @State private var showShareURLAlert: Bool = false
 25  
 26    @State private var showDownloadSheet: Bool = false
 27    @State private var showDeleteAlbumAlert: Bool = false
 28  
 29    var isDownloadScreen: Bool = false
 30  
 31    var body: some View {
 32      ScrollView {
 33        VStack {
 34          if !viewModel.isDownloaded {
 35            LazyImage(url: URL(string: viewModel.album.albumCover)) { state in
 36              if let image = state.image {
 37                image
 38                  .resizable()
 39                  .aspectRatio(contentMode: .fit)
 40                  .frame(width: 300, height: 300)
 41                  .clipShape(
 42                    RoundedRectangle(cornerRadius: 10, style: .continuous)
 43                  )
 44                  .shadow(radius: 5)
 45                  .padding(.top, 10)
 46              } else {
 47                Color("PlayerColor").frame(width: 300, height: 300)
 48                  .cornerRadius(5)
 49                  .padding(.top, 10)
 50              }
 51            }.padding(.bottom, 10)
 52          } else {
 53            if let image = UIImage(
 54              contentsOfFile: viewModel.getAlbumCoverArt(
 55                id: viewModel.album.id, artistName: viewModel.album.artist,
 56                albumName: viewModel.album.name))
 57            {
 58              Image(uiImage: image)
 59                .resizable()
 60                .aspectRatio(contentMode: .fit)
 61                .frame(width: 300, height: 300)
 62                .clipShape(
 63                  RoundedRectangle(cornerRadius: 10, style: .continuous)
 64                )
 65                .shadow(radius: 5)
 66                .padding(.top, 10)
 67            } else {
 68              if let image = UIImage(named: "placeholder") {
 69                Image(uiImage: image)
 70                  .resizable()
 71                  .aspectRatio(contentMode: .fit)
 72                  .frame(width: 300, height: 300)
 73                  .clipShape(
 74                    RoundedRectangle(cornerRadius: 10, style: .continuous)
 75                  )
 76                  .shadow(radius: 5)
 77                  .padding(.top, 10)
 78              }
 79            }
 80          }
 81  
 82          VStack {
 83            Text(viewModel.album.name)
 84              .customFont(.title)
 85              .fontWeight(.bold)
 86              .multilineTextAlignment(.center)
 87              .padding(.bottom, 5)
 88  
 89            Text(viewModel.album.albumArtist)
 90              .customFont(.title3)
 91              .multilineTextAlignment(.center)
 92              .padding(.bottom, 10)
 93  
 94            HStack {
 95              Text(viewModel.album.genre)
 96                .customFont(.subheadline)
 97                .fontWeight(.medium)
 98  
 99              if !viewModel.album.genre.isEmpty && viewModel.album.minYear != 0 {
100                Text("•")
101                  .customFont(.subheadline)
102                  .fontWeight(.medium)
103              }
104  
105              Text(viewModel.album.minYear == 0 ? "" : viewModel.album.minYear.description)
106                .customFont(.subheadline)
107                .fontWeight(.medium)
108            }
109  
110            HStack(spacing: 20) {
111              Button(action: {
112                playerViewModel.playItem(
113                  item: viewModel.album,
114                  isFromLocal: viewModel.isDownloaded)
115              }) {
116                Text("Play")
117                  .foregroundColor(.white)
118                  .customFont(.headline)
119                  .padding(.vertical, 10)
120                  .padding(.horizontal, 30)
121                  .background(Color("PlayerColor"))
122                  .cornerRadius(5)
123              }.disabled(viewModel.album.songs.isEmpty)
124  
125              Button(action: {
126                playerViewModel.shuffleItem(
127                  item: viewModel.album,
128                  isFromLocal: viewModel.isDownloaded)
129              }) {
130                Text("Shuffle")
131                  .foregroundColor(.white)
132                  .customFont(.headline)
133                  .padding(.vertical, 10)
134                  .padding(.horizontal, 30)
135                  .background(Color("PlayerColor"))
136                  .cornerRadius(5)
137              }.disabled(viewModel.album.songs.isEmpty)
138            }.padding(10)
139          }.padding(10)
140  
141          if viewModel.isLoading {
142            ProgressView()
143          }
144  
145          SongView(
146            viewModel: viewModel, playerViewModel: playerViewModel, isDownloadScreen: isDownloadScreen
147          )
148          .environmentObject(downloadViewModel)
149  
150        }.padding(
151          .bottom, playerViewModel.hasNowPlaying() && !playerViewModel.shouldHidePlayer ? 100 : 10)
152      }
153      .toolbar {
154        if !isDownloadScreen {
155          DownloadButton(
156            isDownloading: downloadViewModel.isDownloading(viewModel.album.name),
157            isDownloaded: downloadViewModel.isDownloading(viewModel.album.name)
158              ? downloadViewModel.isDownloaded(viewModel.album.name) : viewModel.isDownloaded,
159            progress: downloadViewModel.getDownloadedTrackProgress(albumName: viewModel.album.name)
160              / 100
161          ) {
162            if viewModel.isDownloaded {
163              showDeleteAlbumAlert.toggle()
164            } else {
165              if downloadViewModel.isDownloading(viewModel.album.name) {
166                downloadViewModel.cancelCurrentAlbumDownload(albumName: viewModel.album.name)
167              } else {
168                viewModel.downloadAlbum(viewModel.album)
169                downloadViewModel.addItem(viewModel.album)
170              }
171            }
172          }
173  
174          Menu {
175            Button(
176              "Album Info",
177              action: {
178                self.showAlbumInfo.toggle()
179                viewModel.getAlbumInfo()
180              })
181            Button(
182              "Share",
183              action: {
184                self.showShareAlert = true
185              })
186          } label: {
187            Label("", systemImage: "ellipsis.circle")
188          }
189        } else {
190          Button(action: {
191            showDeleteAlbumAlert.toggle()
192          }) {
193            Label("", systemImage: "checkmark.circle.fill")
194          }
195        }
196  
197        if downloadViewModel.hasDownloadQueue() {
198          Button(action: {
199            showDownloadSheet.toggle()
200          }) {
201            Label("", systemImage: "icloud.and.arrow.down")
202          }
203        }
204      }
205      .alert("'\(viewModel.album.name)' has been downloaded", isPresented: $showDeleteAlbumAlert) {
206        Button("Cancel", role: .cancel) {
207          showDeleteAlbumAlert.toggle()
208        }
209        if !isDownloadScreen {
210          Button("Redownload Album") {
211            downloadViewModel.addItem(viewModel.album)
212          }
213          Button("Redownload Album (force)", role: .destructive) {
214            downloadViewModel.addItem(viewModel.album, forceAll: true)
215          }
216        }
217        Button("Remove Download", role: .destructive) {
218          viewModel.removeDownloadedAlbum(album: viewModel.album)
219          if isDownloadScreen {
220            dismiss()
221            viewModel.fetchDownloadedAlbums()
222          } else {
223            downloadViewModel.clearCurrentAlbumDownload(albumName: viewModel.album.name)
224          }
225        }
226      }
227      .onReceive(downloadViewModel.$downloadWatcher) { newValue in
228        if newValue {
229          viewModel.setActiveAlbum(album: viewModel.album)
230          downloadViewModel.downloadWatcher = false  // uh anti pattern
231        }
232      }
233      .alert(
234        "Share album '\(viewModel.album.name)'",
235        isPresented: $showShareAlert
236      ) {
237        Button("Cancel", role: .cancel) {
238          self.shareDescription = ""
239        }
240        Button("Share") {
241          self.viewModel.shareAlbum(description: self.shareDescription) { result in
242            UIPasteboard.general.string = result
243  
244            self.generatedShareURL = result
245            self.showShareAlert = false
246            self.showShareURLAlert = true
247          }
248        }
249        TextField("Description (i.e: for my wife)", text: $shareDescription)
250      } message: {
251        Text(
252          "Share features with Download option is disabled, please update directly in Navidrome UI if needed"
253        )
254      }.alert(
255        "Link copied to clipboard! (\(self.generatedShareURL))",
256        isPresented: $showShareURLAlert
257      ) {
258        Button("OK", role: .cancel) {
259          self.shareDescription = ""
260          self.generatedShareURL = ""
261  
262          self.showShareURLAlert = false
263        }
264      }
265      .sheet(isPresented: $showAlbumInfo) {
266        AlbumInfo(album: viewModel.album)
267      }
268      .sheet(isPresented: $showDownloadSheet) {
269        DownloadQueueView().environmentObject(downloadViewModel)
270          .onDisappear {
271            viewModel.setActiveAlbum(album: viewModel.album)
272          }
273      }
274    }
275  }
276  
277  struct AlbumViewPreview_Previews: PreviewProvider {
278    static var songs: [Song] = [
279      Song(
280        id: "0", title: "Song 1", albumId: "", albumName: "Album name", artist: "",
281        trackNumber: 1, discNumber: 0, bitRate: 0,
282        sampleRate: 44100,
283        suffix: "mp4a", duration: 200, mediaFileId: "0"),
284      Song(
285        id: "1", title: "Song 2", albumId: "", albumName: "Album name", artist: "Artist Name",
286        trackNumber: 2, discNumber: 0, bitRate: 0,
287        sampleRate: 44100,
288        suffix: "mp4a", duration: 200, mediaFileId: "1"),
289      Song(
290        id: "2", title: "Song 3", albumId: "", albumName: "Album name", artist: "Artist Name",
291        trackNumber: 3, discNumber: 0, bitRate: 0,
292        sampleRate: 44100,
293        suffix: "mp4a", duration: 200, mediaFileId: "2"),
294      Song(
295        id: "3", title: "Song 4", albumId: "", albumName: "Album name", artist: "Artist Name",
296        trackNumber: 4, discNumber: 0, bitRate: 0,
297        sampleRate: 44100,
298        suffix: "mp4a", duration: 200, mediaFileId: "3"),
299      Song(
300        id: "4", title: "Song 6", albumId: "", albumName: "Album name", artist: "Artist Name",
301        trackNumber: 5, discNumber: 0, bitRate: 0,
302        sampleRate: 44100,
303        suffix: "mp4a", duration: 200, mediaFileId: "4"),
304      Song(
305        id: "5", title: "Song 6", albumId: "", albumName: "Album name", artist: "Artist Name",
306        trackNumber: 6, discNumber: 0, bitRate: 0,
307        sampleRate: 44100,
308        suffix: "mp4a", duration: 200, mediaFileId: "5"),
309      Song(
310        id: "6", title: "Song 7", albumId: "", albumName: "Album name", artist: "Artist Name",
311        trackNumber: 7, discNumber: 0, bitRate: 0,
312        sampleRate: 44100,
313        suffix: "mp4a", duration: 200, mediaFileId: "6"),
314      Song(
315        id: "7", title: "Song 8", albumId: "", albumName: "Album name", artist: "Artist Name",
316        trackNumber: 8, discNumber: 0, bitRate: 0,
317        sampleRate: 44100,
318        suffix: "mp4a", duration: 200, mediaFileId: "7"),
319    ]
320  
321    static var album: Album = Album(
322      name: "Album name", artist: "Artist name", songs: songs)
323  
324    @StateObject static var viewModel: AlbumViewModel = AlbumViewModel(album: album)
325  
326    static var previews: some View {
327      AlbumView(viewModel: viewModel).environmentObject(
328        PlayerViewModel())
329    }
330  }
331  
332  extension AlbumView {
333    struct AlbumInfo: View {
334      var album: Album
335  
336      var body: some View {
337        ScrollView {
338          VStack {
339            Spacer()
340  
341            LazyImage(url: URL(string: album.albumCover)) { state in
342              if let image = state.image {
343                image
344                  .resizable()
345                  .aspectRatio(contentMode: .fit)
346                  .frame(width: 300, height: 300)
347                  .clipShape(
348                    RoundedRectangle(cornerRadius: 10, style: .continuous)
349                  )
350                  .shadow(radius: 5)
351                  .padding(.top, 10)
352              } else {
353                Color("PlayerColor").frame(width: 300, height: 300)
354                  .cornerRadius(5)
355                  .padding(.top, 10)
356              }
357            }.padding()
358  
359            VStack {
360  
361              Text(album.name)
362                .customFont(.title2)
363                .fontWeight(.bold)
364                .multilineTextAlignment(.center)
365                .padding(.bottom, 5)
366  
367              Text(album.albumArtist)
368                .customFont(.title3)
369                .multilineTextAlignment(.center)
370                .padding(.bottom, 10)
371  
372              Text(
373                "\(album.genre.isEmpty ? "Unknown genre" : album.genre) • \(album.minYear == 0 ? "Unknown release year" : album.minYear.description)"
374              )
375              .customFont(.subheadline)
376              .fontWeight(.medium)
377            }.padding(.bottom, 20)
378  
379            Spacer()
380  
381            Text(album.info)
382              .customFont(.subheadline)
383              .multilineTextAlignment(.center)
384              .lineSpacing(5)
385  
386            Spacer()
387          }.padding()
388          Spacer()
389        }
390      }
391    }
392  }