diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 480b71c..f0776a1 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/View/Components/Upload/MultiImageUploader.swift b/wake/View/Components/Upload/ImageMultiUploader.swift similarity index 100% rename from wake/View/Components/Upload/MultiImageUploader.swift rename to wake/View/Components/Upload/ImageMultiUploader.swift diff --git a/wake/View/Components/Upload/MaterialService.swift b/wake/View/Components/Upload/MaterialService.swift deleted file mode 100644 index e120cf6..0000000 --- a/wake/View/Components/Upload/MaterialService.swift +++ /dev/null @@ -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)") - } - } -} diff --git a/wake/View/Components/Upload/MediaPicker.swift b/wake/View/Components/Upload/MediaPicker.swift index a6a0a74..dfb3f57 100644 --- a/wake/View/Components/Upload/MediaPicker.swift +++ b/wake/View/Components/Upload/MediaPicker.swift @@ -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" } } -} \ No newline at end of file +} diff --git a/wake/View/Components/Upload/MediaPickerWithLogger.swift b/wake/View/Components/Upload/MediaPickerWithLogger.swift deleted file mode 100644 index bf9628a..0000000 --- a/wake/View/Components/Upload/MediaPickerWithLogger.swift +++ /dev/null @@ -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" - } - } -} \ No newline at end of file diff --git a/wake/View/Examples/MediaUpload.swift b/wake/View/Components/Upload/MediaUpload.swift similarity index 99% rename from wake/View/Examples/MediaUpload.swift rename to wake/View/Components/Upload/MediaUpload.swift index 2585f68..36515c0 100644 --- a/wake/View/Examples/MediaUpload.swift +++ b/wake/View/Components/Upload/MediaUpload.swift @@ -186,7 +186,7 @@ struct MediaUploadExample: View { } .navigationTitle("媒体上传") .sheet(isPresented: $showMediaPicker) { - MediaPickerWithLogging( + MediaPicker( selectedMedia: $uploadManager.selectedMedia, selectionLimit: 5, onDismiss: { showMediaPicker = false } diff --git a/wake/View/Components/Upload/MediaUploader.swift b/wake/View/Components/Upload/MediaUploader.swift deleted file mode 100644 index eb922b6..0000000 --- a/wake/View/Components/Upload/MediaUploader.swift +++ /dev/null @@ -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) - } - } -} diff --git a/wake/View/Components/Upload/Upload.swift b/wake/View/Components/Upload/Upload.swift deleted file mode 100644 index e69de29..0000000 diff --git a/wake/View/Components/Upload/VideoPicker.swift b/wake/View/Components/Upload/VideoPicker.swift deleted file mode 100644 index b50aa4f..0000000 --- a/wake/View/Components/Upload/VideoPicker.swift +++ /dev/null @@ -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?() - } - } - } - } - } -} diff --git a/wake/View/Examples/MediaDemo.swift b/wake/View/Examples/MediaDemo.swift index 25f8239..1f1e97d 100644 --- a/wake/View/Examples/MediaDemo.swift +++ b/wake/View/Examples/MediaDemo.swift @@ -23,7 +23,7 @@ struct MediaUploadDemo: View { } .padding(.horizontal) .sheet(isPresented: $showMediaPicker) { - MediaPickerWithLogging( + MediaPicker( selectedMedia: $uploadManager.selectedMedia, selectionLimit: 10, onDismiss: {