feat: 暂提
This commit is contained in:
parent
6229ddcd6c
commit
86ac98a465
@ -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)
|
||||
// 只处理状态为pending或failed的媒体
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user