wake-ios/wake/View/Components/Upload/MediaUploader.swift
2025-08-21 19:39:30 +08:00

139 lines
4.8 KiB
Swift

import SwiftUI
import os.log
class MediaUploader: ObservableObject {
@Published var selectedMedia: [MediaType] = []
@Published private(set) var isUploading = false
@Published private(set) var uploadProgress: [Int: Double] = [:] //
@Published var showError = false
@Published private(set) var errorMessage = ""
@Published var showMediaPicker = false
let maxSelection: Int
var onUploadComplete: ([(MediaType, URL)]?) -> Void
var uploadFunction: (MediaType, @escaping (Double) -> Void) async throws -> URL
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaUploader")
init(maxSelection: Int,
onUploadComplete: @escaping ([(MediaType, URL)]?) -> Void,
uploadFunction: @escaping (MediaType, @escaping (Double) -> Void) async throws -> URL) {
self.maxSelection = maxSelection
self.onUploadComplete = onUploadComplete
self.uploadFunction = uploadFunction
}
//
func showPicker() {
DispatchQueue.main.async { [weak self] in
self?.showMediaPicker = true
}
}
//
func startUpload() async -> [(MediaType, URL)]? {
guard !selectedMedia.isEmpty else { return nil }
isUploading = true
uploadProgress.removeAll()
do {
var uploadedResults: [(MediaType, URL)] = []
for (index, media) in selectedMedia.enumerated() {
let url = try await uploadFunction(media) { [weak self] progress in
DispatchQueue.main.async {
self?.uploadProgress[index] = progress
}
}
uploadedResults.append((media, url))
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.isUploading = false
self.onUploadComplete(uploadedResults)
self.selectedMedia.removeAll()
}
return uploadedResults
} catch {
logger.error("上传失败: \(error.localizedDescription)")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.errorMessage = "上传失败: \(error.localizedDescription)"
self.showError = true
self.isUploading = false
}
return nil
}
}
}
struct MediaUploaderView: View {
@ObservedObject var mediaUploader: MediaUploader
var body: some View {
VStack(alignment: .leading, spacing: 16) {
//
if !mediaUploader.selectedMedia.isEmpty {
Text("已选择 \(mediaUploader.selectedMedia.count) 个文件")
.foregroundColor(.secondary)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
.fullScreenCover(isPresented: $mediaUploader.showMediaPicker) {
MediaPicker(
selectedMedia: $mediaUploader.selectedMedia,
selectionLimit: mediaUploader.maxSelection
) {
mediaUploader.showMediaPicker = false
}
}
.alert("错误", isPresented: $mediaUploader.showError) {
Button("确定", role: .cancel) {}
} message: {
Text(mediaUploader.errorMessage)
}
}
}
// MARK: -
struct MediaUploader_Previews: PreviewProvider {
static var previews: some View {
Group {
MediaUploaderView(
mediaUploader: MediaUploader(
maxSelection: 5,
onUploadComplete: { _ in },
uploadFunction: { _, _ in
//
try await Task.sleep(nanoseconds: 1_000_000_000)
return URL(string: "https://example.com/uploaded")!
}
)
)
.padding()
.previewLayout(.sizeThatFits)
MediaUploaderView(
mediaUploader: MediaUploader(
maxSelection: 5,
onUploadComplete: { _ in },
uploadFunction: { _, _ in
//
try await Task.sleep(nanoseconds: 1_000_000_000)
return URL(string: "https://example.com/uploaded")!
}
)
)
.padding()
.previewLayout(.sizeThatFits)
.preferredColorScheme(.dark)
}
}
}