import AVFoundation import Combine import Foundation import Photos import UIKit final class SharingMiddleware { static let shared = SharingMiddleware() // swiftlint:disable:next function_body_length func middleware(state _: AppState, action: AppAction) -> AnyPublisher { switch action { case .sharingAction(.checkCameraAccess): return Future { promise in let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: promise(.success(.sharingAction(.setCameraAccess(true)))) case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { granted in promise(.success(.sharingAction(.setCameraAccess(granted)))) } case .denied, .restricted: promise(.success(.sharingAction(.setCameraAccess(false)))) @unknown default: promise(.success(.sharingAction(.setCameraAccess(false)))) } } .eraseToAnyPublisher() case .sharingAction(.checkGalleryAccess): return Future { promise in let status = PHPhotoLibrary.authorizationStatus() switch status { case .authorized, .limited: promise(.success(.sharingAction(.setGalleryAccess(true)))) case .notDetermined: PHPhotoLibrary.requestAuthorization { status in promise(.success(.sharingAction(.setGalleryAccess(status == .authorized)))) } case .denied, .restricted: promise(.success(.sharingAction(.setGalleryAccess(false)))) @unknown default: promise(.success(.sharingAction(.setGalleryAccess(false)))) } } .eraseToAnyPublisher() case .sharingAction(.fetchGallery): return Future { promise in DispatchQueue.global(qos: .background).async { let fetchOptions = PHFetchOptions() fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] let assets = PHAsset.fetchAssets(with: fetchOptions) let manager = PHImageManager.default() let option = PHImageRequestOptions() option.isSynchronous = true var items: [SharingGalleryItem] = [] let group = DispatchGroup() assets.enumerateObjects { asset, _, _ in if asset.mediaType == .image { group.enter() manager.requestImage( for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: option ) { image, _ in if image != nil { items.append(.init(id: asset.localIdentifier, type: .photo)) group.leave() } } } else if asset.mediaType == .video { group.enter() manager.requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if avAsset != nil { items.append(.init(id: asset.localIdentifier, type: .video, duration: asset.duration.minAndSec)) group.leave() } } } } group.notify(queue: .main) { promise(.success(.sharingAction(.galleryFetched(items)))) } } } .eraseToAnyPublisher() case .sharingAction(.galleryFetched(let items)): return Future { promise in DispatchQueue.global(qos: .background).async { let ids = items .filter { $0.thumbnail == nil } .map { $0.id } let assets = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil) assets.enumerateObjects { asset, _, _ in let manager = PHImageManager.default() if asset.mediaType == .image { manager.requestImage( for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil ) { image, _ in image?.scaleAndCropImage(toExampleSize: CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) { image in if let image { let data = image.jpegData(compressionQuality: 1.0) ?? Data() DispatchQueue.main.async { store.dispatch(.sharingAction(.thumbnailUpdated(data, asset.localIdentifier))) } } } } } else if asset.mediaType == .video { manager.requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let avAsset { let imageGenerator = AVAssetImageGenerator(asset: avAsset) imageGenerator.appliesPreferredTrackTransform = true let time = CMTimeMake(value: 1, timescale: 2) do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) let thumbnail = UIImage(cgImage: imageRef) thumbnail.scaleAndCropImage(toExampleSize: CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize), completion: { image in if let image { let data = image.jpegData(compressionQuality: 1.0) ?? Data() DispatchQueue.main.async { store.dispatch(.sharingAction(.thumbnailUpdated(data, asset.localIdentifier))) } } }) } catch { print("Failed to create thumbnail image") } } } } } } promise(.success(.empty)) } .eraseToAnyPublisher() default: return Empty().eraseToAnyPublisher() } } }