From ce85b7dff9f92c4e622ca63382fa7157d136cee9 Mon Sep 17 00:00:00 2001 From: fmodf Date: Wed, 3 Jul 2024 13:50:59 +0200 Subject: [PATCH] wip --- .../Attachments/AttachmentMediaManager.swift | 220 ++++----- .../AttachmentMediaPickerView.swift | 454 +++++++++++++----- 2 files changed, 447 insertions(+), 227 deletions(-) diff --git a/ConversationsClassic/View/Screens/Attachments/AttachmentMediaManager.swift b/ConversationsClassic/View/Screens/Attachments/AttachmentMediaManager.swift index bef45d6..fa66f8c 100644 --- a/ConversationsClassic/View/Screens/Attachments/AttachmentMediaManager.swift +++ b/ConversationsClassic/View/Screens/Attachments/AttachmentMediaManager.swift @@ -1,110 +1,110 @@ -import AVFoundation -import Photos -import SwiftUI -import UIKit - -class MediaManager: NSObject, ObservableObject { - // @Published var photos: [UIImage] = [] - @Published var cameraFeed: UIImage? - - // @Published var galleryAccessLevel: PHAuthorizationStatus = .notDetermined - @Published var cameraAccessLevel: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) - - override init() { - super.init() - NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) - - // DispatchQueue.main.async { [weak self] in - // // self?.fetchPhotos() - // } - } - - // private func fetchPhotos() { - // galleryAccessLevel = PHPhotoLibrary.authorizationStatus() - // - // let fetchOptions = PHFetchOptions() - // fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] - // let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions) - // - // let manager = PHImageManager.default() - // let option = PHImageRequestOptions() - // option.isSynchronous = true - // - // assets.enumerateObjects { asset, _, _ in - // manager.requestImage(for: asset, targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: option) { image, _ in - // if let image = image { - // DispatchQueue.main.async { - // self.photos.append(image) - // } - // } - // } - // } - // } - - private func setupCameraFeed() { - cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video) - - let captureSession = AVCaptureSession() - captureSession.sessionPreset = .medium - - guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { - print("Unable to access the back camera!") - return - } - - do { - let input = try AVCaptureDeviceInput(device: backCamera) - if captureSession.canAddInput(input) { - captureSession.addInput(input) - } - } catch { - print("Error Unable to initialize back camera: \(error.localizedDescription)") - } - - let videoOutput = AVCaptureVideoDataOutput() - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue")) - if captureSession.canAddOutput(videoOutput) { - captureSession.addOutput(videoOutput) - } - - captureSession.startRunning() - } -} - -extension MediaManager: AVCaptureVideoDataOutputSampleBufferDelegate { - func captureOutput(_: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) { - print("Capturing output started") - guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { - return - } - - let ciImage = CIImage(cvPixelBuffer: pixelBuffer) - let context = CIContext() - guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { - return - } - - DispatchQueue.main.async { - self.cameraFeed = UIImage(cgImage: cgImage) - print("Updated camera feed") - } - } -} - -extension MediaManager { - func openAppSettings() { - if - let appSettingsUrl = URL(string: UIApplication.openSettingsURLString), - UIApplication.shared.canOpenURL(appSettingsUrl) - { - UIApplication.shared.open(appSettingsUrl, completionHandler: nil) - } - } - - @objc private func appDidBecomeActive() { - // Update access levels - // galleryAccessLevel = PHPhotoLibrary.authorizationStatus() - cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video) - setupCameraFeed() - } -} +// import AVFoundation +// import Photos +// import SwiftUI +// import UIKit +// +// class MediaManager: NSObject, ObservableObject { +// // @Published var photos: [UIImage] = [] +// @Published var cameraFeed: UIImage? +// +// // @Published var galleryAccessLevel: PHAuthorizationStatus = .notDetermined +// @Published var cameraAccessLevel: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) +// +// override init() { +// super.init() +// NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) +// +// // DispatchQueue.main.async { [weak self] in +// // // self?.fetchPhotos() +// // } +// } +// +// // private func fetchPhotos() { +// // galleryAccessLevel = PHPhotoLibrary.authorizationStatus() +// // +// // let fetchOptions = PHFetchOptions() +// // fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] +// // let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions) +// // +// // let manager = PHImageManager.default() +// // let option = PHImageRequestOptions() +// // option.isSynchronous = true +// // +// // assets.enumerateObjects { asset, _, _ in +// // manager.requestImage(for: asset, targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: option) { image, _ in +// // if let image = image { +// // DispatchQueue.main.async { +// // self.photos.append(image) +// // } +// // } +// // } +// // } +// // } +// +// private func setupCameraFeed() { +// cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video) +// +// let captureSession = AVCaptureSession() +// captureSession.sessionPreset = .medium +// +// guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { +// print("Unable to access the back camera!") +// return +// } +// +// do { +// let input = try AVCaptureDeviceInput(device: backCamera) +// if captureSession.canAddInput(input) { +// captureSession.addInput(input) +// } +// } catch { +// print("Error Unable to initialize back camera: \(error.localizedDescription)") +// } +// +// let videoOutput = AVCaptureVideoDataOutput() +// videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue")) +// if captureSession.canAddOutput(videoOutput) { +// captureSession.addOutput(videoOutput) +// } +// +// captureSession.startRunning() +// } +// } +// +// extension MediaManager: AVCaptureVideoDataOutputSampleBufferDelegate { +// func captureOutput(_: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) { +// print("Capturing output started") +// guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { +// return +// } +// +// let ciImage = CIImage(cvPixelBuffer: pixelBuffer) +// let context = CIContext() +// guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { +// return +// } +// +// DispatchQueue.main.async { +// self.cameraFeed = UIImage(cgImage: cgImage) +// print("Updated camera feed") +// } +// } +// } +// +// extension MediaManager { +// func openAppSettings() { +// if +// let appSettingsUrl = URL(string: UIApplication.openSettingsURLString), +// UIApplication.shared.canOpenURL(appSettingsUrl) +// { +// UIApplication.shared.open(appSettingsUrl, completionHandler: nil) +// } +// } +// +// @objc private func appDidBecomeActive() { +// // Update access levels +// // galleryAccessLevel = PHPhotoLibrary.authorizationStatus() +// cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video) +// setupCameraFeed() +// } +// } diff --git a/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift b/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift index 8396a4e..0305e68 100644 --- a/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift +++ b/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift @@ -1,141 +1,361 @@ +import AVFoundation +import Photos import SwiftUI struct AttachmentMediaPickerView: View { - @StateObject private var mediaManager = MediaManager() + @State private var isCameraAccessGranted = AVCaptureDevice.authorizationStatus(for: .video) == .authorized + + @State private var isGalleryAccessGranted = PHPhotoLibrary.authorizationStatus() == .authorized + @State private var images = [UIImage]() + + let gridSize = UIScreen.main.bounds.width / 3 var body: some View { - ScrollView { - LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 3)) { - ForEach(elements) { element in - element + let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3) + + ScrollView(showsIndicators: false) { + LazyVGrid(columns: columns, spacing: 0) { + // For camera + if isCameraAccessGranted { + ZStack { + CameraView() + .aspectRatio(1, contentMode: .fit) + .frame(maxWidth: .infinity) + Image(systemName: "camera") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 40, height: 40) + .foregroundColor(.white) + .padding(8) + .background(Color.black.opacity(0.5)) + .clipShape(Circle()) + .padding(8) + } + } else { + Button { + openAppSettings() + } label: { + ZStack { + Rectangle() + .fill(Color.Main.backgroundLight) + .overlay { + VStack { + Image(systemName: "camera") + .foregroundColor(.Material.tortoiseLight300) + .font(.system(size: 30)) + Text("Allow camera access") + .foregroundColor(.Main.black) + .font(.body3) + } + } + .frame(height: 100) + } + } + } + + // For pictures + if isGalleryAccessGranted { + ForEach(images.indices, id: \.self) { index in + Image(uiImage: images[index]) + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(maxWidth: .infinity) + .clipped() + } + } else { + Button { + openAppSettings() + } label: { + ZStack { + Rectangle() + .fill(Color.Main.backgroundLight) + .overlay { + VStack { + Image(systemName: "photo") + .foregroundColor(.Material.tortoiseLight300) + .font(.system(size: 30)) + Text("Allow gallery access") + .foregroundColor(.Main.black) + .font(.body3) + } + } + .frame(height: 100) + } + } } } - .padding(.horizontal, 8) } - .padding(.vertical, 8) + .onAppear { + checkCameraAccess() + checkGalleryAccess() + } } - private var elements: [GridElement] { - print("Creating elements") - var result: [GridElement] = [] + private func checkCameraAccess() { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .authorized: + isCameraAccessGranted = true - // camera - if let feed = mediaManager.cameraFeed, mediaManager.cameraAccessLevel == .authorized { - result.append(GridElement(id: UUID(), type: .cameraFeed, content: feed) { - print("Go to capture???") - }) - print("Added camera feed") - } else if mediaManager.cameraAccessLevel == .restricted { - result.append(GridElement(id: UUID(), type: .cameraRestricted, content: nil) { - print("Show alert") - }) - print("Added camera restricted") - } else { - result.append(GridElement(id: UUID(), type: .cameraAskButton, content: nil) { - mediaManager.openAppSettings() - }) - print("Added camera ask button") + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { granted in + DispatchQueue.main.async { + self.isCameraAccessGranted = granted + } + } + + case .denied, .restricted: + isCameraAccessGranted = false + + @unknown default: + isCameraAccessGranted = false } + } - // photos - // if mediaManager.galleryAccessLevel == .restricted { - // result.append(GridElement(id: UUID(), type: .photoRestricted, content: nil)) - // } else { - // for photo in mediaManager.photos { - // result.append(GridElement(id: UUID(), type: .photo, content: photo)) - // } - // if mediaManager.galleryAccessLevel != .authorized { - // result.append(GridElement(id: UUID(), type: .photoAskButton, content: nil)) - // } - // } + private func checkGalleryAccess() { + let status = PHPhotoLibrary.authorizationStatus() + switch status { + case .authorized, .limited: + isGalleryAccessGranted = true + fetchImages() + case .notDetermined: + PHPhotoLibrary.requestAuthorization { status in + DispatchQueue.main.async { + self.isGalleryAccessGranted = status == .authorized + if self.isGalleryAccessGranted { + self.fetchImages() + } + } + } + + case .denied, .restricted: + isGalleryAccessGranted = false + + @unknown default: + isGalleryAccessGranted = false + } + } + + private func fetchImages() { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions) + + let manager = PHImageManager.default() + let option = PHImageRequestOptions() + option.isSynchronous = true + + assets.enumerateObjects { asset, _, _ in + manager.requestImage( + for: asset, + targetSize: PHImageManagerMaximumSize, + contentMode: .aspectFill, + options: option + ) { image, _ in + if let image = image { + DispatchQueue.main.async { + if let img = scaleAndCropImage(image, toSize: CGSize(width: gridSize, height: gridSize)) { + self.images.append(img) + } + } + } + } + } + } + + func openAppSettings() { + if + let appSettingsUrl = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(appSettingsUrl) + { + UIApplication.shared.open(appSettingsUrl, completionHandler: nil) + } + } + + func scaleAndCropImage(_ image: UIImage, toSize size: CGSize) -> UIImage? { + let imageView = UIImageView(frame: CGRect(origin: .zero, size: size)) + imageView.contentMode = .scaleAspectFill + imageView.image = image + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + guard let context = UIGraphicsGetCurrentContext() else { return nil } + imageView.layer.render(in: context) + let result = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() return result } } -private enum GridElementType { - case cameraFeed - case cameraAskButton - case cameraRestricted - case photo - case photoAskButton - case photoRestricted -} +class CameraUIView: UIView { + var previewLayer: AVCaptureVideoPreviewLayer? -private struct GridElement: View, Identifiable { - let id: UUID - let type: GridElementType - let content: UIImage? - let action: () -> Void - - var body: some View { - switch type { - case .cameraFeed: - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .clipped() - - case .cameraAskButton: - Button { - action() - } label: { - RoundedRectangle(cornerRadius: 5) - .stroke(Color.Main.backgroundDark, lineWidth: 2) - .overlay { - Image(systemName: "camera") - .foregroundColor(.Material.tortoiseLight300) - .font(.system(size: 40)) - } - .frame(height: 100) - // .resizable() - // .aspectRatio(contentMode: .fill) - // .frame(width: 100, height: 100) - // .clipped() - } - - case .photo: - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .clipped() - - case .photoAskButton: - Button { - action() - } label: { - Image(systemName: "photo.badge.plus") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .clipped() - } - - case .photoRestricted, .cameraRestricted: - Button { - action() - } label: { - Image(systemName: "cross") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .clipped() - } - } - } - - private var image: Image { - guard let content = content else { - return Image(systemName: "questionmark.square.dashed") - } - return Image(uiImage: content) + override func layoutSubviews() { + super.layoutSubviews() + previewLayer?.frame = bounds } } -struct AttachmentMediaPickerView_Previews: PreviewProvider { - static var previews: some View { - AttachmentMediaPickerView() +struct CameraView: UIViewRepresentable { + func makeUIView(context _: Context) -> CameraUIView { + let view = CameraUIView() + + let captureSession = AVCaptureSession() + guard let captureDevice = AVCaptureDevice.default(for: .video) else { return view } + guard let input = try? AVCaptureDeviceInput(device: captureDevice) else { return view } + captureSession.addInput(input) + + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + view.previewLayer = previewLayer + + captureSession.startRunning() + + return view + } + + func updateUIView(_ uiView: CameraUIView, context _: Context) { + // Update the previewLayer frame when the view updates + uiView.previewLayer?.frame = uiView.bounds } } + +// struct AttachmentMediaPickerView: View { +// @StateObject private var mediaManager = MediaManager() +// +// var body: some View { +// ScrollView { +// LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 3)) { +// ForEach(elements) { element in +// element +// } +// } +// .padding(.horizontal, 8) +// } +// .padding(.vertical, 8) +// } +// +// private var elements: [GridElement] { +// print("Creating elements") +// var result: [GridElement] = [] +// +// // camera +// if let feed = mediaManager.cameraFeed, mediaManager.cameraAccessLevel == .authorized { +// result.append(GridElement(id: UUID(), type: .cameraFeed, content: feed) { +// print("Go to capture???") +// }) +// print("Added camera feed") +// } else if mediaManager.cameraAccessLevel == .restricted { +// result.append(GridElement(id: UUID(), type: .cameraRestricted, content: nil) { +// print("Show alert") +// }) +// print("Added camera restricted") +// } else { +// result.append(GridElement(id: UUID(), type: .cameraAskButton, content: nil) { +// mediaManager.openAppSettings() +// }) +// print("Added camera ask button") +// } +// +// // photos +// // if mediaManager.galleryAccessLevel == .restricted { +// // result.append(GridElement(id: UUID(), type: .photoRestricted, content: nil)) +// // } else { +// // for photo in mediaManager.photos { +// // result.append(GridElement(id: UUID(), type: .photo, content: photo)) +// // } +// // if mediaManager.galleryAccessLevel != .authorized { +// // result.append(GridElement(id: UUID(), type: .photoAskButton, content: nil)) +// // } +// // } +// +// return result +// } +// } +// +// private enum GridElementType { +// case cameraFeed +// case cameraAskButton +// case cameraRestricted +// case photo +// case photoAskButton +// case photoRestricted +// } +// +// private struct GridElement: View, Identifiable { +// let id: UUID +// let type: GridElementType +// let content: UIImage? +// let action: () -> Void +// +// var body: some View { +// switch type { +// case .cameraFeed: +// image +// .resizable() +// .aspectRatio(contentMode: .fill) +// .frame(width: 100, height: 100) +// .clipped() +// +// case .cameraAskButton: +// Button { +// action() +// } label: { +// RoundedRectangle(cornerRadius: 5) +// .stroke(Color.Main.backgroundDark, lineWidth: 2) +// .overlay { +// Image(systemName: "camera") +// .foregroundColor(.Material.tortoiseLight300) +// .font(.system(size: 40)) +// } +// .frame(height: 100) +// // .resizable() +// // .aspectRatio(contentMode: .fill) +// // .frame(width: 100, height: 100) +// // .clipped() +// } +// +// case .photo: +// image +// .resizable() +// .aspectRatio(contentMode: .fill) +// .frame(width: 100, height: 100) +// .clipped() +// +// case .photoAskButton: +// Button { +// action() +// } label: { +// Image(systemName: "photo.badge.plus") +// .resizable() +// .aspectRatio(contentMode: .fill) +// .frame(width: 100, height: 100) +// .clipped() +// } +// +// case .photoRestricted, .cameraRestricted: +// Button { +// action() +// } label: { +// Image(systemName: "cross") +// .resizable() +// .aspectRatio(contentMode: .fill) +// .frame(width: 100, height: 100) +// .clipped() +// } +// } +// } +// +// private var image: Image { +// guard let content = content else { +// return Image(systemName: "questionmark.square.dashed") +// } +// return Image(uiImage: content) +// } +// } +// +// struct AttachmentMediaPickerView_Previews: PreviewProvider { +// static var previews: some View { +// AttachmentMediaPickerView() +// } +// }