import SwiftUI // MARK: - Main View @MainActor struct MediaUploadView: View { @StateObject private var uploadManager = MediaUploadManager() @State private var showMediaPicker = false @State private var selectedMedia: MediaType? = nil @State private var selectedIndices: Set = [] var body: some View { VStack() { // 固定的顶部导航栏 HStack { Button(action: { Router.shared.pop() }) { Image(systemName: "chevron.left") .font(.system(size: 17, weight: .semibold)) .foregroundColor(.themeTextMessageMain) } .padding(.leading, 16) Spacer() Text("Complete Your Profile") .font(Typography.font(for: .title2, family: .quicksandBold)) .foregroundColor(.themeTextMessageMain) Spacer() // 添加一个透明的占位视图来平衡布局 Color.clear .frame(width: 24, height: 24) .padding(.trailing, 16) } .background(Color.themeTextWhiteSecondary) .padding(.horizontal) .padding(.bottom, -20) .zIndex(1) // 确保导航栏在最上层 HStack() { Text("The upload process will take approximately 2 minutes. Thank you for your patience. ") .font(Typography.font(for: .caption)) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) .padding(6) .background( LinearGradient( gradient: Gradient(colors: [ Color(red: 1.0, green: 0.97, blue: 0.87), .white, Color(red: 1.0, green: 0.97, blue: 0.84) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) } .padding() Spacer() .frame(height: 20) MainUploadArea( uploadManager: uploadManager, showMediaPicker: $showMediaPicker, selectedMedia: $selectedMedia, selectedIndices: $selectedIndices ) .padding() .id("mainUploadArea\(uploadManager.selectedMedia.count)") Spacer() // Navigation button Button(action: { // Router.shared.navigate(to: .avatarBox) }) { Text("Continue") .font(.headline) .foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain) .frame(maxWidth: .infinity) .frame(height: 56) .background(uploadManager.selectedMedia.isEmpty ? Color.white : Color.themePrimary) .cornerRadius(28) .padding(.horizontal, 24) } .buttonStyle(PlainButtonStyle()) Spacer() } .background(Color.themeTextWhiteSecondary) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) .sheet(isPresented: $showMediaPicker) { MediaPicker( selectedMedia: $uploadManager.selectedMedia, imageSelectionLimit: 20, videoSelectionLimit: 5, onDismiss: handleMediaPickerDismiss, onUploadProgress: { index, progress in // 更新单个文件的上传进度 print("File \(index) upload progress: \(progress * 100)%") } ) } .onChange(of: uploadManager.selectedMedia) { newMedia in handleMediaChange(newMedia) } } // MARK: - Private Methods private func handleMediaPickerDismiss() { self.uploadManager.startUpload() // showMediaPicker = false // // 确保选择器完全关闭后再开始上传 // DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { // if !self.uploadManager.selectedMedia.isEmpty { // self.selectedMedia = self.uploadManager.selectedMedia.first // self.selectedIndices = [0] // // 在选择器完全关闭后再开始上传 // self.uploadManager.startUpload() // } // } } private func handleMediaChange(_ newMedia: [MediaType]) { if newMedia.isEmpty { selectedMedia = nil selectedIndices = [] return } // 只在需要时更新 if selectedIndices.isEmpty || selectedIndices.first! >= newMedia.count { selectedMedia = newMedia.first selectedIndices = [0] } else if let selectedIndex = selectedIndices.first, selectedIndex < newMedia.count { selectedMedia = newMedia[selectedIndex] } // 当有新媒体时自动开始上传 if !newMedia.isEmpty && !isUploading() { uploadManager.startUpload() } } // 添加辅助方法检查上传状态 private func isUploading() -> Bool { return uploadManager.uploadStatus.values.contains { status in if case .uploading = status { return true } return false } } } // MARK: - Main Upload Area struct MainUploadArea: View { @ObservedObject var uploadManager: MediaUploadManager @Binding var showMediaPicker: Bool @Binding var selectedMedia: MediaType? @Binding var selectedIndices: Set var body: some View { VStack(spacing: 16) { Text("Click to upload 20 images and 5 videos to generate your next blind box.") .font(Typography.font(for: .title2, family: .quicksandBold)) .fontWeight(.bold) .foregroundColor(.black) .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .leading) .padding() if let media = selectedMedia { MediaPreview(media: media, uploadManager: uploadManager) .frame(width: 225, height: 225) .onTapGesture { showMediaPicker = true } .padding(.bottom, 10) } else { UploadPromptView(showMediaPicker: $showMediaPicker) } if !uploadManager.selectedMedia.isEmpty { ThumbnailScrollView( uploadManager: uploadManager, selectedIndices: $selectedIndices, selectedMedia: $selectedMedia ) .id("thumbnailScroll\(uploadManager.selectedMedia.count)") } } .background(Color.white) .cornerRadius(16) } } // MARK: - Upload Prompt View struct UploadPromptView: View { @Binding var showMediaPicker: Bool var body: some View { Button(action: { showMediaPicker = true }) { SVGImage(svgName: "IP") .frame(width: 225, height: 225) .contentShape(Rectangle()) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(style: StrokeStyle( lineWidth: 5, lineCap: .round, dash: [12, 8] )) .foregroundColor(Color.themePrimary) ) } } } // MARK: - Thumbnail Scroll View @MainActor struct ThumbnailScrollView: View { @ObservedObject var uploadManager: MediaUploadManager @Binding var selectedIndices: Set @Binding var selectedMedia: MediaType? // Track the currently selected index directly for faster access @State private var selectedIndex: Int = 0 var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(0.. Void var body: some View { Button(action: onTap) { ZStack(alignment: .topTrailing) { Group { if let thumbnail = media.thumbnail { Image(uiImage: thumbnail) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 80, height: 80) .clipped() } else { Color.gray .frame(width: 80, height: 80) } } .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isSelected ? Color.themePrimary : Color.clear, lineWidth: 2) ) // Selection checkmark if isSelected && showCheckmark { Image(systemName: "checkmark.circle.fill") .font(.system(size: 20)) .foregroundColor(.white) .background(Circle().fill(Color.themePrimary)) .offset(x: 6, y: -6) // Adjusted offset to ensure visibility .zIndex(1) // Ensure checkmark is above video icon } // Video icon if media.isVideo { Image(systemName: "video.fill") .font(.caption) .foregroundColor(.white) .padding(4) .background(Color.black.opacity(0.6)) .clipShape(Circle()) .padding(4) .offset(x: -4, y: 4) // Slight adjustment for better positioning } } .frame(width: 80, height: 80) .contentShape(Rectangle()) .padding(4) // Add padding to prevent clipping } .buttonStyle(PlainButtonStyle()) } } // MARK: - Add More Button struct AddMoreButton: View { @Binding var showMediaPicker: Bool var body: some View { Button(action: { showMediaPicker = true }) { Image(systemName: "plus") .font(.title2) .foregroundColor(.gray) .frame(width: 80, height: 80) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(style: StrokeStyle( lineWidth: 1, dash: [5, 3] )) .foregroundColor(.gray) ) } } } // MARK: - Media Preview struct MediaPreview: View { let media: MediaType @ObservedObject var uploadManager: MediaUploadManager private var uploadProgress: Double { // Get the upload progress for this media item using its index if let index = uploadManager.selectedMedia.firstIndex(where: { $0 == media }) { let status = uploadManager.uploadStatus["\(index)"] if case .uploading(let progress) = status { return progress } } return 0 } private var isUploading: Bool { if let index = uploadManager.selectedMedia.firstIndex(where: { $0 == media }) { let status = uploadManager.uploadStatus["\(index)"] if case .uploading = status { return true } } return false } var body: some View { ZStack { // Main media content Group { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .cornerRadius(12) .drawingGroup() case .video(_, let thumbnail): if let thumbnail = thumbnail { Image(uiImage: thumbnail) .resizable() .scaledToFit() .overlay( Image(systemName: "play.circle.fill") .font(.system(size: 48)) .foregroundColor(.white) .shadow(radius: 10) ) .cornerRadius(12) .drawingGroup() } } } // Upload progress border if isUploading { Circle() .stroke( Color.themePrimary.opacity(0.3), style: StrokeStyle(lineWidth: 4, lineCap: .round) ) .rotationEffect(.degrees(-90)) .padding(4) Circle() .trim(from: 0, to: uploadProgress) .stroke( Color.themePrimary, style: StrokeStyle(lineWidth: 4, lineCap: .round) ) .rotationEffect(.degrees(-90)) .animation(.linear, value: uploadProgress) .padding(4) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.themeTextWhiteSecondary) .cornerRadius(16) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(Color.themePrimary, lineWidth: 2) ) .contentShape(Rectangle()) } } // MARK: - Preview struct MediaUploadView_Previews: PreviewProvider { static var previews: some View { NavigationView { MediaUploadView() } } }