wake-ios/wake/View/Components/Upload/MediaPicker.swift
2025-08-21 09:57:19 +08:00

202 lines
8.0 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 PhotosUI
import os.log
import AVKit
///
public enum MediaType: Equatable {
case image(UIImage)
case video(URL, UIImage?) // URL UIImage
public var thumbnail: UIImage? {
switch self {
case .image(let image):
return image
case .video(_, let thumbnail):
return thumbnail
}
}
public var isVideo: Bool {
if case .video = self {
return true
}
return false
}
public static func == (lhs: MediaType, rhs: MediaType) -> Bool {
switch (lhs, rhs) {
case (.image(let lhsImage), .image(let rhsImage)):
return lhsImage.pngData() == rhsImage.pngData()
case (.video(let lhsURL, _), .video(let rhsURL, _)):
return lhsURL == rhsURL
default:
return false
}
}
}
struct MediaPicker: 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: MediaPicker
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker")
init(_ parent: MediaPicker) {
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"
}
}
}