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: { // 只处理界面相关的逻辑 showMediaPicker = false }, onUploadProgress: { index, progress in print("File \(index) upload progress: \(progress * 100)%") } ) } .onChange(of: uploadManager.selectedMedia) { newMedia in print("onChange1111111", uploadManager.selectedMedia) // 在这里处理媒体变化 if !newMedia.isEmpty { // 当有新的媒体时开始上传 uploadManager.startUpload() } } } // MARK: - Private Methods private func handleMediaPickerDismiss() { self.uploadManager.startUpload() print("handleMediaPickerDismiss1111111", uploadManager.selectedMedia) // 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) .padding(.horizontal) if !uploadManager.selectedMedia.isEmpty { // 显示媒体预览网格 ScrollView { LazyVGrid(columns: [ GridItem(.flexible(), spacing: 16), GridItem(.flexible(), spacing: 16) ], spacing: 16) { ForEach(0.. @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) { // Main thumbnail content ZStack { 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) ) // Upload progress border if let uploadManager = uploadManager, let index = uploadManager.selectedMedia.firstIndex(where: { $0 == media }) { let status = uploadManager.uploadStatus["\(index)"] if case .uploading(let progress) = status, progress > 0 && progress < 1 { ZStack { Circle() .stroke( Color.themePrimary.opacity(0.3), style: StrokeStyle(lineWidth: 2, lineCap: .round) ) .rotationEffect(.degrees(-90)) .padding(2) Circle() .trim(from: 0, to: progress) .stroke( Color.themePrimary, style: StrokeStyle(lineWidth: 2, lineCap: .round) ) .rotationEffect(.degrees(-90)) .animation(.linear, value: progress) .padding(2) } .frame(width: 20, height: 20) .offset(x: 30, y: -30) } } } // 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) .zIndex(1) } // 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) } } .frame(width: 80, height: 80) .contentShape(Rectangle()) .padding(4) } .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 { guard let index = uploadManager.selectedMedia.firstIndex(where: { $0 == media }), case .uploading(let progress) = uploadManager.uploadStatus["\(index)"] else { return 0 } return progress } var body: some View { ZStack { // 媒体内容 Group { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFill() case .video(_, let thumbnail): if let thumbnail = thumbnail { Image(uiImage: thumbnail) .resizable() .scaledToFill() .overlay( Image(systemName: "play.circle.fill") .font(.system(size: 36)) .foregroundColor(.white) .shadow(radius: 8) ) } else { Color.gray } } } .aspectRatio(1, contentMode: .fill) .clipped() .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.themePrimary.opacity(0.3), lineWidth: 1) ) } } } // MARK: - Preview struct MediaUploadView_Previews: PreviewProvider { static var previews: some View { NavigationView { MediaUploadView() } } }