diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 6be78ad..4a784f3 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -34,6 +34,9 @@ "attachment.sensitive-content" = "Sensitive content"; "attachment.media-hidden" = "Media hidden"; "bookmarks" = "Bookmarks"; +"camera-access.title" = "Camera access needed"; +"camera-access.description" = "Open system settings to allow camera access"; +"camera-access.open-system-settings" = "Open system settings"; "cancel" = "Cancel"; "error" = "Error"; "favorites" = "Favorites"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 9e84660..731b034 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -19,11 +19,15 @@ D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; }; D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; }; D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; }; - D036768E2593E6DE005DF15A /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; }; D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; }; D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; }; D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; }; D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA16254CA823009094DF /* StatusBodyView.swift */; }; + D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; }; + D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; }; + D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; }; + D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */; }; + D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; }; D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC10F259C4F20000B67DF /* NewStatusView.swift */; }; D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; }; D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; }; @@ -61,7 +65,6 @@ D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; }; D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52ED257D757100FA2C5F /* CompositionView.swift */; }; D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; }; - D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; }; D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; }; D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; }; D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; }; @@ -105,7 +108,6 @@ D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; }; D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */; }; D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */; }; - D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */; }; D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */; }; D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */; }; D0EA59402522AC8700804347 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA593F2522AC8700804347 /* CardView.swift */; }; @@ -115,9 +117,7 @@ D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B125251A90F400942152 /* AccountListCell.swift */; }; D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.swift */; }; D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */; }; - D0F2D4DB257F018300986197 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; }; D0F2D54025818C4B00986197 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = D0F2D53F25818C4B00986197 /* Kingfisher */; }; - D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; }; D0F2D54B2581CF7D00986197 /* VisualEffectBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */; }; D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */; }; D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */; }; @@ -859,24 +859,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */, D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */, D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */, D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */, - D0F2D4DB257F018300986197 /* Array+Extensions.swift in Sources */, - D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */, D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */, D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */, D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */, - D036768E2593E6DE005DF15A /* Status+Extensions.swift in Sources */, D080413F258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */, - D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */, D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */, + D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */, D065966725899E910096AC5D /* CompositionAttachmentsDataSource.swift in Sources */, D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */, + D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */, + D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */, D0804134258D902900AD6139 /* CompositionAttachmentView.swift in Sources */, - D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */, D065966225899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */, + D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1129,6 +1129,7 @@ CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 82HL67AXQ2; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; INFOPLIST_FILE = "Share Extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.2; LD_RUNPATH_SEARCH_PATHS = ( @@ -1142,6 +1143,7 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG IS_SHARE_EXTENSION"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1153,6 +1155,7 @@ CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 82HL67AXQ2; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = "Share Extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.2; LD_RUNPATH_SEARCH_PATHS = ( @@ -1166,6 +1169,7 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = IS_SHARE_EXTENSION; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/Supporting Files/Info.plist b/Supporting Files/Info.plist index dfbdd44..5a2039d 100644 --- a/Supporting Files/Info.plist +++ b/Supporting Files/Info.plist @@ -2,6 +2,10 @@ + NSMicrophoneUsageDescription + Enables Metatext to take videos and add them to your posts. + NSCameraUsageDescription + Enables Metatext to take photos and videos and add them to your posts. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/View Controllers/NewStatusViewController.swift b/View Controllers/NewStatusViewController.swift index 66dcfb0..19424ca 100644 --- a/View Controllers/NewStatusViewController.swift +++ b/View Controllers/NewStatusViewController.swift @@ -1,9 +1,11 @@ // Copyright © 2020 Metabolist. All rights reserved. +import AVFoundation import Combine import Kingfisher import PhotosUI import UIKit +import UniformTypeIdentifiers import ViewModels final class NewStatusViewController: UIViewController { @@ -17,6 +19,7 @@ final class NewStatusViewController: UIViewController { target: nil, action: nil) private let mediaSelections = PassthroughSubject<[PHPickerResult], Never>() + private let imagePickerResults = PassthroughSubject<[UIImagePickerController.InfoKey: Any]?, Never>() private var cancellables = Set() init(viewModel: NewStatusViewModel) { @@ -82,11 +85,31 @@ extension NewStatusViewController: PHPickerViewControllerDelegate { } } +extension NewStatusViewController: UIImagePickerControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + imagePickerResults.send(info) + dismiss(animated: true) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + imagePickerResults.send(nil) + dismiss(animated: true) + } +} + +// Required by UIImagePickerController +extension NewStatusViewController: UINavigationControllerDelegate {} + private extension NewStatusViewController { func handle(event: NewStatusViewModel.Event) { switch event { case let .presentMediaPicker(compositionViewModel): presentMediaPicker(compositionViewModel: compositionViewModel) + case let .presentCamera(compositionViewModel): + #if !IS_SHARE_EXTENSION + presentCamera(compositionViewModel: compositionViewModel) + #endif } } @@ -196,6 +219,53 @@ private extension NewStatusViewController { present(picker, animated: true) } + #if !IS_SHARE_EXTENSION + func presentCamera(compositionViewModel: CompositionViewModel) { + if AVCaptureDevice.authorizationStatus(for: .video) == .denied { + let alertController = UIAlertController( + title: NSLocalizedString("camera-access.title", comment: ""), + message: NSLocalizedString("camera-access.description", comment: ""), + preferredStyle: .alert) + + let openSystemSettingsAction = UIAlertAction( + title: NSLocalizedString("camera-access.open-system-settings", comment: ""), + style: .default) { _ in + guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } + + UIApplication.shared.open(settingsUrl) + } + let cancelAction = UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in } + + alertController.addAction(openSystemSettingsAction) + alertController.addAction(cancelAction) + + present(alertController, animated: true) + + return + } + + imagePickerResults.first().sink { [weak self] in + guard let self = self, let info = $0 else { return } + + if let url = info[.mediaURL] as? URL, let itemProvider = NSItemProvider(contentsOf: url) { + self.viewModel.attach(itemProvider: itemProvider, to: compositionViewModel) + } else if let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage { + self.viewModel.attach(itemProvider: NSItemProvider(object: image), to: compositionViewModel) + } + } + .store(in: &cancellables) + + let picker = UIImagePickerController() + + picker.sourceType = .camera + picker.allowsEditing = true + picker.mediaTypes = [UTType.image.description, UTType.movie.description] + picker.modalPresentationStyle = .overFullScreen + picker.delegate = self + present(picker, animated: true) + } + #endif + func changeIdentityButton(identification: Identification) -> UIButton { let changeIdentityButton = UIButton() let downsampled = KingfisherOptionsInfo.downsampled( diff --git a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift index 11bdc30..68d73a5 100644 --- a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift @@ -45,6 +45,7 @@ public final class NewStatusViewModel: ObservableObject { public extension NewStatusViewModel { enum Event { case presentMediaPicker(CompositionViewModel) + case presentCamera(CompositionViewModel) } enum PostingState { @@ -76,6 +77,10 @@ public extension NewStatusViewModel { eventsSubject.send(.presentMediaPicker(viewModel)) } + func presentCamera(viewModel: CompositionViewModel) { + eventsSubject.send(.presentCamera(viewModel)) + } + func insert(after: CompositionViewModel) { guard let index = compositionViewModels.firstIndex(where: { $0 === after }) else { return } diff --git a/Views/CompositionInputAccessoryView.swift b/Views/CompositionInputAccessoryView.swift index b1f53a9..971fb41 100644 --- a/Views/CompositionInputAccessoryView.swift +++ b/Views/CompositionInputAccessoryView.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import AVFoundation import Combine import Mastodon import UIKit @@ -59,6 +60,25 @@ private extension CompositionInputAccessoryView { }, for: .touchUpInside) + #if !IS_SHARE_EXTENSION + if AVCaptureDevice.authorizationStatus(for: .video) != .restricted { + let cameraButton = UIButton() + + stackView.addArrangedSubview(cameraButton) + cameraButton.setImage( + UIImage( + systemName: "camera", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), + for: .normal) + cameraButton.addAction(UIAction { [weak self] _ in + guard let self = self else { return } + + self.parentViewModel.presentCamera(viewModel: self.viewModel) + }, + for: .touchUpInside) + } + #endif + let pollButton = UIButton() stackView.addArrangedSubview(pollButton)