#25 Support of sending images from Files and Camera

This commit is contained in:
Marcin Czachurski 2023-04-23 21:18:14 +02:00
parent 3ed8347304
commit 71278400a8
8 changed files with 161 additions and 5 deletions

View File

@ -149,6 +149,9 @@
"compose.title.tryToUpload" = "Try to upload"; "compose.title.tryToUpload" = "Try to upload";
"compose.title.delete" = "Delete"; "compose.title.delete" = "Delete";
"compose.title.edit" = "Edit"; "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.loadingPhotosFailed" = "Cannot retreive image from library.";
"compose.error.postingPhotoFailed" = "Error during posting photo."; "compose.error.postingPhotoFailed" = "Error during posting photo.";
"compose.error.postingStatusFailed" = "Error during posting status."; "compose.error.postingStatusFailed" = "Error during posting status.";

View File

@ -149,6 +149,9 @@
"compose.title.tryToUpload" = "Saiatu igotzen"; "compose.title.tryToUpload" = "Saiatu igotzen";
"compose.title.delete" = "Ezabatu"; "compose.title.delete" = "Ezabatu";
"compose.title.edit" = "Editatu"; "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.loadingPhotosFailed" = "Ezin da liburutegiko irudia eskuratu.";
"compose.error.postingPhotoFailed" = "Errorea argazkia argitaratzean."; "compose.error.postingPhotoFailed" = "Errorea argazkia argitaratzean.";
"compose.error.postingStatusFailed" = "Errorea egoera argitaratzean."; "compose.error.postingStatusFailed" = "Errorea egoera argitaratzean.";

View File

@ -149,6 +149,9 @@
"compose.title.tryToUpload" = "Essayer de télécharger"; "compose.title.tryToUpload" = "Essayer de télécharger";
"compose.title.delete" = "Supprimer"; "compose.title.delete" = "Supprimer";
"compose.title.edit" = "Editer"; "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.loadingPhotosFailed" = "Impossible de récupérer l'image depuis la bibliothèque.";
"compose.error.postingPhotoFailed" = "Erreur pendant le post de la photo."; "compose.error.postingPhotoFailed" = "Erreur pendant le post de la photo.";
"compose.error.postingStatusFailed" = "Erreur pendant le post du statut."; "compose.error.postingStatusFailed" = "Erreur pendant le post du statut.";

View File

@ -149,6 +149,9 @@
"compose.title.tryToUpload" = "Ponów"; "compose.title.tryToUpload" = "Ponów";
"compose.title.delete" = "Usuń"; "compose.title.delete" = "Usuń";
"compose.title.edit" = "Edytuj"; "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.loadingPhotosFailed" = "Nie można pobrać zdjęcia z biblioteki.";
"compose.error.postingPhotoFailed" = "Błąd podczas publikowania zdjęcia."; "compose.error.postingPhotoFailed" = "Błąd podczas publikowania zdjęcia.";
"compose.error.postingStatusFailed" = "Błąd podczas wysyłania statusu."; "compose.error.postingStatusFailed" = "Błąd podczas wysyłania statusu.";

View File

@ -1437,6 +1437,7 @@
INFOPLIST_FILE = Vernissage/Info.plist; INFOPLIST_FILE = Vernissage/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Vernissage; INFOPLIST_KEY_CFBundleDisplayName = Vernissage;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSCameraUsageDescription = "Uploading photos to Pixelfed";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -1478,6 +1479,7 @@
INFOPLIST_FILE = Vernissage/Info.plist; INFOPLIST_FILE = Vernissage/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Vernissage; INFOPLIST_KEY_CFBundleDisplayName = Vernissage;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSCameraUsageDescription = "Uploading photos to Pixelfed";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Saving photos from Pixelfed";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;

View File

