2024-07-03 11:50:59 +00:00
|
|
|
import AVFoundation
|
|
|
|
import Photos
|
2024-07-02 09:56:27 +00:00
|
|
|
import SwiftUI
|
2024-07-02 10:23:27 +00:00
|
|
|
|
2024-07-04 09:17:05 +00:00
|
|
|
struct SelectedMedia {
|
2024-07-04 09:53:45 +00:00
|
|
|
let id: String
|
2024-07-04 09:17:05 +00:00
|
|
|
}
|
|
|
|
|
2024-07-02 09:56:27 +00:00
|
|
|
struct AttachmentMediaPickerView: View {
|
2024-07-03 11:50:59 +00:00
|
|
|
@State private var isCameraAccessGranted = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
|
|
|
@State private var isGalleryAccessGranted = PHPhotoLibrary.authorizationStatus() == .authorized
|
2024-07-03 13:47:25 +00:00
|
|
|
|
2024-07-04 09:17:05 +00:00
|
|
|
@State private var thumbnails = [ThumbnailView]()
|
|
|
|
@State private var selectedMedia = [SelectedMedia]()
|
2024-07-03 11:50:59 +00:00
|
|
|
|
|
|
|
let gridSize = UIScreen.main.bounds.width / 3
|
2024-07-02 09:56:27 +00:00
|
|
|
|
|
|
|
var body: some View {
|
2024-07-03 11:50:59 +00:00
|
|
|
let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
|
|
|
|
|
2024-07-04 09:53:45 +00:00
|
|
|
VStack(spacing: 0) {
|
|
|
|
// List of media
|
|
|
|
ScrollView(showsIndicators: false) {
|
|
|
|
LazyVGrid(columns: columns, spacing: 0) {
|
|
|
|
// For camera
|
|
|
|
if isCameraAccessGranted {
|
2024-07-03 11:50:59 +00:00
|
|
|
ZStack {
|
2024-07-04 09:53:45 +00:00
|
|
|
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.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)
|
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
2024-07-04 09:53:45 +00:00
|
|
|
.frame(height: 100)
|
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-04 09:53:45 +00:00
|
|
|
// 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)
|
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
2024-07-04 09:53:45 +00:00
|
|
|
.frame(height: 100)
|
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
|
|
|
}
|
2024-07-02 10:23:27 +00:00
|
|
|
}
|
2024-07-02 09:56:27 +00:00
|
|
|
}
|
2024-07-04 09:53:45 +00:00
|
|
|
|
|
|
|
// 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 {
|
|
|
|
print("Send media files")
|
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
.onAppear {
|
2024-07-03 13:47:25 +00:00
|
|
|
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
|
|
|
|
checkCameraAccess()
|
|
|
|
checkGalleryAccess()
|
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
}
|
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
private func checkCameraAccess() {
|
|
|
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
|
|
switch status {
|
|
|
|
case .authorized:
|
|
|
|
isCameraAccessGranted = true
|
2024-07-03 10:47:59 +00:00
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
case .notDetermined:
|
|
|
|
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.isCameraAccessGranted = granted
|
|
|
|
}
|
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
case .denied, .restricted:
|
|
|
|
isCameraAccessGranted = false
|
2024-07-03 10:47:59 +00:00
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
@unknown default:
|
|
|
|
isCameraAccessGranted = false
|
|
|
|
}
|
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
private func checkGalleryAccess() {
|
|
|
|
let status = PHPhotoLibrary.authorizationStatus()
|
|
|
|
switch status {
|
|
|
|
case .authorized, .limited:
|
|
|
|
isGalleryAccessGranted = true
|
|
|
|
fetchImages()
|
2024-07-03 10:47:59 +00:00
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
case .notDetermined:
|
|
|
|
PHPhotoLibrary.requestAuthorization { status in
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.isGalleryAccessGranted = status == .authorized
|
|
|
|
if self.isGalleryAccessGranted {
|
|
|
|
self.fetchImages()
|
2024-07-03 10:47:59 +00:00
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
}
|
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
case .denied, .restricted:
|
|
|
|
isGalleryAccessGranted = false
|
|
|
|
|
|
|
|
@unknown default:
|
|
|
|
isGalleryAccessGranted = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func fetchImages() {
|
|
|
|
let fetchOptions = PHFetchOptions()
|
|
|
|
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
2024-07-04 08:47:08 +00:00
|
|
|
let assets = PHAsset.fetchAssets(with: fetchOptions)
|
2024-07-03 11:50:59 +00:00
|
|
|
|
|
|
|
let manager = PHImageManager.default()
|
|
|
|
let option = PHImageRequestOptions()
|
|
|
|
option.isSynchronous = true
|
2024-07-03 10:47:59 +00:00
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
assets.enumerateObjects { asset, _, _ in
|
2024-07-04 08:47:08 +00:00
|
|
|
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 {
|
2024-07-04 09:53:45 +00:00
|
|
|
self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia))
|
2024-07-04 08:47:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} 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 {
|
2024-07-04 09:53:45 +00:00
|
|
|
self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia, duration: asset.duration.minAndSec))
|
2024-07-04 08:47:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} catch {
|
|
|
|
print("Failed to create thumbnail image")
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
|
|
|
}
|
2024-07-04 08:47:08 +00:00
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
func openAppSettings() {
|
|
|
|
if
|
|
|
|
let appSettingsUrl = URL(string: UIApplication.openSettingsURLString),
|
|
|
|
UIApplication.shared.canOpenURL(appSettingsUrl)
|
|
|
|
{
|
|
|
|
UIApplication.shared.open(appSettingsUrl, completionHandler: nil)
|
2024-07-02 09:56:27 +00:00
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
2024-07-03 13:47:25 +00:00
|
|
|
}
|
|
|
|
|
2024-07-04 09:17:05 +00:00
|
|
|
private struct ThumbnailView: Identifiable, View {
|
2024-07-04 09:53:45 +00:00
|
|
|
let id: String
|
2024-07-03 13:47:25 +00:00
|
|
|
let gridSize: CGFloat
|
2024-07-04 08:47:08 +00:00
|
|
|
let duration: String?
|
2024-07-03 13:47:25 +00:00
|
|
|
|
|
|
|
@State private var image: UIImage
|
|
|
|
@State private var ready = false
|
2024-07-04 09:17:05 +00:00
|
|
|
@State private var selected = false
|
|
|
|
@Binding var selectedMedia: [SelectedMedia]
|
2024-07-03 11:50:59 +00:00
|
|
|
|
2024-07-04 09:53:45 +00:00
|
|
|
init(id: String, image: UIImage, gridSize: CGFloat, selected: Binding<[SelectedMedia]>, duration: String? = nil) {
|
|
|
|
self.id = id
|
2024-07-03 13:47:25 +00:00
|
|
|
self.image = image
|
|
|
|
self.gridSize = gridSize
|
2024-07-04 09:17:05 +00:00
|
|
|
_selectedMedia = selected
|
2024-07-04 08:47:08 +00:00
|
|
|
self.duration = duration
|
2024-07-03 13:47:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
if ready {
|
2024-07-04 08:47:08 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-04 09:17:05 +00:00
|
|
|
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 {
|
2024-07-04 09:53:45 +00:00
|
|
|
selectedMedia.append(SelectedMedia(id: id))
|
2024-07-04 09:17:05 +00:00
|
|
|
} else {
|
2024-07-04 09:53:45 +00:00
|
|
|
selectedMedia.removeAll { $0.id == id }
|
2024-07-04 09:17:05 +00:00
|
|
|
}
|
|
|
|
}
|
2024-07-04 08:47:08 +00:00
|
|
|
}
|
2024-07-03 13:47:25 +00:00
|
|
|
} else {
|
|
|
|
ZStack {
|
|
|
|
Rectangle()
|
2024-07-04 08:21:12 +00:00
|
|
|
.fill(Color.Material.Background.light)
|
2024-07-03 13:47:25 +00:00
|
|
|
.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
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-07-02 09:56:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-03 11:50:59 +00:00
|
|
|
class CameraUIView: UIView {
|
|
|
|
var previewLayer: AVCaptureVideoPreviewLayer?
|
|
|
|
|
|
|
|
override func layoutSubviews() {
|
|
|
|
super.layoutSubviews()
|
|
|
|
previewLayer?.frame = bounds
|
2024-07-02 09:56:27 +00:00
|
|
|
}
|
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|