#25 Support of sending images from Files and Camera
This commit is contained in:
parent
3ed8347304
commit
71278400a8
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue