import SwiftUI import UniformTypeIdentifiers struct BatchUploadView: View { @ObservedObject private var manager = ZipImportManager.shared @ObservedObject private var settings = CompanionSettings.shared @State private var showFilePicker = false @State private var album = "" @State private var artist = "" @State private var albumArtist = "" @State private var genre = "" @State private var year = "" @State private var keepOffline = false private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) var body: some View { NavigationStack { List { if !settings.isEnabled { Section { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .font(.system(size: 28)) .foregroundColor(.yellow) Text("Companion API Not Configured") .font(.system(size: 15, weight: .medium)) .foregroundColor(.white) Text("Set your Companion API host and port in Settings to upload tracks.") .font(.system(size: 13)) .foregroundColor(.gray) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 12) } } else { // Step 1: Import importSection // Step 2: Extracted files if !manager.extractedFiles.isEmpty { metadataSection fileListSection uploadSection } } } .navigationTitle("Upload Music") .navigationBarTitleDisplayMode(.inline) .toolbar { if manager.isUploading { ToolbarItem(placement: .navigationBarTrailing) { Button("Cancel") { manager.cancelAll() } .foregroundColor(.red) } } } .fileImporter( isPresented: $showFilePicker, allowedContentTypes: [UTType.zip, UTType.archive], allowsMultipleSelection: false ) { result in switch result { case .success(let urls): if let url = urls.first { manager.extractZip(at: url) } case .failure(let error): DebugLogger.shared.log("File picker error: \(error.localizedDescription)", category: "Upload") } } } } // MARK: - Import Section private var importSection: some View { Section { Button(action: { manager.reset() showFilePicker = true }) { HStack { Image(systemName: "doc.zipper") .font(.system(size: 20)) .foregroundColor(accentPink) VStack(alignment: .leading, spacing: 2) { Text("Import Zip Archive") .font(.system(size: 15, weight: .medium)) .foregroundColor(.white) Text("Select a .zip containing audio files") .font(.system(size: 12)) .foregroundColor(.gray) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 12)) .foregroundColor(.gray) } } if manager.isExtracting { HStack(spacing: 12) { ProgressView() .tint(accentPink) Text(manager.extractionProgress) .font(.system(size: 13)) .foregroundColor(.gray) } } else if !manager.extractionProgress.isEmpty && manager.extractedFiles.isEmpty { Text(manager.extractionProgress) .font(.system(size: 13)) .foregroundColor(.gray) } } header: { Text("Import") } } // MARK: - Metadata Section private var metadataSection: some View { Section { TextField("Album", text: $album) .textInputAutocapitalization(.words) TextField("Artist", text: $artist) .textInputAutocapitalization(.words) TextField("Album Artist", text: $albumArtist) .textInputAutocapitalization(.words) TextField("Genre", text: $genre) .textInputAutocapitalization(.words) TextField("Year", text: $year) .keyboardType(.numberPad) Toggle("Keep Offline After Upload", isOn: $keepOffline) .tint(accentPink) } header: { Text("Batch Tags") } footer: { Text(keepOffline ? "Uploaded files will be kept in local storage for offline playback." : "Uploaded files will be deleted locally. Re-download from server for offline use.") } } // MARK: - File List private var fileListSection: some View { Section { ForEach(manager.extractedFiles) { file in HStack(spacing: 12) { // Status indicator statusIcon(for: file.filename) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { Text(file.filename) .font(.system(size: 14, weight: .medium)) .foregroundColor(.white) .lineLimit(1) Text(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .file)) .font(.system(size: 11)) .foregroundColor(.gray) } Spacer() // Progress for uploading if case .uploading(let pct) = manager.uploadStates[file.filename] { Text("\(Int(pct * 100))%") .font(.system(size: 11, design: .monospaced)) .foregroundColor(accentPink) } } } } header: { Text("\(manager.extractedFiles.count) Tracks") } } // MARK: - Upload Section private var uploadSection: some View { Section { if manager.isUploading { VStack(spacing: 8) { ProgressView(value: manager.uploadProgress) .tint(accentPink) let completed = manager.uploadStates.values.filter { $0 == .completed }.count let failed = manager.uploadStates.values.filter { if case .failed = $0 { return true } return false }.count Text("Uploading: \(completed)/\(manager.extractedFiles.count) complete" + (failed > 0 ? ", \(failed) failed" : "")) .font(.system(size: 12)) .foregroundColor(.gray) } } else { Button(action: { let meta = UploadMetadata( title: "", // Per-file from filename artist: artist, album: album, albumArtist: albumArtist.isEmpty ? artist : albumArtist, genre: genre, year: year, trackNumber: "" // Auto-assigned per file ) manager.startBatchUpload(metadata: meta, keepOffline: keepOffline) }) { HStack { Image(systemName: "icloud.and.arrow.up") .foregroundColor(.white) Text("Upload All") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) } .frame(maxWidth: .infinity) .padding(.vertical, 8) } .listRowBackground(accentPink) .disabled(artist.isEmpty || album.isEmpty) } } footer: { if !manager.isUploading && (artist.isEmpty || album.isEmpty) { Text("Artist and Album are required.") } } } // MARK: - Status Icon @ViewBuilder private func statusIcon(for filename: String) -> some View { switch manager.uploadStates[filename] { case .pending, .none: Image(systemName: "circle") .font(.system(size: 14)) .foregroundColor(.gray) case .uploading: ProgressView() .scaleEffect(0.7) case .completed: Image(systemName: "checkmark.circle.fill") .font(.system(size: 14)) .foregroundColor(.green) case .failed: Image(systemName: "xmark.circle.fill") .font(.system(size: 14)) .foregroundColor(.red) } } }