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 }