another.im-ios/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift
2024-07-09 15:13:37 +02:00

453 lines
17 KiB
Swift

import AVFoundation
import MobileCoreServices
import Photos
import SwiftUI
struct SelectedMedia {
let id: String
}
struct AttachmentMediaPickerView: View {
@State private var isCameraAccessGranted = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
@State private var isGalleryAccessGranted = PHPhotoLibrary.authorizationStatus() == .authorized
@State private var thumbnails = [ThumbnailView]()
@State private var selectedMedia = [SelectedMedia]()
@State private var showCameraPicker = false
let gridSize = UIScreen.main.bounds.width / 3
var body: some View {
let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
VStack(spacing: 0) {
// List of media
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)
}
.onTapGesture {
showCameraPicker = true
}
} else {
Button {
openAppSettings()
} label: {
ZStack {
Rectangle()
.fill(Color.Material.Background.light)
.overlay {
VStack {
Image(systemName: "camera")
.foregroundColor(.Material.Elements.active)
.font(.system(size: 30))
Text("Allow camera access")
.foregroundColor(.Material.Text.main)
.font(.body3)
}
}
.frame(height: 100)
}
}
}
// For gallery
if isGalleryAccessGranted {
ForEach(thumbnails) { photo in
photo
}
} else {
Button {
openAppSettings()
} label: {
ZStack {
Rectangle()
.fill(Color.Material.Background.light)
.overlay {
VStack {
Image(systemName: "photo")
.foregroundColor(.Material.Elements.active)
.font(.system(size: 30))
Text("Allow gallery access")
.foregroundColor(.Material.Text.main)
.font(.body3)
}
}
.frame(height: 100)
}
}
}
}
}
.fullScreenCover(isPresented: $showCameraPicker) {
CameraPicker(sourceType: .camera) { data, type in
store.dispatch(.conversationAction(.sendAttachment(.init(
id: UUID().uuidString,
items: [.init(type: type, data: data, string: "")]
))))
showCameraPicker = false
}
.edgesIgnoringSafeArea(.all)
}
// Send panel
Rectangle()
.foregroundColor(.Material.Shape.black)
.frame(maxWidth: .infinity)
.frame(height: self.selectedMedia.isEmpty ? 0 : 50)
.overlay {
HStack {
Text(L10n.Attachment.Send.media)
.foregroundColor(.Material.Text.white)
.font(.body1)
Image(systemName: "arrow.up.circle")
.foregroundColor(.Material.Text.white)
.font(.body1)
.padding(.leading, 8)
}
.padding()
}
.clipped()
.onTapGesture {
let ids = selectedMedia.map { $0.id }
sendGalleryMedia(ids: ids)
store.dispatch(.conversationAction(.showAttachmentPicker(false)))
}
}
.onAppear {
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
checkCameraAccess()
checkGalleryAccess()
}
}
}
private func checkCameraAccess() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
isCameraAccessGranted = true
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
self.isCameraAccessGranted = granted
}
}
case .denied, .restricted:
isCameraAccessGranted = false
@unknown default:
isCameraAccessGranted = false
}
}
private func checkGalleryAccess() {
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized, .limited:
isGalleryAccessGranted = true
fetchGallery()
case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
self.isGalleryAccessGranted = status == .authorized
if self.isGalleryAccessGranted {
self.fetchGallery()
}
}
}
case .denied, .restricted:
isGalleryAccessGranted = false
@unknown default:
isGalleryAccessGranted = false
}
}
private func fetchGallery() {
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
assets.enumerateObjects { asset, _, _ in
if asset.mediaType == .image {
manager.requestImage(
for: asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFill,
options: option
) { image, _ in
image?.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in
if let image {
DispatchQueue.main.async {
self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia))
}
}
})
}
} 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: gridSize, height: gridSize), completion: { image in
if let image {
DispatchQueue.main.async {
self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia, duration: asset.duration.minAndSec))
}
}
})
} catch {
print("Failed to create thumbnail image")
}
}
}
}
}
}
private func sendGalleryMedia(ids: [String]) {
var media: [AttachmentItem] = []
let dispatchGroup = DispatchGroup()
let asset = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil)
asset.enumerateObjects { asset, _, _ in
dispatchGroup.enter()
if asset.mediaType == .image {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
option.isSynchronous = true
manager.requestImage(
for: asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFill,
options: option
) { image, _ in
if let image {
let data = image.jpegData(compressionQuality: 1.0) ?? Data()
media.append(.init(type: .image, data: data, string: ""))
}
dispatchGroup.leave()
}
} else if asset.mediaType == .video {
let manager = PHImageManager.default()
let option = PHVideoRequestOptions()
option.version = .current
option.deliveryMode = .highQualityFormat
manager.requestAVAsset(forVideo: asset, options: option) { avAsset, _, _ in
if let avAsset {
let url = (avAsset as? AVURLAsset)?.url
let data = try? Data(contentsOf: url ?? URL(fileURLWithPath: ""))
media.append(.init(type: .movie, data: data ?? Data(), string: ""))
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
store.dispatch(.conversationAction(.sendAttachment(.init(
id: UUID().uuidString,
items: media
))))
}
}
}
private struct ThumbnailView: Identifiable, View {
let id: String
let gridSize: CGFloat
let duration: String?
@State private var image: UIImage
@State private var ready = false
@State private var selected = false
@Binding var selectedMedia: [SelectedMedia]
init(id: String, image: UIImage, gridSize: CGFloat, selected: Binding<[SelectedMedia]>, duration: String? = nil) {
self.id = id
self.image = image
self.gridSize = gridSize
_selectedMedia = selected
self.duration = duration
}
var body: some View {
if ready {
ZStack {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: gridSize, height: gridSize)
.clipped()
if let duration {
VStack {
Spacer()
HStack {
Spacer()
Text(duration)
.foregroundColor(.Material.Text.white)
.font(.sub1)
.shadow(color: .black, radius: 2)
.padding(4)
}
}
}
if selected {
VStack {
HStack {
Spacer()
Circle()
.frame(width: 30, height: 30)
.shadow(color: .black, radius: 2)
.foregroundColor(.Material.Shape.white)
.overlay {
Image(systemName: "checkmark")
.foregroundColor(.Material.Elements.active)
.font(.body3)
}
.padding(4)
}
Spacer()
}
}
}
.onTapGesture {
withAnimation {
selected.toggle()
if selected {
selectedMedia.append(SelectedMedia(id: id))
} else {
selectedMedia.removeAll { $0.id == id }
}
}
}
} else {
ZStack {
Rectangle()
.fill(Color.Material.Background.light)
.overlay {
ProgressView()
}
.frame(width: gridSize, height: gridSize)
}
.onAppear {
image.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in
if let image {
self.image = image
ready = true
}
})
}
}
}
}
class CameraUIView: UIView {
var previewLayer: AVCaptureVideoPreviewLayer?
override func layoutSubviews() {
super.layoutSubviews()
previewLayer?.frame = bounds
}
}
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) {
uiView.previewLayer?.frame = uiView.bounds
}
}
struct CameraPicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType
var completionHandler: (Data, AttachmentType) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
picker.mediaTypes = [UTType.movie.identifier, UTType.image.identifier]
picker.videoQuality = .typeHigh
picker.videoMaximumDuration = 60 // 60 sec Limit
picker.view.backgroundColor = .clear
return picker
}
func updateUIViewController(_: UIImagePickerController, context _: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: CameraPicker
init(_ parent: CameraPicker) {
self.parent = parent
}
func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
// swiftlint:disable:next force_cast
let mediaType = info[.mediaType] as! String
if mediaType == UTType.image.identifier {
if let image = info[.originalImage] as? UIImage {
let data = image.jpegData(compressionQuality: 1.0) ?? Data()
parent.completionHandler(data, .image)
}
} else if mediaType == UTType.movie.identifier {
if let url = info[.mediaURL] as? URL {
let data = try? Data(contentsOf: url)
parent.completionHandler(data ?? Data(), .movie)
}
}
}
}
}