wake-ios/wake/View/Components/Upload/MediaUpload.swift

444 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import os.log
///
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 abs(lhsProgress - rhsProgress) < 0.01
case (.completed(let lhsId), .completed(let rhsId)):
return lhsId == rhsId
case (.failed, .failed):
return false // Errors don't need to be equatable
default:
return false
}
}
public var description: String {
switch self {
case .pending: return "等待上传"
case .uploading(let progress): return "上传中 \(Int(progress * 100))%"
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 private(set) var selectedMedia: [MediaType] = []
///
@Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:]
///
@Published public private(set) var uploadResults: [String: UploadResult] = [:]
private let uploader = ImageUploadService() // Use ImageUploadService
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
public init() {}
///
public func addMedia(_ media: [MediaType]) {
let newMedia = media.filter { newItem in
!self.selectedMedia.contains { $0.id == newItem.id }
}
var updatedMedia = self.selectedMedia
for item in newMedia {
updatedMedia.append(item)
self.uploadStatus[item.id] = .pending
}
self.selectedMedia = updatedMedia
//
if !newMedia.isEmpty, let firstMedia = newMedia.first, self.selectedMedia.count == newMedia.count {
NotificationCenter.default.post(
name: .didAddFirstMedia,
object: nil,
userInfo: ["media": firstMedia]
)
}
}
/// ID
public func removeMedia(id: String) {
Task { @MainActor in
self.selectedMedia.removeAll { $0.id == id }
self.uploadStatus.removeValue(forKey: id)
self.uploadResults.removeValue(forKey: id)
}
}
///
public func clearAllMedia() {
selectedMedia.removeAll()
uploadStatus.removeAll()
uploadResults.removeAll()
}
///
public func startUpload() {
logger.info("🔄 开始批量上传 \(self.selectedMedia.count) 个文件")
// pendingfailed
let mediaToUpload = self.selectedMedia.filter { media in
guard let status = self.uploadStatus[media.id] else { return true }
return !status.isCompleted && !status.isUploading
}
//
self.uploadResults.removeAll()
for media in mediaToUpload {
self.uploadMedia(media)
}
}
///
public func getUploadResults() -> [String: UploadResult] {
return uploadResults
}
///
public var isAllUploaded: Bool {
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
///
public struct UploadResult: Codable, Equatable {
public let fileId: String
public let thumbnailId: String?
public init(fileId: String, thumbnailId: String? = nil) {
self.fileId = fileId
self.thumbnailId = thumbnailId
}
public static func == (lhs: UploadResult, rhs: UploadResult) -> Bool {
return lhs.fileId == rhs.fileId && lhs.thumbnailId == rhs.thumbnailId
}
}
private func uploadMedia(_ media: MediaType) {
logger.info("🔄 开始处理媒体: \(media.id)")
//
updateStatus(for: media.id, status: .uploading(progress: 0))
//
let uploadMedia: ImageUploadService.MediaType
switch media {
case .image(let uiImage):
uploadMedia = .image(uiImage)
case .video(let url, let thumbnail):
uploadMedia = .video(url as URL, thumbnail)
}
//
uploader.uploadMedia(uploadMedia,
progress: { progress in
//
Task { @MainActor in
self.updateStatus(for: media.id, status: .uploading(progress: progress.progress))
}
},
completion: { [weak self] result in
guard let self = self else { return }
Task { @MainActor in
switch result {
case .success(let uploadResult):
//
let fileId: String
let thumbnailId: String?
switch uploadResult {
case .file(let result):
fileId = result.fileId
thumbnailId = nil
case .video(let video, let thumbnail):
fileId = video.fileId
thumbnailId = thumbnail?.fileId
}
//
let result = UploadResult(fileId: fileId, thumbnailId: thumbnailId)
self.uploadResults[media.id] = result
self.logger.info("✅ 上传成功 (\(media.id)): \(fileId), 缩略图ID: \(thumbnailId ?? "")")
self.updateStatus(for: media.id, status: .completed(fileId: fileId))
//
if self.isAllUploaded {
self.printUploadResults()
}
case .failure(let 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: - Upload Results
///
private func printUploadResults() {
let results = self.selectedMedia.compactMap { media -> [String: String]? in
guard let result = self.uploadResults[media.id] else { return nil }
return [
"file_id": result.fileId,
"preview_file_id": result.thumbnailId ?? result.fileId
]
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: results, options: [.prettyPrinted, .sortedKeys])
if let jsonString = String(data: jsonData, encoding: .utf8) {
print("📦 上传完成文件ID列表:")
print(jsonString)
}
} catch {
print("❌ 无法序列化上传结果: \(error)")
}
}
}
// MARK: - Preview Helper
/// 使 MediaUploadManager
struct MediaUploadExample: View {
@StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false
let imageSelectionLimit: Int
let videoSelectionLimit: Int
init(imageSelectionLimit: Int = 10, videoSelectionLimit: Int = 10) {
self.imageSelectionLimit = imageSelectionLimit
self.videoSelectionLimit = videoSelectionLimit
}
var body: some View {
VStack(spacing: 20) {
//
Button(action: { showMediaPicker = true }) {
Label("选择媒体", systemImage: "photo.on.rectangle")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding(.horizontal)
//
VStack(alignment: .leading, spacing: 4) {
Text("选择限制:")
.font(.subheadline)
.foregroundColor(.secondary)
Text("• 最多选择 \(imageSelectionLimit) 张图片")
.font(.caption)
.foregroundColor(.secondary)
Text("• 最多选择 \(videoSelectionLimit) 个视频")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
//
MediaSelectionView(uploadManager: uploadManager)
//
Button(action: { uploadManager.startUpload() }) {
Text("开始上传")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(uploadManager.selectedMedia.isEmpty ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding(.horizontal)
.disabled(uploadManager.selectedMedia.isEmpty)
Spacer()
}
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
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 }
)
}
}
}
///
struct MediaSelectionView: View {
@ObservedObject var uploadManager: MediaUploadManager
var body: some View {
if !uploadManager.selectedMedia.isEmpty {
VStack(spacing: 10) {
Text("已选择 \(uploadManager.selectedMedia.count) 个媒体文件")
.font(.headline)
//
List {
ForEach(uploadManager.selectedMedia, id: \.id) { media in
let status = uploadManager.uploadStatus[media.id] ?? .pending
HStack {
//
MediaThumbnailView(media: media, onDelete: {
uploadManager.removeMedia(id: media.id)
})
.frame(width: 60, height: 60)
VStack(alignment: .leading, spacing: 4) {
Text(media.isVideo ? "视频" : "图片")
.font(.subheadline)
//
Text(status.description)
.font(.caption)
.foregroundColor(statusColor(status))
//
if case .uploading(let progress) = status {
ProgressView(value: progress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle())
.frame(height: 4)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.vertical, 8)
}
}
.frame(height: 300)
}
.padding(.top)
} else {
Text("未选择任何媒体")
.foregroundColor(.secondary)
.padding(.top, 50)
}
}
private func statusColor(_ status: MediaUploadStatus) -> Color {
switch status {
case .pending: return .secondary
case .uploading: return .blue
case .completed: return .green
case .failed: return .red
}
}
}
///
private struct MediaThumbnailView: View {
let media: MediaType
let onDelete: (() -> Void)?
var body: some View {
ZStack(alignment: .topTrailing) {
if let thumbnail = media.thumbnail {
Image(uiImage: thumbnail)
.resizable()
.scaledToFill()
.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)
}
}
}
}
#Preview {
//
MediaUploadExample(
imageSelectionLimit: 5,
videoSelectionLimit: 2
)
.environmentObject(AuthState.shared)
}