feat: 暂提

This commit is contained in:
jinyaqiu 2025-08-26 12:29:49 +08:00
parent 6229ddcd6c
commit 86ac98a465
4 changed files with 156 additions and 96 deletions

View File

@ -2,18 +2,27 @@ import SwiftUI
import os.log
///
public enum MediaUploadStatus: Equatable {
public enum MediaUploadStatus: Equatable, Identifiable {
case pending
case uploading(progress: Double)
case completed(fileId: String)
case failed(Error)
public var id: String {
switch self {
case .pending: return "pending"
case .uploading: return "uploading"
case .completed(let fileId): return "completed_\(fileId)"
case .failed: return "failed"
}
}
public static func == (lhs: MediaUploadStatus, rhs: MediaUploadStatus) -> Bool {
switch (lhs, rhs) {
case (.pending, .pending):
return true
case (.uploading(let lhsProgress), .uploading(let rhsProgress)):
return lhsProgress == rhsProgress
return abs(lhsProgress - rhsProgress) < 0.01
case (.completed(let lhsId), .completed(let rhsId)):
return lhsId == rhsId
case (.failed, .failed):
@ -27,40 +36,53 @@ public enum MediaUploadStatus: Equatable {
switch self {
case .pending: return "等待上传"
case .uploading(let progress): return "上传中 \(Int(progress * 100))%"
case .completed(let fileId): return "上传完成 (ID: \(fileId.prefix(8))...)"
case .completed(let fileId): return "上传完成 (ID: \(fileId.prefix(8)))..."
case .failed(let error): return "上传失败: \(error.localizedDescription)"
}
}
public var isCompleted: Bool {
if case .completed = self { return true }
return false
}
public var isUploading: Bool {
if case .uploading = self { return true }
return false
}
}
///
@MainActor
public class MediaUploadManager: ObservableObject {
///
@Published public var selectedMedia: [MediaType] = []
@Published public private(set) var selectedMedia: [MediaType] = []
///
@Published public var uploadStatus: [String: MediaUploadStatus] = [:]
@Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:]
private let uploader = ImageUploadService()
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
public init() {}
///
public func addMedia(_ media: [MediaType]) {
print("Adding \(media.count) media items")
let newMedia = media.filter { newItem in
!selectedMedia.contains { $0.id == newItem.id }
!self.selectedMedia.contains { $0.id == newItem.id }
}
print("After filtering duplicates: \(newMedia.count) new items")
let isFirstMedia = selectedMedia.isEmpty
selectedMedia.append(contentsOf: newMedia)
// 使
Task { [weak self] in
guard let self = self else { return }
var updatedMedia = self.selectedMedia
for item in newMedia {
uploadStatus[item.id] = .pending
updatedMedia.append(item)
self.uploadStatus[item.id] = .pending
}
self.selectedMedia = updatedMedia
//
if isFirstMedia, let firstMedia = newMedia.first {
if !newMedia.isEmpty, let firstMedia = newMedia.first, self.selectedMedia.count == newMedia.count {
NotificationCenter.default.post(
name: .didAddFirstMedia,
object: nil,
@ -68,48 +90,36 @@ public class MediaUploadManager: ObservableObject {
)
}
}
}
///
public func removeMedia(at index: Int) {
guard index < selectedMedia.count else { return }
selectedMedia.remove(at: index)
//
var newStatus: [String: MediaUploadStatus] = [:]
uploadStatus.forEach { key, value in
if let keyInt = Int(key), keyInt < index {
newStatus[key] = value
} else if let keyInt = Int(key), keyInt > index {
newStatus["\(keyInt - 1)"] = value
/// ID
public func removeMedia(id: String) {
Task { @MainActor in
self.selectedMedia.removeAll { $0.id == id }
self.uploadStatus.removeValue(forKey: id)
}
}
uploadStatus = newStatus
}
///
public func clearAllMedia() {
selectedMedia.removeAll()
uploadStatus.removeAll()
Task { @MainActor in
self.selectedMedia.removeAll()
self.uploadStatus.removeAll()
}
}
///
public func startUpload() {
print("🔄 开始批量上传 \(selectedMedia.count) 个文件")
//
uploadStatus.removeAll()
logger.info("🔄 开始批量上传 \(self.selectedMedia.count) 个文件")
for (index, media) in selectedMedia.enumerated() {
let id = "\(index)"
uploadStatus[id] = .pending
// Convert MediaType to ImageUploadService.MediaType
let uploadMediaType: ImageUploadService.MediaType
switch media {
case .image(let image):
uploadMediaType = .image(image)
case .video(let url, let thumbnail):
uploadMediaType = .video(url, thumbnail)
// pendingfailed
let mediaToUpload = self.selectedMedia.filter { media in
guard let status = self.uploadStatus[media.id] else { return true }
return !status.isCompleted && !status.isUploading
}
uploadMedia(uploadMediaType, id: id)
for media in mediaToUpload {
self.uploadMedia(media)
}
}
@ -126,42 +136,59 @@ public class MediaUploadManager: ObservableObject {
///
public var isAllUploaded: Bool {
guard !selectedMedia.isEmpty else { return false }
return uploadStatus.allSatisfy { _, status in
if case .completed = status { return true }
guard !self.selectedMedia.isEmpty else { return false }
return self.selectedMedia.allSatisfy { media in
if case .completed = self.uploadStatus[media.id] { return true }
return false
}
}
// MARK: - Private Methods
private func uploadMedia(_ media: ImageUploadService.MediaType, id: String) {
print("🔄 开始处理媒体: \(id)")
uploadStatus[id] = .uploading(progress: 0)
private func uploadMedia(_ media: MediaType) {
logger.info("🔄 开始处理媒体: \(media.id)")
//
updateStatus(for: media.id, status: .uploading(progress: 0))
//
let uploadMediaType: ImageUploadService.MediaType
switch media {
case .image(let image):
uploadMediaType = .image(image)
case .video(let url, let thumbnail):
uploadMediaType = .video(url, thumbnail)
}
//
uploader.uploadMedia(
media,
progress: { progress in
print("📊 上传进度 (\(id)): \(progress.current)%")
DispatchQueue.main.async {
self.uploadStatus[id] = .uploading(progress: progress.progress)
uploadMediaType,
progress: { [weak self] progress in
guard let self = self else { return }
Task { @MainActor in
self.updateStatus(for: media.id, status: .uploading(progress: progress.progress))
}
},
completion: { [weak self] result in
guard let self = self else { return }
DispatchQueue.main.async {
Task { @MainActor in
switch result {
case .success(let uploadResult):
print("✅ 上传成功 (\(id)): \(uploadResult.fileId)")
self.uploadStatus[id] = .completed(fileId: uploadResult.fileId)
self.logger.info("✅ 上传成功 (\(media.id)): \(uploadResult.fileId)")
self.updateStatus(for: media.id, status: .completed(fileId: uploadResult.fileId))
case .failure(let error):
print("❌ 上传失败 (\(id)): \(error.localizedDescription)")
self.uploadStatus[id] = .failed(error)
self.logger.error("❌ 上传失败 (\(media.id)): \(error.localizedDescription)")
self.updateStatus(for: media.id, status: .failed(error))
}
}
}
)
}
@MainActor
private func updateStatus(for mediaId: String, status: MediaUploadStatus) {
uploadStatus[mediaId] = status
}
}
// MARK: - Preview Helper
@ -171,7 +198,6 @@ struct MediaUploadExample: View {
@StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false
//
let imageSelectionLimit: Int
let videoSelectionLimit: Int
@ -231,7 +257,15 @@ struct MediaUploadExample: View {
.navigationTitle("媒体上传")
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
selectedMedia: $uploadManager.selectedMedia,
selectedMedia: Binding(
get: { self.uploadManager.selectedMedia },
set: { newMedia in
Task { @MainActor in
self.uploadManager.clearAllMedia()
self.uploadManager.addMedia(newMedia)
}
}
),
imageSelectionLimit: imageSelectionLimit,
videoSelectionLimit: videoSelectionLimit,
onDismiss: { showMediaPicker = false }
@ -253,14 +287,14 @@ struct MediaSelectionView: View {
//
List {
ForEach(0..<uploadManager.selectedMedia.count, id: \.self) { index in
let media = uploadManager.selectedMedia[index]
let mediaId = "\(index)"
let status = uploadManager.uploadStatus[mediaId] ?? .pending
ForEach(uploadManager.selectedMedia, id: \.id) { media in
let status = uploadManager.uploadStatus[media.id] ?? .pending
HStack {
//
MediaThumbnailView(media: media, onDelete: nil)
MediaThumbnailView(media: media, onDelete: {
uploadManager.removeMedia(id: media.id)
})
.frame(width: 60, height: 60)
VStack(alignment: .leading, spacing: 4) {
@ -283,11 +317,6 @@ struct MediaSelectionView: View {
}
.padding(.vertical, 8)
}
.onDelete { indexSet in
indexSet.forEach { index in
uploadManager.removeMedia(at: index)
}
}
}
.frame(height: 300)
}
@ -320,18 +349,35 @@ private struct MediaThumbnailView: View {
Image(uiImage: thumbnail)
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.frame(width: 60, height: 60)
.cornerRadius(8)
.clipped()
if media.isVideo {
Image(systemName: "video.fill")
.foregroundColor(.white)
.font(.system(size: 12))
.padding(4)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
.padding(4)
}
//
if let onDelete = onDelete {
Button(action: onDelete) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.font(.system(size: 16))
.background(Color.black.opacity(0.8))
.clipShape(Circle())
}
.padding(4)
}
} else {
Color.gray.opacity(0.3)
.frame(width: 60, height: 60)
.cornerRadius(8)
}
}
}

View File

@ -24,17 +24,21 @@ struct MediaUploadDemo: View {
.padding(.horizontal)
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
selectedMedia: $uploadManager.selectedMedia,
selectedMedia: Binding(
get: { uploadManager.selectedMedia },
set: { newMedia in
uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
}
),
imageSelectionLimit: 1,
videoSelectionLimit: 0,
allowedMediaTypes: .imagesOnly, // This needs to come before selectionMode
selectionMode: .single, // This was moved after allowedMediaTypes
allowedMediaTypes: .imagesOnly,
selectionMode: .single,
onDismiss: {
showMediaPicker = false
//
if !uploadManager.selectedMedia.isEmpty {
isUploading = true
uploadManager.startUpload()
// Start upload logic here
}
}
)

View File

@ -113,7 +113,13 @@ public struct AvatarPicker: View {
}
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
selectedMedia: $uploadManager.selectedMedia,
selectedMedia: Binding(
get: { uploadManager.selectedMedia },
set: { newMedia in
uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
}
),
imageSelectionLimit: 1,
videoSelectionLimit: 0,
allowedMediaTypes: .imagesOnly,
@ -165,7 +171,8 @@ public struct AvatarPicker: View {
.sheet(isPresented: $showImageCapture) {
CustomCameraView(isPresented: $showImageCapture) { image in
selectedImage = image
uploadManager.selectedMedia = [.image(image)]
uploadManager.clearAllMedia()
uploadManager.addMedia([.image(image)])
withAnimation {
isUploading = true
}

View File

@ -143,7 +143,8 @@ struct MediaUploadView: View {
if !newItems.isEmpty {
//
let newMedia = newItems + uploadManager.selectedMedia
uploadManager.selectedMedia = newMedia
uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
//
if selectedIndices.isEmpty {
@ -225,7 +226,8 @@ struct MediaUploadView: View {
updatedMedia.append(contentsOf: newItems)
//
uploadManager.selectedMedia = updatedMedia
uploadManager.clearAllMedia()
uploadManager.addMedia(updatedMedia)
//
if selectedIndices.isEmpty && !newItems.isEmpty {
@ -419,8 +421,9 @@ struct MainUploadArea: View {
//
Button(action: {
//
uploadManager.selectedMedia.remove(at: index)
// 使API
uploadManager.removeMedia(id: media.id)
//
if selectedMedia == media {
selectedMedia = nil