feat: uploadmore

This commit is contained in:
jinyaqiu 2025-08-26 10:27:03 +08:00
parent 9a432f65ac
commit 6229ddcd6c
3 changed files with 113 additions and 37 deletions

View File

@ -334,9 +334,19 @@ public class ImageUploadService {
// MARK: - Supporting Types // MARK: - Supporting Types
/// ///
public enum MediaType { public enum MediaType: Equatable {
case image(UIImage) case image(UIImage)
case video(URL, UIImage?) case video(URL, UIImage?)
// id
var id: String {
switch self {
case .image(let uiImage):
return "image_\(uiImage.hashValue)"
case .video(let url, _):
return "video_\(url.absoluteString.hashValue)"
}
}
} }
/// ///

View File

@ -46,7 +46,27 @@ public class MediaUploadManager: ObservableObject {
/// ///
public func addMedia(_ media: [MediaType]) { public func addMedia(_ media: [MediaType]) {
selectedMedia.append(contentsOf: media) print("Adding \(media.count) media items")
let newMedia = media.filter { newItem in
!selectedMedia.contains { $0.id == newItem.id }
}
print("After filtering duplicates: \(newMedia.count) new items")
let isFirstMedia = selectedMedia.isEmpty
selectedMedia.append(contentsOf: newMedia)
for item in newMedia {
uploadStatus[item.id] = .pending
}
//
if isFirstMedia, let firstMedia = newMedia.first {
NotificationCenter.default.post(
name: .didAddFirstMedia,
object: nil,
userInfo: ["media": firstMedia]
)
}
} }
/// ///

View File

@ -1,5 +1,8 @@
import SwiftUI import SwiftUI
extension Notification.Name {
static let didAddFirstMedia = Notification.Name("didAddFirstMedia")
}
/// ///
/// ///
@MainActor @MainActor
@ -30,8 +33,7 @@ struct MediaUploadView: View {
MainUploadArea( MainUploadArea(
uploadManager: uploadManager, uploadManager: uploadManager,
showMediaPicker: $showMediaPicker, showMediaPicker: $showMediaPicker,
selectedMedia: $selectedMedia, selectedMedia: $selectedMedia
selectedIndices: $selectedIndices
) )
.padding() .padding()
.id("mainUploadArea\(uploadManager.selectedMedia.count)") .id("mainUploadArea\(uploadManager.selectedMedia.count)")
@ -262,12 +264,10 @@ struct MainUploadArea: View {
@Binding var showMediaPicker: Bool @Binding var showMediaPicker: Bool
/// ///
@Binding var selectedMedia: MediaType? @Binding var selectedMedia: MediaType?
///
@Binding var selectedIndices: Set<Int>
// MARK: - // MARK: -
var body: some View { var body: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
// //
Text("Click to upload 20 images and 5 videos to generate your next blind box.") Text("Click to upload 20 images and 5 videos to generate your next blind box.")
@ -276,16 +276,43 @@ struct MainUploadArea: View {
.foregroundColor(.black) .foregroundColor(.black)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal) .padding(.horizontal)
//
// if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
UploadPromptView(showMediaPicker: $showMediaPicker) MediaPreview(media: mediaToDisplay, uploadManager: uploadManager)
.id(mediaToDisplay.id)
.frame(width: 225, height: 225)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.themePrimary, lineWidth: 5) // 使2线
)
.cornerRadius(16)
.shadow(radius: 4)
.padding(.horizontal)
.transition(.opacity)
} else {
UploadPromptView(showMediaPicker: $showMediaPicker)
}
// //
mediaPreviewSection mediaPreviewSection
} }
.onAppear {
print("MainUploadArea appeared")
print("Selected media count: \(uploadManager.selectedMedia.count)")
if selectedMedia == nil, let firstMedia = uploadManager.selectedMedia.first {
print("Selecting first media: \(firstMedia.id)")
selectedMedia = firstMedia
}
}
.onReceive(NotificationCenter.default.publisher(for: .didAddFirstMedia)) { notification in
if let media = notification.userInfo?["media"] as? MediaType, selectedMedia == nil {
selectedMedia = media
}
}
.background(Color.white) .background(Color.white)
.cornerRadius(16) .cornerRadius(16)
.shadow(radius: 2) .shadow(radius: 2)
.animation(.default, value: selectedMedia?.id) // selectedMedia id
} }
// MARK: - // MARK: -
@ -371,20 +398,31 @@ struct MainUploadArea: View {
.padding([.top, .leading], 0), .padding([.top, .leading], 0),
alignment: .topLeading alignment: .topLeading
) )
// mediaItemView onTapGesture
// .onTapGesture {
// print("Tapped media at index: \(index)") //
// withAnimation {
// selectedIndices = [index]
// selectedMedia = media
// }
// }
// .contentShape(Rectangle()) //
// mediaItemView onTapGesture
.onTapGesture { .onTapGesture {
// print("点击了媒体项,索引: \(index)")
selectedIndices = [index] withAnimation {
selectedMedia = media selectedMedia = media //
}
} }
.contentShape(Rectangle()) //
} }
// //
Button(action: { Button(action: {
// //
uploadManager.selectedMedia.remove(at: index) uploadManager.selectedMedia.remove(at: index)
// //
if selectedIndices.contains(index) { if selectedMedia == media {
selectedIndices = []
selectedMedia = nil selectedMedia = nil
} }
}) { }) {
@ -594,7 +632,7 @@ struct MediaPreview: View {
var body: some View { var body: some View {
ZStack { ZStack {
// // 1.
if let image = image { if let image = image {
loadedImageView(image) loadedImageView(image)
@ -602,16 +640,30 @@ struct MediaPreview: View {
if case .video = media { if case .video = media {
playButton playButton
} }
} else if case .failure(let error) = loadState {
// // 100%
errorView(error: error) if isUploading && uploadProgress < 1.0 {
VStack {
Spacer()
ProgressView(value: uploadProgress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: .white))
.frame(height: 2)
.padding(.horizontal, 4)
.padding(.bottom, 2)
}
}
} else { } else {
// // 2.
placeholderView placeholderView
.onAppear { .onAppear {
loadImage() loadImage()
} }
} }
// 3.
if case .failure(let error) = loadState, image == nil {
errorView(error: error)
}
} }
.aspectRatio(1, contentMode: .fill) .aspectRatio(1, contentMode: .fill)
.clipped() .clipped()
@ -622,18 +674,12 @@ struct MediaPreview: View {
) )
} }
// MARK: - //
///
private var placeholderView: some View { private var placeholderView: some View {
Color.gray.opacity(0.1) Color.gray.opacity(0.1)
.overlay(
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
)
} }
/// //
private func loadedImageView(_ image: UIImage) -> some View { private func loadedImageView(_ image: UIImage) -> some View {
Image(uiImage: image) Image(uiImage: image)
.resizable() .resizable()
@ -736,15 +782,15 @@ struct MediaPreview: View {
/// MediaType Identifiable /// MediaType Identifiable
extension MediaType: Identifiable { extension MediaType: Identifiable {
/// ///
public var id: String { public var id: String {
switch self { switch self {
case .image(let uiImage): case .image(let uiImage):
return "image_\(uiImage.hashValue)" return "image_\(uiImage.hashValue)"
case .video(let url, _): case .video(let url, _):
return "video_\(url.absoluteString)" return "video_\(url.absoluteString.hashValue)"
}
} }
} }
}
// MARK: - // MARK: -