@ -19,6 +19,9 @@ public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashabl
/// Information about image from share extension. /// Information about image from share extension.
public let nsItemProvider: NSItemProvider? public let nsItemProvider: NSItemProvider?
/// Information about image from camera sheet.
public let uiImage: UIImage?
/// Variable used for presentation layer. /// Variable used for presentation layer.
@Published public var photoData: Data? @Published public var photoData: Data?
@ -34,11 +37,12 @@ public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashabl
/// Error from device. /// Error from device.
@Published public var loadError: Error? @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.id = UUID().uuidString
self.photosPickerItem = photosPickerItem self.photosPickerItem = photosPickerItem
self.nsItemProvider = nsItemProvider self.nsItemProvider = nsItemProvider
self.uiImage = uiImage
} }
public static func == (lhs: PhotoAttachment, rhs: PhotoAttachment) -> Bool { public static func == (lhs: PhotoAttachment, rhs: PhotoAttachment) -> Bool {
@ -54,6 +58,8 @@ public extension PhotoAttachment {
@MainActor @MainActor
func loadImage() async throws { func loadImage() async throws {
// Load images from Photos app.
if let pickerItem = self.photosPickerItem, if let pickerItem = self.photosPickerItem,
let transferable = try await pickerItem.createImageFileTranseferable() { let transferable = try await pickerItem.createImageFileTranseferable() {
self.photoUrl = transferable.url self.photoUrl = transferable.url
@ -62,6 +68,7 @@ public extension PhotoAttachment {
return return
} }
// Load images from share sheet (files app).
if let itemProvider = self.nsItemProvider, if let itemProvider = self.nsItemProvider,
let identifier = itemProvider.registeredTypeIdentifiers.first, let identifier = itemProvider.registeredTypeIdentifiers.first,
let handledItemType = FileTypeSupported(rawValue: identifier), let handledItemType = FileTypeSupported(rawValue: identifier),
@ -71,6 +78,17 @@ public extension PhotoAttachment {
return 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
}
} }
} }

View File

@ -31,10 +31,21 @@ public struct BaseComposeView: View {
@State private var photosAreUploading = false @State private var photosAreUploading = false
@State private var photosPickerVisible = 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] @State private var attachments: [NSItemProvider]
/// Images from Photos app.
@State private var selectedItems: [PhotosPickerItem] = [] @State private var selectedItems: [PhotosPickerItem] = []
/// Processed array with images.
@State private var photosAttachment: [PhotoAttachment] = [] @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 visibility = Pixelfed.Statuses.Visibility.pub
@State private var visibilityText: LocalizedStringKey = "compose.title.everyone" @State private var visibilityText: LocalizedStringKey = "compose.title.everyone"
@State private var visibilityImage = "globe.europe.africa" @State private var visibilityImage = "globe.europe.africa"
@ -144,6 +155,30 @@ public struct BaseComposeView: View {
selection: $selectedItems, selection: $selectedItems,
maxSelectionCount: 4, maxSelectionCount: 4,
matching: .images) 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) .interactiveDismissDisabled(self.interactiveDismissDisabled)
} }
@ -197,6 +232,10 @@ public struct BaseComposeView: View {
item != photoAttachment.nsItemProvider item != photoAttachment.nsItemProvider
}) })
self.images = self.images.filter({ item in
item != photoAttachment.uiImage
})
self.refreshScreenState() self.refreshScreenState()
} upload: { } upload: {
Task { Task {
@ -399,10 +438,30 @@ public struct BaseComposeView: View {
HStack { HStack {
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 20) {
Button { Menu {
hideKeyboard() Button {
self.focusedField = .unknown hideKeyboard()
self.photosPickerVisible = true 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: { } label: {
Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle") Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle")
} }
@ -506,6 +565,14 @@ public struct BaseComposeView: View {
return false 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 { private func loadPhotos() async {
self.photosAreUploading = true self.photosAreUploading = true
self.publishDisabled = self.isPublishButtonDisabled() self.publishDisabled = self.isPublishButtonDisabled()
@ -534,6 +601,16 @@ public struct BaseComposeView: View {
temporaryPhotosAttachment.append(PhotoAttachment(nsItemProvider: item)) 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. // We can show new list on the screen.
self.photosAttachment = temporaryPhotosAttachment self.photosAttachment = temporaryPhotosAttachment

View File

@ -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)
}
}