From 037c7875b5e714d1584324d176352f16f187fa94 Mon Sep 17 00:00:00 2001 From: Michael Neuwert Date: Thu, 24 Sep 2020 13:40:01 +0200 Subject: [PATCH] Pro Photo Upload Settings (#759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Started implementing EXIF metadata viewer * Conversion of APEX units. EXIF AUX section * Added histogram created with CoreImage * Using more appropriate image creation method * Added GPS location parsing / presentation * Made image details formatting more compact * Added header with image thumbnail, file size info And.. IPTC meta-data parsing * Added TIFF meta-data * Added TIFF:PhotometricInterpretation mapping * Small fixes in the EXIF meta data view - Progress indicator while data is being generated - Fixed parsing of ISO speed rating (film sensitivity) - Not displaying meta-data where value is empty * Moved GPS data to the top, improved exposure bias formatting * few text formatting fixes * Layout changes to make metadata view look nicer on iPad * Pro photo features configured as IAP * Localization changes * Fixed wrong text formatting * Started implemented pro photo upload settings * Added extended photo upload settings - Prefer uploading originals (non-edited) - Prefer uploading RAW images * First functioning implementation * Fixed issue with image preview rotation * Fixed rotation issue in image preview * Fixed incorrect image dimensions in case of RAW photo * Added a RAW photo badge in photo picker * Added ‘Pro Photo’ IAP as a single purchasable product * Changed wording and added localized strings * Check user eligibility for using pro photo upload features - Allow all enterprise users to use RAW / original photo upload - Allow users who has purchased an IAP * Fixed code review findings * Fixed review findings - Color space was wrongly named as exposure bias (copy-paste error) - Reduced precision of displayed altitude to two decimal digits * Changed app store IAP identifier for Pro Photo package AppStore Connect doesnt support hyphens * Update oC SDK * Fixed issues with previously merged code not buildable * Updated oc SDK * Merged latest changes and made branch buildable again * - revert reverted changes in ownCloud File Provider/OCItem+FileProviderItem.m * - revert logging-related reverts performed by 72c2d80aef726f9a18a97cc1ee86a7c74d3e0d54 * - revert bugfix removal originating from 905f485 * Avoid recomputing badge images used in table view cells Plus change access level of some properties from fileprivate to private * Corrected review findings - Avoiding blocking main thread - Observing product license asynchronously - Reacting to enterprise account addition / removal by observing corresponding notification * Fixed latest review findings * Made code more reliable in terms of reacting to account changes in a multi window environment * Changed the logic how photo resource is chosen for export * More robust detection of RAW assets Soe apps don’t seem to follow guidelines for storing RAW photos suggested by Apple. So instead of using alternatePhoto, they store RAW as main resource * Don’t show prefer RAW option if there is no camera on board which would support shooting RAW * Fixed compilation issues with Xcode 11.7 Co-authored-by: Michael Neuwert Co-authored-by: Matthias Hühne Co-authored-by: Felix Schwarz Co-authored-by: Michael Neuwert --- Podfile.lock | 2 +- ios-sdk | 2 +- .../OCItem+FileProviderItem.m | 2 +- .../CreateFolderIntentHandler.swift | 10 +- .../DeletePathItemIntentHandler.swift | 8 +- .../GetAccountIntentHandler.swift | 6 +- .../GetAccountsIntentHandler.swift | 4 +- .../GetDirectoryListingIntentHandler.swift | 4 +- ownCloud Intents/GetFileIntentHandler.swift | 8 +- .../OCBookmarkManager+Extension.swift | 4 +- .../PathExistsIntentHandler.swift | 8 +- ownCloud Intents/SaveFileIntentHandler.swift | 16 +-- ownCloud.xcodeproj/project.pbxproj | 9 ++ .../Client/PhotoSelectionViewController.swift | 64 ++++++---- .../CIImage+Extensions.swift | 2 +- .../Media Uploads/MediaUploadOperation.swift | 17 ++- .../PhotoKit Extensions/PHAsset+Upload.swift | 54 ++++++--- .../raw-badge.imageset/Contents.json | 15 +++ .../raw-badge.imageset/raw_badge.pdf | Bin 0 -> 6966 bytes .../Resources/en.lproj/Localizable.strings | 6 + .../Settings/LogSettingsViewController.swift | 1 + .../MediaUploadSettingsViewController.swift | 63 ++++++++-- .../ProPhotoUploadSettingsSection.swift | 109 ++++++++++++++++++ .../SDK Extensions/OCBookmark+Extension.swift | 17 +++ ownCloudAppShared/Tools/Log.swift | 2 + 25 files changed, 348 insertions(+), 85 deletions(-) create mode 100644 ownCloud/Resources/Assets.xcassets/raw-badge.imageset/Contents.json create mode 100644 ownCloud/Resources/Assets.xcassets/raw-badge.imageset/raw_badge.pdf create mode 100644 ownCloud/Settings/ProPhotoUploadSettingsSection.swift diff --git a/Podfile.lock b/Podfile.lock index d24e91d81..93167dc1a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -13,4 +13,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9075b8f92281024401a0039c3477c9a16650b092 -COCOAPODS: 1.8.4 +COCOAPODS: 1.9.3 diff --git a/ios-sdk b/ios-sdk index f868051b7..38e556c98 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit f868051b73e8daddc54045fa7e037d3552808080 +Subproject commit 38e556c98a7f532cd552a20c844620f470bbed18 diff --git a/ownCloud File Provider/OCItem+FileProviderItem.m b/ownCloud File Provider/OCItem+FileProviderItem.m index 81331df81..2ab255fe7 100644 --- a/ownCloud File Provider/OCItem+FileProviderItem.m +++ b/ownCloud File Provider/OCItem+FileProviderItem.m @@ -148,7 +148,7 @@ - (NSString *)filename @"mindnode" : @"com.mindnode.mindnode.mindmap", @"itmz" : @"com.toketaware.uti.ithoughts.itmz", - + @"pdf" : @"com.adobe.pdf" }; }); diff --git a/ownCloud Intents/CreateFolderIntentHandler.swift b/ownCloud Intents/CreateFolderIntentHandler.swift index fb1e1e08c..518e03330 100644 --- a/ownCloud Intents/CreateFolderIntentHandler.swift +++ b/ownCloud Intents/CreateFolderIntentHandler.swift @@ -24,7 +24,7 @@ import ownCloudAppShared @available(iOS 13.0, *) public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { - public func handle(intent: CreateFolderIntent, completion: @escaping (CreateFolderIntentResponse) -> Void) { + public func handle(intent: CreateFolderIntent, completion: @escaping (CreateFolderIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(CreateFolderIntentResponse(code: .disabled, userActivity: nil)) @@ -83,7 +83,7 @@ public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { } } - public func resolveName(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolveName(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let name = intent.name { completion(INStringResolutionResult.success(with: name)) } else { @@ -91,7 +91,7 @@ public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { } } - public func resolveAccount(for intent: CreateFolderIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + public func resolveAccount(for intent: CreateFolderIntent, with completion: @escaping (AccountResolutionResult) -> Void) { if let account = intent.account { completion(AccountResolutionResult.success(with: account)) } else { @@ -99,11 +99,11 @@ public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { } } - public func provideAccountOptions(for intent: CreateFolderIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + public func provideAccountOptions(for intent: CreateFolderIntent, with completion: @escaping ([Account]?, Error?) -> Void) { completion(OCBookmarkManager.shared.accountList, nil) } - public func resolvePath(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolvePath(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let path = intent.path { completion(INStringResolutionResult.success(with: path)) } else { diff --git a/ownCloud Intents/DeletePathItemIntentHandler.swift b/ownCloud Intents/DeletePathItemIntentHandler.swift index c5f584d01..8a5d8cdf4 100644 --- a/ownCloud Intents/DeletePathItemIntentHandler.swift +++ b/ownCloud Intents/DeletePathItemIntentHandler.swift @@ -24,7 +24,7 @@ import ownCloudAppShared @available(iOS 13.0, *) public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling { - public func handle(intent: DeletePathItemIntent, completion: @escaping (DeletePathItemIntentResponse) -> Void) { + public func handle(intent: DeletePathItemIntent, completion: @escaping (DeletePathItemIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(DeletePathItemIntentResponse(code: .disabled, userActivity: nil)) @@ -72,7 +72,7 @@ public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling } } - public func resolveAccount(for intent: DeletePathItemIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + public func resolveAccount(for intent: DeletePathItemIntent, with completion: @escaping (AccountResolutionResult) -> Void) { if let account = intent.account { completion(AccountResolutionResult.success(with: account)) } else { @@ -80,11 +80,11 @@ public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling } } - public func provideAccountOptions(for intent: DeletePathItemIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + public func provideAccountOptions(for intent: DeletePathItemIntent, with completion: @escaping ([Account]?, Error?) -> Void) { completion(OCBookmarkManager.shared.accountList, nil) } - public func resolvePath(for intent: DeletePathItemIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolvePath(for intent: DeletePathItemIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let path = intent.path { completion(INStringResolutionResult.success(with: path)) } else { diff --git a/ownCloud Intents/GetAccountIntentHandler.swift b/ownCloud Intents/GetAccountIntentHandler.swift index f41a90c16..e1986193a 100644 --- a/ownCloud Intents/GetAccountIntentHandler.swift +++ b/ownCloud Intents/GetAccountIntentHandler.swift @@ -24,7 +24,7 @@ import ownCloudAppShared @available(iOS 13.0, *) public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { - public func handle(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + public func handle(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) @@ -54,7 +54,7 @@ public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { completion(GetAccountIntentResponse.success(account: account)) } - public func resolveAccountUUID(for intent: GetAccountIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolveAccountUUID(for intent: GetAccountIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let account = intent.accountUUID { completion(INStringResolutionResult.success(with: account)) } else { @@ -62,7 +62,7 @@ public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { } } - public func confirm(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + private func confirm(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { completion(GetAccountIntentResponse(code: .ready, userActivity: nil)) } } diff --git a/ownCloud Intents/GetAccountsIntentHandler.swift b/ownCloud Intents/GetAccountsIntentHandler.swift index 96f25dbbe..427e2cb5a 100644 --- a/ownCloud Intents/GetAccountsIntentHandler.swift +++ b/ownCloud Intents/GetAccountsIntentHandler.swift @@ -24,7 +24,7 @@ import ownCloudAppShared @available(iOS 13.0, *) public class GetAccountsIntentHandler: NSObject, GetAccountsIntentHandling { - public func handle(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + public func handle(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(GetAccountsIntentResponse(code: .disabled, userActivity: nil)) @@ -42,7 +42,7 @@ public class GetAccountsIntentHandler: NSObject, GetAccountsIntentHandling { completion(GetAccountsIntentResponse.success(accountList: OCBookmarkManager.shared.accountList)) } - public func confirm(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + private func confirm(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { completion(GetAccountsIntentResponse(code: .ready, userActivity: nil)) } } diff --git a/ownCloud Intents/GetDirectoryListingIntentHandler.swift b/ownCloud Intents/GetDirectoryListingIntentHandler.swift index b713a6fbb..994f765c2 100644 --- a/ownCloud Intents/GetDirectoryListingIntentHandler.swift +++ b/ownCloud Intents/GetDirectoryListingIntentHandler.swift @@ -29,7 +29,7 @@ public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingInte var completion : GetDirectoryListingCompletionHandler? - public func resolvePath(for intent: GetDirectoryListingIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolvePath(for intent: GetDirectoryListingIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let path = intent.path { completion(INStringResolutionResult.success(with: path)) @@ -124,7 +124,7 @@ public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingInte self.completion?(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) } - public func confirm(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { + private func confirm(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { completion(GetDirectoryListingIntentResponse(code: .ready, userActivity: nil)) } } diff --git a/ownCloud Intents/GetFileIntentHandler.swift b/ownCloud Intents/GetFileIntentHandler.swift index 907065c57..4c63de4ea 100644 --- a/ownCloud Intents/GetFileIntentHandler.swift +++ b/ownCloud Intents/GetFileIntentHandler.swift @@ -27,7 +27,7 @@ typealias GetFileCompletionHandler = (GetFileIntentResponse) -> Void @available(iOS 13.0, *) public class GetFileIntentHandler: NSObject, GetFileIntentHandling { - public func handle(intent: GetFileIntent, completion: @escaping (GetFileIntentResponse) -> Void) { + public func handle(intent: GetFileIntent, completion: @escaping (GetFileIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(GetFileIntentResponse(code: .disabled, userActivity: nil)) @@ -77,7 +77,7 @@ public class GetFileIntentHandler: NSObject, GetFileIntentHandling { } } - public func resolvePath(for intent: GetFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolvePath(for intent: GetFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let path = intent.path { completion(INStringResolutionResult.success(with: path)) @@ -86,11 +86,11 @@ public class GetFileIntentHandler: NSObject, GetFileIntentHandling { } } - public func provideAccountOptions(for intent: GetFileIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + public func provideAccountOptions(for intent: GetFileIntent, with completion: @escaping ([Account]?, Error?) -> Void) { completion(OCBookmarkManager.shared.accountList, nil) } - public func resolveAccount(for intent: GetFileIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + public func resolveAccount(for intent: GetFileIntent, with completion: @escaping (AccountResolutionResult) -> Void) { if let account = intent.account { completion(AccountResolutionResult.success(with: account)) } else { diff --git a/ownCloud Intents/OCBookmarkManager+Extension.swift b/ownCloud Intents/OCBookmarkManager+Extension.swift index fbfe3776d..6ee506ddc 100644 --- a/ownCloud Intents/OCBookmarkManager+Extension.swift +++ b/ownCloud Intents/OCBookmarkManager+Extension.swift @@ -22,7 +22,7 @@ import ownCloudSDK @available(iOS 13.0, *) extension OCBookmarkManager { - public var accountList : [Account] { + var accountList : [Account] { var accountList : [Account] = [] accountList = OCBookmarkManager.shared.bookmarks.map { (bookmark) -> Account in let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) @@ -40,7 +40,7 @@ extension OCBookmarkManager { return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first } - public func accountBookmark(for uuidString: String) -> (OCBookmark, Account)? { + func accountBookmark(for uuidString: String) -> (OCBookmark, Account)? { if let bookmark = bookmark(for: uuidString) { let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) account.name = bookmark.shortName diff --git a/ownCloud Intents/PathExistsIntentHandler.swift b/ownCloud Intents/PathExistsIntentHandler.swift index 49453fc05..a5bb28a79 100644 --- a/ownCloud Intents/PathExistsIntentHandler.swift +++ b/ownCloud Intents/PathExistsIntentHandler.swift @@ -24,7 +24,7 @@ import ownCloudAppShared @available(iOS 13.0, *) public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { - public func handle(intent: PathExistsIntent, completion: @escaping (PathExistsIntentResponse) -> Void) { + public func handle(intent: PathExistsIntent, completion: @escaping (PathExistsIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(PathExistsIntentResponse(code: .disabled, userActivity: nil)) @@ -62,7 +62,7 @@ public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { } } - public func resolveAccount(for intent: PathExistsIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + public func resolveAccount(for intent: PathExistsIntent, with completion: @escaping (AccountResolutionResult) -> Void) { if let account = intent.account { completion(AccountResolutionResult.success(with: account)) } else { @@ -70,11 +70,11 @@ public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { } } - public func provideAccountOptions(for intent: PathExistsIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + public func provideAccountOptions(for intent: PathExistsIntent, with completion: @escaping ([Account]?, Error?) -> Void) { completion(OCBookmarkManager.shared.accountList, nil) } - public func resolvePath(for intent: PathExistsIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolvePath(for intent: PathExistsIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let path = intent.path { completion(INStringResolutionResult.success(with: path)) } else { diff --git a/ownCloud Intents/SaveFileIntentHandler.swift b/ownCloud Intents/SaveFileIntentHandler.swift index 012ab3603..17587de07 100644 --- a/ownCloud Intents/SaveFileIntentHandler.swift +++ b/ownCloud Intents/SaveFileIntentHandler.swift @@ -24,7 +24,7 @@ import ownCloudAppShared @available(iOS 13.0, *) public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { - public func handle(intent: SaveFileIntent, completion: @escaping (SaveFileIntentResponse) -> Void) { + public func handle(intent: SaveFileIntent, completion: @escaping (SaveFileIntentResponse) -> Void) { guard IntentSettings.shared.isEnabled else { completion(SaveFileIntentResponse(code: .disabled, userActivity: nil)) @@ -115,11 +115,11 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { } } - public func provideAccountOptions(for intent: SaveFileIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + public func provideAccountOptions(for intent: SaveFileIntent, with completion: @escaping ([Account]?, Error?) -> Void) { completion(OCBookmarkManager.shared.accountList, nil) } - public func resolveAccount(for intent: SaveFileIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + public func resolveAccount(for intent: SaveFileIntent, with completion: @escaping (AccountResolutionResult) -> Void) { if let account = intent.account { completion(AccountResolutionResult.success(with: account)) } else { @@ -127,7 +127,7 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { } } - public func resolvePath(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolvePath(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let path = intent.path { completion(INStringResolutionResult.success(with: path)) } else { @@ -135,7 +135,7 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { } } - public func resolveFile(for intent: SaveFileIntent, with completion: @escaping (INFileResolutionResult) -> Void) { + public func resolveFile(for intent: SaveFileIntent, with completion: @escaping (INFileResolutionResult) -> Void) { if let file = intent.file { completion(INFileResolutionResult.success(with: file)) } else { @@ -143,15 +143,15 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { } } - public func resolveFilename(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolveFilename(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { completion(INStringResolutionResult.success(with: intent.filename ?? "")) } - public func resolveFileextension(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolveFileextension(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { completion(INStringResolutionResult.success(with: intent.fileextension ?? "")) } - public func resolveShouldOverwrite(for intent: SaveFileIntent, with completion: @escaping (INBooleanResolutionResult) -> Void) { + public func resolveShouldOverwrite(for intent: SaveFileIntent, with completion: @escaping (INBooleanResolutionResult) -> Void) { var shouldOverwrite = false if let overwrite = intent.shouldOverwrite?.boolValue { shouldOverwrite = overwrite diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index b31f36acd..1baf1e80f 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 02072E6023E46022006548A7 /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02072E5F23E46022006548A7 /* UIWindow+Extension.swift */; }; 0233F45E246E9D960095A799 /* UploadCameraMediaAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */; }; + 025F063324AA163C009D8FC5 /* DisplayExifMetadataAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */; }; + 025F063A24AA18C7009D8FC5 /* ImageMetadataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F063924AA18C7009D8FC5 /* ImageMetadataViewController.swift */; }; 024F3A2124A3AB410083E11E /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 024F3A2024A3AB410083E11E /* CrashReporter */; }; 025F063324AA163C009D8FC5 /* DisplayExifMetadataAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */; }; 025F063A24AA18C7009D8FC5 /* ImageMetadataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F063924AA18C7009D8FC5 /* ImageMetadataViewController.swift */; }; @@ -21,6 +23,10 @@ 02633EFF2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02633EFE2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift */; }; 0269F589244DED02002E9D99 /* UIAlertController+UniversalLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0269F588244DED02002E9D99 /* UIAlertController+UniversalLinks.swift */; }; 0287DD7D249131E000C912CA /* AppStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287DD7C249131E000C912CA /* AppStatistics.swift */; }; + 02DC7C9024CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DC7C8F24CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift */; }; + 2308F94321467F6200CF0B91 /* ClientDirectoryPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2308F93C21467F6200CF0B91 /* ClientDirectoryPickerViewController.swift */; }; + 232B01F42126B0CE00366FA0 /* MoreViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232B01F32126B0CE00366FA0 /* MoreViewHeader.swift */; }; + 232B01F62126B10900366FA0 /* MoreStaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232B01F52126B10900366FA0 /* MoreStaticTableViewController.swift */; }; 02AE32E424D2FA8B00A19476 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 02AE32E324D2FA8B00A19476 /* CrashReporter */; }; 02F2891424BFAF0100E3D35C /* MigrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2891024BFAF0100E3D35C /* MigrationViewController.swift */; }; 02F2891524BFAF0100E3D35C /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2891124BFAF0100E3D35C /* Migration.swift */; }; @@ -883,6 +889,7 @@ 02633EFE2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNUserNotificationCenter+Extensions.swift"; sourceTree = ""; }; 0269F588244DED02002E9D99 /* UIAlertController+UniversalLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+UniversalLinks.swift"; sourceTree = ""; }; 0287DD7C249131E000C912CA /* AppStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStatistics.swift; sourceTree = ""; }; + 02DC7C8F24CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProPhotoUploadSettingsSection.swift; sourceTree = ""; }; 02F2890F24BFAF0100E3D35C /* LegacyCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCredentials.swift; sourceTree = ""; }; 02F2891024BFAF0100E3D35C /* MigrationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationViewController.swift; sourceTree = ""; }; 02F2891124BFAF0100E3D35C /* Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; @@ -2887,6 +2894,7 @@ 025FC71F247810AB009307A7 /* MediaUploadSettingsViewController.swift */, 025FC7262478123E009307A7 /* MediaExportSettingsSection.swift */, 025FC72824781659009307A7 /* AutoUploadSettingsSection.swift */, + 02DC7C8F24CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift */, 025FC744247EF0F1009307A7 /* BackgroundUploadsSettingsSection.swift */, ); path = Settings; @@ -3912,6 +3920,7 @@ 4C464BF22187AF1500D30602 /* PDFSearchViewController.swift in Sources */, DC3393A422E0A75C00DD3DA4 /* ClientItemResolvingCell.swift in Sources */, DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */, + 02DC7C9024CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift in Sources */, DCC832F6242CC5F700153F8C /* CardIssueMessagePresenter.swift in Sources */, DCD1301123A23F4E00255779 /* OCLicenseManager+AppStore.swift in Sources */, DC1B2707209CF0D3004715E1 /* IssuesDismissalAnimator.swift in Sources */, diff --git a/ownCloud/Client/PhotoSelectionViewController.swift b/ownCloud/Client/PhotoSelectionViewController.swift index aad1d9b1e..a80294a30 100644 --- a/ownCloud/Client/PhotoSelectionViewController.swift +++ b/ownCloud/Client/PhotoSelectionViewController.swift @@ -21,6 +21,7 @@ import Photos import PhotosUI import ownCloudApp import ownCloudAppShared +import CoreServices private extension UICollectionView { func indexPathsForElements(in rect: CGRect) -> [IndexPath] { @@ -29,32 +30,51 @@ private extension UICollectionView { } } +private extension PHAssetResource { + var isRaw: Bool { + self.type == .alternatePhoto || self.uniformTypeIdentifier == String(kUTTypeRawImage) || self.uniformTypeIdentifier == AVFileType.dng.rawValue + } +} + typealias PhotosSelectedCallback = ([PHAsset]) -> Void class PhotoSelectionViewController: UICollectionViewController, Themeable { // MARK: - Constants - fileprivate let thumbnailSizeMultiplier: CGFloat = 0.205 - fileprivate let verticalInset: CGFloat = 1.0 - fileprivate let horizontalInset: CGFloat = 1.0 - fileprivate let itemSpacing: CGFloat = 1.0 - fileprivate let thumbnailMaxWidthPad: CGFloat = 120.0 - fileprivate let thumbnailMaxWidthPhone: CGFloat = 80.0 + private let thumbnailSizeMultiplier: CGFloat = 0.205 + private let verticalInset: CGFloat = 1.0 + private let horizontalInset: CGFloat = 1.0 + private let itemSpacing: CGFloat = 1.0 + private let thumbnailMaxWidthPad: CGFloat = 120.0 + private let thumbnailMaxWidthPhone: CGFloat = 80.0 // MARK: - Instance variables - var fetchResult: PHFetchResult! - var assetCollection: PHAssetCollection? - var availableWidth: CGFloat = 0 - var selectionCallback: PhotosSelectedCallback? + private var fetchResult: PHFetchResult! + private var availableWidth: CGFloat = 0 + + private var thumbnailSize: CGSize! + private var previousPreheatRect = CGRect.zero + private var thumbnailWidth: CGFloat = 80.0 + + private let imageManager = PHCachingImageManager() + private let layout = UICollectionViewFlowLayout() + private lazy var durationFormatter = DateComponentsFormatter() + private var uploadButtonItem: UIBarButtonItem? - fileprivate var thumbnailSize: CGSize! - fileprivate var previousPreheatRect = CGRect.zero - fileprivate var thumbnailWidth: CGFloat = 80.0 + private lazy var rawBadgeImage: UIImage? = { + UIImage(named: "raw-badge")?.withRenderingMode(.alwaysTemplate).tinted(with: UIColor.white) + }() - fileprivate let imageManager = PHCachingImageManager() - fileprivate let layout = UICollectionViewFlowLayout() - fileprivate lazy var durationFormatter = DateComponentsFormatter() - fileprivate var uploadButtonItem: UIBarButtonItem? + private lazy var cameraBadgeImage: UIImage? = { + UIImage(named: "camera-badge")?.withRenderingMode(.alwaysTemplate).tinted(with: UIColor.white) + }() + + private lazy var livePhotoBadgeImage: UIImage = { + PHLivePhotoView.livePhotoBadgeImage(options: .overContent) + }() + + var assetCollection: PHAssetCollection? + var selectionCallback: PhotosSelectedCallback? public var focussedIndexPath: IndexPath? { didSet { @@ -240,12 +260,12 @@ class PhotoSelectionViewController: UICollectionViewController, Themeable { // Add a badge to the cell if the PHAsset represents a Live Photo. if asset.mediaSubtypes.contains(.photoLive) { - cell.mediaBadgeImage = PHLivePhotoView.livePhotoBadgeImage(options: .overContent) - } - - if asset.mediaType == .video { + cell.mediaBadgeImage = livePhotoBadgeImage + } else if asset.mediaType == .video { cell.videoDurationLabel.text = durationFormatter.string(from: asset.duration) - cell.mediaBadgeImage = UIImage(named: "camera-badge")?.withRenderingMode(.alwaysTemplate).tinted(with: UIColor.white) + cell.mediaBadgeImage = cameraBadgeImage + } else if PHAssetResource.assetResources(for: asset).filter({$0.isRaw}).count > 0 { + cell.mediaBadgeImage = rawBadgeImage } // Request an image for the asset from the PHCachingImageManager. diff --git a/ownCloud/Media Uploads/CoreImage Extensions/CIImage+Extensions.swift b/ownCloud/Media Uploads/CoreImage Extensions/CIImage+Extensions.swift index b05a0394f..89cbb6b8c 100644 --- a/ownCloud/Media Uploads/CoreImage Extensions/CIImage+Extensions.swift +++ b/ownCloud/Media Uploads/CoreImage Extensions/CIImage+Extensions.swift @@ -30,7 +30,7 @@ extension CIImage { // Conversion to JPEG required let colorSpace = CGColorSpaceCreateDeviceRGB() - var ciContext = CIContext() + let ciContext = CIContext() var imageData : Data? var outError: Error? diff --git a/ownCloud/Media Uploads/MediaUploadOperation.swift b/ownCloud/Media Uploads/MediaUploadOperation.swift index 84f91bc29..d2264fbe8 100644 --- a/ownCloud/Media Uploads/MediaUploadOperation.swift +++ b/ownCloud/Media Uploads/MediaUploadOperation.swift @@ -156,6 +156,7 @@ class MediaUploadOperation : Operation { // Determine the list of preferred media formats var utisToConvert = [String]() var preserveOriginalNames = false + var preferredResourceTypes = [PHAssetResourceType]() if let userDefaults = OCAppIdentity.shared.userDefaults { if userDefaults.convertHeic { @@ -165,9 +166,23 @@ class MediaUploadOperation : Operation { utisToConvert.append(AVFileType.mov.rawValue) } preserveOriginalNames = userDefaults.preserveOriginalMediaFileNames + + if userDefaults.preferOriginalPhotos { + preferredResourceTypes.append(.photo) + } + + if userDefaults.preferRawPhotos { + preferredResourceTypes.append(.alternatePhoto) + } } - if let result = asset.upload(with: core, at: rootItem, utisToConvert: utisToConvert, preserveOriginalName: preserveOriginalNames, progressHandler: nil, uploadCompleteHandler: { + if let result = asset.upload(with: core, + at: rootItem, + utisToConvert: utisToConvert, + preferredResourceTypes: preferredResourceTypes, + preserveOriginalName: preserveOriginalNames, + progressHandler: nil, + uploadCompleteHandler: { uploadCompletion() }) { if let error = result.1 { diff --git a/ownCloud/Media Uploads/PhotoKit Extensions/PHAsset+Upload.swift b/ownCloud/Media Uploads/PhotoKit Extensions/PHAsset+Upload.swift index f206f7c1e..5aeb5a9bf 100644 --- a/ownCloud/Media Uploads/PhotoKit Extensions/PHAsset+Upload.swift +++ b/ownCloud/Media Uploads/PhotoKit Extensions/PHAsset+Upload.swift @@ -94,7 +94,7 @@ extension PHAssetResource { ext = "m4v" case String(kUTTypeTIFF): ext = "tiff" - case String(kUTTypeRawImage): + case String(kUTTypeRawImage), AVFileType.dng.rawValue: ext = "dng" default: break @@ -225,17 +225,34 @@ extension PHAsset { - parameter resources: array of PHAssetResource objects belonging to PHAsset - parameter fileName: name for the exported asset including file extension - parameter utisToConvert: list of file UTIs for image formats which shall be converted to JPEG format + - parameter preferredResourceTypes: list of resource types which shall be preferrably exported - parameter completionHandler: called when the file is written to disk or if an error occurs */ - func exportPhoto(resources:[PHAssetResource], fileName:String, utisToConvert:[String] = [], completionHandler: @escaping (_ url:URL?, _ error:Error?) -> Void) { - + func exportPhoto(resources:[PHAssetResource], + fileName:String, + utisToConvert:[String] = [], + preferredResourceTypes:[PHAssetResourceType] = [], + completionHandler: @escaping (_ url:URL?, _ error:Error?) -> Void) { + + // Filter resources which are prefered for export + let filteredResources = resources.filter({preferredResourceTypes.contains($0.type)}) var resourceToExport:PHAssetResource? var outError: Error? - // For edited photo pick the edited version - resourceToExport = resources.filter({$0.type == .fullSizePhoto}).first + // Pick RAW photo if available in matched resources + resourceToExport = filteredResources.filter({$0.type == .alternatePhoto}).first - // If edited photo is not avaialable, pick the original + // Pick the original if available in matched resources + if resourceToExport == nil { + resourceToExport = filteredResources.filter({$0.type == .photo}).first + } + + // Pick edited photo as fallback + if resourceToExport == nil { + resourceToExport = resources.filter({$0.type == .fullSizePhoto}).first + } + + // Pick original photo as fallback if resourceToExport == nil { resourceToExport = resources.filter({$0.type == .photo}).first } @@ -253,8 +270,8 @@ extension PHAsset { // Prepare export URL and remove path extension which will depend on output format var exportURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension() - // Check if conversion is required? - if utisToConvert.contains(resource.uniformTypeIdentifier) { + // Check if conversion is required? Don't convert RAW though + if utisToConvert.contains(resource.uniformTypeIdentifier) && resource.type != .alternatePhoto { // Since conversion to JPEG is desired, we have first to get the actual data var assetData = Data() @@ -417,14 +434,19 @@ extension PHAsset { - parameter fileName: name for the exported asset including file extension - parameter utisToConvert: list of file UTIs for media formats which shall be converted + - parameter preferredResourceTypes: list of resource types which shall be preferrably exported - parameter completion: called when the file is written to disk or if an error occurs */ - func export(fileName:String, utisToConvert:[String] = [], completion:@escaping (_ url:URL?, _ error:Error?) -> Void) { + func export(fileName:String, utisToConvert:[String] = [], preferredResourceTypes:[PHAssetResourceType] = [], completion:@escaping (_ url:URL?, _ error:Error?) -> Void) { let assetResources = PHAssetResource.assetResources(for: self) if assetResources.count > 0 { // We have actual data on the device and we can export it directly if self.mediaType == .image { - exportPhoto(resources: assetResources, fileName: fileName, utisToConvert: utisToConvert, completionHandler: { (url, error) in + exportPhoto(resources: assetResources, + fileName: fileName, + utisToConvert: utisToConvert, + preferredResourceTypes: preferredResourceTypes, + completionHandler: { (url, error) in completion(url, error) }) } else if self.mediaType == .video { @@ -437,11 +459,14 @@ extension PHAsset { } else { // It could be that we don't have any asset resources locally e.g. since we have to deal with an asset from a cloud album if self.mediaType == .image { - exportPhoto(fileName: fileName, utisToConvert: utisToConvert, completionHandler: { (url, error) in + exportPhoto(fileName: fileName, + utisToConvert: utisToConvert, + completionHandler: { (url, error) in completion(url, error) }) } else if self.mediaType == .video { - exportVideo(fileName: fileName, utisToConvert: utisToConvert) { (url, error) in + exportVideo(fileName: fileName, + utisToConvert: utisToConvert) { (url, error) in completion(url, error) } } else { @@ -455,12 +480,13 @@ extension PHAsset { - parameter core: Reference to the core to be used for the upload - parameter rootItem: Directory item where the media file shall be uploaded - parameter utisToConvert: Array of UTI identifiers describing desired output formats + - parameter preferredResourceTypes: list of resource types which shall be preferrably exported - parameter preserveOriginalName If true, use original file name from the photo library - parameter completionHandler: Completion handler called after the media file is imported into the core and placeholder item is created. - parameter progressHandler: Receives progress of the at the moment running activity - parameter uploadCompleteHandler: Called when core reports that upload is done */ - func upload(with core:OCCore?, at rootItem:OCItem, utisToConvert:[String] = [], preserveOriginalName:Bool = true, progressHandler:((_ progress:Progress) -> Void)? = nil, uploadCompleteHandler:(() -> Void)? = nil) -> (OCItem?, Error?)? { + func upload(with core:OCCore?, at rootItem:OCItem, utisToConvert:[String] = [], preferredResourceTypes:[PHAssetResourceType] = [], preserveOriginalName:Bool = true, progressHandler:((_ progress:Progress) -> Void)? = nil, uploadCompleteHandler:(() -> Void)? = nil) -> (OCItem?, Error?)? { func performUpload(sourceURL:URL, copySource:Bool, cellularSwitchIdentifier:OCCellularSwitchIdentifier?) -> (OCItem?, Error?)? { @@ -533,7 +559,7 @@ extension PHAsset { // Synchronously export asset let semaphore = DispatchSemaphore(value: 0) - export(fileName: assetName, utisToConvert: utisToConvert) { (url, error) in + export(fileName: assetName, utisToConvert: utisToConvert, preferredResourceTypes: preferredResourceTypes) { (url, error) in exportedAssetURL = url outError = error semaphore.signal() diff --git a/ownCloud/Resources/Assets.xcassets/raw-badge.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/raw-badge.imageset/Contents.json new file mode 100644 index 000000000..d485f1b5e --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/raw-badge.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "raw_badge.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/ownCloud/Resources/Assets.xcassets/raw-badge.imageset/raw_badge.pdf b/ownCloud/Resources/Assets.xcassets/raw-badge.imageset/raw_badge.pdf new file mode 100644 index 0000000000000000000000000000000000000000..97c62b4667535905f5fe0eb472e19a74d062014f GIT binary patch literal 6966 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 literal 0 HcmV?d00001 diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 2eb351ea5..d7f22aefd 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -816,3 +816,9 @@ "W" = "W"; "N" = "N"; "S" = "S"; + +/* Pro photo upload settings */ +"Extended upload settings" = "Extended upload settings"; +"Prefer unedited photos" = "Prefer unedited photos"; +"Prefer RAW photos" = "Prefer RAW photos"; + diff --git a/ownCloud/Settings/LogSettingsViewController.swift b/ownCloud/Settings/LogSettingsViewController.swift index 92d2dc114..bfa20ea59 100644 --- a/ownCloud/Settings/LogSettingsViewController.swift +++ b/ownCloud/Settings/LogSettingsViewController.swift @@ -147,6 +147,7 @@ class LogSettingsViewController: StaticTableViewController { logLevelSection = StaticTableViewSection(headerTitle: "Log Level".localized) let logLevels : [[String:Any]] = [ + [ OCLogLevel.verbose.label : OCLogLevel.verbose.rawValue ], [ OCLogLevel.debug.label : OCLogLevel.debug.rawValue ], [ OCLogLevel.info.label : OCLogLevel.info.rawValue ], [ OCLogLevel.warning.label : OCLogLevel.warning.rawValue ], diff --git a/ownCloud/Settings/MediaUploadSettingsViewController.swift b/ownCloud/Settings/MediaUploadSettingsViewController.swift index 5b74905db..23e392bdb 100644 --- a/ownCloud/Settings/MediaUploadSettingsViewController.swift +++ b/ownCloud/Settings/MediaUploadSettingsViewController.swift @@ -7,31 +7,74 @@ // /* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - +* Copyright (C) 2020, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ import UIKit import ownCloudSDK +import ownCloudApp import ownCloudAppShared class MediaUploadSettingsViewController: StaticTableViewController { + private var proPhotoSettingsSection: StaticTableViewSection? + private var autoUploadSection : AutoUploadSettingsSection? + private var licenseObserver : OCLicenseObserver? + + deinit { + NotificationCenter.default.removeObserver(self, name: .OCBookmarkManagerListChanged, object: nil) + } + override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Media Upload".localized if let userDefaults = OCAppIdentity.shared.userDefaults { + proPhotoSettingsSection = ProPhotoUploadSettingsSection(userDefaults: userDefaults) self.addSection(MediaExportSettingsSection(userDefaults: userDefaults)) + } + + NotificationCenter.default.addObserver(self, selector: #selector(reconsiderSections), name: .OCBookmarkManagerListChanged, object: nil) + + licenseObserver = OCLicenseManager.shared.observeProducts(nil, features: [ .photoProFeatures ], in: OCLicenseEnvironment(), withOwner: self) { [weak self] (_, _, _) in + self?.reconsiderSections() + } + } + + @objc private func reconsiderSections() { + OnMainThread { + guard let proSettingsSection = self.proPhotoSettingsSection else { return } + if OCBookmarkManager.shared.bookmarks.count > 0 { - self.addSection(AutoUploadSettingsSection(userDefaults: userDefaults)) + if self.autoUploadSection == nil, let userDefaults = OCAppIdentity.shared.userDefaults { + self.autoUploadSection = AutoUploadSettingsSection(userDefaults: userDefaults) + } + + if let autoUploadSection = self.autoUploadSection, !autoUploadSection.attached { + self.addSection(autoUploadSection) + } // TODO: Re-add this section when we re-gain an ability to run background NSURLSessions //self.addSection(BackgroundUploadsSettingsSection(userDefaults: userDefaults)) + } else { + if let autoUploadSection = self.autoUploadSection, autoUploadSection.attached { + self.removeSection(autoUploadSection) + self.autoUploadSection = nil + } + } + + if OCLicenseEnterpriseProvider.numberOfEnterpriseAccounts > 0 || OCLicenseManager.shared.authorizationStatus(forFeature: .photoProFeatures, in: OCLicenseEnvironment()) == .granted { + if !proSettingsSection.attached { + self.addSection(proSettingsSection) + } + } else { + if proSettingsSection.attached { + self.removeSection(proSettingsSection) + } } } } diff --git a/ownCloud/Settings/ProPhotoUploadSettingsSection.swift b/ownCloud/Settings/ProPhotoUploadSettingsSection.swift new file mode 100644 index 000000000..94e0024ee --- /dev/null +++ b/ownCloud/Settings/ProPhotoUploadSettingsSection.swift @@ -0,0 +1,109 @@ +// +// ProPhotoUploadSettingsSection.swift +// ownCloud +// +// Created by Michael Neuwert on 24.07.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2020, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit +import ownCloudAppShared +import AVFoundation + +extension AVCaptureDevice { + var supportsRaw : Bool { + let session = AVCaptureSession() + let output = AVCapturePhotoOutput() + + guard let input = try? AVCaptureDeviceInput(device: self) else { + return false + } + + guard session.canAddInput(input) else { return false } + guard session.canAddOutput(output) else { return false } + + session.beginConfiguration() + session.sessionPreset = .photo + session.addInput(input) + session.addOutput(output) + session.commitConfiguration() + + return !output.availableRawPhotoPixelFormatTypes.isEmpty + } + + class func rawCameraDeviceAvailable() -> Bool { + let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: + [.builtInTrueDepthCamera, .builtInDualCamera, .builtInWideAngleCamera], + mediaType: .video, position: .unspecified) + for device in discoverySession.devices { + if device.supportsRaw { + return true + } + } + + return false + } +} + +extension UserDefaults { + enum ProPhotoUploadSettingsKeys : String { + case PreferOriginals = "pro-photo-upload-prefer-originals" + case PreferRAW = "pro-photo-upload-prefer-raw" + } + + public var preferOriginalPhotos: Bool { + set { + self.set(newValue, forKey: ProPhotoUploadSettingsKeys.PreferOriginals.rawValue) + } + + get { + return self.bool(forKey: ProPhotoUploadSettingsKeys.PreferOriginals.rawValue) + } + } + + public var preferRawPhotos: Bool { + set { + self.set(newValue, forKey: ProPhotoUploadSettingsKeys.PreferRAW.rawValue) + } + + get { + return self.bool(forKey: ProPhotoUploadSettingsKeys.PreferRAW.rawValue) + } + } +} + +class ProPhotoUploadSettingsSection: SettingsSection { + + override init(userDefaults: UserDefaults) { + super.init(userDefaults: userDefaults) + self.headerTitle = "Extended upload settings".localized + + let preferOriginalsRow = StaticTableViewRow(switchWithAction: { (_, sender) in + if let enableSwitch = sender as? UISwitch { + userDefaults.preferOriginalPhotos = enableSwitch.isOn + } + }, title: "Prefer unedited photos".localized, value: self.userDefaults.preferOriginalPhotos, identifier: "prefer-originals") + + self.add(row: preferOriginalsRow) + + if AVCaptureDevice.rawCameraDeviceAvailable() { + let preferRawRow = StaticTableViewRow(switchWithAction: { (_, sender) in + if let enableSwitch = sender as? UISwitch { + userDefaults.preferRawPhotos = enableSwitch.isOn + } + }, title: "Prefer RAW photos".localized, value: self.userDefaults.preferRawPhotos, identifier: "prefer-raw") + + self.add(row: preferRawRow) + } + } +} diff --git a/ownCloudAppShared/SDK Extensions/OCBookmark+Extension.swift b/ownCloudAppShared/SDK Extensions/OCBookmark+Extension.swift index 26f11fc54..f82f1cf77 100644 --- a/ownCloudAppShared/SDK Extensions/OCBookmark+Extension.swift +++ b/ownCloudAppShared/SDK Extensions/OCBookmark+Extension.swift @@ -47,3 +47,20 @@ public extension OCBookmark { } } } + +public extension OCBookmark { + + enum Edition : String { + case Enterprise, Community, Unknown + } + + var edition : Edition { + if let statusDict = self.userInfo["statusInfo"] as? [String : Any] { + guard let editionValue = statusDict["edition"] as? String else { + return .Unknown + } + return Edition(rawValue: editionValue) ?? .Unknown + } + return .Unknown + } +} diff --git a/ownCloudAppShared/Tools/Log.swift b/ownCloudAppShared/Tools/Log.swift index 7c69579c5..bb1fd76b4 100644 --- a/ownCloudAppShared/Tools/Log.swift +++ b/ownCloudAppShared/Tools/Log.swift @@ -23,6 +23,8 @@ import ownCloudSDK public extension OCLogLevel { var label : String { switch self { + case .verbose: + return "Verbose".localized case .debug: return "Debug".localized case .info: