2024-07-03 11:50:59 +00:00
|
|
|
import AVFoundation
|
2024-07-04 10:40:27 +00:00
|
|
|
import MobileCoreServices
|
2024-07-03 11:50:59 +00:00
|
|
|
import Photos
|
2024-07-02 09:56:27 +00:00
|
|
|
import SwiftUI
|
2024-07-02 10:23:27 +00:00
|
|
|
|
2024-07-13 13:58:38 +00:00
|
|
|
struct SharingMediaPickerView: View {
|
2024-07-10 17:49:36 +00:00
|
|
|
@EnvironmentObject var store: AppStore
|
2024-07-04 10:40:27 +00:00
|
|
|
@State private var showCameraPicker = false
|
2024-07-10 18:13:17 +00:00
|
|
|
@State private var cameraReady = false
|
|
|
|
|
|
|
|
@State private var selectedItems: [String] = []
|
2024-07-04 10:40:27 +00:00
|
|
|
|
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
|
2024-07-10 17:49:36 +00:00
|
|
|
if store.state.sharingState.isCameraAccessGranted {
|
2024-07-10 18:13:17 +00:00
|
|
|
if cameraReady {
|
|
|
|
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 {
|
|
|
|
ProgressView()
|
2024-07-04 09:53:45 +00:00
|
|
|
.frame(maxWidth: .infinity)
|
2024-07-10 18:13:17 +00:00
|
|
|
.frame(height: 100)
|
|
|
|
.onAppear {
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
|
|
|
cameraReady = true
|
|
|
|
}
|
|
|
|
}
|
2024-07-04 10:40:27 +00:00
|
|
|
}
|
2024-07-04 09:53:45 +00:00
|
|
|
} 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
|
2024-07-10 17:49:36 +00:00
|
|
|
if store.state.sharingState.isGalleryAccessGranted {
|
|
|
|
ForEach(store.state.sharingState.galleryItems) { item in
|
2024-07-10 18:13:17 +00:00
|
|
|
GridViewItem(item: item, selected: $selectedItems)
|
2024-07-04 09:53:45 +00:00
|
|
|
}
|
|
|
|
} 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 10:40:27 +00:00
|
|
|
.fullScreenCover(isPresented: $showCameraPicker) {
|
2024-07-10 17:49:36 +00:00
|
|
|
CameraPicker(sourceType: .camera) { data, type in
|
|
|
|
store.dispatch(.sharingAction(.cameraCaptured(media: data, type: type)))
|
|
|
|
showCameraPicker = false
|
|
|
|
store.dispatch(.sharingAction(.showSharing(false)))
|
|
|
|
}
|
|
|
|
.edgesIgnoringSafeArea(.all)
|
2024-07-04 10:40:27 +00:00
|
|
|
}
|
2024-07-04 09:53:45 +00:00
|
|
|
|
|
|
|
// Send panel
|
|
|
|
Rectangle()
|
|
|
|
.foregroundColor(.Material.Shape.black)
|
|
|
|
.frame(maxWidth: .infinity)
|
2024-07-10 18:13:17 +00:00
|
|
|
.frame(height: self.selectedItems.isEmpty ? 0 : 50)
|
2024-07-04 09:53:45 +00:00
|
|
|
.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 {
|
2024-07-11 11:24:43 +00:00
|
|
|
store.dispatch(.sharingAction(.shareMedia(ids: selectedItems)))
|
2024-07-10 14:13:47 +00:00
|
|
|
store.dispatch(.sharingAction(.showSharing(false)))
|
2024-07-04 09:53:45 +00:00
|
|
|
}
|
2024-07-03 10:47:59 +00:00
|
|
|
}
|
2024-07-03 11:50:59 +00:00
|
|
|
.onAppear {
|
2024-07-10 17:49:36 +00:00
|
|
|
store.dispatch(.sharingAction(.checkCameraAccess))
|
|
|
|
store.dispatch(.sharingAction(.checkGalleryAccess))
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
2024-07-10 17:49:36 +00:00
|
|
|
.onChange(of: store.state.sharingState.isGalleryAccessGranted) { granted in
|
|
|
|
if granted {
|
|
|
|
store.dispatch(.sharingAction(.fetchGallery))
|
2024-07-03 11:50:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-03 13:47:25 +00:00
|
|
|
}
|
|
|
|
|
2024-07-10 17:49:36 +00:00
|
|
|
private struct GridViewItem: View {
|
|
|
|
let item: SharingGalleryItem
|
2024-07-10 18:13:17 +00:00
|
|
|
@Binding var selected: [String]
|
|
|
|
@State var isSelected = false
|
2024-07-03 13:47:25 +00:00
|
|
|
|
|
|
|
var body: some View {
|
2024-07-10 17:49:36 +00:00
|
|
|
if let data = item.thumbnail {
|
2024-07-04 08:47:08 +00:00
|
|
|
ZStack {
|
2024-07-10 17:49:36 +00:00
|
|
|
Image(uiImage: UIImage(data: data) ?? UIImage())
|
2024-07-04 08:47:08 +00:00
|
|
|
.resizable()
|
|
|
|
.aspectRatio(contentMode: .fill)
|
2024-07-10 17:49:36 +00:00
|
|
|
.frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
2024-07-04 08:47:08 +00:00
|
|
|
.clipped()
|
2024-07-10 17:49:36 +00:00
|
|
|
if let duration = item.duration {
|
2024-07-04 08:47:08 +00:00
|
|
|
VStack {
|
|
|
|
Spacer()
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
Text(duration)
|
|
|
|
.foregroundColor(.Material.Text.white)
|
|
|
|
.font(.sub1)
|
|
|
|
.shadow(color: .black, radius: 2)
|
|
|
|
.padding(4)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-10 18:13:17 +00:00
|
|
|
if isSelected {
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
2024-07-04 09:17:05 +00:00
|
|
|
}
|
|
|
|
.onTapGesture {
|
2024-07-10 18:13:17 +00:00
|
|
|
isSelected.toggle()
|
|
|
|
if isSelected {
|
|
|
|
selected.append(item.id)
|
2024-07-11 11:24:43 +00:00
|
|
|
} else {
|
|
|
|
selected.removeAll { $0 == item.id }
|
2024-07-10 18:13:17 +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()
|
|
|
|
}
|
2024-07-10 17:49:36 +00:00
|
|
|
.frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
2024-07-03 13:47:25 +00:00
|
|
|
}
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
}
|
2024-07-04 10:40:27 +00:00
|
|
|
|
|
|
|
struct CameraPicker: UIViewControllerRepresentable {
|
|
|
|
var sourceType: UIImagePickerController.SourceType
|
2024-07-10 17:49:36 +00:00
|
|
|
var completionHandler: (Data, SharingCameraMediaType) -> Void
|
2024-07-04 10:40:27 +00:00
|
|
|
|
|
|
|
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
|
2024-07-10 17:49:36 +00:00
|
|
|
picker.videoMaximumDuration = Const.videoDurationLimit
|
2024-07-04 10:40:27 +00:00
|
|
|
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]) {
|
2024-07-09 12:43:38 +00:00
|
|
|
// swiftlint:disable:next force_cast
|
|
|
|
let mediaType = info[.mediaType] as! String
|
2024-07-09 12:37:51 +00:00
|
|
|
|
2024-07-10 17:49:36 +00:00
|
|
|
if mediaType == UTType.image.identifier {
|
|
|
|
if let image = info[.originalImage] as? UIImage {
|
|
|
|
let data = image.jpegData(compressionQuality: 1.0) ?? Data()
|
|
|
|
parent.completionHandler(data, .photo)
|
|
|
|
}
|
|
|
|
} else if mediaType == UTType.movie.identifier {
|
|
|
|
if let url = info[.mediaURL] as? URL {
|
|
|
|
let data = try? Data(contentsOf: url)
|
|
|
|
parent.completionHandler(data ?? Data(), .video)
|
|
|
|
}
|
|
|
|
}
|
2024-07-04 10:40:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|