From 71278400a8acbf8834dc30a86f51b7e274b5597a Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sun, 23 Apr 2023 21:18:14 +0200 Subject: [PATCH] #25 Support of sending images from Files and Camera --- Localization/en.lproj/Localizable.strings | 3 + Localization/eu.lproj/Localizable.strings | 3 + Localization/fr.lproj/Localizable.strings | 3 + Localization/pl.lproj/Localizable.strings | 3 + Vernissage.xcodeproj/project.pbxproj | 2 + .../WidgetsKit/Models/PhotoAttachment.swift | 20 ++++- .../WidgetsKit/Views/BaseComposeView.swift | 85 ++++++++++++++++++- .../WidgetsKit/Views/CameraPickerView.swift | 47 ++++++++++ 8 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 WidgetsKit/Sources/WidgetsKit/Views/CameraPickerView.swift diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings index 366119f..7431351 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -149,6 +149,9 @@ "compose.title.tryToUpload" = "Try to upload"; "compose.title.delete" = "Delete"; "compose.title.edit" = "Edit"; +"compose.title.photos" = "Photos library"; +"compose.title.camera" = "Take photo"; +"compose.title.files" = "Browse files"; "compose.error.loadingPhotosFailed" = "Cannot retreive image from library."; "compose.error.postingPhotoFailed" = "Error during posting photo."; "compose.error.postingStatusFailed" = "Error during posting status."; diff --git a/Localization/eu.lproj/Localizable.strings b/Localization/eu.lproj/Localizable.strings index 3ada5fc..0e2b619 100644 --- a/Localization/eu.lproj/Localizable.strings +++ b/Localization/eu.lproj/Localizable.strings @@ -149,6 +149,9 @@ "compose.title.tryToUpload" = "Saiatu igotzen"; "compose.title.delete" = "Ezabatu"; "compose.title.edit" = "Editatu"; +"compose.title.photos" = "Argazki-liburutegia"; +"compose.title.camera" = "Egin argazkia"; +"compose.title.files" = "Arakatu fitxategiak"; "compose.error.loadingPhotosFailed" = "Ezin da liburutegiko irudia eskuratu."; "compose.error.postingPhotoFailed" = "Errorea argazkia argitaratzean."; "compose.error.postingStatusFailed" = "Errorea egoera argitaratzean."; diff --git a/Localization/fr.lproj/Localizable.strings b/Localization/fr.lproj/Localizable.strings index 525cef3..68cdd73 100644 --- a/Localization/fr.lproj/Localizable.strings +++ b/Localization/fr.lproj/Localizable.strings @@ -149,6 +149,9 @@ "compose.title.tryToUpload" = "Essayer de télécharger"; "compose.title.delete" = "Supprimer"; "compose.title.edit" = "Editer"; +"compose.title.photos" = "Photos library"; +"compose.title.camera" = "Take photo"; +"compose.title.files" = "Browse files"; "compose.error.loadingPhotosFailed" = "Impossible de récupérer l'image depuis la bibliothèque."; "compose.error.postingPhotoFailed" = "Erreur pendant le post de la photo."; "compose.error.postingStatusFailed" = "Erreur pendant le post du statut."; diff --git a/Localization/pl.lproj/Localizable.strings b/Localization/pl.lproj/Localizable.strings index a2ea75d..5e45841 100644 --- a/Localization/pl.lproj/Localizable.strings +++ b/Localization/pl.lproj/Localizable.strings @@ -149,6 +149,9 @@ "compose.title.tryToUpload" = "Ponów"; "compose.title.delete" = "Usuń"; "compose.title.edit" = "Edytuj"; +"compose.title.photos" = "Biblioteka zdjęć"; +"compose.title.camera" = "Zrób zdjęcie"; +"compose.title.files" = "Przeglądaj pliki"; "compose.error.loadingPhotosFailed" = "Nie można pobrać zdjęcia z biblioteki."; "compose.error.postingPhotoFailed" = "Błąd podczas publikowania zdjęcia."; "compose.error.postingStatusFailed" = "Błąd podczas wysyłania statusu."; diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 4216561..9c339de 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -1437,6 +1437,7 @@ INFOPLIST_FILE = Vernissage/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Vernissage; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; + INFOPLIST_KEY_NSCameraUsageDescription = "Uploading photos to Pixelfed"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -1478,6 +1479,7 @@ INFOPLIST_FILE = Vernissage/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Vernissage; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; + INFOPLIST_KEY_NSCameraUsageDescription = "Uploading photos to Pixelfed"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/WidgetsKit/Sources/WidgetsKit/Models/PhotoAttachment.swift b/WidgetsKit/Sources/WidgetsKit/Models/PhotoAttachment.swift index 9c16a00..56748ac 100644 --- a/WidgetsKit/Sources/WidgetsKit/Models/PhotoAttachment.swift +++ b/WidgetsKit/Sources/WidgetsKit/Models/PhotoAttachment.swift @@ -19,6 +19,9 @@ public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashabl /// Information about image from share extension. public let nsItemProvider: NSItemProvider? + /// Information about image from camera sheet. + public let uiImage: UIImage? + /// Variable used for presentation layer. @Published public var photoData: Data? @@ -34,11 +37,12 @@ public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashabl /// Error from device. @Published public var loadError: Error? - public init(photosPickerItem: PhotosPickerItem? = nil, nsItemProvider: NSItemProvider? = nil) { + public init(photosPickerItem: PhotosPickerItem? = nil, nsItemProvider: NSItemProvider? = nil, uiImage: UIImage? = nil) { self.id = UUID().uuidString self.photosPickerItem = photosPickerItem self.nsItemProvider = nsItemProvider + self.uiImage = uiImage } public static func == (lhs: PhotoAttachment, rhs: PhotoAttachment) -> Bool { @@ -54,6 +58,8 @@ public extension PhotoAttachment { @MainActor func loadImage() async throws { + + // Load images from Photos app. if let pickerItem = self.photosPickerItem, let transferable = try await pickerItem.createImageFileTranseferable() { self.photoUrl = transferable.url @@ -62,6 +68,7 @@ public extension PhotoAttachment { return } + // Load images from share sheet (files app). if let itemProvider = self.nsItemProvider, let identifier = itemProvider.registeredTypeIdentifiers.first, let handledItemType = FileTypeSupported(rawValue: identifier), @@ -71,6 +78,17 @@ public extension PhotoAttachment { return } + + // Load images from camera. + if let image = self.uiImage, let data = image.getJpegData() { + let fileUrl = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).jpg") + try data.write(to: fileUrl) + + self.photoUrl = fileUrl + self.photoData = data + + return + } } } diff --git a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift index ae4bdb4..55025c8 100644 --- a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift +++ b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift @@ -31,10 +31,21 @@ public struct BaseComposeView: View { @State private var photosAreUploading = false @State private var photosPickerVisible = false + /// Images from camera pickler. + @State private var images: [UIImage] = [] + + /// Images from share sheet or files application. @State private var attachments: [NSItemProvider] + + /// Images from Photos app. @State private var selectedItems: [PhotosPickerItem] = [] + + /// Processed array with images. @State private var photosAttachment: [PhotoAttachment] = [] + @State private var isCameraPickerPresented: Bool = false + @State private var isFileImporterPresented: Bool = false + @State private var visibility = Pixelfed.Statuses.Visibility.pub @State private var visibilityText: LocalizedStringKey = "compose.title.everyone" @State private var visibilityImage = "globe.europe.africa" @@ -144,6 +155,30 @@ public struct BaseComposeView: View { selection: $selectedItems, maxSelectionCount: 4, matching: .images) + .fileImporter(isPresented: $isFileImporterPresented, + allowedContentTypes: [.image], + allowsMultipleSelection: true) { result in + Task { + if let urls = try? result.get() { + await self.processFiles(urls: urls) + } + } + } + .fullScreenCover(isPresented: $isCameraPickerPresented, content: { + CameraPickerView(selectedImage: .init( + get: { nil }, + set: { image in + if let image { + self.images.append(image) + + Task { + await self.loadPhotos() + } + } + } + )) + .background(.black) + }) .interactiveDismissDisabled(self.interactiveDismissDisabled) } @@ -197,6 +232,10 @@ public struct BaseComposeView: View { item != photoAttachment.nsItemProvider }) + self.images = self.images.filter({ item in + item != photoAttachment.uiImage + }) + self.refreshScreenState() } upload: { Task { @@ -399,10 +438,30 @@ public struct BaseComposeView: View { HStack { ScrollView(.horizontal) { HStack(alignment: .center, spacing: 20) { - Button { - hideKeyboard() - self.focusedField = .unknown - self.photosPickerVisible = true + Menu { + Button { + hideKeyboard() + self.focusedField = .unknown + self.photosPickerVisible = true + } label: { + Label("compose.title.photos", systemImage: "photo") + } + + Button { + hideKeyboard() + self.focusedField = .unknown + self.isCameraPickerPresented = true + } label: { + Label("compose.title.camera", systemImage: "camera") + } + + Button { + hideKeyboard() + self.focusedField = .unknown + isFileImporterPresented = true + } label: { + Label("compose.title.files", systemImage: "folder") + } } label: { Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle") } @@ -506,6 +565,14 @@ public struct BaseComposeView: View { return false } + private func processFiles(urls: [URL]) async { + let items = urls.filter { $0.startAccessingSecurityScopedResource() } + .compactMap { NSItemProvider(contentsOf: $0) } + + self.attachments.append(contentsOf: items) + await self.loadPhotos() + } + private func loadPhotos() async { self.photosAreUploading = true self.publishDisabled = self.isPublishButtonDisabled() @@ -534,6 +601,16 @@ public struct BaseComposeView: View { temporaryPhotosAttachment.append(PhotoAttachment(nsItemProvider: item)) } + // Add to collection photos from camera picker. + for item in self.images { + if let photoAttachment = self.photosAttachment.first(where: { $0.uiImage == item }) { + temporaryPhotosAttachment.append(photoAttachment) + continue + } + + temporaryPhotosAttachment.append(PhotoAttachment(uiImage: item)) + } + // We can show new list on the screen. self.photosAttachment = temporaryPhotosAttachment diff --git a/WidgetsKit/Sources/WidgetsKit/Views/CameraPickerView.swift b/WidgetsKit/Sources/WidgetsKit/Views/CameraPickerView.swift new file mode 100644 index 0000000..c3d6b3c --- /dev/null +++ b/WidgetsKit/Sources/WidgetsKit/Views/CameraPickerView.swift @@ -0,0 +1,47 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import UIKit +import SwiftUI + +struct CameraPickerView: UIViewControllerRepresentable { + @Environment(\.presentationMode) var isPresented + @Binding var selectedImage: UIImage? + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let picker: CameraPickerView + + init(picker: CameraPickerView) { + self.picker = picker + } + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + guard let selectedImage = info[.originalImage] as? UIImage else { + return + } + + self.picker.selectedImage = selectedImage + self.picker.isPresented.wrappedValue.dismiss() + } + } + + func makeUIViewController(context: Context) -> UIImagePickerController { + let imagePicker = UIImagePickerController() + imagePicker.sourceType = .camera + imagePicker.delegate = context.coordinator + + return imagePicker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { + } + + func makeCoordinator() -> Coordinator { + Coordinator(picker: self) + } +}