feat: 多选先关闭弹窗再上传

This commit is contained in:
jinyaqiu 2025-08-25 14:46:10 +08:00
parent daf8476dd4
commit 2a9ef552fa
2 changed files with 136 additions and 54 deletions

View File

@ -57,6 +57,7 @@ struct MediaPicker: UIViewControllerRepresentable {
let onDismiss: (() -> Void)? let onDismiss: (() -> Void)?
let allowedMediaTypes: MediaTypeFilter let allowedMediaTypes: MediaTypeFilter
let selectionMode: SelectionMode let selectionMode: SelectionMode
let onUploadProgress: ((Int, Double) -> Void)?
/// ///
enum SelectionMode { enum SelectionMode {
@ -72,19 +73,22 @@ struct MediaPicker: UIViewControllerRepresentable {
} }
init(selectedMedia: Binding<[MediaType]>, init(selectedMedia: Binding<[MediaType]>,
imageSelectionLimit: Int = 10, imageSelectionLimit: Int = 10,
videoSelectionLimit: Int = 10, videoSelectionLimit: Int = 10,
allowedMediaTypes: MediaTypeFilter = .all, allowedMediaTypes: MediaTypeFilter = .all,
selectionMode: SelectionMode = .multiple, selectionMode: SelectionMode = .multiple,
onDismiss: (() -> Void)? = nil) { onDismiss: (() -> Void)? = nil,
onUploadProgress: ((Int, Double) -> Void)? = nil) {
self._selectedMedia = selectedMedia self._selectedMedia = selectedMedia
self.imageSelectionLimit = imageSelectionLimit self.imageSelectionLimit = imageSelectionLimit
self.videoSelectionLimit = videoSelectionLimit self.videoSelectionLimit = videoSelectionLimit
self.allowedMediaTypes = allowedMediaTypes self.allowedMediaTypes = allowedMediaTypes
self.selectionMode = selectionMode self.selectionMode = selectionMode
self.onDismiss = onDismiss self.onDismiss = onDismiss
self.onUploadProgress = onUploadProgress
} }
func makeUIViewController(context: Context) -> PHPickerViewController { func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = allowedMediaTypes.pickerFilter configuration.filter = allowedMediaTypes.pickerFilter
@ -182,17 +186,26 @@ struct MediaPicker: UIViewControllerRepresentable {
return return
} }
// //
processSelectedMedia(results: results, picker: picker, processedMedia: &processedMedia) picker.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
//
var processedMedia = self.parent.selectionMode == .single ? [] : self.parent.selectedMedia
self.processSelectedMedia(results: results, picker: picker, processedMedia: &processedMedia)
// onDismiss
self.parent.onDismiss?()
}
} }
private func processSelectedMedia(results: [PHPickerResult], private func processSelectedMedia(results: [PHPickerResult],
picker: PHPickerViewController, picker: PHPickerViewController,
processedMedia: inout [MediaType]) { processedMedia: inout [MediaType]) {
let group = DispatchGroup() let group = DispatchGroup()
let mediaCollector = MediaCollector() let mediaCollector = MediaCollector()
for result in results { for (index, result) in results.enumerated() {
let itemProvider = result.itemProvider let itemProvider = result.itemProvider
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
@ -202,6 +215,10 @@ struct MediaPicker: UIViewControllerRepresentable {
processImage(itemProvider: itemProvider) { media in processImage(itemProvider: itemProvider) { media in
if let media = media { if let media = media {
mediaCollector.add(media: media) mediaCollector.add(media: media)
//
DispatchQueue.main.async {
self.parent.onUploadProgress?(index, 1.0) //
}
} }
group.leave() group.leave()
} }
@ -212,21 +229,19 @@ struct MediaPicker: UIViewControllerRepresentable {
processVideo(itemProvider: itemProvider) { media in processVideo(itemProvider: itemProvider) { media in
if let media = media { if let media = media {
mediaCollector.add(media: media) mediaCollector.add(media: media)
//
DispatchQueue.main.async {
self.parent.onUploadProgress?(index, 1.0) //
}
} }
group.leave() group.leave()
} }
} }
} }
// Create a local copy of the parent reference
let parent = self.parent
group.notify(queue: .main) { group.notify(queue: .main) {
let finalMedia = mediaCollector.mediaItems let finalMedia = mediaCollector.mediaItems
parent.selectedMedia = finalMedia self.parent.selectedMedia = finalMedia
picker.dismiss(animated: true) {
parent.onDismiss?()
}
} }
} }

View File

