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(.bottom, -24) .zIndex(1) // 确保导航栏在最上层 // 主体内容 HStack(spacing: 20) { 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: 10, videoSelectionLimit: 10, onDismiss: handleMediaPickerDismiss ) } .onChange(of: uploadManager.selectedMedia) { newMedia in handleMediaChange(newMedia) } } // MARK: - Private Methods private func handleMediaPickerDismiss() { showMediaPicker = false if !uploadManager.selectedMedia.isEmpty && selectedMedia == nil { selectedMedia = uploadManager.selectedMedia.first selectedIndices = [0] // Start upload when media picker is dismissed with new media uploadManager.startUpload() } } private func handleMediaChange(_ newMedia: [MediaType]) { if newMedia.isEmpty { selectedMedia = nil selectedIndices = [] return } // Only update if needed if selectedIndices.isEmpty || selectedIndices.first! >= newMedia.count { selectedMedia = newMedia.first selectedIndices = [0] } else if let selectedIndex = selectedIndices.first, selectedIndex < newMedia.count { selectedMedia = newMedia[selectedIndex] } // Auto-upload when new media is added if !newMedia.isEmpty && !uploadManager.isUploading { uploadManager.startUpload() } } } // 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) .frame(width: 225, height: 225) .onTapGesture { showMediaPicker = true } } 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 var body: some View { Group { switch media { case .image(let uiImage): Image(uiImage: uiImage) .resizable() .scaledToFit() .cornerRadius(12) .drawingGroup() // Improves performance for large images 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() // Improves performance for thumbnails } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.themeTextWhiteSecondary) .cornerRadius(16) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(Color.themePrimary, lineWidth: 2) ) .contentShape(Rectangle()) // Better tap target } } // MARK: - Preview struct MediaUploadView_Previews: PreviewProvider { static var previews: some View { NavigationView { MediaUploadView() } } }