diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index c5ebc1c..9dd1884 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/MediaPicker.swift b/wake/View/Components/Upload/MediaPicker.swift index ec671a5..b3dd945 100644 --- a/wake/View/Components/Upload/MediaPicker.swift +++ b/wake/View/Components/Upload/MediaPicker.swift @@ -173,49 +173,4 @@ struct MediaPicker: UIViewControllerRepresentable { } } } -} - -// MARK: - 预览辅助视图 -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) - .clipped() - .cornerRadius(8) - } else { - Color.gray - .frame(width: 80, height: 80) - .cornerRadius(8) - } - - // 视频标识 - if media.isVideo { - Image(systemName: "video.fill") - .foregroundColor(.white) - .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(.red) - .background(Color.white) - .clipShape(Circle()) - } - .offset(x: 8, y: -8) - } - } - } -} +} \ No newline at end of file diff --git a/wake/View/Components/Upload/MediaPickerWithLogger.swift b/wake/View/Components/Upload/MediaPickerWithLogger.swift new file mode 100644 index 0000000..bf9628a --- /dev/null +++ b/wake/View/Components/Upload/MediaPickerWithLogger.swift @@ -0,0 +1,168 @@ +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/Examples/MediaUpload.swift index 6db94e5..768f4f9 100644 --- a/wake/View/Examples/MediaUpload.swift +++ b/wake/View/Examples/MediaUpload.swift @@ -1,99 +1,211 @@ import SwiftUI import os.log -public struct ExampleView: View { - @StateObject private var mediaUploader = MediaUploader( - maxSelection: 5, - onUploadComplete: { _ in }, - uploadFunction: { media, progress in - // 模拟上传进度 - for i in 0...10 { - try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒 - progress(Double(i) / 10.0) - } - - // 这里替换为您的实际上传逻辑 - switch media { - case .image(let image): - // 上传图片 - guard let url = URL(string: "https://example.com/images/\(UUID().uuidString).jpg") else { - throw NSError(domain: "com.example.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) - } - return url - case .video(let url, _): - // 上传视频 - guard let url = URL(string: "https://example.com/videos/\(UUID().uuidString).mp4") else { - throw NSError(domain: "com.example.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) - } - return url +/// 媒体上传示例视图 +struct ExampleView: View { + /// 是否显示媒体选择器 + @State private var showMediaPicker = false + /// 已选媒体文件 + @State private var selectedMedia: [MediaType] = [] + /// 上传状态 + @State private var uploadStatus: [String: UploadStatus] = [:] + /// 上传管理器 + private let uploader = ImageUploadService() + + /// 上传状态 + private enum UploadStatus: Equatable { + case pending + case uploading(progress: Double) + case completed(fileId: String) + case failed(Error) + + static func == (lhs: UploadStatus, rhs: UploadStatus) -> 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 + default: + return false } } - ) - @State private var uploadedURLs: [URL] = [] - @EnvironmentObject private var authState: AuthState - - public init() {} - - public var body: some View { - NavigationView { - VStack { - // 添加媒体按钮 - Button(action: { - mediaUploader.showPicker() - }) { - HStack { - Image(systemName: "plus.circle.fill") - Text("添加媒体") - } - .frame(maxWidth: .infinity) - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(8) - } - .padding(.horizontal) - - // 媒体上传组件 - MediaUploaderView(mediaUploader: mediaUploader) - .onChange(of: mediaUploader.selectedMedia) { newValue in - if !newValue.isEmpty { - Task { - if let results = await mediaUploader.startUpload() { - let urls = results.map { $0.1 } - uploadedURLs.append(contentsOf: urls) - } - } - } - } - - // 显示已上传的URL - List(uploadedURLs, id: \.self) { url in - Text(url.absoluteString) - .font(.caption) - .padding(.vertical, 4) - } - .listStyle(PlainListStyle()) + + 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)" } - .navigationTitle("媒体上传示例") } } - // 实际上传函数示例 - private func uploadMedia(_ media: MediaType, progress: @escaping (Double) -> Void) async throws -> URL { - // 模拟上传进度 - for i in 0...10 { - try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒 - progress(Double(i) / 10.0) + 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) + + // 显示已选媒体数量 + if !selectedMedia.isEmpty { + VStack(spacing: 10) { + Text("已选择 \(selectedMedia.count) 个媒体文件") + .font(.headline) + + // 显示媒体缩略图和上传状态 + List { + ForEach(0.. 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) + } + } } } }