wake-ios/wake/View/Components/Upload/MediaUpload.swift
2025-08-22 18:58:08 +08:00

327 lines
12 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 {
case pending
case uploading(progress: Double)
case completed(fileId: String)
case failed(Error)
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
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 class MediaUploadManager: ObservableObject {
///
@Published public var selectedMedia: [MediaType] = []
///
@Published public var uploadStatus: [String: MediaUploadStatus] = [:]
private let uploader = ImageUploadService()
public init() {}
///
public func addMedia(_ media: [MediaType]) {
selectedMedia.append(contentsOf: media)
}
///
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
}
}
uploadStatus = newStatus
}
///
public func clearAllMedia() {
selectedMedia.removeAll()
uploadStatus.removeAll()
}
///
public func startUpload() {
print("🔄 开始批量上传 \(selectedMedia.count) 个文件")
//
uploadStatus.removeAll()
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)
}
uploadMedia(uploadMediaType, id: id)
}
}
///
public func getUploadResults() -> [String: String] {
var results: [String: String] = [:]
for (id, status) in uploadStatus {
if case .completed(let fileId) = status {
results[id] = fileId
}
}
return results
}
///
public var isAllUploaded: Bool {
guard !selectedMedia.isEmpty else { return false }
return uploadStatus.allSatisfy { _, status in
if case .completed = status { return true }
return false
}
}
// MARK: - Private Methods
private func uploadMedia(_ media: ImageUploadService.MediaType, id: String) {
print("🔄 开始处理媒体: \(id)")
uploadStatus[id] = .uploading(progress: 0)
uploader.uploadMedia(
media,
progress: { progress in
print("📊 上传进度 (\(id)): \(progress.current)%")
DispatchQueue.main.async {
self.uploadStatus[id] = .uploading(progress: progress.progress)
}
},
completion: { [weak self] result in
guard let self = self else { return }
DispatchQueue.main.async {
switch result {
case .success(let uploadResult):
print("✅ 上传成功 (\(id)): \(uploadResult.fileId)")
self.uploadStatus[id] = .completed(fileId: uploadResult.fileId)
case .failure(let error):
print("❌ 上传失败 (\(id)): \(error.localizedDescription)")
self.uploadStatus[id] = .failed(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 {
NavigationView {
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()
}
.navigationTitle("媒体上传")
.sheet(isPresented: $showMediaPicker) {
MediaPicker(
selectedMedia: $uploadManager.selectedMedia,
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(0..<uploadManager.selectedMedia.count, id: \.self) { index in
let media = uploadManager.selectedMedia[index]
let mediaId = "\(index)"
let status = uploadManager.uploadStatus[mediaId] ?? .pending
HStack {
//
MediaThumbnailView(media: media, onDelete: nil)
.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)
}
.onDelete { indexSet in
indexSet.forEach { index in
uploadManager.removeMedia(at: index)
}
}
}
.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: 80, height: 80)
.cornerRadius(8)
.clipped()
if media.isVideo {
Image(systemName: "video.fill")
.foregroundColor(.white)
.padding(4)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
.padding(4)
}
}
}
}
}
#Preview {
//
MediaUploadExample(
imageSelectionLimit: 5,
videoSelectionLimit: 2
)
.environmentObject(AuthState.shared)
}