@ -35,10 +35,11 @@ struct MediaUploadView: View {
.padding(.trailing, 16) .padding(.trailing, 16)
} }
.background(Color.themeTextWhiteSecondary) .background(Color.themeTextWhiteSecondary)
.padding(.bottom, -24) .padding(.horizontal)
.padding(.bottom, -20)
.zIndex(1) // .zIndex(1) //
//
HStack(spacing: 20) { HStack() {
Text("The upload process will take approximately 2 minutes. Thank you for your patience. ") Text("The upload process will take approximately 2 minutes. Thank you for your patience. ")
.font(Typography.font(for: .caption)) .font(Typography.font(for: .caption))
.foregroundColor(.black) .foregroundColor(.black)
@ -95,9 +96,13 @@ struct MediaUploadView: View {
.sheet(isPresented: $showMediaPicker) { .sheet(isPresented: $showMediaPicker) {
MediaPicker( MediaPicker(
selectedMedia: $uploadManager.selectedMedia, selectedMedia: $uploadManager.selectedMedia,
imageSelectionLimit: 10, imageSelectionLimit: 20,
videoSelectionLimit: 10, videoSelectionLimit: 5,
onDismiss: handleMediaPickerDismiss onDismiss: handleMediaPickerDismiss,
onUploadProgress: { index, progress in
//
print("File \(index) upload progress: \(progress * 100)%")
}
) )
} }
.onChange(of: uploadManager.selectedMedia) { newMedia in .onChange(of: uploadManager.selectedMedia) { newMedia in
@ -108,13 +113,18 @@ struct MediaUploadView: View {
// MARK: - Private Methods // MARK: - Private Methods
private func handleMediaPickerDismiss() { private func handleMediaPickerDismiss() {
showMediaPicker = false self.uploadManager.startUpload()
if !uploadManager.selectedMedia.isEmpty && selectedMedia == nil { // showMediaPicker = false
selectedMedia = uploadManager.selectedMedia.first
selectedIndices = [0] // //
// Start upload when media picker is dismissed with new media // DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
uploadManager.startUpload() // if !self.uploadManager.selectedMedia.isEmpty {
} // self.selectedMedia = self.uploadManager.selectedMedia.first
// self.selectedIndices = [0]
// //
// self.uploadManager.startUpload()
// }
// }
} }
private func handleMediaChange(_ newMedia: [MediaType]) { private func handleMediaChange(_ newMedia: [MediaType]) {
@ -124,7 +134,7 @@ struct MediaUploadView: View {
return return
} }
// Only update if needed //
if selectedIndices.isEmpty || selectedIndices.first! >= newMedia.count { if selectedIndices.isEmpty || selectedIndices.first! >= newMedia.count {
selectedMedia = newMedia.first selectedMedia = newMedia.first
selectedIndices = [0] selectedIndices = [0]
@ -132,11 +142,21 @@ struct MediaUploadView: View {
selectedMedia = newMedia[selectedIndex] selectedMedia = newMedia[selectedIndex]
} }
// Auto-upload when new media is added //
if !newMedia.isEmpty && !uploadManager.isUploading { if !newMedia.isEmpty && !isUploading() {
uploadManager.startUpload() 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 // MARK: - Main Upload Area
@ -158,9 +178,10 @@ struct MainUploadArea: View {
.padding() .padding()
if let media = selectedMedia { if let media = selectedMedia {
MediaPreview(media: media) MediaPreview(media: media, uploadManager: uploadManager)
.frame(width: 225, height: 225) .frame(width: 225, height: 225)
.onTapGesture { showMediaPicker = true } .onTapGesture { showMediaPicker = true }
.padding(.bottom, 10)
} else { } else {
UploadPromptView(showMediaPicker: $showMediaPicker) UploadPromptView(showMediaPicker: $showMediaPicker)
} }
@ -333,31 +354,77 @@ struct AddMoreButton: View {
struct MediaPreview: View { struct MediaPreview: View {
let media: MediaType 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 { var body: some View {
Group { ZStack {
switch media { // Main media content
case .image(let uiImage): Group {
Image(uiImage: uiImage) switch media {
.resizable() case .image(let uiImage):
.scaledToFit() Image(uiImage: uiImage)
.cornerRadius(12)
.drawingGroup() // Improves performance for large images
case .video(_, let thumbnail):
if let thumbnail = thumbnail {
Image(uiImage: thumbnail)
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.overlay(
Image(systemName: "play.circle.fill")
.font(.system(size: 48))
.foregroundColor(.white)
.shadow(radius: 10)
)
.cornerRadius(12) .cornerRadius(12)
.drawingGroup() // Improves performance for thumbnails .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) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.themeTextWhiteSecondary) .background(Color.themeTextWhiteSecondary)
@ -366,7 +433,7 @@ struct MediaPreview: View {
RoundedRectangle(cornerRadius: 16) RoundedRectangle(cornerRadius: 16)
.stroke(Color.themePrimary, lineWidth: 2) .stroke(Color.themePrimary, lineWidth: 2)
) )
.contentShape(Rectangle()) // Better tap target .contentShape(Rectangle())
} }
} }