feat: 项目文件整合
This commit is contained in:
parent
1a2c1bf959
commit
4670b27942
@ -1,65 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// 素材服务,处理与素材相关的网络请求
|
||||
class MaterialService {
|
||||
|
||||
/// 单例实例
|
||||
static let shared = MaterialService()
|
||||
|
||||
private init() {}
|
||||
|
||||
/// 上传素材信息
|
||||
/// - Parameters:
|
||||
/// - fileId: 原文件ID
|
||||
/// - previewFileId: 预览文件ID
|
||||
/// - completion: 完成回调,返回是否成功
|
||||
func uploadMaterialInfo(fileId: String,
|
||||
previewFileId: String,
|
||||
completion: @escaping (Bool, String?) -> Void) {
|
||||
|
||||
let materialData: [String: Any] = [
|
||||
"material": [
|
||||
"file_id": fileId,
|
||||
"preview_file_id": previewFileId
|
||||
]
|
||||
]
|
||||
|
||||
guard let url = URL(string: "\(APIConfig.baseURL)/material") else {
|
||||
completion(false, "无效的URL")
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = APIConfig.authHeaders
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: materialData)
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(false, "上传失败: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if (200...299).contains(httpResponse.statusCode) {
|
||||
completion(true, nil)
|
||||
} else {
|
||||
let statusCode = httpResponse.statusCode
|
||||
let errorMessage = String(data: data ?? Data(), encoding: .utf8) ?? ""
|
||||
completion(false, "服务器返回错误状态码: \(statusCode), 响应: \(errorMessage)")
|
||||
}
|
||||
} else {
|
||||
completion(false, "无效的服务器响应")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
} catch {
|
||||
completion(false, "请求数据序列化失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import AVFoundation
|
||||
import os.log
|
||||
import AVKit
|
||||
|
||||
/// 媒体类型
|
||||
public enum MediaType: Equatable {
|
||||
@ -38,13 +38,11 @@ public enum MediaType: Equatable {
|
||||
|
||||
struct MediaPicker: UIViewControllerRepresentable {
|
||||
@Binding var selectedMedia: [MediaType]
|
||||
var selectionLimit: Int
|
||||
var onDismiss: (() -> Void)?
|
||||
let selectionLimit: Int
|
||||
let onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
|
||||
// 配置支持图片和视频
|
||||
configuration.filter = .any(of: [.videos, .images])
|
||||
configuration.selectionLimit = selectionLimit
|
||||
configuration.preferredAssetRepresentationMode = .current
|
||||
@ -63,9 +61,6 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let parent: MediaPicker
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker")
|
||||
private var processedCount = 0
|
||||
private var totalToProcess = 0
|
||||
private var tempMedia: [MediaType] = []
|
||||
|
||||
init(_ parent: MediaPicker) {
|
||||
self.parent = parent
|
||||
@ -77,26 +72,36 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
return
|
||||
}
|
||||
|
||||
processedCount = 0
|
||||
totalToProcess = results.count
|
||||
tempMedia = []
|
||||
var processedMedia: [MediaType] = []
|
||||
let group = DispatchGroup()
|
||||
|
||||
for result in results {
|
||||
let itemProvider = result.itemProvider
|
||||
|
||||
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||
processImage(itemProvider: itemProvider) { [weak self] media in
|
||||
self?.handleProcessedMedia(media)
|
||||
group.enter()
|
||||
processImage(itemProvider: itemProvider) { media in
|
||||
if let media = media {
|
||||
processedMedia.append(media)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
processVideo(itemProvider: itemProvider) { [weak self] media in
|
||||
self?.handleProcessedMedia(media)
|
||||
group.enter()
|
||||
processVideo(itemProvider: itemProvider) { media in
|
||||
if let media = media {
|
||||
processedMedia.append(media)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
} else {
|
||||
processedCount += 1
|
||||
checkCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
self.parent.selectedMedia = processedMedia
|
||||
self.printMediaInfo(media: processedMedia)
|
||||
self.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
|
||||
private func processImage(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
|
||||
@ -118,25 +123,21 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建临时文件URL
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
let targetURL = tempDirectory.appendingPathComponent("\(UUID().uuidString).\(videoURL.pathExtension)")
|
||||
|
||||
do {
|
||||
// 将视频复制到临时目录
|
||||
if FileManager.default.fileExists(atPath: targetURL.path) {
|
||||
try FileManager.default.removeItem(at: targetURL)
|
||||
}
|
||||
try FileManager.default.copyItem(at: videoURL, to: targetURL)
|
||||
|
||||
// 提取视频缩略图
|
||||
MediaUtils.extractFirstFrame(from: targetURL) { result in
|
||||
switch result {
|
||||
case .success(let thumbnail):
|
||||
completion(.video(targetURL, thumbnail))
|
||||
case .failure(let error):
|
||||
self.logger.error("Failed to extract video thumbnail: \(error.localizedDescription)")
|
||||
// 即使缩略图提取失败,也继续处理视频
|
||||
completion(.video(targetURL, nil))
|
||||
}
|
||||
}
|
||||
@ -147,30 +148,54 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleProcessedMedia(_ media: MediaType?) {
|
||||
if let media = media {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.tempMedia.append(media)
|
||||
self?.checkCompletion()
|
||||
private func printMediaInfo(media: [MediaType]) {
|
||||
print("=== Selected Media Information ===")
|
||||
for (index, media) in media.enumerated() {
|
||||
print("\nItem \(index + 1):")
|
||||
|
||||
switch media {
|
||||
case .image(let image):
|
||||
print("Type: Image")
|
||||
print("Dimensions: \(Int(image.size.width))x\(Int(image.size.height))")
|
||||
if let data = image.jpegData(compressionQuality: 1.0) {
|
||||
print("File Size: \(formatFileSize(Int64(data.count)))")
|
||||
}
|
||||
|
||||
case .video(let url, _):
|
||||
print("Type: Video")
|
||||
print("File Name: \(url.lastPathComponent)")
|
||||
print("File Path: \(url.path)")
|
||||
|
||||
if let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
|
||||
let fileSize = attributes[.size] as? Int64 {
|
||||
print("File Size: \(formatFileSize(fileSize))")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url)
|
||||
let duration = asset.duration.seconds
|
||||
print("Duration: \(formatTimeInterval(duration))")
|
||||
|
||||
if let track = asset.tracks(withMediaType: .video).first {
|
||||
let size = track.naturalSize
|
||||
print("Video Dimensions: \(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processedCount += 1
|
||||
checkCompletion()
|
||||
}
|
||||
print("================================\n")
|
||||
}
|
||||
|
||||
private func checkCompletion() {
|
||||
processedCount += 1
|
||||
|
||||
if processedCount >= totalToProcess {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if !self.tempMedia.isEmpty {
|
||||
self.parent.selectedMedia = self.tempMedia
|
||||
}
|
||||
self.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
private func formatFileSize(_ bytes: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: bytes)
|
||||
}
|
||||
|
||||
private func formatTimeInterval(_ interval: TimeInterval) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: interval) ?? "00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,168 +0,0 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import os.log
|
||||
import AVKit
|
||||
|
||||
struct MediaPickerWithLogging: UIViewControllerRepresentable {
|
||||
@Binding var selectedMedia: [MediaType]
|
||||
let selectionLimit: Int
|
||||
let onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.filter = .any(of: [.videos, .images])
|
||||
configuration.selectionLimit = selectionLimit
|
||||
configuration.preferredAssetRepresentationMode = .current
|
||||
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let parent: MediaPickerWithLogging
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPickerWithLogging")
|
||||
|
||||
init(_ parent: MediaPickerWithLogging) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard !results.isEmpty else {
|
||||
parent.onDismiss?()
|
||||
return
|
||||
}
|
||||
|
||||
var processedMedia: [MediaType] = []
|
||||
let group = DispatchGroup()
|
||||
|
||||
for result in results {
|
||||
let itemProvider = result.itemProvider
|
||||
|
||||
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||
group.enter()
|
||||
processImage(itemProvider: itemProvider) { media in
|
||||
if let media = media {
|
||||
processedMedia.append(media)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
group.enter()
|
||||
processVideo(itemProvider: itemProvider) { media in
|
||||
if let media = media {
|
||||
processedMedia.append(media)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
self.parent.selectedMedia = processedMedia
|
||||
self.printMediaInfo(media: processedMedia)
|
||||
self.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
|
||||
private func processImage(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
|
||||
itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
|
||||
if let image = object as? UIImage {
|
||||
completion(.image(image))
|
||||
} else {
|
||||
self.logger.error("Failed to load image: \(error?.localizedDescription ?? "Unknown error")")
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processVideo(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
|
||||
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
|
||||
guard let videoURL = url, error == nil else {
|
||||
self.logger.error("Failed to load video: \(error?.localizedDescription ?? "Unknown error")")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
let targetURL = tempDirectory.appendingPathComponent("\(UUID().uuidString).\(videoURL.pathExtension)")
|
||||
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: targetURL.path) {
|
||||
try FileManager.default.removeItem(at: targetURL)
|
||||
}
|
||||
try FileManager.default.copyItem(at: videoURL, to: targetURL)
|
||||
|
||||
MediaUtils.extractFirstFrame(from: targetURL) { result in
|
||||
switch result {
|
||||
case .success(let thumbnail):
|
||||
completion(.video(targetURL, thumbnail))
|
||||
case .failure(let error):
|
||||
self.logger.error("Failed to extract video thumbnail: \(error.localizedDescription)")
|
||||
completion(.video(targetURL, nil))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.logger.error("Failed to copy video file: \(error.localizedDescription)")
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func printMediaInfo(media: [MediaType]) {
|
||||
print("=== Selected Media Information ===")
|
||||
for (index, media) in media.enumerated() {
|
||||
print("\nItem \(index + 1):")
|
||||
|
||||
switch media {
|
||||
case .image(let image):
|
||||
print("Type: Image")
|
||||
print("Dimensions: \(Int(image.size.width))x\(Int(image.size.height))")
|
||||
if let data = image.jpegData(compressionQuality: 1.0) {
|
||||
print("File Size: \(formatFileSize(Int64(data.count)))")
|
||||
}
|
||||
|
||||
case .video(let url, _):
|
||||
print("Type: Video")
|
||||
print("File Name: \(url.lastPathComponent)")
|
||||
print("File Path: \(url.path)")
|
||||
|
||||
if let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
|
||||
let fileSize = attributes[.size] as? Int64 {
|
||||
print("File Size: \(formatFileSize(fileSize))")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url)
|
||||
let duration = asset.duration.seconds
|
||||
print("Duration: \(formatTimeInterval(duration))")
|
||||
|
||||
if let track = asset.tracks(withMediaType: .video).first {
|
||||
let size = track.naturalSize
|
||||
print("Video Dimensions: \(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
}
|
||||
}
|
||||
print("================================\n")
|
||||
}
|
||||
|
||||
private func formatFileSize(_ bytes: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: bytes)
|
||||
}
|
||||
|
||||
private func formatTimeInterval(_ interval: TimeInterval) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: interval) ?? "00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,7 +186,7 @@ struct MediaUploadExample: View {
|
||||
}
|
||||
.navigationTitle("媒体上传")
|
||||
.sheet(isPresented: $showMediaPicker) {
|
||||
MediaPickerWithLogging(
|
||||
MediaPicker(
|
||||
selectedMedia: $uploadManager.selectedMedia,
|
||||
selectionLimit: 5,
|
||||
onDismiss: { showMediaPicker = false }
|
||||
@ -1,138 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import AVFoundation
|
||||
import os.log
|
||||
|
||||
struct VideoPicker: UIViewControllerRepresentable {
|
||||
@Binding var selectedVideoURL: URL?
|
||||
@Binding var thumbnailImage: UIImage?
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.filter = .videos
|
||||
configuration.selectionLimit = 1
|
||||
configuration.preferredAssetRepresentationMode = .current
|
||||
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let parent: VideoPicker
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "VideoPicker")
|
||||
|
||||
init(_ parent: VideoPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard let result = results.first else {
|
||||
parent.onDismiss?()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取视频URL
|
||||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [weak self] url, error in
|
||||
guard let self = self, let videoURL = url, error == nil else {
|
||||
self?.logger.error("Failed to load video: \(error?.localizedDescription ?? "Unknown error")")
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.onDismiss?()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建临时文件URL
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
let targetURL = tempDirectory.appendingPathComponent("\(UUID().uuidString).\(videoURL.pathExtension)")
|
||||
|
||||
do {
|
||||
// 将视频复制到临时目录
|
||||
if FileManager.default.fileExists(atPath: targetURL.path) {
|
||||
try FileManager.default.removeItem(at: targetURL)
|
||||
}
|
||||
try FileManager.default.copyItem(at: videoURL, to: targetURL)
|
||||
|
||||
// 提取视频首帧
|
||||
MediaUtils.extractFirstFrame(from: targetURL) { [weak self] result in
|
||||
switch result {
|
||||
case .success(let image):
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.thumbnailImage = image
|
||||
self?.parent.selectedVideoURL = targetURL
|
||||
self?.parent.onDismiss?()
|
||||
}
|
||||
case .failure(let error):
|
||||
self?.logger.error("Failed to extract video thumbnail: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.logger.error("Failed to copy video file: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,7 @@ struct MediaUploadDemo: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.sheet(isPresented: $showMediaPicker) {
|
||||
MediaPickerWithLogging(
|
||||
MediaPicker(
|
||||
selectedMedia: $uploadManager.selectedMedia,
|
||||
selectionLimit: 10,
|
||||
onDismiss: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user