From 9a0bc93215b786e715b0a89cc9aa793fd121407b Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 31 Oct 2019 09:43:15 +0100 Subject: [PATCH 1/9] [fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547) * - Fix Swift and SwiftLint warnings - Remove unused UploadsSettingsSection (was replaced by MediaUploadSettings) * - address libzip Xcode project upgrade warning - add research note to FileProviderExtension - update SDK * - Allow offline browsing of folders in the File Provider --- external/libzip/libzip.xcodeproj/project.pbxproj | 1 + ios-sdk | 2 +- ownCloud File Provider/FileProviderExtension.m | 5 +++++ ownCloud File Provider/OCItem+FileProviderItem.m | 8 ++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/external/libzip/libzip.xcodeproj/project.pbxproj b/external/libzip/libzip.xcodeproj/project.pbxproj index 7cf933b83..f03b4736f 100644 --- a/external/libzip/libzip.xcodeproj/project.pbxproj +++ b/external/libzip/libzip.xcodeproj/project.pbxproj @@ -467,6 +467,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = DCE93FD921FCA42B000E14F2; productRefGroup = DCE93FE421FCA42C000E14F2 /* Products */; diff --git a/ios-sdk b/ios-sdk index edc8c00cb..494224c7f 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit edc8c00cbc66665d5c71a1289f987acbcbd2beb7 +Subproject commit 494224c7fc20904457f1c2be0e959d5459b7f5c7 diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index a5747cadf..94ff6b72f 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -896,3 +896,8 @@ - (void)core:(OCCore *)core handleError:(NSError *)error issue:(OCIssue *)issue @end OCClaimExplicitIdentifier OCClaimExplicitIdentifierFileProvider = @"fileProvider"; + +/* + Additional information: + - NSExtensionFileProviderSupportsPickingFolders: https://twitter.com/palmin/status/1177860144258076673 +*/ diff --git a/ownCloud File Provider/OCItem+FileProviderItem.m b/ownCloud File Provider/OCItem+FileProviderItem.m index 81396450f..ecbcbf370 100644 --- a/ownCloud File Provider/OCItem+FileProviderItem.m +++ b/ownCloud File Provider/OCItem+FileProviderItem.m @@ -123,6 +123,14 @@ - (BOOL)isDownloaded return (YES); } + if (self.type == OCItemTypeCollection) + { + // Needs to return YES for folders in order to allow browsing while offline + // Otherwise Files.app will bring up an alert "You're not connected to the Internet" + // (big thanks to @palmin who pointed me to this possibility) + return (YES); + } + return (NO); } From 4969dbd903531be2288189735f1f8c4e864a34e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20H=C3=BChne?= Date: Thu, 31 Oct 2019 09:44:21 +0100 Subject: [PATCH 2/9] Revert "[fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547)" (#553) This reverts commit 9a0bc93215b786e715b0a89cc9aa793fd121407b. --- external/libzip/libzip.xcodeproj/project.pbxproj | 1 - ios-sdk | 2 +- ownCloud File Provider/FileProviderExtension.m | 5 ----- ownCloud File Provider/OCItem+FileProviderItem.m | 8 -------- 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/external/libzip/libzip.xcodeproj/project.pbxproj b/external/libzip/libzip.xcodeproj/project.pbxproj index f03b4736f..7cf933b83 100644 --- a/external/libzip/libzip.xcodeproj/project.pbxproj +++ b/external/libzip/libzip.xcodeproj/project.pbxproj @@ -467,7 +467,6 @@ hasScannedForEncodings = 0; knownRegions = ( en, - Base, ); mainGroup = DCE93FD921FCA42B000E14F2; productRefGroup = DCE93FE421FCA42C000E14F2 /* Products */; diff --git a/ios-sdk b/ios-sdk index 494224c7f..edc8c00cb 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 494224c7fc20904457f1c2be0e959d5459b7f5c7 +Subproject commit edc8c00cbc66665d5c71a1289f987acbcbd2beb7 diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index 94ff6b72f..a5747cadf 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -896,8 +896,3 @@ - (void)core:(OCCore *)core handleError:(NSError *)error issue:(OCIssue *)issue @end OCClaimExplicitIdentifier OCClaimExplicitIdentifierFileProvider = @"fileProvider"; - -/* - Additional information: - - NSExtensionFileProviderSupportsPickingFolders: https://twitter.com/palmin/status/1177860144258076673 -*/ diff --git a/ownCloud File Provider/OCItem+FileProviderItem.m b/ownCloud File Provider/OCItem+FileProviderItem.m index ecbcbf370..81396450f 100644 --- a/ownCloud File Provider/OCItem+FileProviderItem.m +++ b/ownCloud File Provider/OCItem+FileProviderItem.m @@ -123,14 +123,6 @@ - (BOOL)isDownloaded return (YES); } - if (self.type == OCItemTypeCollection) - { - // Needs to return YES for folders in order to allow browsing while offline - // Otherwise Files.app will bring up an alert "You're not connected to the Internet" - // (big thanks to @palmin who pointed me to this possibility) - return (YES); - } - return (NO); } From 9143136fed7043ec04a6d35ec78a5bd4a5b3ff3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20H=C3=BChne?= Date: Tue, 26 Nov 2019 19:44:49 +0100 Subject: [PATCH 3/9] [feature/shortcuts] iOS 13 Add Shortcut parameter support (#491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * #463 Add Shortcut support with Parameters (iOS 13) * fixed save file intent, use security scope for copying file * added create folder intent action * - removed progress instance variable - added error completion handler * added accept custom file types for parameter INFile * show always the account selection sheet and added a note, that only one file can be imported at once * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * [feature/bundle-import] Add support for certain bundle formats to the share sheet (#471) * - Add support for import of bundle-based document formats through transparent zipping - moved previous FileProvider functionality into ownCloudApp.framework - new method OCCore.importItemNamed() ensures correct handling of bundle-based document formats - fixes finding (4) in #445 - Changed LSSupportsOpeningDocumentsInPlace to NO address two issues: - the app is now listed as "Copy to ownCloud" (previously "Open in ownCloud", which was technically not correct) - previously, documents using "Open to ownCloud" would no longer open in f.ex. Pages, but in the ownCloud app when selected in the Files app * - Refactored ImportFilesController around using NSFileCoordinator, fixing finding (1) in #471 * changed path for moved Info.plist for target ownCloudAppFramework * Version Bump to 133 * [feature/sort direction] Allow sorting asc/desc for all sort methods (#474) * #470 added change sort direction asc/desc for all sort methods * - removed comments - changed translation keys * fixes sort comparator for sort method "shared" * use correct images for sort direction asc and desc * updated choosing sort method from a tableView/Popover instead of a UIAlertViewController * set maximum width for popover and set arrow direction * Added show sort direction in UISegmentControl * - make sure arrows of popovers appear in same color as the popover contents - fix issue where direction was toggled during setup of SortBar (setting of initial value from user defaults), so logging in, logging out and logging in would change the sort direction - reset sort direction to ascendant when switching sort methods - SortMethodTableViewController - remove extraneous instance lets - enforce row height used for table view height calculation - change table view height calculation to account for the last divider (which could look a bit off if present) * tap on segment control was fired twice, because there was a missing check, if selected segment changed * fixed changing width on selecting an other segment * fixed jumping width of sortSegmentControl items by setting the same width for all items by calculating the longest width before * [fix/index-bar] Make Index Bar available in general file list (#469) * #413 show index bar in general file list - Index bar is only visible, if sorting is "Sort by name" - Index bar is only visible, if more than one different letters are available - Index bar is only visible, if more items are available as visible * - adopt to new sort methods - reverse index list, if sort direction is descendant * Version Bump to 134 * Version Bump to 135 * Version Bump to 136 * Change behaviour of default expiration date (#476) * Enabling expiration date switch if default expiration date is configured Checking files_sharing.public.expire_date.expire_date.days server capability. If it is set then it is used as default date and the switch is enabled even if the expiration date is not enforced. * Fixed QA issue (1) * Fixed QA finding (2): limiting the date range if expiration date is enforced * Permission increasing UX in reshares (#467) * Not showing permissions which can’t be elevated Affecting items shared with the current users which are being reshared * Fixed an issue with wrong permissions displayed when creating public link - Checking if permissions can be elevated for the public link - Only viable options are displayed * Fixed a trailing whitespace warning * Fixed another compiler warning * Inhereting permissions from the share when creating public link * Fixed QA finding (4) * Fixed QA finding (1) * Fixed QA finding (2) * Version Bump to 137 * added Get File Info Intent * [feature/itempolicy] Item Policy / Available Offline support (#456) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * - added error handling - moving duplicated code into helper and extensions * improved failure handling * added Intent for checking if a given path exists * improved variable check * added Delete Item at Path Intent handler * return path for created folder and saved file in shortcuts intents * - changed all intent handler to provide accounts - changed formatting for display name - added "Get Account" intent - added descriptions * removed privateLink property from FileInfo * - added sort type and sort direction WIP - added error handling for account failure * Implemented Sort Direction and Sort Type for Directory Listing * adopt fastlane file for new "Intent" target * code signing changes for fastlane * - fixed validation errors on AppStore connect upload - new build number - fixed comments (cherry picked from commit 5ccdebd3bb10ef594a493d02ef99fe961b53ab1a) * fixed crash on OS version below iOS 13 * removing type identifier, because files of type "text/plain" was not handled correct in Shortcuts.app. Instead of using text content, file name was used. Maybe this is a bug in iOS and will be submitted as bug report to Apple (cherry picked from commit 3fd6f9249656d761e4f2af3a116567ebc02a0461) * added optional parameter "filename" for SaveFileIntent (cherry picked from commit 067ccd93559e4bf51efac46ee4db76b4b9c4ab7e) * added setting optional file extension * added localization support for Siri Intents * Code refactoring after code review findings * - code refactoring - fixed duplicated symbols for intent definition * - moved shared code from app to framework - better error handling for some intents - using newest SDK, which fixes path problems - set parameter name, extension in SaveFileIntent to optional * moved shared file to framework * - Update SDK to include latest fixes, including for the latest "/Documents" vs "/Documents/" issue * - correct the targeted commit for the ios-sdk git module --- fastlane/Fastfile | 32 +- ios-sdk | 2 +- ownCloud Intents/Info.plist | 58 + ownCloud Intents/IntentHandler.swift | 47 + .../ownCloud Intents.entitlements | 14 + ownCloud.xcodeproj/project.pbxproj | 497 +++- .../xcschemes/ownCloud File Provider.xcscheme | 33 +- .../ownCloud File ProviderUI.xcscheme | 33 +- .../xcschemes/ownCloud Intents.xcscheme | 100 + .../xcshareddata/xcschemes/ownCloud.xcscheme | 10 + .../Actions+Extensions/MoveAction.swift | 1 + ownCloud/Client/SortBar.swift | 1 + .../SortMethodTableViewController.swift | 1 + .../QueryFileListTableViewController.swift | 1 + ownCloud/Key Commands/KeyCommands.swift | 1 + .../cloud-available-offline@2x.png | Bin 729 -> 1021 bytes ownCloud/Resources/Info.plist | 9 + .../Client/SortMethod.swift | 10 +- ownCloudAppShared/Info.plist | 22 + .../Base.lproj/Intents.intentdefinition | 2545 +++++++++++++++++ .../Intent/CreateFolderIntentHandler.swift | 112 + .../Intent/DeletePathItemIntentHandler.swift | 83 + .../OCBookmarkManager+Extension.swift | 56 + .../Intent/GetAccountIntentHandler.swift | 68 + .../Intent/GetAccountsIntentHandler.swift | 50 + .../GetDirectoryListingIntentHandler.swift | 128 + .../Intent/GetFileInfoIntentHandler.swift | 101 + .../Intent/GetFileIntentHandler.swift | 99 + .../Intent/PathExistsIntentHandler.swift | 83 + .../Intent/SaveFileIntentHandler.swift | 134 + .../Intent/en.lproj/Intents.strings | 92 + .../SDK Extensions/OCItem+Extension.swift | 339 +++ .../SDK Extensions/OCItemTracker.swift | 41 + ownCloudAppShared/Tools/AppLockHelper.swift | 35 + .../UIKit Extension/Calendar+Extension.swift | 26 + .../UIKit Extension}/String+Extension.swift | 17 +- ownCloudAppShared/ownCloudAppShared.h | 29 + 37 files changed, 4858 insertions(+), 52 deletions(-) create mode 100644 ownCloud Intents/Info.plist create mode 100644 ownCloud Intents/IntentHandler.swift create mode 100644 ownCloud Intents/ownCloud Intents.entitlements create mode 100644 ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Intents.xcscheme rename {ownCloud => ownCloudAppShared}/Client/SortMethod.swift (93%) create mode 100644 ownCloudAppShared/Info.plist create mode 100644 ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition create mode 100644 ownCloudAppShared/Intent/CreateFolderIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift create mode 100644 ownCloudAppShared/Intent/GetAccountIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/GetAccountsIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/GetFileIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/PathExistsIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/SaveFileIntentHandler.swift create mode 100644 ownCloudAppShared/Intent/en.lproj/Intents.strings create mode 100644 ownCloudAppShared/SDK Extensions/OCItem+Extension.swift create mode 100644 ownCloudAppShared/SDK Extensions/OCItemTracker.swift create mode 100644 ownCloudAppShared/Tools/AppLockHelper.swift create mode 100644 ownCloudAppShared/UIKit Extension/Calendar+Extension.swift rename {ownCloud/UIKit Extensions => ownCloudAppShared/UIKit Extension}/String+Extension.swift (81%) create mode 100644 ownCloudAppShared/ownCloudAppShared.h diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 557ce862d..2e7571396 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -37,7 +37,8 @@ platform :ios do provisioningProfiles: { "com.owncloud.ios-app" => "match AdHoc com.owncloud.ios-app", "com.owncloud.ios-app.ownCloud-File-Provider" => "match AdHoc com.owncloud.ios-app.ownCloud-File-Provider", - "com.owncloud.ios-app.ownCloud-File-ProviderUI" => "match AdHoc com.owncloud.ios-app.ownCloud-File-ProviderUI" + "com.owncloud.ios-app.ownCloud-File-ProviderUI" => "match AdHoc com.owncloud.ios-app.ownCloud-File-ProviderUI", + "com.owncloud.ios-app.ownCloud-File-Intents" => "match AdHoc com.owncloud.ios-app.ownCloud-Intents" #Add more Provisioning Profiles when extensions are added } } @@ -71,6 +72,8 @@ platform :ios do ENTERPRISE_FP_PROFILE = "Enterprise Distribution iOS Neo File Provider" ENTERPRISE_FP_UI_ID = "com.owncloud.enterprise.ios-app.ownCloud-File-ProviderUI" ENTERPRISE_FP_UI_PROFILE = "Enterprise Distribution iOS Neo File ProviderUI" + ENTERPRISE_INTENT_ID = "com.owncloud.enterprise.ios-app.ownCloud-Intents" + ENTERPRISE_INTENT_PROFILE = "Enterprise Distribution iOS Neo Intent" ENTERPRISE_APP_FW_ID = "com.owncloud.enterprise.ownCloudApp" ENTERPRISE_TEAM = "5QNK8L2PSC" ENTERPRISE_IDENTITY = "iPhone Distribution: ownCloud GmbH" @@ -99,6 +102,12 @@ platform :ios do app_identifier: ENTERPRISE_APP_FW_ID ) + update_app_identifier( + xcodeproj: "ownCloud.xcodeproj", + plist_path: "ownCloud Intents/Info.plist", + app_identifier: ENTERPRISE_INTENT_ID + ) + update_app_group_identifiers( entitlements_file: "ownCloud/ownCloud.entitlements", app_group_identifiers: ["group.com.owncloud.enterprise.ios-app"] @@ -109,10 +118,18 @@ platform :ios do app_group_identifiers: ["group.com.owncloud.enterprise.ios-app"] ) + update_app_group_identifiers( + entitlements_file: "ownCloud Intents/ownCloud Intents.entitlements", + app_group_identifiers: ["group.com.owncloud.enterprise.ios-app"] + ) + set_info_plist_value(path: "ownCloud File Provider/Info.plist", key: "OCAppGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") set_info_plist_value(path: "ownCloud File Provider/Info.plist", key: "OCKeychainAccessGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") set_info_plist_value(path: "ownCloud File Provider/Info.plist", key: "NSExtension", subkey: "NSExtensionFileProviderDocumentGroup", value: "group.com.owncloud.enterprise.ios-app") + set_info_plist_value(path: "ownCloud Intents/Info.plist", key: "OCAppGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") + set_info_plist_value(path: "ownCloud Intents/Info.plist", key: "OCKeychainAccessGroupIdentifier", value: "group.com.owncloud.enterprise.ios-app") + automatic_code_signing( path: "ownCloud.xcodeproj", use_automatic_signing: false, @@ -143,6 +160,16 @@ platform :ios do targets: ["ownCloud File ProviderUI"] ) + automatic_code_signing( + path: "ownCloud.xcodeproj", + use_automatic_signing: false, + team_id: ENTERPRISE_TEAM, + code_sign_identity: ENTERPRISE_IDENTITY, + profile_name: ENTERPRISE_INTENT_PROFILE, + bundle_identifier: ENTERPRISE_INTENT_ID, + targets: ["ownCloud Intents"] + ) + automatic_code_signing( path: "ownCloud.xcodeproj", use_automatic_signing: false, @@ -172,7 +199,8 @@ platform :ios do provisioningProfiles: { ENTERPRISE_APP_ID => ENTERPRISE_APP_PROFILE, ENTERPRISE_FP_ID => ENTERPRISE_FP_PROFILE, - ENTERPRISE_FP_UI_ID => ENTERPRISE_FP_UI_PROFILE + ENTERPRISE_FP_UI_ID => ENTERPRISE_FP_UI_PROFILE, + ENTERPRISE_INTENT_ID => ENTERPRISE_INTENT_PROFILE } } ) diff --git a/ios-sdk b/ios-sdk index 9f0a8c14b..ac411087b 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 9f0a8c14b3c97d98601343cfefe059f92896aba2 +Subproject commit ac411087bbe1fb342dec5c3edd3b86844ebd4bec diff --git a/ownCloud Intents/Info.plist b/ownCloud Intents/Info.plist new file mode 100644 index 000000000..3bbe97d22 --- /dev/null +++ b/ownCloud Intents/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + SiriKit + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(APP_SHORT_VERSION) + CFBundleVersion + 140 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsRestrictedWhileProtectedDataUnavailable + + IntentsSupported + + CreateFolderIntent + DeletePathItemIntent + GetAccountIntent + GetAccountsIntent + GetDirectoryListingIntent + GetFileInfoIntent + GetFileIntent + PathExistsIntent + SaveFileIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + OCAppGroupIdentifier + group.com.owncloud.ios-app + OCAppIdentifierPrefix + $(AppIdentifierPrefix) + OCHasFileProvider + + OCKeychainAccessGroupIdentifier + group.com.owncloud.ios-app + + diff --git a/ownCloud Intents/IntentHandler.swift b/ownCloud Intents/IntentHandler.swift new file mode 100644 index 000000000..384eed5d4 --- /dev/null +++ b/ownCloud Intents/IntentHandler.swift @@ -0,0 +1,47 @@ +// +// IntentHandler.swift +// SiriKit +// +// Created by Matthias Hühne on 23.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudAppShared + +class IntentHandler: INExtension { + + override func handler(for intent: INIntent) -> Any { + if intent is GetAccountsIntent { + return GetAccountsIntentHandler() + } else if intent is GetAccountIntent { + return GetAccountIntentHandler() + } else if intent is GetDirectoryListingIntent { + return GetDirectoryListingIntentHandler() + } else if intent is GetFileIntent { + return GetFileIntentHandler() + } else if intent is SaveFileIntent { + return SaveFileIntentHandler() + } else if intent is CreateFolderIntent { + return CreateFolderIntentHandler() + } else if intent is GetFileInfoIntent { + return GetFileInfoIntentHandler() + } else if intent is PathExistsIntent { + return PathExistsIntentHandler() + } else if intent is DeletePathItemIntent { + return DeletePathItemIntentHandler() + } + + fatalError("Unhandled intent type: \(intent)") + } +} diff --git a/ownCloud Intents/ownCloud Intents.entitlements b/ownCloud Intents/ownCloud Intents.entitlements new file mode 100644 index 000000000..988c47786 --- /dev/null +++ b/ownCloud Intents/ownCloud Intents.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.com.owncloud.ios-app + + keychain-access-groups + + $(AppIdentifierPrefix)group.com.owncloud.ios-app + + + diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 78deb86ed..ae06f197a 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 233BDEAA204FEFE500C06732 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 233BDEA8204FEFE500C06732 /* LaunchScreen.storyboard */; }; 233BDEB5204FEFE500C06732 /* OwnCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233BDEB4204FEFE500C06732 /* OwnCloudTests.swift */; }; 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E0FD72099F11D00C3D8D5 /* SecuritySettingsSection.swift */; }; - 2347446A20761BB700859C93 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2347446920761BB700859C93 /* String+Extension.swift */; }; 236735A621217C3500E5834A /* MoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236735A521217C3500E5834A /* MoreViewController.swift */; }; 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23957A6C209AFFE8003C8537 /* MoreSettingsSection.swift */; }; 239F1319205A693A0029F186 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F1318205A693A0029F186 /* UIColor+Extension.swift */; }; @@ -30,17 +29,33 @@ 23EC77592137F3DD0032D4E6 /* DisplayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC77552137F3DC0032D4E6 /* DisplayExtension.swift */; }; 23EC775B2137F3DD0032D4E6 /* OCExtensionType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC77572137F3DD0032D4E6 /* OCExtensionType+Extension.swift */; }; 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */; }; - 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F6238020B587EF004FDE8B /* SortMethod.swift */; }; 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; + 39057AA3233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; + 39057AA4233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; + 39057AA7233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; + 39057AA8233BA7A60008E6C0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 390B51E02292DBB100935E24 /* SharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */; }; 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39104E0A223991C8002FC02F /* UIButton+Extension.swift */; }; + 3912208223436EB80026C290 /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3912208123436EB80026C290 /* SortMethod.swift */; }; 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */; }; 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */; }; + 392557FE2278703300E83F60 /* UISearchBar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */; }; + 3940C4F02326985B008227AE /* GetAccountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */; }; 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */; }; + 394A0AED22EEE32C00603813 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A7138222E79C6700089423 /* IntentHandler.swift */; }; 394E1FDA233E2D64009D2897 /* FavoriteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */; }; + 394A0AFD22EEFC2C00603813 /* ownCloudAppShared.h in Headers */ = {isa = PBXBuildFile; fileRef = 394A0AFB22EEFC2C00603813 /* ownCloudAppShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 394A0B0022EEFC2C00603813 /* ownCloudAppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; }; + 394A0B0122EEFC2C00603813 /* ownCloudAppShared.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 394A0B0522EEFCA300603813 /* GetAccountsIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A7139722E858E400089423 /* GetAccountsIntentHandler.swift */; }; 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */; }; + 394A0B0922EEFCE400603813 /* ownCloudAppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; }; + 394A0B0A22EEFCF500603813 /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; 394E1FFF233E43F5009D2897 /* LinksAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FFE233E43F5009D2897 /* LinksAction.swift */; }; 394E200C233E477F009D2897 /* OpenItemUserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */; }; + 395E16FA22F03CAF00DE89A1 /* GetFileIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */; }; + 395E16FD22F06A7300DE89A1 /* SaveFileIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */; }; + 395E16FF22F172C900DE89A1 /* CreateFolderIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */; }; 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3961281522F8730A0087BD3A /* SceneDelegate.swift */; }; 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */; }; @@ -49,18 +64,27 @@ 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */; }; 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754F22327A33500119FCB /* OpenSceneAction.swift */; }; + 397754E223279EED00119FCB /* OCItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754E123279EED00119FCB /* OCItem+Extension.swift */; }; + 3984F5712319202200DC2639 /* DeletePathItemIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; + 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */; }; 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */; }; 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D422411EDF00B66713 /* BorderedLabel.swift */; }; 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */; }; + 399A4C002317CC460027DDD6 /* AppLockHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */; }; + 399A4C032317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */; }; + 399A4C1023190ADF0027DDD6 /* PathExistsIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */; }; 399DD7C722A691BC00B45EB2 /* UnshareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */; }; 39A5135322608836002CF1AA /* OCShare+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A5135222608836002CF1AA /* OCShare+Extension.swift */; }; 39A513AC22674E56002CF1AA /* OCCore+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */; }; + 39A5C3A1231566D9009D9EE3 /* GetFileInfoIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */; }; + 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 39A7138022E79C6700089423 /* ownCloud Intents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 39AFC3D1225E72FB00A6D3AE /* GroupSharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */; }; 39AFC3D8225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */; }; 39B289A8226F1EE000BE0E11 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B289A7226F1EE000BE0E11 /* MessageView.swift */; }; 39B9675022BE0FBA0074DB22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; + 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BE385C23435AFE0062A2FE /* String+Extension.swift */; }; 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8AE5228C12100020253B /* Array+Extension.swift */; }; 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */; }; 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */; }; @@ -70,8 +94,12 @@ 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */; }; + 39E6DE84233CC39A008DAE04 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; }; + 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */; }; 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */; }; + 39F689AB22F018C100E63429 /* GetDirectoryListingIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F689AA22F018C100E63429 /* GetDirectoryListingIntentHandler.swift */; }; + 39F689AC22F0206900E63429 /* OCBookmark+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */; }; 46B9D336BF7FE50321823888 /* Pods_ownCloudScreenshotsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */; }; 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */; }; 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */; }; @@ -360,6 +388,34 @@ remoteGlobalIDString = DC7E0A77203732B3006111FA; remoteInfo = Ocean; }; + 394A0AFE22EEFC2C00603813 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 394A0AF822EEFC2C00603813; + remoteInfo = ownCloudAppShared; + }; + 394A0B0722EEFCD900603813 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 394A0AF822EEFC2C00603813; + remoteInfo = ownCloudAppShared; + }; + 394A0B0B22EEFD2800603813 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDEBF204FEFF300C06732 /* ownCloudSDK.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = DCC8F9AA202852A200EB6701; + remoteInfo = ownCloudSDK; + }; + 39A7138522E79C6700089423 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 39A7137F22E79C6700089423; + remoteInfo = SiriKit; + }; 59056CAF22414F3C00A18A22 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 233BDE94204FEFE500C06732 /* Project object */; @@ -531,6 +587,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 39A7138D22E79DDE00089423 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 59799F7C224157E0007E8008 /* EarlGrey Copy Files */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -571,6 +637,7 @@ dstSubfolderSpec = 10; files = ( DC63207E21FCA731007EC0A8 /* libzip.framework in Copy Frameworks */, + 394A0B0122EEFC2C00603813 /* ownCloudAppShared.framework in Copy Frameworks */, DCC085722293F1FD008CC05C /* ownCloudApp.framework in Copy Frameworks */, DCFBAD0C21BE67A100943F76 /* ownCloudUI.framework in Copy Frameworks */, DC7DBA20207F5A0400E7337D /* PocketSVG.framework in Copy Frameworks */, @@ -586,6 +653,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed App Extensions */, DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed App Extensions */, ); name = "Embed App Extensions"; @@ -609,7 +677,6 @@ 233BDEB6204FEFE500C06732 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 233BDEBF204FEFF300C06732 /* ownCloudSDK.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ownCloudSDK.xcodeproj; path = "ios-sdk/ownCloudSDK.xcodeproj"; sourceTree = ""; }; 233E0FD72099F11D00C3D8D5 /* SecuritySettingsSection.swift */ = {isa = PBXFileReference; indentWidth = 8; lastKnownFileType = sourcecode.swift; path = SecuritySettingsSection.swift; sourceTree = ""; tabWidth = 8; usesTabs = 1; }; - 2347446920761BB700859C93 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 236735A521217C3500E5834A /* MoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoreViewController.swift; sourceTree = ""; }; 23957A6C209AFFE8003C8537 /* MoreSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreSettingsSection.swift; sourceTree = ""; }; 239F1318205A693A0029F186 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; @@ -624,17 +691,27 @@ 23EC77552137F3DC0032D4E6 /* DisplayExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayExtension.swift; sourceTree = ""; }; 23EC77572137F3DD0032D4E6 /* OCExtensionType+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCExtensionType+Extension.swift"; sourceTree = ""; }; 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDisplayViewController.swift; sourceTree = ""; }; - 23F6238020B587EF004FDE8B /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 23FA23E520BFD3D8009A6D73 /* SortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBar.swift; sourceTree = ""; }; + 39057AA9233BA7A60008E6C0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; + 39057AB1233BA7AE0008E6C0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingTableViewController.swift; sourceTree = ""; }; 39104E0A223991C8002FC02F /* UIButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; + 3912208123436EB80026C290 /* SortMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListTableViewController.swift; sourceTree = ""; }; 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryTableViewController.swift; sourceTree = ""; }; + 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Extension.swift"; sourceTree = ""; }; + 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetAccountIntentHandler.swift; sourceTree = ""; }; 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadCrumbTableViewController.swift; sourceTree = ""; }; + 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ownCloudAppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteAction.swift; sourceTree = ""; }; + 394A0AFB22EEFC2C00603813 /* ownCloudAppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ownCloudAppShared.h; sourceTree = ""; }; + 394A0AFC22EEFC2C00603813 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnfavoriteAction.swift; sourceTree = ""; }; 394E1FFE233E43F5009D2897 /* LinksAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinksAction.swift; sourceTree = ""; }; 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenItemUserActivity.swift; sourceTree = ""; }; + 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFileIntentHandler.swift; sourceTree = ""; }; + 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFileIntentHandler.swift; sourceTree = ""; }; + 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFolderIntentHandler.swift; sourceTree = ""; }; 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; 3961281522F8730A0087BD3A /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingSharesTableViewController.swift; sourceTree = ""; }; @@ -642,20 +719,32 @@ 396C82FA2319AFDD00938262 /* CollaborateAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollaborateAction.swift; sourceTree = ""; }; 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardSceneAction.swift; sourceTree = ""; }; 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; + 397754E123279EED00119FCB /* OCItem+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCItem+Extension.swift"; sourceTree = ""; }; 397754F22327A33500119FCB /* OpenSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSceneAction.swift; sourceTree = ""; }; + 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeletePathItemIntentHandler.swift; sourceTree = ""; }; 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; 39880BAA233B5236006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 39880BB0233B524B006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; + 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extension.swift"; sourceTree = ""; }; 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedInfoView.swift; sourceTree = ""; }; 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 3998F5D422411EDF00B66713 /* BorderedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedLabel.swift; sourceTree = ""; }; 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCertificate+Extension.swift"; sourceTree = ""; }; + 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockHelper.swift; sourceTree = ""; }; + 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Extension.swift"; sourceTree = ""; }; + 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathExistsIntentHandler.swift; sourceTree = ""; }; 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnshareAction.swift; sourceTree = ""; }; 39A5135222608836002CF1AA /* OCShare+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCShare+Extension.swift"; sourceTree = ""; }; 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCore+Extension.swift"; sourceTree = ""; }; + 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFileInfoIntentHandler.swift; sourceTree = ""; }; + 39A7138022E79C6700089423 /* ownCloud Intents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "ownCloud Intents.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 39A7138222E79C6700089423 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 39A7138422E79C6700089423 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 39A7139722E858E400089423 /* GetAccountsIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAccountsIntentHandler.swift; sourceTree = ""; }; 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingTableViewController.swift; sourceTree = ""; }; 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingEditTableViewController.swift; sourceTree = ""; }; 39B289A7226F1EE000BE0E11 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 39BE385C23435AFE0062A2FE /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 39CC8AE5228C12100020253B /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUploadSettingsSection.swift; sourceTree = ""; }; 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientItemCell.swift; sourceTree = ""; }; @@ -665,8 +754,11 @@ 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableHeaderView.swift; sourceTree = ""; }; 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeRoundedButton.swift; sourceTree = ""; }; 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCommands.swift; sourceTree = ""; }; + 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCItemTracker.swift; sourceTree = ""; }; 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; + 39F689A922EF5EDC00E63429 /* ownCloud Intents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ownCloud Intents.entitlements"; sourceTree = ""; }; + 39F689AA22F018C100E63429 /* GetDirectoryListingIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetDirectoryListingIntentHandler.swift; sourceTree = ""; }; 3D753147564B1E4F47826109 /* Pods-ownCloud Screenshots Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.debug.xcconfig"; sourceTree = ""; }; 42866B2892DC9EDC65D844E7 /* Pods_ownCloud_Screenshots_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloud_Screenshots_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMediaUploadTaskExtension.swift; sourceTree = ""; }; @@ -950,6 +1042,7 @@ DC3BE0D72077BC5D002A0AC0 /* openssl.framework in Frameworks */, DC3BE0D82077BC5D002A0AC0 /* ownCloudSDK.framework in Frameworks */, DCC085712293F1FD008CC05C /* ownCloudApp.framework in Frameworks */, + 394A0B0022EEFC2C00603813 /* ownCloudAppShared.framework in Frameworks */, DCE0275E21F1DF7E00F2544E /* ownCloudUI.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -966,6 +1059,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 394A0AF622EEFC2C00603813 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0B0A22EEFCF500603813 /* ownCloudSDK.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A7137D22E79C6700089423 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0B0922EEFCE400603813 /* ownCloudAppShared.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 59056CA722414F3C00A18A22 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1047,6 +1156,8 @@ DC7DBA3B207F86E900E7337D /* Tools */, DCC6564720C9B7E400110A97 /* ownCloud File Provider */, DCC6565820C9B7E400110A97 /* ownCloud File ProviderUI */, + 39A7138122E79C6700089423 /* ownCloud Intents */, + 394A0AFA22EEFC2C00603813 /* ownCloudAppShared */, 59056CAB22414F3C00A18A22 /* ownCloudScreenshotsTests */, 233BDE9D204FEFE500C06732 /* Products */, DC85573220513CC700189B9A /* Frameworks */, @@ -1065,6 +1176,8 @@ DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */, DCC085642293F1FD008CC05C /* ownCloudAppTests.xctest */, 59056CAA22414F3C00A18A22 /* ownCloudScreenshotsTests.xctest */, + 39A7138022E79C6700089423 /* ownCloud Intents.appex */, + 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */, ); name = Products; sourceTree = ""; @@ -1165,7 +1278,6 @@ children = ( 39104E0A223991C8002FC02F /* UIButton+Extension.swift */, 239F1318205A693A0029F186 /* UIColor+Extension.swift */, - 2347446920761BB700859C93 /* String+Extension.swift */, DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */, DC018F8220A0F56300135198 /* UIView+Extension.swift */, 6E83C78320A33C180066EC23 /* LAContext+Extension.swift */, @@ -1213,6 +1325,14 @@ path = Sharing; sourceTree = ""; }; + 3912208023436E9B0026C290 /* Client */ = { + isa = PBXGroup; + children = ( + 3912208123436EB80026C290 /* SortMethod.swift */, + ); + path = Client; + sourceTree = ""; + }; 3918FE742287DB9B00BACE03 /* Library */ = { isa = PBXGroup; children = ( @@ -1223,6 +1343,38 @@ path = Library; sourceTree = ""; }; + 394A0AFA22EEFC2C00603813 /* ownCloudAppShared */ = { + isa = PBXGroup; + children = ( + 3912208023436E9B0026C290 /* Client */, + 399725DF233DF37300FC3B94 /* UIKit Extension */, + 397754E023279EC100119FCB /* SDK Extensions */, + 399A4BF92317CC300027DDD6 /* Tools */, + 395E16FB22F0691F00DE89A1 /* Intent */, + 394A0AFB22EEFC2C00603813 /* ownCloudAppShared.h */, + 394A0AFC22EEFC2C00603813 /* Info.plist */, + ); + path = ownCloudAppShared; + sourceTree = ""; + }; + 395E16FB22F0691F00DE89A1 /* Intent */ = { + isa = PBXGroup; + children = ( + 399A4C012317D1BA0027DDD6 /* Extensions */, + 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */, + 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */, + 39A7139722E858E400089423 /* GetAccountsIntentHandler.swift */, + 39F689AA22F018C100E63429 /* GetDirectoryListingIntentHandler.swift */, + 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */, + 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */, + 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */, + 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */, + 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */, + 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */, + ); + path = Intent; + sourceTree = ""; + }; 394E2005233E4765009D2897 /* Window */ = { isa = PBXGroup; children = ( @@ -1239,6 +1391,58 @@ path = "Key Commands"; sourceTree = ""; }; + 397328E822D6067B006CFAA4 /* Import */ = { + isa = PBXGroup; + children = ( + 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */, + ); + path = Import; + sourceTree = ""; + }; + 397754E023279EC100119FCB /* SDK Extensions */ = { + isa = PBXGroup; + children = ( + 397754E123279EED00119FCB /* OCItem+Extension.swift */, + 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */, + ); + path = "SDK Extensions"; + sourceTree = ""; + }; + 399725DF233DF37300FC3B94 /* UIKit Extension */ = { + isa = PBXGroup; + children = ( + 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */, + 39BE385C23435AFE0062A2FE /* String+Extension.swift */, + ); + path = "UIKit Extension"; + sourceTree = ""; + }; + 399A4BF92317CC300027DDD6 /* Tools */ = { + isa = PBXGroup; + children = ( + 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */, + ); + path = Tools; + sourceTree = ""; + }; + 399A4C012317D1BA0027DDD6 /* Extensions */ = { + isa = PBXGroup; + children = ( + 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 39A7138122E79C6700089423 /* ownCloud Intents */ = { + isa = PBXGroup; + children = ( + 39F689A922EF5EDC00E63429 /* ownCloud Intents.entitlements */, + 39A7138222E79C6700089423 /* IntentHandler.swift */, + 39A7138422E79C6700089423 /* Info.plist */, + ); + path = "ownCloud Intents"; + sourceTree = ""; + }; 4C51727422DE04BD001BC97F /* Tasks */ = { isa = PBXGroup; children = ( @@ -1479,7 +1683,6 @@ DC63208221FCAC1E007EC0A8 /* ClientActivityViewController.swift */, DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */, DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */, - 23F6238020B587EF004FDE8B /* SortMethod.swift */, 23FA23E520BFD3D8009A6D73 /* SortBar.swift */, 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */, 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */, @@ -1922,6 +2125,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 394A0AF422EEFC2C00603813 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0AFD22EEFC2C00603813 /* ownCloudAppShared.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DCC085572293F1FD008CC05C /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1963,6 +2174,8 @@ DCC6566120C9B7E400110A97 /* PBXTargetDependency */, DCC6566420C9B7E400110A97 /* PBXTargetDependency */, DCC085702293F1FD008CC05C /* PBXTargetDependency */, + 39A7138622E79C6700089423 /* PBXTargetDependency */, + 394A0AFF22EEFC2C00603813 /* PBXTargetDependency */, ); name = ownCloud; productName = ownCloud; @@ -1993,6 +2206,44 @@ productReference = 233BDEB0204FEFE500C06732 /* ownCloudTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 394A0AF822EEFC2C00603813 /* ownCloudAppShared */ = { + isa = PBXNativeTarget; + buildConfigurationList = 394A0B0222EEFC2C00603813 /* Build configuration list for PBXNativeTarget "ownCloudAppShared" */; + buildPhases = ( + 394A0AF422EEFC2C00603813 /* Headers */, + 394A0AF522EEFC2C00603813 /* Sources */, + 394A0AF622EEFC2C00603813 /* Frameworks */, + 394A0AF722EEFC2C00603813 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 394A0B0C22EEFD2800603813 /* PBXTargetDependency */, + ); + name = ownCloudAppShared; + productName = ownCloudAppShared; + productReference = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; + productType = "com.apple.product-type.framework"; + }; + 39A7137F22E79C6700089423 /* ownCloud Intents */ = { + isa = PBXNativeTarget; + buildConfigurationList = 39A7138822E79C6700089423 /* Build configuration list for PBXNativeTarget "ownCloud Intents" */; + buildPhases = ( + 39A7137C22E79C6700089423 /* Sources */, + 39A7137D22E79C6700089423 /* Frameworks */, + 39A7137E22E79C6700089423 /* Resources */, + 39A7138D22E79DDE00089423 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 394A0B0822EEFCD900603813 /* PBXTargetDependency */, + ); + name = "ownCloud Intents"; + productName = SiriKit; + productReference = 39A7138022E79C6700089423 /* ownCloud Intents.appex */; + productType = "com.apple.product-type.app-extension"; + }; 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */ = { isa = PBXNativeTarget; buildConfigurationList = 59056CB122414F3C00A18A22 /* Build configuration list for PBXNativeTarget "ownCloudScreenshotsTests" */; @@ -2111,7 +2362,7 @@ 233BDE94204FEFE500C06732 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1010; + LastSwiftUpdateCheck = 1100; LastUpgradeCheck = 1020; ORGANIZATIONNAME = "ownCloud GmbH"; TargetAttributes = { @@ -2140,6 +2391,14 @@ ProvisioningStyle = Automatic; TestTargetID = 233BDE9B204FEFE500C06732; }; + 394A0AF822EEFC2C00603813 = { + CreatedOnToolsVersion = 11.0; + ProvisioningStyle = Automatic; + }; + 39A7137F22E79C6700089423 = { + CreatedOnToolsVersion = 11.0; + ProvisioningStyle = Automatic; + }; 59056CA922414F3C00A18A22 = { CreatedOnToolsVersion = 10.1; ProvisioningStyle = Automatic; @@ -2151,6 +2410,7 @@ }; DCC0855B2293F1FD008CC05C = { CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1100; ProvisioningStyle = Automatic; }; DCC085632293F1FD008CC05C = { @@ -2225,7 +2485,9 @@ DC7DBA33207F84BF00E7337D /* MakeTVG */, DCC6564520C9B7E300110A97 /* ownCloud File Provider */, DCC6565620C9B7E400110A97 /* ownCloud File ProviderUI */, + 39A7137F22E79C6700089423 /* ownCloud Intents */, DCC0855B2293F1FD008CC05C /* ownCloudApp */, + 394A0AF822EEFC2C00603813 /* ownCloudAppShared */, DCC085632293F1FD008CC05C /* ownCloudAppTests */, 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */, ); @@ -2369,6 +2631,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 394A0AF722EEFC2C00603813 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A7137E22E79C6700089423 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 59056CA822414F3C00A18A22 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2553,7 +2829,6 @@ files = ( 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */, DC3DEC7B22AFA1F000F3352D /* DownloadItemsHUDViewController.swift in Sources */, - 2347446A20761BB700859C93 /* String+Extension.swift in Sources */, DCF4F17920519F8C00189B9A /* StaticTableViewController.swift in Sources */, DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */, 4CB8ADE322DF6BA700F1FEBC /* PHAsset+Upload.swift in Sources */, @@ -2663,7 +2938,6 @@ 23E22BB720C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift in Sources */, 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */, 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */, - 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, @@ -2720,6 +2994,7 @@ DC321261207EB01B00DB171D /* ThemeImage.swift in Sources */, DC7DBA54207FA80C00E7337D /* TVGImage.swift in Sources */, DC33939622E0747400DD3DA4 /* MakeAvailableOfflineAction.swift in Sources */, + 39057AA3233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 6E586CFE2199A75900F680C4 /* MoveAction.swift in Sources */, DC625148225CEB2C00736874 /* UploadFileAction.swift in Sources */, 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */, @@ -2746,6 +3021,7 @@ 233BDEB5204FEFE500C06732 /* OwnCloudTests.swift in Sources */, 59B09E6F21AD61F4007827B8 /* CreateBookmarkTests.swift in Sources */, 59B09E6E21AD61F4007827B8 /* EditBookmarkTests.swift in Sources */, + 39057AA4233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 59538A0321E4A9C2005E543B /* CreateFolderTests.swift in Sources */, DC869A592153B1F60088977E /* OCMockingManager+SwiftTools.swift in Sources */, EA9337E32226DB070054971F /* SettingsTests.swift in Sources */, @@ -2764,10 +3040,44 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 394A0AF522EEFC2C00603813 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 395E16FA22F03CAF00DE89A1 /* GetFileIntentHandler.swift in Sources */, + 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */, + 397754E223279EED00119FCB /* OCItem+Extension.swift in Sources */, + 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */, + 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */, + 3984F5712319202200DC2639 /* DeletePathItemIntentHandler.swift in Sources */, + 39F689AC22F0206900E63429 /* OCBookmark+Extension.swift in Sources */, + 395E16FF22F172C900DE89A1 /* CreateFolderIntentHandler.swift in Sources */, + 395E16FD22F06A7300DE89A1 /* SaveFileIntentHandler.swift in Sources */, + 39E6DE84233CC39A008DAE04 /* Intents.intentdefinition in Sources */, + 3940C4F02326985B008227AE /* GetAccountIntentHandler.swift in Sources */, + 39A5C3A1231566D9009D9EE3 /* GetFileInfoIntentHandler.swift in Sources */, + 394A0B0522EEFCA300603813 /* GetAccountsIntentHandler.swift in Sources */, + 399A4C1023190ADF0027DDD6 /* PathExistsIntentHandler.swift in Sources */, + 39F689AB22F018C100E63429 /* GetDirectoryListingIntentHandler.swift in Sources */, + 399A4C002317CC460027DDD6 /* AppLockHelper.swift in Sources */, + 3912208223436EB80026C290 /* SortMethod.swift in Sources */, + 399A4C032317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A7137C22E79C6700089423 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 394A0AED22EEE32C00603813 /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 59056CA622414F3C00A18A22 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 39057AA8233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 59056CAD22414F3C00A18A22 /* ownCloudScreenshotsTests.swift in Sources */, 59056CB422414F8000A18A22 /* SnapshotHelper.swift in Sources */, 59296652224CD1DB0078F13D /* OCBookmarkManager+Tools.swift in Sources */, @@ -2799,6 +3109,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 39057AA7233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, DCC0856C2293F1FD008CC05C /* ownCloudAppTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2835,6 +3146,26 @@ target = 233BDE9B204FEFE500C06732 /* ownCloud */; targetProxy = 233BDEB1204FEFE500C06732 /* PBXContainerItemProxy */; }; + 394A0AFF22EEFC2C00603813 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 394A0AF822EEFC2C00603813 /* ownCloudAppShared */; + targetProxy = 394A0AFE22EEFC2C00603813 /* PBXContainerItemProxy */; + }; + 394A0B0822EEFCD900603813 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 394A0AF822EEFC2C00603813 /* ownCloudAppShared */; + targetProxy = 394A0B0722EEFCD900603813 /* PBXContainerItemProxy */; + }; + 394A0B0C22EEFD2800603813 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ownCloudSDK; + targetProxy = 394A0B0B22EEFD2800603813 /* PBXContainerItemProxy */; + }; + 39A7138622E79C6700089423 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 39A7137F22E79C6700089423 /* ownCloud Intents */; + targetProxy = 39A7138522E79C6700089423 /* PBXContainerItemProxy */; + }; 59056CB022414F3C00A18A22 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 233BDE9B204FEFE500C06732 /* ownCloud */; @@ -2936,6 +3267,15 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */ = { + isa = PBXVariantGroup; + children = ( + 39057AA9233BA7A60008E6C0 /* Base */, + 39057AB1233BA7AE0008E6C0 /* en */, + ); + name = Intents.intentdefinition; + sourceTree = ""; + }; 593A821320C7D4C5000E2A90 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -3211,6 +3551,114 @@ }; name = Release; }; + 394A0B0322EEFC2C00603813 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = ownCloudAppShared/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudAppShared; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 394A0B0422EEFC2C00603813 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ownCloudAppShared/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudAppShared; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 39A7138922E79C6700089423 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_ENTITLEMENTS = "ownCloud Intents/ownCloud Intents.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4AP2STM4H5; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ownCloud Intents/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.owncloud.ios-app.ownCloud-Intents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 39A7138A22E79C6700089423 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_ENTITLEMENTS = "ownCloud Intents/ownCloud Intents.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4AP2STM4H5; + INFOPLIST_FILE = "ownCloud Intents/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.owncloud.ios-app.ownCloud-Intents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 59056CB222414F3C00A18A22 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = A80C5F83C59F9D52F8F64890 /* Pods-ownCloudScreenshotsTests.debug.xcconfig */; @@ -3298,6 +3746,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -3314,13 +3763,16 @@ ); INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.2; + INTENTS_CODEGEN_LANGUAGE = Swift; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudApp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -3331,6 +3783,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -3343,12 +3796,14 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.2; + INTENTS_CODEGEN_LANGUAGE = Swift; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudApp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -3358,7 +3813,6 @@ DCC085772293F1FD008CC05C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -3382,7 +3836,6 @@ DCC085782293F1FD008CC05C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -3510,6 +3963,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 394A0B0222EEFC2C00603813 /* Build configuration list for PBXNativeTarget "ownCloudAppShared" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 394A0B0322EEFC2C00603813 /* Debug */, + 394A0B0422EEFC2C00603813 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 39A7138822E79C6700089423 /* Build configuration list for PBXNativeTarget "ownCloud Intents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39A7138922E79C6700089423 /* Debug */, + 39A7138A22E79C6700089423 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 59056CB122414F3C00A18A22 /* Build configuration list for PBXNativeTarget "ownCloudScreenshotsTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme index 59b4a46c5..b9aae8e4f 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme @@ -42,6 +42,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -73,24 +82,24 @@ ReferencedContainer = "container:ownCloud.xcodeproj"> + + + + - - - - - - - - + + + + @@ -73,24 +82,24 @@ ReferencedContainer = "container:ownCloud.xcodeproj"> + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index 6d67db6c3..ff3da31ca 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -116,6 +116,16 @@ ReferencedContainer = "container:ownCloud.xcodeproj"> + + + + Px&w@E}nR9Fe^mwRZ=Q5eTJn#9MBA}DxYC)%09Wy^>L&ok(Ni7-gX6qm{V{7PpFeAI~Q4W*a=G!A>ZLPhbP7d<=adtYi}4 z4!E&ZKNvQ!mUtJu1wXj8KhT#0VWl?#?t#U%vI1R)Fu`Zy9LvyR z5J5K_{c~`~QvEQr245@@v;T&{9&lH!&gVEaGgPU2VOCvl!-|q91J`GHL$d~&QE0Tn zuAk}$Kz}iV#81S0D>2S+9fr}cPAfOM1_5>X=oUA7E0+b>no4)gy8xfP=oTuHz$Xix#Z`G2Xf ze}5R6jkBAK?xY1_0tWc%0(TRsmQ9YQl)(&jgpj%TDgh-_5F$<_SN+c1M9!Mw1+PO# zni32n^KjNyYih|P;7f25sk?|r)1iIrcIpk6}kMPSo-ga}Io`>5G>_qE6hMS2^=chekzj zDE}GY25HH?-eF8yZ6ux}GUw1z zLvuJ^5*2*MB(&P7yi8>7WKf!J={mR6b!oM+c%8`H%)m6=c9e-49-v4ojmJAg<{Ad% z-$rCu>TXqn+<)hs%-YP!)C^5QMdqi`CguYob1D6kb~3>y9L;!CYyy5H62G%QgV3)^ zoJOz}^)@*l6PfdABkyD)@he9%Anm%0Nc_e!RI0?8L}D{)H7%bLnbRqu${&(>grSf$ zDUsQgYE8=bL}p_uH4|SEnG+~dslEA%$Z=mfiu)3Yw|^LbTFt``M54_4_M~j&$C2gEegL7i6LmTMfo}Z7=9r#JJ4#Y5s6DEyiFt~qM4S+Up!N03p%2a ziNq@uZh!B com.owncloud.ios-app.openAccount com.owncloud.ios-app.openItem + CreateFolderIntent + DeletePathItemIntent + GetAccountIntent + GetAccountsIntent + GetDirectoryListingIntent + GetFileInfoIntent + GetFileIntent + PathExistsIntent + SaveFileIntent OCAppGroupIdentifier group.$(PRODUCT_BUNDLE_IDENTIFIER) diff --git a/ownCloud/Client/SortMethod.swift b/ownCloudAppShared/Client/SortMethod.swift similarity index 93% rename from ownCloud/Client/SortMethod.swift rename to ownCloudAppShared/Client/SortMethod.swift index 742b2f83f..71fa56490 100644 --- a/ownCloud/Client/SortMethod.swift +++ b/ownCloudAppShared/Client/SortMethod.swift @@ -19,7 +19,7 @@ import Foundation import ownCloudSDK -typealias OCSort = Comparator +public typealias OCSort = Comparator public enum SortDirection: Int { case ascendant = 0 @@ -34,9 +34,9 @@ public enum SortMethod: Int { case date = 3 case shared = 4 - static var all: [SortMethod] = [alphabetically, type, size, date, shared] + public static var all: [SortMethod] = [alphabetically, type, size, date, shared] - func localizedName() -> String { + public func localizedName() -> String { var name = "" switch self { @@ -55,8 +55,8 @@ public enum SortMethod: Int { return name } - func comparator(direction: SortDirection) -> OCSort { - var comparator : OCSort + public func comparator(direction: SortDirection) -> OCSort { + var comparator: OCSort var combinedComparator: OCSort? let alphabeticComparator : OCSort = { (left, right) in diff --git a/ownCloudAppShared/Info.plist b/ownCloudAppShared/Info.plist new file mode 100644 index 000000000..9bcb24442 --- /dev/null +++ b/ownCloudAppShared/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition new file mode 100644 index 000000000..1e2d01032 --- /dev/null +++ b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition @@ -0,0 +1,2545 @@ + + + + + INEnums + + + INEnumDisplayName + Sorting Type + INEnumDisplayNameID + CwmgYp + INEnumGeneratesHeader + + INEnumName + SortingType + INEnumType + Regular + INEnumValues + + + INEnumValueDisplayName + unknown + INEnumValueDisplayNameID + 0w7gOP + INEnumValueName + unknown + + + INEnumValueDisplayName + Name + INEnumValueDisplayNameID + 8CnkN3 + INEnumValueIndex + 1 + INEnumValueName + name + + + INEnumValueDisplayName + Type + INEnumValueDisplayNameID + vY7pxV + INEnumValueIndex + 2 + INEnumValueName + type + + + INEnumValueDisplayName + Size + INEnumValueDisplayNameID + eZdCum + INEnumValueIndex + 3 + INEnumValueName + size + + + INEnumValueDisplayName + Date + INEnumValueDisplayNameID + lsb3bf + INEnumValueIndex + 4 + INEnumValueName + date + + + INEnumValueDisplayName + Shared + INEnumValueDisplayNameID + E5BAmD + INEnumValueIndex + 5 + INEnumValueName + shared + + + + + INEnumDisplayName + Sorting Direction + INEnumDisplayNameID + nZsCMf + INEnumGeneratesHeader + + INEnumName + SortingDirection + INEnumType + Regular + INEnumValues + + + INEnumValueDisplayName + unknown + INEnumValueDisplayNameID + JRUplN + INEnumValueName + unknown + + + INEnumValueDisplayName + Ascending + INEnumValueDisplayNameID + tHgDsi + INEnumValueIndex + 1 + INEnumValueName + ascending + + + INEnumValueDisplayName + Descending + INEnumValueDisplayNameID + ZB0VB7 + INEnumValueIndex + 2 + INEnumValueName + descending + + + + + INIntentDefinitionModelVersion + 1.1 + INIntentDefinitionNamespace + K5U8aR + INIntentDefinitionSystemVersion + 18G95 + INIntentDefinitionToolsBuildVersion + 11A420a + INIntentDefinitionToolsVersion + 11.0 + INIntents + + + INIntentCategory + request + INIntentConfigurable + + INIntentDescription + Get a list of all configured accounts + INIntentDescriptionID + wK4bqS + INIntentIneligibleForSuggestions + + INIntentManagedParameterCombinations + + + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Account List + INIntentParameterCombinationTitleID + j7guHG + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetAccounts + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeFormatString + Here is your account list + INIntentResponseCodeFormatStringID + gxBJkl + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeFormatString + Here is your account list + INIntentResponseCodeFormatStringID + yrLv2M + INIntentResponseCodeName + successList + INIntentResponseCodeSuccess + + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + DSSHvU + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + iDEwxe + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseLastParameterTag + 6 + INIntentResponseOutput + accountList + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Account List + INIntentResponseParameterDisplayNameID + Ul4Kf0 + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + accountList + INIntentResponseParameterObjectType + Account + INIntentResponseParameterObjectTypeNamespace + K5U8aR + INIntentResponseParameterSupportsMultipleValues + + INIntentResponseParameterTag + 5 + INIntentResponseParameterType + Object + + + + INIntentTitle + Get Accounts + INIntentTitleID + XKCjDe + INIntentType + Custom + INIntentVerb + Request + + + INIntentCategory + request + INIntentConfigurable + + INIntentDescription + Get a directory listing for the given account and path + INIntentDescriptionID + 2IChwb + INIntentIneligibleForSuggestions + + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 10 + INIntentManagedParameterCombinations + + path,account,sortType,sortDirection + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Directory Listing for ${path} from ${account} + INIntentParameterCombinationTitleID + axuc4N + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetDirectoryListing + INIntentParameters + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + u8abus + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValue + / + INIntentParameterMetadataDefaultValueID + Ed4d2z + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Which path should be used? + INIntentParameterPromptDialogFormatStringID + uo96Pp + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${path}’. + INIntentParameterPromptDialogFormatStringID + Czb1hO + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + 9eAgmm + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${path}’? + INIntentParameterPromptDialogFormatStringID + wwyxxS + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + XVBTsw + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + fotVdI + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + zAG76o + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + iyefxU + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + dH8vD7 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 6 + INIntentParameterType + Object + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Sort by + INIntentParameterDisplayNameID + IklOI3 + INIntentParameterDisplayPriority + 3 + INIntentParameterEnumType + SortingType + INIntentParameterEnumTypeNamespace + K5U8aR + INIntentParameterName + sortType + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${sortType}’. + INIntentParameterPromptDialogFormatStringID + a5zbSk + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + YkRxhp + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${sortType}’? + INIntentParameterPromptDialogFormatStringID + 2etfeF + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 8 + INIntentParameterType + Integer + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Sort Direction + INIntentParameterDisplayNameID + gFiypL + INIntentParameterDisplayPriority + 4 + INIntentParameterEnumType + SortingDirection + INIntentParameterEnumTypeNamespace + K5U8aR + INIntentParameterName + sortDirection + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${sortDirection}’. + INIntentParameterPromptDialogFormatStringID + ZXvi1J + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + d8ddSN + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${sortDirection}’? + INIntentParameterPromptDialogFormatStringID + 5tW7cP + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 10 + INIntentParameterType + Integer + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + xZDy1P + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + JPFL0e + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + N181Er + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + ZWoKzx + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + SP1wR2 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + QIKvUr + INIntentResponseCodeName + accountFailure + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + directoryListing + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Directory Listing + INIntentResponseParameterDisplayNameID + XEIycV + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + directoryListing + INIntentResponseParameterSupportsMultipleValues + + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + String + + + + INIntentTitle + Get Directory Listing + INIntentTitleID + PHyaws + INIntentType + Custom + INIntentVerb + Request + + + INIntentCategory + download + INIntentConfigurable + + INIntentDescription + Get file for the given path in account + INIntentDescriptionID + nvFl2W + INIntentIneligibleForSuggestions + + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 4 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get File ${path} from ${account} + INIntentParameterCombinationTitleID + kfmZUU + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetFile + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + eGjKAM + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValue + / + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Which File do you need? + INIntentParameterPromptDialogFormatStringID + 38884k + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + sxd9RH + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + VHlRdG + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + 9uzczk + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + 8xEyec + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + Ezlwwe + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 4 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + WDPKZp + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + NEiHz6 + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + sf57GO + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + lrF7G0 + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + 4eTrn2 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + 61YuKy + INIntentResponseCodeName + accountFailure + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + file + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File + INIntentResponseParameterDisplayNameID + 08siGk + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + file + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + File + + + + INIntentTitle + Get File + INIntentTitleID + 8Ziepl + INIntentType + Custom + INIntentVerb + Get + + + INIntentCategory + set + INIntentConfigurable + + INIntentDescription + Save a file into the path in account + INIntentDescriptionID + dtUvRf + INIntentIneligibleForSuggestions + + INIntentInput + file + INIntentKeyParameter + file + INIntentLastParameterTag + 9 + INIntentManagedParameterCombinations + + path,file,account,filename,fileextension + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Save File ${file} to ${path} in ${account} + INIntentParameterCombinationTitleID + 6a3odp + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + SaveFile + INIntentParameters + + + INIntentParameterDisplayName + File + INIntentParameterDisplayNameID + YzkaES + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCustomUTIs + + + UTI + public.data + + + UTI + public.image + + + UTI + public.content + + + INIntentParameterMetadataType + Custom + + INIntentParameterName + file + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Which file to save? + INIntentParameterPromptDialogFormatStringID + ijKWiB + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 5 + INIntentParameterType + File + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + dlIKxE + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Where to save the file? + INIntentParameterPromptDialogFormatStringID + 5bvXKH + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + tiHX6L + INIntentParameterDisplayPriority + 3 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + e5LXg2 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + ogx9ZM + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + Q5StNV + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + o7aBS2 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 7 + INIntentParameterType + Object + + + INIntentParameterDisplayName + File Name (optional) + INIntentParameterDisplayNameID + B1lmXQ + INIntentParameterDisplayPriority + 4 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + filename + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Optional file name for saving file + INIntentParameterPromptDialogFormatStringID + b4VIz0 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 8 + INIntentParameterType + String + + + INIntentParameterDisplayName + File Extension (optional) + INIntentParameterDisplayNameID + cDQ3EQ + INIntentParameterDisplayPriority + 5 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + fileextension + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Optional file extension for saving file + INIntentParameterPromptDialogFormatStringID + dRSMFQ + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 9 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + P6Xeu7 + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + 6JcpPy + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + xgwnia + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + leB73e + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + xIWWfW + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + AtUMKl + INIntentResponseCodeName + accountFailure + + + INIntentResponseLastParameterTag + 1 + INIntentResponseOutput + filePath + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File Path + INIntentResponseParameterDisplayNameID + LJUrKB + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + filePath + INIntentResponseParameterTag + 1 + INIntentResponseParameterType + String + + + + INIntentTitle + Save File + INIntentTitleID + 1UkR2J + INIntentType + Custom + INIntentVerb + Set + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Create a folder with the given name into the path. + INIntentDescriptionID + XmX9YS + INIntentIneligibleForSuggestions + + INIntentInput + name + INIntentKeyParameter + name + INIntentLastParameterTag + 4 + INIntentManagedParameterCombinations + + name,path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Create Folder ${name} in ${path} for ${account} + INIntentParameterCombinationTitleID + Rh6ICd + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + CreateFolder + INIntentParameters + + + INIntentParameterDisplayName + Name + INIntentParameterDisplayNameID + FZOxYk + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + name + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Name of the folder + INIntentParameterPromptDialogFormatStringID + W2jbjk + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + GOVcde + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Where to create the folder? + INIntentParameterPromptDialogFormatStringID + KHwW13 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 2 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + mRWXIl + INIntentParameterDisplayPriority + 3 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + 4BnO3u + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + Fe4YSc + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + LiNwxr + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + Y16qbv + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 4 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + ggYJ45 + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + OBFBYf + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + ctet5s + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + l1ScV6 + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + CtNzv7 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + dWm6eD + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + This folder already exists in the given path + INIntentResponseCodeConciseFormatStringID + 0n7uf3 + INIntentResponseCodeFormatString + This folder already exists in the given path + INIntentResponseCodeFormatStringID + Aviuqh + INIntentResponseCodeName + folderExistsFailure + + + INIntentResponseLastParameterTag + 1 + INIntentResponseOutput + path + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Path + INIntentResponseParameterDisplayNameID + hL4u0G + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + path + INIntentResponseParameterTag + 1 + INIntentResponseParameterType + String + + + + INIntentTitle + Create Folder + INIntentTitleID + AQ4lsZ + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + download + INIntentConfigurable + + INIntentDescription + Retrieve file infos for the given path + INIntentDescriptionID + LdHBeL + INIntentIneligibleForSuggestions + + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get File Info for ${path} in ${account} + INIntentParameterCombinationTitleID + 7Rco9W + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetFileInfo + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + unxhTD + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + For which path you want the file info? + INIntentParameterPromptDialogFormatStringID + 3uG0po + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + wPdYjH + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + xaKzfr + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + q5bi2K + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + upG52E + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + 7NRaX2 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + 0xydIy + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + yeD8D7 + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + 3dRWka + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + sLw6L8 + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + lUzgvo + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + hLfV0h + INIntentResponseCodeName + accountFailure + + + INIntentResponseLastParameterTag + 3 + INIntentResponseOutput + fileInfo + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File Info + INIntentResponseParameterDisplayNameID + 4HFDB7 + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + fileInfo + INIntentResponseParameterObjectType + FileInfo + INIntentResponseParameterObjectTypeNamespace + K5U8aR + INIntentResponseParameterTag + 3 + INIntentResponseParameterType + Object + + + + INIntentTitle + Get File Info + INIntentTitleID + MGAYW2 + INIntentType + Custom + INIntentVerb + Get + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Check if the given path exists + INIntentDescriptionID + PthGCb + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Check if path ${path} exists in ${account} + INIntentParameterCombinationTitleID + 79FdrK + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + PathExists + INIntentParameterCombinations + + path,account + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Check if path ${path} exists in ${account} + INIntentParameterCombinationTitleID + 0jWfGg + + + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + 3allZk + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + For which path you want the file info? + INIntentParameterPromptDialogFormatStringID + vhZe7o + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + uP1q1e + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + odUmdN + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + eJ8Zop + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + c8L2xN + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + mJL3AY + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + eATsqP + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + tMYYpx + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + g1Zin1 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + C4qdxp + INIntentResponseCodeName + accountFailure + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + pathExists + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Path Exists + INIntentResponseParameterDisplayNameID + BOHYUX + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterMetadata + + INIntentResponseParameterMetadataFalseDisplayName + false + INIntentResponseParameterMetadataFalseDisplayNameID + bPAspK + INIntentResponseParameterMetadataTrueDisplayName + true + INIntentResponseParameterMetadataTrueDisplayNameID + 6xEiIO + + INIntentResponseParameterName + pathExists + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + Boolean + + + + INIntentTitle + Path Exists + INIntentTitleID + ds6RJ5 + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Delete the given path in account + INIntentDescriptionID + jkLupt + INIntentInput + path + INIntentKeyParameter + path + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + path,account + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Delete Item at path ${path} in ${account} + INIntentParameterCombinationTitleID + TjffcC + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + DeletePathItem + INIntentParameterCombinations + + path,account + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Delete Item at path ${path} in ${account} + INIntentParameterCombinationTitleID + gYCA4r + + + INIntentParameters + + + INIntentParameterDisplayName + Path + INIntentParameterDisplayNameID + ZhFjkx + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + path + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + For which path you want the file info? + INIntentParameterPromptDialogFormatStringID + qWXOGM + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + Iej0wL + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Select the account + INIntentParameterPromptDialogFormatStringID + YDTXfI + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + b4pgUO + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + HTGZys + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + mqKEhg + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + R54XCc + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + Iq98sA + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + Y87IjO + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + 1KMplM + INIntentResponseCodeName + pathFailure + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + 7leZJ9 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + ABZOUR + INIntentResponseCodeName + accountFailure + + + + INIntentTitle + Delete Path Item + INIntentTitleID + 30uTS2 + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + download + INIntentConfigurable + + INIntentDescription + Get the account for the given account UUID + INIntentDescriptionID + U3l2bk + INIntentInput + accountUUID + INIntentKeyParameter + accountUUID + INIntentLastParameterTag + 1 + INIntentManagedParameterCombinations + + accountUUID + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Account with ${accountUUID} + INIntentParameterCombinationTitleID + D3oUHR + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + GetAccount + INIntentParameterCombinations + + accountUUID + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Get Account with ${accountUUID} + INIntentParameterCombinationTitleID + dCe7NY + + + INIntentParameters + + + INIntentParameterDisplayName + Account UUID + INIntentParameterDisplayNameID + Dt0MVQ + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + accountUUID + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Enter the UUID for the account you want + INIntentParameterPromptDialogFormatStringID + VYw9o4 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + AG0qbP + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + Vg8Ky0 + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + Kr0sps + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + UdyMQ5 + INIntentResponseCodeName + accountFailure + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + account + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Account + INIntentResponseParameterDisplayNameID + GAhtDQ + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + account + INIntentResponseParameterObjectType + Account + INIntentResponseParameterObjectTypeNamespace + K5U8aR + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + Object + + + + INIntentTitle + Get Account + INIntentTitleID + 8ZZD23 + INIntentType + Custom + INIntentVerb + Get + + + INTypes + + + INTypeDisplayName + Account + INTypeDisplayNameID + Cfk0HW + INTypeLastPropertyTag + 103 + INTypeName + Account + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + INTypePropertyDisplayName + Server URL + INTypePropertyDisplayNameID + 3BsNkC + INTypePropertyDisplayPriority + 5 + INTypePropertyName + serverURL + INTypePropertyTag + 101 + INTypePropertyType + URL + + + INTypePropertyDisplayName + Name + INTypePropertyDisplayNameID + ZRW2EV + INTypePropertyDisplayPriority + 6 + INTypePropertyName + name + INTypePropertyTag + 102 + INTypePropertyType + String + + + INTypePropertyDisplayName + Uuid + INTypePropertyDisplayNameID + gDQ2EW + INTypePropertyDisplayPriority + 7 + INTypePropertyName + uuid + INTypePropertyTag + 103 + INTypePropertyType + String + + + + + INTypeDisplayName + File Info + INTypeDisplayNameID + A3Pv0a + INTypeLastPropertyTag + 114 + INTypeName + FileInfo + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + INTypePropertyDisplayName + Last Modified + INTypePropertyDisplayNameID + qD6IIc + INTypePropertyDisplayPriority + 5 + INTypePropertyMetadata + + INTypePropertyMetadataDateStyle + Full + INTypePropertyMetadataFormat + Relative + INTypePropertyMetadataTimeStyle + Full + INTypePropertyMetadataType + DateTime + + INTypePropertyName + lastModified + INTypePropertyTag + 110 + INTypePropertyType + DateComponents + + + INTypePropertyDisplayName + Is Favorite + INTypePropertyDisplayNameID + lIGrzU + INTypePropertyDisplayPriority + 6 + INTypePropertyMetadata + + INTypePropertyMetadataFalseDisplayName + false + INTypePropertyMetadataFalseDisplayNameID + eg3e3D + INTypePropertyMetadataTrueDisplayName + true + INTypePropertyMetadataTrueDisplayNameID + KpcaUP + + INTypePropertyName + isFavorite + INTypePropertyTag + 109 + INTypePropertyType + Boolean + + + INTypePropertyDisplayName + Size + INTypePropertyDisplayNameID + wbCXnN + INTypePropertyDisplayPriority + 7 + INTypePropertyName + size + INTypePropertyTag + 107 + INTypePropertyType + Integer + + + INTypePropertyDisplayName + Creation Date + INTypePropertyDisplayNameID + 38uTf2 + INTypePropertyDisplayPriority + 8 + INTypePropertyMetadata + + INTypePropertyMetadataDateStyle + Full + INTypePropertyMetadataFormat + Relative + INTypePropertyMetadataTimeStyle + Full + INTypePropertyMetadataType + DateTime + + INTypePropertyName + creationDate + INTypePropertyTag + 106 + INTypePropertyType + DateComponents + + + INTypePropertyDisplayName + Mime Type + INTypePropertyDisplayNameID + zKRAWH + INTypePropertyDisplayPriority + 9 + INTypePropertyName + mimeType + INTypePropertyTag + 105 + INTypePropertyType + String + + + INTypePropertyDisplayName + Last Modified Timestamp + INTypePropertyDisplayNameID + lyQhRe + INTypePropertyDisplayPriority + 10 + INTypePropertyName + lastModifiedTimestamp + INTypePropertyTag + 112 + INTypePropertyType + Integer + + + INTypePropertyDisplayName + Creation Date Timestamp + INTypePropertyDisplayNameID + MRTQ9c + INTypePropertyDisplayPriority + 11 + INTypePropertyName + creationDateTimestamp + INTypePropertyTag + 114 + INTypePropertyType + Integer + + + + + + diff --git a/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift new file mode 100644 index 000000000..a0a6bbb2b --- /dev/null +++ b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift @@ -0,0 +1,112 @@ +// +// CreateFolderIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 31.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { + + public func handle(intent: CreateFolderIntent, completion: @escaping (CreateFolderIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(CreateFolderIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path?.pathRepresentation, let uuid = intent.account?.uuid, let name = intent.name else { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(CreateFolderIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item { + let folderPath = String(format: "%@%@", path, name) + // Check, if the folder already exists in the given path + OCItemTracker().item(for: bookmark, at: folderPath) { (error, core, folderPathItem) in + if error == nil, folderPathItem == nil, let core = core { + + let progress = core.createFolder(name, inside: targetItem, options: nil, resultHandler: { (error, _, item, _) in + if error != nil { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } else { + completion(CreateFolderIntentResponse.success(path: item?.path ?? "")) + } + }) + + if progress == nil { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } + } else if core != nil { + completion(CreateFolderIntentResponse(code: .folderExistsFailure, userActivity: nil)) + } else { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } + } + } else if core != nil { + completion(CreateFolderIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(CreateFolderIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveName(for intent: CreateFolderIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let name = intent.name { + completion(INStringResolutionResult.success(with: name)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func resolveAccount(for intent: CreateFolderIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + 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) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } +} + +@available(iOS 13.0, *) +extension CreateFolderIntentResponse { + + public static func success(path: String) -> CreateFolderIntentResponse { + let intentResponse = CreateFolderIntentResponse(code: .success, userActivity: nil) + intentResponse.path = path + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift new file mode 100644 index 000000000..169a2904e --- /dev/null +++ b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift @@ -0,0 +1,83 @@ +// +// DeletePathItemIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling { + + public func handle(intent: DeletePathItemIntent, completion: @escaping (DeletePathItemIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(DeletePathItemIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(DeletePathItemIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item, let core = core { + let progress = core.delete(targetItem, requireMatch: true, resultHandler: { (error, _, _, _) in + if error != nil { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + } else { + completion(DeletePathItemIntentResponse(code: .success, userActivity: nil)) + } + }) + + if progress == nil { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + } + } else if core != nil { + completion(DeletePathItemIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(DeletePathItemIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveAccount(for intent: DeletePathItemIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + 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) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } +} diff --git a/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift b/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift new file mode 100644 index 000000000..067e2434e --- /dev/null +++ b/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift @@ -0,0 +1,56 @@ +// +// Account+Extension.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 29.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 + +@available(iOS 13.0, *) +extension OCBookmarkManager { + + public var accountList : [Account] { + var accountList : [Account] = [] + accountList = OCBookmarkManager.shared.bookmarks.map { (bookmark) -> Account in + let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) + account.name = bookmark.shortName + account.serverURL = bookmark.url + account.uuid = bookmark.uuid.uuidString + + return account + } + + return accountList + } + + public func bookmark(for uuidString: String) -> OCBookmark? { + return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first + } + + public func accountBookmark(for uuidString: String) -> Account? { + if let bookmark = bookmark(for: uuidString) { + let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) + account.name = bookmark.shortName + account.serverURL = bookmark.url + account.uuid = bookmark.uuid.uuidString + + return account + } + + return nil + } + +} diff --git a/ownCloudAppShared/Intent/GetAccountIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift new file mode 100644 index 000000000..64fcf8fb3 --- /dev/null +++ b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift @@ -0,0 +1,68 @@ +// +// GetAccountsIntentHandler.swift +// ownCloud +// +// Created by Matthias Hühne on 29.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { + + public func handle(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetAccountIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let uuid = intent.accountUUID else { + completion(GetAccountIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let accountBookmark = OCBookmarkManager.shared.accountBookmark(for: uuid) else { + completion(GetAccountIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + completion(GetAccountIntentResponse.success(account: accountBookmark)) + } + + public func resolveAccountUUID(for intent: GetAccountIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let account = intent.accountUUID { + completion(INStringResolutionResult.success(with: account)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func confirm(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + completion(GetAccountIntentResponse(code: .ready, userActivity: nil)) + } +} + +@available(iOS 13.0, *) +extension GetAccountIntentResponse { + + public static func success(account: Account) -> GetAccountIntentResponse { + let intentResponse = GetAccountIntentResponse(code: .success, userActivity: nil) + intentResponse.account = account + return intentResponse + } + +} diff --git a/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift new file mode 100644 index 000000000..fd8949f70 --- /dev/null +++ b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift @@ -0,0 +1,50 @@ +// +// GetAccountsIntentHandler.swift +// ownCloud +// +// Created by Matthias Hühne on 24.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class GetAccountsIntentHandler: NSObject, GetAccountsIntentHandling { + + public func handle(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetAccountsIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + completion(GetAccountsIntentResponse.success(accountList: OCBookmarkManager.shared.accountList)) + } + + public func confirm(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + completion(GetAccountsIntentResponse(code: .ready, userActivity: nil)) + } +} + +@available(iOS 13.0, *) +extension GetAccountsIntentResponse { + + public static func success(accountList: [Account]) -> GetAccountsIntentResponse { + let intentResponse = GetAccountsIntentResponse(code: .success, userActivity: nil) + intentResponse.accountList = accountList + return intentResponse + } + +} diff --git a/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift new file mode 100644 index 000000000..02268003a --- /dev/null +++ b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift @@ -0,0 +1,128 @@ +// +// GetDirectoryIntentHandler.swift +// ownCloud +// +// Created by Matthias Hühne on 24.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +typealias GetDirectoryListingCompletionHandler = (GetDirectoryListingIntentResponse) -> Void + +@available(iOS 13.0, *) +public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingIntentHandling, OCQueryDelegate { + + var completion : GetDirectoryListingCompletionHandler? + + public func resolvePath(for intent: GetDirectoryListingIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + + } + + public func provideAccountOptions(for intent: GetDirectoryListingIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolveAccount(for intent: GetDirectoryListingIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func resolveSortType(for intent: GetDirectoryListingIntent, with completion: @escaping (SortingTypeResolutionResult) -> Void) { + completion(SortingTypeResolutionResult.success(with: intent.sortType)) + } + + public func resolveSortDirection(for intent: GetDirectoryListingIntent, with completion: @escaping (SortingDirectionResolutionResult) -> Void) { + completion(SortingDirectionResolutionResult.success(with: intent.sortDirection)) + } + + public func handle(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetDirectoryListingIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path?.pathRepresentation, let uuid = intent.account?.uuid else { + completion(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(GetDirectoryListingIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + self.completion = completion + + OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { (core, error) in + if error == nil { + let targetDirectoryQuery = OCQuery(forPath: path) + targetDirectoryQuery.delegate = self + + if targetDirectoryQuery.sortComparator == nil { + let sort = SortMethod(rawValue: (intent.sortType.rawValue - 1)) ?? SortMethod.alphabetically + + targetDirectoryQuery.sortComparator = sort.comparator(direction: SortDirection(rawValue: (intent.sortDirection.rawValue - 1)) ?? SortDirection.ascendant) + } + core?.start(targetDirectoryQuery) + } else { + self.completion?(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) + } + }) + } + + public func queryHasChangesAvailable(_ query: OCQuery) { + if query.state == .targetRemoved { + self.completion?(GetDirectoryListingIntentResponse(code: .pathFailure, userActivity: nil)) + self.completion = nil + } else if query.state == .idle { + var directoryListing : [String] = [] + if let results = query.queryResults { + directoryListing = results.compactMap { return $0.path } + } + + self.completion?(GetDirectoryListingIntentResponse.success(directoryListing: directoryListing)) + self.completion = nil + } + } + + public func query(_ query: OCQuery, failedWithError error: Error) { + self.completion?(GetDirectoryListingIntentResponse(code: .failure, userActivity: nil)) + } + + public func confirm(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { + completion(GetDirectoryListingIntentResponse(code: .ready, userActivity: nil)) + } +} + +@available(iOS 13.0, *) +extension GetDirectoryListingIntentResponse { + + public static func success(directoryListing: [String]) -> GetDirectoryListingIntentResponse { + let intentResponse = GetDirectoryListingIntentResponse(code: .success, userActivity: nil) + intentResponse.directoryListing = directoryListing + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift new file mode 100644 index 000000000..fa977d110 --- /dev/null +++ b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift @@ -0,0 +1,101 @@ +// +// GetFileInfoIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 27.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class GetFileInfoIntentHandler: NSObject, GetFileInfoIntentHandling { + + public func handle(intent: GetFileInfoIntent, completion: @escaping (GetFileInfoIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetFileInfoIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(GetFileInfoIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(GetFileInfoIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item { + let fileInfo = FileInfo(identifier: targetItem.localID, display: targetItem.name ?? "") + + let calendar = Calendar.current + if let creationDate = targetItem.creationDate { + let components = calendar.dateTimeComponents(from: creationDate) + fileInfo.creationDate = components + fileInfo.creationDateTimestamp = NSNumber(value: creationDate.timeIntervalSince1970) + } + if let lastModified = targetItem.lastModified { + let components = calendar.dateTimeComponents(from: lastModified) + fileInfo.lastModified = components + fileInfo.lastModifiedTimestamp = NSNumber(value: lastModified.timeIntervalSince1970) + } + fileInfo.isFavorite = targetItem.isFavorite + fileInfo.mimeType = targetItem.mimeType + fileInfo.size = NSNumber(value: targetItem.size) + + completion(GetFileInfoIntentResponse.success(fileInfo: fileInfo)) + } else if core != nil { + completion(GetFileInfoIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(GetFileInfoIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveAccount(for intent: GetFileInfoIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: GetFileInfoIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolvePath(for intent: GetFileInfoIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + +} + +@available(iOS 13.0, *) +extension GetFileInfoIntentResponse { + + public static func success(fileInfo: FileInfo) -> GetFileInfoIntentResponse { + let intentResponse = GetFileInfoIntentResponse(code: .success, userActivity: nil) + intentResponse.fileInfo = fileInfo + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/GetFileIntentHandler.swift b/ownCloudAppShared/Intent/GetFileIntentHandler.swift new file mode 100644 index 000000000..0a55cf295 --- /dev/null +++ b/ownCloudAppShared/Intent/GetFileIntentHandler.swift @@ -0,0 +1,99 @@ +// +// GetFileIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +typealias GetFileCompletionHandler = (GetFileIntentResponse) -> Void + +@available(iOS 13.0, *) +public class GetFileIntentHandler: NSObject, GetFileIntentHandling { + + public func handle(intent: GetFileIntent, completion: @escaping (GetFileIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(GetFileIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(GetFileIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(GetFileIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let item = item { + if core?.localCopy(of: item) == nil { + core?.downloadItem(item, options: [ .returnImmediatelyIfOfflineOrUnavailable : true ], resultHandler: { (error, core, item, file) in + if error == nil, let item = item, let file = item.file(with: core), let url = file.url { + let file = INFile(fileURL: url, filename: item.name, typeIdentifier: nil) + completion(GetFileIntentResponse.success(file: file)) + } else { + completion(GetFileIntentResponse(code: .failure, userActivity: nil)) + } + }) + } else if let core = core, let file = item.file(with: core), let url = file.url { + let file = INFile(fileURL: url, filename: item.name, typeIdentifier: nil) + completion(GetFileIntentResponse.success(file: file)) + } + } else if core != nil { + completion(GetFileIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(GetFileIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolvePath(for intent: GetFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + 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) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } +} + +@available(iOS 13.0, *) +extension GetFileIntentResponse { + + public static func success(file: INFile) -> GetFileIntentResponse { + let intentResponse = GetFileIntentResponse(code: .success, userActivity: nil) + intentResponse.file = file + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/PathExistsIntentHandler.swift b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift new file mode 100644 index 000000000..f5f814614 --- /dev/null +++ b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift @@ -0,0 +1,83 @@ +// +// PathExistsIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { + + public func handle(intent: PathExistsIntent, completion: @escaping (PathExistsIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(PathExistsIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path, let uuid = intent.account?.uuid else { + completion(PathExistsIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(PathExistsIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, item != nil { + completion(PathExistsIntentResponse.success(pathExists: true)) + } else if core != nil { + completion(PathExistsIntentResponse.success(pathExists: false)) + } else { + completion(PathExistsIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + public func resolveAccount(for intent: PathExistsIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + 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) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } +} + +@available(iOS 13.0, *) +extension PathExistsIntentResponse { + + public static func success(pathExists: Bool) -> PathExistsIntentResponse { + let intentResponse = PathExistsIntentResponse(code: .success, userActivity: nil) + intentResponse.pathExists = NSNumber(value: pathExists) + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/SaveFileIntentHandler.swift b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift new file mode 100644 index 000000000..b20a1ac01 --- /dev/null +++ b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift @@ -0,0 +1,134 @@ +// +// SaveFileIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 Intents +import ownCloudSDK + +@available(iOS 13.0, *) +public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { + + public func handle(intent: SaveFileIntent, completion: @escaping (SaveFileIntentResponse) -> Void) { + + guard !AppLockHelper().isPassCodeEnabled else { + completion(SaveFileIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let path = intent.path?.pathRepresentation, let uuid = intent.account?.uuid, let file = intent.file, let fileURL = file.fileURL else { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(SaveFileIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + var newFilename = file.filename + if let filename = intent.filename as NSString?, filename.length > 0, let defaultFilename = file.filename as NSString? { + var pathExtention = defaultFilename.pathExtension + if let fileExtension = intent.fileextension, fileExtension.count > 0 { + pathExtention = fileExtension + } + if let changedFilename = filename.appendingPathExtension(pathExtention) { + newFilename = changedFilename + } + } else if let fileExtension = intent.fileextension, fileExtension.count > 0, let defaultFilename = file.filename as NSString? { + let filename = defaultFilename.deletingPathExtension as NSString + if let changedFilename = filename.appendingPathExtension(fileExtension) { + newFilename = changedFilename + } + } + + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let targetItem = item { + if core?.importFileNamed(newFilename, + at: targetItem, + from: fileURL, + isSecurityScoped: true, + options: [OCCoreOption.importByCopying : true], + placeholderCompletionHandler: { (error, item) in + if error != nil { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } + }, + resultHandler: { (error, _ core, _ item, _) in + if error != nil { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } else { + completion(SaveFileIntentResponse.success(filePath: item?.path ?? "")) + } + } + ) == nil { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } + } else if core != nil { + completion(SaveFileIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(SaveFileIntentResponse(code: .failure, userActivity: nil)) + } + } + } + + 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) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func resolvePath(for intent: SaveFileIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let path = intent.path { + completion(INStringResolutionResult.success(with: path)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + public func resolveFile(for intent: SaveFileIntent, with completion: @escaping (INFileResolutionResult) -> Void) { + if let file = intent.file { + completion(INFileResolutionResult.success(with: file)) + } else { + completion(INFileResolutionResult.needsValue()) + } + } + + 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) { + completion(INStringResolutionResult.success(with: intent.fileextension ?? "")) + } +} + +@available(iOS 13.0, *) +extension SaveFileIntentResponse { + + public static func success(filePath: String) -> SaveFileIntentResponse { + let intentResponse = SaveFileIntentResponse(code: .success, userActivity: nil) + intentResponse.filePath = filePath + return intentResponse + } +} diff --git a/ownCloudAppShared/Intent/en.lproj/Intents.strings b/ownCloudAppShared/Intent/en.lproj/Intents.strings new file mode 100644 index 000000000..00af48a78 --- /dev/null +++ b/ownCloudAppShared/Intent/en.lproj/Intents.strings @@ -0,0 +1,92 @@ +"0jWfGg" = "Check if path ${path} exists in ${account}"; + +"1KMplM" = "The given path does not exists"; + +"1UkR2J" = "Save File"; + +"2IChwb" = "Get a directory listing for the given account and path"; + +"30uTS2" = "Delete Path Item"; + +"61YuKy" = "The account with the given UUID does not exists"; + +"6JcpPy" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"8ZZD23" = "Get Account"; + +"8Ziepl" = "Get File"; + +"ABZOUR" = "The account with the given UUID does not exists"; + +"AQ4lsZ" = "Create Folder"; + +"AtUMKl" = "The account with the given UUID does not exists"; + +"C4qdxp" = "The account with the given UUID does not exists"; + +"Iq98sA" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"JPFL0e" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"LdHBeL" = "Retrieve file infos for the given path"; + +"MGAYW2" = "Get File Info"; + +"NEiHz6" = "The given path does not exists"; + +"OBFBYf" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"PHyaws" = "Get Directory Listing"; + +"PthGCb" = "Check if the given path exists"; + +"QIKvUr" = "The account with the given UUID does not exists"; + +"U3l2bk" = "Get the account for the given account UUID"; + +"UdyMQ5" = "The account with the given UUID does not exists"; + +"Vg8Ky0" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"XKCjDe" = "Get Accounts"; + +"XmX9YS" = "Create a folder with the given name into the path."; + +"ZWoKzx" = "The given path does not exists"; + +"dCe7NY" = "Get Account with ${accountUUID}"; + +"dWm6eD" = "The account with the given UUID does not exists"; + +"ds6RJ5" = "Path Exists"; + +"dtUvRf" = "Save a file into the path in account"; + +"gYCA4r" = "Delete Item at path ${path} in ${account}"; + +"gxBJkl" = "Here is your account list"; + +"hLfV0h" = "The account with the given UUID does not exists"; + +"iDEwxe" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"jkLupt" = "Delete the given path in account"; + +"l1ScV6" = "The given path does not exists"; + +"leB73e" = "The given path does not exists"; + +"lrF7G0" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"nvFl2W" = "Get file for the given path in account"; + +"sLw6L8" = "The given path does not exists"; + +"tMYYpx" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"wK4bqS" = "Get a list of all configured accounts"; + +"yeD8D7" = "App is currently locked by pass code. Please disable pass code to proceed."; + +"yrLv2M" = "Here is your account list"; + diff --git a/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift b/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift new file mode 100644 index 000000000..a1a5eb124 --- /dev/null +++ b/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift @@ -0,0 +1,339 @@ +// +// OCItem+Extension.swift +// ownCloud +// +// Created by Felix Schwarz on 13.04.18. +// Copyright © 2018 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2018, 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 + +extension OCItem { + static private let iconNamesByMIMEType : [String:String] = { + var mimeTypeToIconMap : [String:String] = [ + // List taken from https://github.com/owncloud/core/blob/master/core/js/mimetypelist.js + "application/coreldraw": "image", + "application/epub+zip": "text", + "application/font-sfnt": "image", + "application/font-woff": "image", + "application/illustrator": "image", + "application/javascript": "text/code", + "application/json": "text/code", + "application/msaccess": "file", + "application/msexcel": "x-office/spreadsheet", + "application/msonenote": "x-office/document", + "application/mspowerpoint": "x-office/presentation", + "application/msword": "x-office/document", + "application/octet-stream": "file", + "application/postscript": "image", + "application/rss+xml": "application/xml", + "application/vnd.android.package-archive": "package/x-generic", + "application/vnd.lotus-wordpro": "x-office/document", + "application/vnd.ms-excel": "x-office/spreadsheet", + "application/vnd.ms-excel.addin.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-excel.sheet.binary.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-excel.sheet.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-excel.template.macroEnabled.12": "x-office/spreadsheet", + "application/vnd.ms-fontobject": "image", + "application/vnd.ms-powerpoint": "x-office/presentation", + "application/vnd.ms-powerpoint.addin.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-powerpoint.presentation.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-powerpoint.slideshow.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-powerpoint.template.macroEnabled.12": "x-office/presentation", + "application/vnd.ms-word.document.macroEnabled.12": "x-office/document", + "application/vnd.ms-word.template.macroEnabled.12": "x-office/document", + "application/vnd.oasis.opendocument.presentation": "x-office/presentation", + "application/vnd.oasis.opendocument.presentation-template": "x-office/presentation", + "application/vnd.oasis.opendocument.spreadsheet": "x-office/spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template": "x-office/spreadsheet", + "application/vnd.oasis.opendocument.text": "x-office/document", + "application/vnd.oasis.opendocument.text-master": "x-office/document", + "application/vnd.oasis.opendocument.text-template": "x-office/document", + "application/vnd.oasis.opendocument.text-web": "x-office/document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": "x-office/presentation", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow": "x-office/presentation", + "application/vnd.openxmlformats-officedocument.presentationml.template": "x-office/presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "x-office/spreadsheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template": "x-office/spreadsheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "x-office/document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template": "x-office/document", + "application/vnd.visio": "x-office/document", + "application/vnd.wordperfect": "x-office/document", + "application/x-7z-compressed": "package/x-generic", + "application/x-bzip2": "package/x-generic", + "application/x-cbr": "text", + "application/x-compressed": "package/x-generic", + "application/x-dcraw": "image", + "application/x-deb": "package/x-generic", + "application/x-font": "image", + "application/x-gimp": "image", + "application/x-gzip": "package/x-generic", + "application/x-perl": "text/code", + "application/x-photoshop": "image", + "application/x-php": "text/code", + "application/x-rar-compressed": "package/x-generic", + "application/x-tar": "package/x-generic", + "application/x-tex": "text", + "application/xml": "text/html", + "application/yaml": "text/code", + "application/zip": "package/x-generic", + "database": "file", + "httpd/unix-directory": "dir", + "message/rfc822": "text", + "text/css": "text/code", + "text/csv": "x-office/spreadsheet", + "text/html": "text/code", + "text/x-c": "text/code", + "text/x-c++src": "text/code", + "text/x-h": "text/code", + "text/x-java-source": "text/code", + "text/x-python": "text/code", + "text/x-shellscript": "text/code", + "web": "text/code" + ] + + mimeTypeToIconMap.keys.forEach { + let mimeTypeKey = $0 + var mimeType : String? = mimeTypeToIconMap[mimeTypeKey] + var referenceMIMEType : String? = mimeType + + while referenceMIMEType != nil { + referenceMIMEType = mimeTypeToIconMap[referenceMIMEType!] + + if let validMIMEType = referenceMIMEType { + mimeType = validMIMEType + } + } + + mimeTypeToIconMap[mimeTypeKey] = mimeType?.replacingOccurrences(of: "/", with: "-") + } + + return mimeTypeToIconMap + }() + + static private let validIconNames : [String] = [ + // List taken from https://github.com/owncloud/core/blob/master/core/js/mimetypelist.js + "application", + "application-pdf", + "audio", + "file", + "folder", + "folder-create", + "folder-drag-accept", + "folder-external", + "folder-public", + "folder-shared", + "folder-starred", + "image", + "package-x-generic", + "text", + "text-calendar", + "text-code", + "text-vcard", + "video", + "x-office-document", + "x-office-presentation", + "x-office-spreadsheet", + "icon-search" + ] + + static func iconName(for MIMEType: String?) -> String? { + var iconName : String? + + if let mimeType = MIMEType { + iconName = self.iconNamesByMIMEType[mimeType] + + if iconName != nil { + if !(self.validIconNames.contains(iconName!)) { + iconName = nil + } + } + + if iconName == nil { + let flatMIMEType = mimeType.replacingOccurrences(of: "/", with: "-") + + if self.validIconNames.contains(flatMIMEType) { + iconName = flatMIMEType + } else { + if let mimeCategory = mimeType.components(separatedBy: "/").first { + if mimeCategory != "application" { + if self.validIconNames.contains(mimeCategory) { + iconName = mimeCategory + } + } + } + } + } + } + + return iconName + } + + var iconName : String? { + var iconName = OCItem.iconName(for: self.mimeType) + + if iconName == nil { + if self.type == .collection { + iconName = "folder" + } else { + iconName = "file" + } + } + + return iconName + } + + var fileExtension : String? { + return (self.name as NSString?)?.pathExtension + } + + var baseName : String? { + return (self.name as NSString?)?.deletingPathExtension + } + + var sizeLocalized: String { + return OCItem.byteCounterFormatter.string(fromByteCount: Int64(self.size)) + } + + var lastModifiedLocalized: String { + guard let lastModified = self.lastModified else { return "" } + + return OCItem.dateFormatter.string(from: lastModified) + } + + static private let byteCounterFormatter: ByteCountFormatter = { + let byteCounterFormatter = ByteCountFormatter() + byteCounterFormatter.allowsNonnumericFormatting = false + return byteCounterFormatter + }() + + static private let dateFormatter: DateFormatter = { + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .medium + dateFormatter.locale = Locale.current + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + var sharedByPublicLink : Bool { + if self.shareTypesMask.contains(.link) { + return true + } + return false + } + + var isShared : Bool { + if self.shareTypesMask.isEmpty { + return false + } + return true + } + + var sharedByUserOrGroup : Bool { + if self.shareTypesMask.contains(.userShare) || self.shareTypesMask.contains(.groupShare) || self.shareTypesMask.contains(.remote) { + return true + } + return false + } + + func shareRootItem(from core: OCCore) -> OCItem? { + var shareRootItem : OCItem? + + if self.isSharedWithUser { + var parentItem : OCItem? = self + + shareRootItem = self + + repeat { + parentItem = parentItem?.parentItem(from: core) + + if parentItem != nil, parentItem?.isSharedWithUser == true { + shareRootItem = parentItem + } + } while ((parentItem != nil) && (parentItem?.isSharedWithUser == true)) + } + + return shareRootItem + } + + func isShareRootItem(from core: OCCore) -> Bool { + if let shareRootItem = shareRootItem(from: core) { + return shareRootItem.localID == localID + } + + return false + } + + func parentItem(from core: OCCore, completionHandler: ((_ error: Error?, _ parentItem: OCItem?) -> Void)? = nil) -> OCItem? { + var parentItem : OCItem? + + if let parentItemLocalID = self.parentLocalID { + var waitGroup : DispatchGroup? + + if completionHandler == nil { + waitGroup = DispatchGroup() + waitGroup?.enter() + } + + core.retrieveItemFromDatabase(forLocalID: parentItemLocalID) { (error, _, item) in + if parentItem == nil, let parentPath = self.path?.parentPath { + parentItem = try? core.cachedItem(atPath: parentPath) + } + + if completionHandler == nil { + parentItem = item + waitGroup?.leave() + } else { + completionHandler?(error, item) + } + } + + waitGroup?.wait() + } + + return parentItem + } + + func displaysDifferent(than item: OCItem?, in core: OCCore? = nil) -> Bool { + guard let item = item else { + return true + } + + return ( + // Different item + (item.localID != localID) || + + // File contents (and therefore likely metadata) differs + (item.itemVersionIdentifier != itemVersionIdentifier) || + + // File name differs + (item.name != name) || + + // Upload/Download status differs + (item.syncActivity != syncActivity) || + + // Cloud status differs + (item.cloudStatus != cloudStatus) || + + // Available offline status differs + (item.downloadTriggerIdentifier != downloadTriggerIdentifier) || + (core?.availableOfflinePolicyCoverage(of: item) != core?.availableOfflinePolicyCoverage(of: self)) || + + // Sharing attributes differ + (item.shareTypesMask != shareTypesMask) || + (item.permissions != permissions) // these contain sharing info, too + ) + } +} diff --git a/ownCloudAppShared/SDK Extensions/OCItemTracker.swift b/ownCloudAppShared/SDK Extensions/OCItemTracker.swift new file mode 100644 index 000000000..e2bb796af --- /dev/null +++ b/ownCloudAppShared/SDK Extensions/OCItemTracker.swift @@ -0,0 +1,41 @@ +// +// OCItemTracker.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 26.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class OCItemTracker: NSObject { + + var itemTracking : OCCoreItemTracking? + + public func item(for bookmark: OCBookmark, at path: String, completionHandler: @escaping (_ error: Error?, _ core: OCCore?, _ item: OCItem?) -> Void) { + + OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { (core, error) in + if error == nil, let core = core { + self.itemTracking = core.trackItem(atPath: path, trackingHandler: { (error, item, isInitial) in + if isInitial { + self.itemTracking = nil + } + completionHandler(error, core, item) + }) + } else { + completionHandler(error, nil, nil) + } + }) + } +} diff --git a/ownCloudAppShared/Tools/AppLockHelper.swift b/ownCloudAppShared/Tools/AppLockHelper.swift new file mode 100644 index 000000000..b1ecd0c07 --- /dev/null +++ b/ownCloudAppShared/Tools/AppLockHelper.swift @@ -0,0 +1,35 @@ +// +// AppLockHelper.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 29.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class AppLockHelper: NSObject { + + var isPassCodeEnabled : Bool { + get { + let defaults = OCAppIdentity.shared.userDefaults + if let applockEnabled = defaults?.bool(forKey: "applock-lock-enabled") { + return applockEnabled + } + + return false + } + } + +} diff --git a/ownCloudAppShared/UIKit Extension/Calendar+Extension.swift b/ownCloudAppShared/UIKit Extension/Calendar+Extension.swift new file mode 100644 index 000000000..b96ac89eb --- /dev/null +++ b/ownCloudAppShared/UIKit Extension/Calendar+Extension.swift @@ -0,0 +1,26 @@ +// +// Calendar+Extension.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 27.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +extension Calendar { + func dateTimeComponents(from date: Date) -> DateComponents { + let components : Set = [.day, .month, .year, .hour, .minute, .second] + return self.dateComponents(components, from: date) + } +} diff --git a/ownCloud/UIKit Extensions/String+Extension.swift b/ownCloudAppShared/UIKit Extension/String+Extension.swift similarity index 81% rename from ownCloud/UIKit Extensions/String+Extension.swift rename to ownCloudAppShared/UIKit Extension/String+Extension.swift index 1492056b6..9060f0acc 100644 --- a/ownCloud/UIKit Extensions/String+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/String+Extension.swift @@ -21,7 +21,7 @@ import UIKit extension String { - var localized: String { + public var localized: String { return NSLocalizedString(self, comment: "") } @@ -30,13 +30,20 @@ extension String { return !self.isEmpty && rangeOfCharacter(from: nonDigitsCharacterSet) == nil } - func matches(regExp: String) -> Bool { + var pathRepresentation : String { + if !self.hasSuffix("/") { + return String(format: "%@/", self) + } + return self + } + + public func matches(regExp: String) -> Bool { guard let regex = try? NSRegularExpression(pattern: regExp) else { return false } let range = NSRange(location: 0, length: self.count) return regex.firstMatch(in: self, options: [], range: range) != nil } - func matches(for regex: String) -> [String] { + public func matches(for regex: String) -> [String] { do { let regex = try NSRegularExpression(pattern: regex) let results = regex.matches(in: self, @@ -49,14 +56,14 @@ extension String { } } - func replacingOccurrences(for regex: String) -> String { + public func replacingOccurrences(for regex: String) -> String { if let regex = try? NSRegularExpression(pattern: regex, options: .caseInsensitive) { return regex.stringByReplacingMatches(in: self, options: [], range: NSRange(location: 0, length: self.count), withTemplate: "") } return self } - func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { + public func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) diff --git a/ownCloudAppShared/ownCloudAppShared.h b/ownCloudAppShared/ownCloudAppShared.h new file mode 100644 index 000000000..227543450 --- /dev/null +++ b/ownCloudAppShared/ownCloudAppShared.h @@ -0,0 +1,29 @@ +// +// ownCloudAppShared.h +// ownCloudAppShared +// +// Created by Matthias Hühne on 29.07.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +//! Project version number for ownCloudAppShared. +FOUNDATION_EXPORT double ownCloudAppSharedVersionNumber; + +//! Project version string for ownCloudAppShared. +FOUNDATION_EXPORT const unsigned char ownCloudAppSharedVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + From e18fd9eaec1e7389f3274cb6615869c4e4a2743c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Thu, 12 Dec 2019 10:35:45 +0100 Subject: [PATCH 4/9] added error responses to Shortcut intents, if Shortcuts is disabled or subscription for feature is not valid --- .../Base.lproj/Intents.intentdefinition | 222 +++++++++++++++++- .../Intent/CreateFolderIntentHandler.swift | 7 + .../Intent/DeletePathItemIntentHandler.swift | 7 + .../Intent/GetAccountIntentHandler.swift | 7 + .../Intent/GetAccountsIntentHandler.swift | 7 + .../GetDirectoryListingIntentHandler.swift | 7 + .../Intent/GetFileInfoIntentHandler.swift | 7 + .../Intent/GetFileIntentHandler.swift | 7 + .../Intent/PathExistsIntentHandler.swift | 7 + .../Intent/SaveFileIntentHandler.swift | 7 + 10 files changed, 282 insertions(+), 3 deletions(-) diff --git a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition index 1e2d01032..a7c27a30a 100644 --- a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition +++ b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition @@ -126,11 +126,11 @@ INIntentDefinitionNamespace K5U8aR INIntentDefinitionSystemVersion - 18G95 + 19B88 INIntentDefinitionToolsBuildVersion - 11A420a + 11B53 INIntentDefinitionToolsVersion - 11.0 + 11.2.1 INIntents @@ -200,6 +200,30 @@ INIntentResponseCodeName authenticationRequired + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 74Rz1p + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + xzMJ1y + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + y8oko2 + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + VSQYEZ + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 6 @@ -571,6 +595,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 6JHJJP + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + pLbUOb + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + fjiVU2 + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 0pF5fq + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 2 @@ -792,6 +840,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + SW8AiH + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + cfhOuF + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + rzbAOP + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 8a8rsQ + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 2 @@ -1126,6 +1198,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + WfA15u + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + OmLLGX + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + 81IjYk + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + yTzyi1 + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 1 @@ -1389,6 +1485,30 @@ INIntentResponseCodeName folderExistsFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + je9RXz + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + 4WGDPz + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + X0Zskf + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + bfU2dy + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 1 @@ -1606,6 +1726,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 4JjSYi + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + znA04X + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + idUnAy + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + lNNcvh + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 3 @@ -1829,6 +1973,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + tiPumg + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + sdm6CF + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + 65dv1W + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 9tIXKj + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 2 @@ -2071,6 +2239,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 5bfpVi + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + 6oQ3E3 + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + sKUo1c + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + 1rpPPR + INIntentResponseCodeName + unlicensed + INIntentTitle @@ -2202,6 +2394,30 @@ INIntentResponseCodeName accountFailure + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + 7bVLsg + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + 4U4XOu + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeConciseFormatStringID + ceG0fs + INIntentResponseCodeFormatString + Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + INIntentResponseCodeFormatStringID + GJ25u2 + INIntentResponseCodeName + unlicensed + INIntentResponseLastParameterTag 2 diff --git a/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift index a0a6bbb2b..c0c952bfe 100644 --- a/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift +++ b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift @@ -25,6 +25,13 @@ public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { public func handle(intent: CreateFolderIntent, completion: @escaping (CreateFolderIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(CreateFolderIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift index 169a2904e..a40c8c9db 100644 --- a/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift +++ b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift @@ -25,6 +25,13 @@ public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling public func handle(intent: DeletePathItemIntent, completion: @escaping (DeletePathItemIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(DeletePathItemIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/GetAccountIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift index 64fcf8fb3..fb39d5518 100644 --- a/ownCloudAppShared/Intent/GetAccountIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift @@ -25,6 +25,13 @@ public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { public func handle(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(GetAccountIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift index fd8949f70..b0ecdde50 100644 --- a/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift @@ -25,6 +25,13 @@ public class GetAccountsIntentHandler: NSObject, GetAccountsIntentHandling { public func handle(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(GetAccountsIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift index 02268003a..dee1e2731 100644 --- a/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift @@ -60,6 +60,13 @@ public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingInte public func handle(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(GetDirectoryListingIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift index fa977d110..8137808af 100644 --- a/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift @@ -25,6 +25,13 @@ public class GetFileInfoIntentHandler: NSObject, GetFileInfoIntentHandling { public func handle(intent: GetFileInfoIntent, completion: @escaping (GetFileInfoIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(GetFileInfoIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/GetFileIntentHandler.swift b/ownCloudAppShared/Intent/GetFileIntentHandler.swift index 0a55cf295..cc9bd814a 100644 --- a/ownCloudAppShared/Intent/GetFileIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetFileIntentHandler.swift @@ -28,6 +28,13 @@ public class GetFileIntentHandler: NSObject, GetFileIntentHandling { public func handle(intent: GetFileIntent, completion: @escaping (GetFileIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(GetFileIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/PathExistsIntentHandler.swift b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift index f5f814614..1742d22ef 100644 --- a/ownCloudAppShared/Intent/PathExistsIntentHandler.swift +++ b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift @@ -25,6 +25,13 @@ public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { public func handle(intent: PathExistsIntent, completion: @escaping (PathExistsIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(PathExistsIntentResponse(code: .authenticationRequired, userActivity: nil)) return diff --git a/ownCloudAppShared/Intent/SaveFileIntentHandler.swift b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift index b20a1ac01..ddc6da7af 100644 --- a/ownCloudAppShared/Intent/SaveFileIntentHandler.swift +++ b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift @@ -25,6 +25,13 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { public func handle(intent: SaveFileIntent, completion: @escaping (SaveFileIntentResponse) -> Void) { + // Todo: + // if Shortcuts not enabled + //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + + // if enabled, but not a valid license + //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard !AppLockHelper().isPassCodeEnabled else { completion(SaveFileIntentResponse(code: .authenticationRequired, userActivity: nil)) return From 3b43c007be408b39ba8f583bdf07df7756de225a Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 20 Dec 2019 16:00:58 +0100 Subject: [PATCH 5/9] [milestone/1.2] Milestone 1.2 (#549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - Fix Swift and SwiftLint warnings - Remove unused UploadsSettingsSection (was replaced by MediaUploadSettings) * - address libzip Xcode project upgrade warning - add research note to FileProviderExtension - update SDK * [feature/multiple-windows] Multiple window support for iPadOS (#498) * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * show always the account selection sheet and added a note, that only one file can be imported at once * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * first draft for supporting multiple windows * implemented opening an account in a new window * added row action * - open account, now opens file list - implemented open account from table view edit action - implemented contextual menu for account row * Starting implementing state restoration * Implemented UI restoration state for window scenes * Implemented UI state restoration for opening a OCItem * - only show close window item on iPad - fixed icons for location * - only show "Open in new Window" on iPad - better view restoration - deselect row * - added new menu item to open a new window - fixed tint color for icons * - added missing localization strings - removed no longer needed class * - fixed code review findings - code refactoring * - moved creating file list stack code into ClientRootViewController class - added iOS 13 available query * fixed merge error * fixed drag and drop between accounts (when dragging items from one window to the other window on iPad) * prevent dragging folders from one account to an other account * [feature/docs] Docs support (#494) * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * show always the account selection sheet and added a note, that only one file can be imported at once * - Update ios-sdk * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * [feature/bundle-import] Add support for certain bundle formats to the share sheet (#471) * - Add support for import of bundle-based document formats through transparent zipping - moved previous FileProvider functionality into ownCloudApp.framework - new method OCCore.importItemNamed() ensures correct handling of bundle-based document formats - fixes finding (4) in #445 - Changed LSSupportsOpeningDocumentsInPlace to NO address two issues: - the app is now listed as "Copy to ownCloud" (previously "Open in ownCloud", which was technically not correct) - previously, documents using "Open to ownCloud" would no longer open in f.ex. Pages, but in the ownCloud app when selected in the Files app * - Refactored ImportFilesController around using NSFileCoordinator, fixing finding (1) in #471 * changed path for moved Info.plist for target ownCloudAppFramework * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Version Bump to 133 * [feature/sort direction] Allow sorting asc/desc for all sort methods (#474) * #470 added change sort direction asc/desc for all sort methods * - removed comments - changed translation keys * fixes sort comparator for sort method "shared" * use correct images for sort direction asc and desc * updated choosing sort method from a tableView/Popover instead of a UIAlertViewController * set maximum width for popover and set arrow direction * Added show sort direction in UISegmentControl * - make sure arrows of popovers appear in same color as the popover contents - fix issue where direction was toggled during setup of SortBar (setting of initial value from user defaults), so logging in, logging out and logging in would change the sort direction - reset sort direction to ascendant when switching sort methods - SortMethodTableViewController - remove extraneous instance lets - enforce row height used for table view height calculation - change table view height calculation to account for the last divider (which could look a bit off if present) * tap on segment control was fired twice, because there was a missing check, if selected segment changed * fixed changing width on selecting an other segment * fixed jumping width of sortSegmentControl items by setting the same width for all items by calculating the longest width before * [fix/index-bar] Make Index Bar available in general file list (#469) * #413 show index bar in general file list - Index bar is only visible, if sorting is "Sort by name" - Index bar is only visible, if more than one different letters are available - Index bar is only visible, if more items are available as visible * - adopt to new sort methods - reverse index list, if sort direction is descendant * Version Bump to 134 * Version Bump to 135 * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * Version Bump to 136 * Change behaviour of default expiration date (#476) * Enabling expiration date switch if default expiration date is configured Checking files_sharing.public.expire_date.expire_date.days server capability. If it is set then it is used as default date and the switch is enabled even if the expiration date is not enforced. * Fixed QA issue (1) * Fixed QA finding (2): limiting the date range if expiration date is enforced * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * Permission increasing UX in reshares (#467) * Not showing permissions which can’t be elevated Affecting items shared with the current users which are being reshared * Fixed an issue with wrong permissions displayed when creating public link - Checking if permissions can be elevated for the public link - Only viable options are displayed * Fixed a trailing whitespace warning * Fixed another compiler warning * Inhereting permissions from the share when creating public link * Fixed QA finding (4) * Fixed QA finding (1) * Fixed QA finding (2) * Version Bump to 137 * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * [feature/itempolicy] Item Policy / Available Offline support (#456) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Version Bump to 138 * [feature/reorder buttons] Reorder Navigation Bar Actions (#478) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * #477 reorganize navigation bar with create and more button and moved select button to sort menu * added safeAreaLayoutGuide.rightAnchor * - Update SDK * changed "Select" image * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * addresses: - missing multiselection UI in toolbar - missing select all / deselect all button in navigation bar * set if selectButton should be shown * disable temporary some tests due to bitrise failures * - Added new document scanner action - supports saving as PDF, JPEG or PNG - supports saving multiple pages into one PDF file, or one PDF file per page - supports several scans per file - uses CoreGraphics to save JPEG-compressed images, saving storage (2.7 MB vs 31 MB for a two page document) and encoding/upload time compared to using Apple's PDFKit - Extended StaticTableViewRow with better support for custom views and accessories * Version Bump to 139 * Feature/task scheduling (#484) * #386 Added relevant classes * First draft of the scheduled task manager * Just changed some comments * Implemented background fetch * Implemented background update fetch * Clean up of the code * Fixed merge problems from previous commit And eliminated some warnings * Small changes in background fetch operation * Some refactoring done * Added some user defaults for media upload * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * UI for bookmark / upload path selection * Added dummy implementation of media upload task * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * Refactored current implementation of media upload * Added method to fetch photos from photo library * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * First draft of instant photo upload implementation * Calling task completion handler after upload was finished * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * Fixed QA finding (1) * Using PHAsset.creationDate to check which media files shall be instantly uploaded * Fixed naming of class / group * Moved Tasks folder * Fixed some review findings * - Fix issue where a busy bookmark alert is followed by duplicating the bookmark * Fixed some review findings * Improved background update fetching * Fixed some issues in media upload * Fixed a type in settings key * Improvements in the instant media upload task * Added missing returnCore in the selection of the instant upload path * Fixed QA finding (3) * Fixes for the correct core returning / requesting * Small cosmetic code changes * Fixed video and serialized photo / video uploads * Moved upload() method to UploadBaseAction class * Some refactoring in InstantMediaUploadTaskExtension * Improvements for bookmark / path selection for instant upload * Remove instant upload configuration if bookmark is not available * minor refactoring * Added possibility to cancel account selection * Small fix: reset the path if new account is selected * Using item tracking instead of OCQuery - Disabling instant photo upload if target directory is removed - Item tracking is potentially faster since it can use cached item information and is not always issuing PROPFIND * Made some ivars private * Detect in settings that instant upload path is gone In this case instant upload is disabled * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Showing warning that instant upload was disabled * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * Added alert shown when in settings when instant upload is disabled * Added fetchUpdates() within which target folder tracking is started * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Update to the latest changes in the SDK Those changes reduce memory consumption used for hash calculation * Change required to accomodate latest SDK changes * Less parallel photo uploads * - update SDK to address stuck sync action issues - no longer auto-open the last selected bookmark when launched by iOS in the background. Instead, the last selected bookmark is now auto-openend only when the app comes to the foreground. - update FileProviderInterfaceManager to better support the case where the app ships without File Provider. * Fixed compilation warning * - Add ATS exemption for File Provider * - Update SDK * - Update SDK + add colored debugging option to scheme * Addressed code review comments * Removed unneeded weak self * Removed another unwanted weak self * - Fix OCEvent-dropping issue via SDK update with fix - Make UploadMediaAction use OCBackgroundTask to protect while exporting * - Added missing OCBackgroundTask.start call in UploadMediaAction - Update SDK * Fixed missing copyright information and some warnings * Version Bump to 140 * Media upload pending flag added - Displaying a warning alert on the next launch, in case media upload couldn’t be completed due e.g. app being terminated in a backround or even due to an app crash. - Fixed an issue with DispatchGroup.notify() called prematurely in MediaUploadQueue.uploadAssets() * - sortbar was sometimes hidden - fixing layout issue * fixed location identifier for actions * Added more logs in PHAsset+Upload extension * Version Bump to 141 * Version Bump to 142 * - refactor ScanImageView into FixedHeightImageView - adopt SF Symbols for ScanAction icon - replace unused, commented out commented code with a longer comment in a different place, explaining why Quartz is used instead of PDFKit for PDF export - various smaller cleanups and syntax improvements * Version Bump to 143 * Version Bump to 144 * Version Bump to 145 * - Merge fixes * - Remove change of beta alert from branch * - address finding (1): if the file name is removed and empty, disable the "Save" button in the Document Scanner * [fix/2ndLevelSort] Sort alphabetically if sort attribute is equal (#546) * - Update SDK to disable background NSURLSession usage in the app * - Fix alphabetically random order when sorting by anything but alphabetically: now items that share the same sort attribute value are subsequently also sorted alphabetically * - Update SDK * [feature/keyboard-commands] Keyboard Commands for all actions on iPad (#495) * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * show always the account selection sheet and added a note, that only one file can be imported at once * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * [feature/bundle-import] Add support for certain bundle formats to the share sheet (#471) * - Add support for import of bundle-based document formats through transparent zipping - moved previous FileProvider functionality into ownCloudApp.framework - new method OCCore.importItemNamed() ensures correct handling of bundle-based document formats - fixes finding (4) in #445 - Changed LSSupportsOpeningDocumentsInPlace to NO address two issues: - the app is now listed as "Copy to ownCloud" (previously "Open in ownCloud", which was technically not correct) - previously, documents using "Open to ownCloud" would no longer open in f.ex. Pages, but in the ownCloud app when selected in the Files app * - Refactored ImportFilesController around using NSFileCoordinator, fixing finding (1) in #471 * changed path for moved Info.plist for target ownCloudAppFramework * Version Bump to 133 * [feature/sort direction] Allow sorting asc/desc for all sort methods (#474) * #470 added change sort direction asc/desc for all sort methods * - removed comments - changed translation keys * fixes sort comparator for sort method "shared" * use correct images for sort direction asc and desc * updated choosing sort method from a tableView/Popover instead of a UIAlertViewController * set maximum width for popover and set arrow direction * Added show sort direction in UISegmentControl * - make sure arrows of popovers appear in same color as the popover contents - fix issue where direction was toggled during setup of SortBar (setting of initial value from user defaults), so logging in, logging out and logging in would change the sort direction - reset sort direction to ascendant when switching sort methods - SortMethodTableViewController - remove extraneous instance lets - enforce row height used for table view height calculation - change table view height calculation to account for the last divider (which could look a bit off if present) * tap on segment control was fired twice, because there was a missing check, if selected segment changed * fixed changing width on selecting an other segment * fixed jumping width of sortSegmentControl items by setting the same width for all items by calculating the longest width before * [fix/index-bar] Make Index Bar available in general file list (#469) * #413 show index bar in general file list - Index bar is only visible, if sorting is "Sort by name" - Index bar is only visible, if more than one different letters are available - Index bar is only visible, if more items are available as visible * - adopt to new sort methods - reverse index list, if sort direction is descendant * Version Bump to 134 * Version Bump to 135 * Version Bump to 136 * Change behaviour of default expiration date (#476) * Enabling expiration date switch if default expiration date is configured Checking files_sharing.public.expire_date.expire_date.days server capability. If it is set then it is used as default date and the switch is enabled even if the expiration date is not enforced. * Fixed QA issue (1) * Fixed QA finding (2): limiting the date range if expiration date is enforced * Implement UIKeyCommands to access all actions and navigate through table views via hardware key shortcuts * Permission increasing UX in reshares (#467) * Not showing permissions which can’t be elevated Affecting items shared with the current users which are being reshared * Fixed an issue with wrong permissions displayed when creating public link - Checking if permissions can be elevated for the public link - Only viable options are displayed * Fixed a trailing whitespace warning * Fixed another compiler warning * Inhereting permissions from the share when creating public link * Fixed QA finding (4) * Fixed QA finding (1) * Fixed QA finding (2) * fixed table selection in StaticTableView with key command * Version Bump to 137 * - moved KeyCommands extension into own file - added new commands * [feature/itempolicy] Item Policy / Available Offline support (#456) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Version Bump to 138 * [feature/reorder buttons] Reorder Navigation Bar Actions (#478) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * #477 reorganize navigation bar with create and more button and moved select button to sort menu * added safeAreaLayoutGuide.rightAnchor * - Update SDK * changed "Select" image * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * addresses: - missing multiselection UI in toolbar - missing select all / deselect all button in navigation bar * set if selectButton should be shown * disable temporary some tests due to bitrise failures * using correct SDK after merge * fixed changed selectButton variable name after merge * - added missing commands - added photo album, photo selection - fixed deselecting row * fixed folder actions commands * added sharing and links key commands * - added key commands for LibrarySharing - added localization * - added missing key commands - fixed keep selection, if FileList was reloaded - fixed localization - implemented copy and paste commands - implemented new selection commands (letters, top, bottom, page) * added commands for bookmark selection * Determine, if the internal pasteboard is the current item and use it * - implemented "Cut"-Command, "Favorite"-Command - implemented Next, Previous Command for image gallery and PDF viewer - fixed Sort-Type Command Key, because of system conflicts - changed command key for adding a new account * - refactored code to own actions - cleanup code - use extensions for key commands * - fixed code review findings - moving pasteboard actions copy, import and cut to separate PR * Fixed QA finding (6), unfavorite was not possible, favorite was wrong * - fixed QA finding (1), directory picker key commands fixed - fixed QA finding (4), create folder in root folder - fixed QA finding (5), available offline for selected item - fixed QA finding (6), make unavailable - fixed QA finding (8), dismiss manage account view * added keyboard commands for - open account in new window - scan document - discard window - open window * - fixed duplicated keyboard commands - fixed crash on selecting next item in list * Finding (7) fixed - added keyboard commands for IssuesViewController * fixed findings (3), (7), (9), (11) - Cancel Search - Commands for internal Alert Views - Change slider value - Crash in welcome view * added missing localization strings * fixed finding (13) - Cancel did not dismiss alert view - Only show alert view key commands * Finding: (14), (15) crash fixed * Fixed finding (17) and do not show tab bar controllers for modal views, added share command * - Remove document scanning feature (pending licensing feature) * - Allow offline browsing of folders in the File Provider (#554) * [feature/ios13_ql_preview] QuickLookPreview and QuickLookThumbnailing integration (#493) * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * show always the account selection sheet and added a note, that only one file can be imported at once * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * [feature/bundle-import] Add support for certain bundle formats to the share sheet (#471) * - Add support for import of bundle-based document formats through transparent zipping - moved previous FileProvider functionality into ownCloudApp.framework - new method OCCore.importItemNamed() ensures correct handling of bundle-based document formats - fixes finding (4) in #445 - Changed LSSupportsOpeningDocumentsInPlace to NO address two issues: - the app is now listed as "Copy to ownCloud" (previously "Open in ownCloud", which was technically not correct) - previously, documents using "Open to ownCloud" would no longer open in f.ex. Pages, but in the ownCloud app when selected in the Files app * - Refactored ImportFilesController around using NSFileCoordinator, fixing finding (1) in #471 * changed path for moved Info.plist for target ownCloudAppFramework * Version Bump to 133 * [feature/sort direction] Allow sorting asc/desc for all sort methods (#474) * #470 added change sort direction asc/desc for all sort methods * - removed comments - changed translation keys * fixes sort comparator for sort method "shared" * use correct images for sort direction asc and desc * updated choosing sort method from a tableView/Popover instead of a UIAlertViewController * set maximum width for popover and set arrow direction * Added show sort direction in UISegmentControl * - make sure arrows of popovers appear in same color as the popover contents - fix issue where direction was toggled during setup of SortBar (setting of initial value from user defaults), so logging in, logging out and logging in would change the sort direction - reset sort direction to ascendant when switching sort methods - SortMethodTableViewController - remove extraneous instance lets - enforce row height used for table view height calculation - change table view height calculation to account for the last divider (which could look a bit off if present) * tap on segment control was fired twice, because there was a missing check, if selected segment changed * fixed changing width on selecting an other segment * fixed jumping width of sortSegmentControl items by setting the same width for all items by calculating the longest width before * [fix/index-bar] Make Index Bar available in general file list (#469) * #413 show index bar in general file list - Index bar is only visible, if sorting is "Sort by name" - Index bar is only visible, if more than one different letters are available - Index bar is only visible, if more items are available as visible * - adopt to new sort methods - reverse index list, if sort direction is descendant * Version Bump to 134 * Version Bump to 135 * Version Bump to 136 * Change behaviour of default expiration date (#476) * Enabling expiration date switch if default expiration date is configured Checking files_sharing.public.expire_date.expire_date.days server capability. If it is set then it is used as default date and the switch is enabled even if the expiration date is not enforced. * Fixed QA issue (1) * Fixed QA finding (2): limiting the date range if expiration date is enforced * Permission increasing UX in reshares (#467) * Not showing permissions which can’t be elevated Affecting items shared with the current users which are being reshared * Fixed an issue with wrong permissions displayed when creating public link - Checking if permissions can be elevated for the public link - Only viable options are displayed * Fixed a trailing whitespace warning * Fixed another compiler warning * Inhereting permissions from the share when creating public link * Fixed QA finding (4) * Fixed QA finding (1) * Fixed QA finding (2) * Version Bump to 137 * First draft * [feature/itempolicy] Item Policy / Available Offline support (#456) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Version Bump to 138 * [feature/reorder buttons] Reorder Navigation Bar Actions (#478) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * #477 reorganize navigation bar with create and more button and moved select button to sort menu * added safeAreaLayoutGuide.rightAnchor * - Update SDK * changed "Select" image * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * addresses: - missing multiselection UI in toolbar - missing select all / deselect all button in navigation bar * set if selectButton should be shown * disable temporary some tests due to bitrise failures * Experimented with QLPreview for viewing different document types * Implemented QuickLookThumbnailing support for iOS 13 * Using QLPreviewController for images and office documents * Fixed double-tap to zoom and added single tap to hide bars * Version Bump to 139 * Feature/task scheduling (#484) * #386 Added relevant classes * First draft of the scheduled task manager * Just changed some comments * Implemented background fetch * Implemented background update fetch * Clean up of the code * Fixed merge problems from previous commit And eliminated some warnings * Small changes in background fetch operation * Some refactoring done * Added some user defaults for media upload * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * UI for bookmark / upload path selection * Added dummy implementation of media upload task * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * Refactored current implementation of media upload * Added method to fetch photos from photo library * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * First draft of instant photo upload implementation * Calling task completion handler after upload was finished * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * Fixed QA finding (1) * Using PHAsset.creationDate to check which media files shall be instantly uploaded * Fixed naming of class / group * Moved Tasks folder * Fixed some review findings * - Fix issue where a busy bookmark alert is followed by duplicating the bookmark * Fixed some review findings * Improved background update fetching * Fixed some issues in media upload * Fixed a type in settings key * Improvements in the instant media upload task * Added missing returnCore in the selection of the instant upload path * Fixed QA finding (3) * Fixes for the correct core returning / requesting * Small cosmetic code changes * Fixed video and serialized photo / video uploads * Moved upload() method to UploadBaseAction class * Some refactoring in InstantMediaUploadTaskExtension * Improvements for bookmark / path selection for instant upload * Remove instant upload configuration if bookmark is not available * minor refactoring * Added possibility to cancel account selection * Small fix: reset the path if new account is selected * Using item tracking instead of OCQuery - Disabling instant photo upload if target directory is removed - Item tracking is potentially faster since it can use cached item information and is not always issuing PROPFIND * Made some ivars private * Detect in settings that instant upload path is gone In this case instant upload is disabled * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Showing warning that instant upload was disabled * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * Added alert shown when in settings when instant upload is disabled * Added fetchUpdates() within which target folder tracking is started * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Update to the latest changes in the SDK Those changes reduce memory consumption used for hash calculation * Change required to accomodate latest SDK changes * Less parallel photo uploads * - update SDK to address stuck sync action issues - no longer auto-open the last selected bookmark when launched by iOS in the background. Instead, the last selected bookmark is now auto-openend only when the app comes to the foreground. - update FileProviderInterfaceManager to better support the case where the app ships without File Provider. * Fixed compilation warning * - Add ATS exemption for File Provider * - Update SDK * - Update SDK + add colored debugging option to scheme * Addressed code review comments * Removed unneeded weak self * Removed another unwanted weak self * - Fix OCEvent-dropping issue via SDK update with fix - Make UploadMediaAction use OCBackgroundTask to protect while exporting * - Added missing OCBackgroundTask.start call in UploadMediaAction - Update SDK * Fixed missing copyright information and some warnings * Version Bump to 140 * Media upload pending flag added - Displaying a warning alert on the next launch, in case media upload couldn’t be completed due e.g. app being terminated in a backround or even due to an app crash. - Fixed an issue with DispatchGroup.notify() called prematurely in MediaUploadQueue.uploadAssets() * Reordered the code for better readability * Adding QLThumbnailing generated thumbnails to the vault cache in iOS13 - QLThumbnailing is only used if requesting thumbnail from server doesn’t provide a result - Thumbnail generated by QLThumbnailing is added to the vault thumbnail cache * Code clean up * Using weak OCCore reference * Added alternative way of matching MIME types in the DisplayExtension - Simpler and less error-prone than regular expressions - Example implementation in MediaDisplayViewController * Fix for documents sometimes not appearing * Changed ivars / methods naming based on code review feedback * Using iOS file icons for files for which theme doesn’t provide them * Excluded some MIME type icons from the Theme on iOS13 * Implemented approach (3) as suggested by @felix-scwarz * Remove match files * Making sure all image formats are opened in QLPreviewController. Tried also with SVG and animated GIF * Fixed downloading unsupported mime types * Query updates not triggering view controller updates if it’s view is not visible * Only updating the ClientItemCell if actual item has changed * Thumbnail updates optimized * Some fixes for QLPreviewController * Fixed some thumbnail issues * Not using QLPreviewController for images * Only requesting content based thumbnais using QLThumbnailing * Commiting .gitignore and Gemfile.lock * Fixed some file types not being opened * Fixed issue (7) in PR #493 * Fixed some small issues - Opening GIFs with WKWebView based preview to avoid visual artefacts seen with QLPreviewController - Not trying to open files which are not supported by the preview subsystem * [fix/uti-conversion] work around broken MIMEType->UTI conversions in iOS (#558) * - FileProvider now allows a manual override of UTIs based on MIMEType and suffix - used to fix a problem where iOS 13 does not return usable UTIs for OpenDocument formats (#557) - initially provides overrides for all OpenDocument formats based on MIMEType (where implemented by the server) and suffix (where not implemented on the server) * - Addition to previous commit, with MIMEType/suffix-based conversion * - Clean up commented out suffix -> UTI table entries - Add comments explaining the reasoning behind leaving commented out conversions in the source code * Autoplay Media / Show album artwork in the media player view (#566) * [fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547) * - Fix Swift and SwiftLint warnings - Remove unused UploadsSettingsSection (was replaced by MediaUploadSettings) * - address libzip Xcode project upgrade warning - add research note to FileProviderExtension - update SDK * - Allow offline browsing of folders in the File Provider * Revert "[fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547)" (#553) This reverts commit 9a0bc93215b786e715b0a89cc9aa793fd121407b. * Autoplay media files implemented as described in issue #59 * Added album artwork as overlay in the player * Fixed playing next media item in BG and lock screen - Now multiple items can be played contignuously in the background - Now playing info in the lock screen contains artwork, title, artist info and displays correct playback timeline - Audio can be paused / resumed from the lock screen - Added skip controls allowing to jump 10s backwards and forwards from the play-head position in the lock screen * Small fixes * [fix/open-in-on-ipad] Share sheet not visible on iPad (#570) * [fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547) * - Fix Swift and SwiftLint warnings - Remove unused UploadsSettingsSection (was replaced by MediaUploadSettings) * - address libzip Xcode project upgrade warning - add research note to FileProviderExtension - update SDK * - Allow offline browsing of folders in the File Provider * Revert "[fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547)" (#553) This reverts commit 9a0bc93215b786e715b0a89cc9aa793fd121407b. * Fix for issue #568: Share sheet was now visible on iPad, if tableview was scrolled down, after first visible page rect * Changed app version to 1.2.0 * [fix/improve_photo_upload_responsiveness] More reliable photo upload (#548) * #516 truncate middle of file name instead of tail (#517) * new app version and build version * Started implementing more secure photo upload * Added a fix for background media playback - Added audio session activation / deactivation code - Also fixed potential issue for background video playback (PiP) as suggested by Apple in https://developer.apple.com/documentation/avfoundation/media_assets_playback_and_editing/creating_a_basic_video_player_ios_and_tvos/playing_audio_from_a_video_asset_in_the_background * Fixed naming of uploaded edited photos * Changes to the mechanism of storing pending PHAsset IDs * Pending media upload task added - When uploading photo / video assets, their local identifiers along with target path are stored in the KV-store of the bookmark - When the app is killed, at next launch, task is spawned which checks if there are any pending assets not yet imported by the core - Pending assets are uploaded * Updated with latest sdk version * Uploading multiple assets within OCCore.schedul(inCoreQueue:) block * Fixed some issues with uploading pending assets * Reworked media uploads - Performing most of the operations synchronously and trying to not spawn to many worker threads - Executing file imports withing OCCore.schedule() call to post-pone execution of sync operations and to reduce their amount * Small fixes * Refactored PendingMediaUploadTaskExtension class * Removed unccessary scheduling of tasks in the next run loop iteration - Removed OnMainThread{..} wrapping from scheduleTasks() - Made sure scheduleTasks() is called after properties affecting current context are set * Modal UI presented when media files are imported * Fixed some review findings * Addressed review findings * Ignoring fastlane sub-folder * Fixed some compiler warnings * Fixed the issue with overwriting asset downloads Now able to store multiple paths along with single asset in the key value store * Added OCActivitiy publishing to MediaUploadQueue * Refactored media upload related OCActivity into separate class * First working implementation of refactored MediaUploadQueue - Simplified the architecture - Separate KV based storadge of upload jobs into MediaUploadStorage class which takes care of handling MediaUploadJob objects - Improved inter-process concurrency handling * Removing stored upload jobs upon activity cancellation * MediaUploadQueue: - switch to "needsScheduling" mechanism to ensure that assets that get added to the queue are processed as timely as possible. Previously, if assets were added while the queue is being processed, no new run that would include the newly added assets would be triggered. - fix broken processing locking: creating an OCProcessSession via "OCProcessSession()" creates a session that is never valid, so the queue would get scheduled multiple times, which could lead to many duplicate uploads. Instead OCProcessManager.shared.processSession is now used, which returns a valid session of the current process. - make control over MediaUploadStorage.processing atomic to avoid race conditions - use copies of MediaUploadStorage.queue and MediaUploadStorage.jobs as starting point for scheduling, to avoid running into issues if MediaUploadStorage is cached by OCKeyValueStorage and a change to these is made during enumeration - added additional error logging in case of upload errors - make addUpload() trigger the newly introduced setNeedsScheduling() so that calls to scheduleUploads() become unnecessary when adding assets to the queue. MediaUploadStorage: - convert property-like methods into actual properties Localizable.strings: - fix two errors that led to the file becoming unparseable ios-sdk: - SDK update to avoid unneeded deserializations * - Remove duplicate file * Fixed problem with updating key-value store for every single media asset - When key value store is updated separately for every media item, there is huge number of background tasks, worker threads etc. being spawned. - Now adding assets to be uploaded within single [OCKeyValueStore updateObjectForKey:…] block * [fix/fp-word] Fix Word FileProvider issue (#574) * - Overhaul -importDocumentAtURL: to use file coordination and return placeholders only after they've been added to the database (via updated SDK) - Fix app toolbar icons glitch when re-opening the client browser for an account after launch * - Switch from develop to master branch for ios-sdk * - Update SDK to fix finding (1) * Fixed (4) and (5) - Disabled skip X seconds commands for now - Added play next / previous track commands - Removing system-wide now playing info if corresponding media playback view controller gets released * #550 fixed QA finding (1) for regression test 1.2.0, Import panel was not displayed on iOS 13 * Fixed a now playing issue with jumping time * Fixed issue with images not being uploaded when MP4 conversion option was active * new build number * Temporarily removed conversion of videos to MP4 * disable beta warning * Translation Sync (#525) * [tx] updated from transifex * Removed the es lang because was wrong and it is not necessary to map it (es -> es) (#310) * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * use newest SDK version * changed file encoding for localization files * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * - using correct SDK - solved encoding problems for localization files * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * change version number 1.0 -> 1.0.1 * Fixed QA finding (1). "Share item" menu item was not visible, if item was not shared * Fix finding (3) * Fixed an issue with go-to button not being shown in PDF viewer (#403) * Fixed an issue with go-to button not being shown in PDF viewer * Added accessibility labels to bar button items in the PDF viewer * Version Bump to 121 * - Action class: - extend ActionWillRunHandler with a completionHandler and adding a new .perform() method to wrap .willRun() and .run(), allowing for actions to trigger view controller dismissals, wait for them to finish and then present their own view controllers - Card upgrades, improvement and fixes - fix MoreViewController "shaking" on initial presentation - create new CardPresentationSizing protocol to allow view controllers to provide sizing information to CardPresentationController - allow disabling the handle and the ability to dismiss by tapping in the background - create CardViewController as base class for future view controllers that should be presented as cards - Add new DownloadItemsHUDViewController class (a CardViewController subclass) to modally download files, provide a summary, allow cancellation and simplified retrieval of the corresponding OCFile objects - Clean up OpenInAction and rewrite most of it to use DownloadItemsHUDViewController - KVOWaiter allows waiting for an object's value to change, check if the change meets the criteria and then run a block once. Ended up not being used and is therefore not included in the builds. But since it may be useful in the future, left the source code regardless. - Add ProgressSummary.update(progressView:) convenience method * [tx] updated from transifex * - Switch to release/1.0.1 branch for ios-sdk * - Add "Preparing" message to DownloadItemsHUDViewController while scheduling downloads - Update ios-sdk, fixing findings (5) + (6) * - Fix premature release of UIDocumentInteractionController in OpenInAction * using correct release SDK * [tx] updated from transifex * - Add support for Progress.isCancelled to ProgressSummarizer * Version Bump to 122 * - Addressing (5) via SDK update rescheduling/retrying dropped requests * - Addressing (5) via SDK update rescheduling/retrying dropped requests * - Make File Provider use new SDK option to return immediately from a thumbnail request when offline * [tx] updated from transifex * Version Bump to 123 * - Add new APP_SHORT_VERSION variable to build settings - Make Info.plist for app, File Provider and File Provider UI use it * - Fix remaining (5) issue for cases where the upload succeeded but the server response was cut off, prompting a retry and resulting in a "item already exists" error * - Update ios-sdk * [tx] updated from transifex * [tx] updated from transifex * changed wrong file encoding from UTF-16LE to UTF-8 * QA finding (14) removed ownCloud name from localizable stings * removed ownCloud string in localizable strings * fixed merge conclicts * using newest release SDK * changed file encoding from UTF-16LE to UTF-8 * changed file encoding from UTF-16LE to UTF-8 * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * [tx] updated from transifex * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * [tx] updated from transifex * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * [tx] updated from transifex * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * show always the account selection sheet and added a note, that only one file can be imported at once * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * [feature/bundle-import] Add support for certain bundle formats to the share sheet (#471) * - Add support for import of bundle-based document formats through transparent zipping - moved previous FileProvider functionality into ownCloudApp.framework - new method OCCore.importItemNamed() ensures correct handling of bundle-based document formats - fixes finding (4) in #445 - Changed LSSupportsOpeningDocumentsInPlace to NO address two issues: - the app is now listed as "Copy to ownCloud" (previously "Open in ownCloud", which was technically not correct) - previously, documents using "Open to ownCloud" would no longer open in f.ex. Pages, but in the ownCloud app when selected in the Files app * - Refactored ImportFilesController around using NSFileCoordinator, fixing finding (1) in #471 * changed path for moved Info.plist for target ownCloudAppFramework * Version Bump to 133 * [feature/sort direction] Allow sorting asc/desc for all sort methods (#474) * #470 added change sort direction asc/desc for all sort methods * - removed comments - changed translation keys * fixes sort comparator for sort method "shared" * use correct images for sort direction asc and desc * updated choosing sort method from a tableView/Popover instead of a UIAlertViewController * set maximum width for popover and set arrow direction * Added show sort direction in UISegmentControl * - make sure arrows of popovers appear in same color as the popover contents - fix issue where direction was toggled during setup of SortBar (setting of initial value from user defaults), so logging in, logging out and logging in would change the sort direction - reset sort direction to ascendant when switching sort methods - SortMethodTableViewController - remove extraneous instance lets - enforce row height used for table view height calculation - change table view height calculation to account for the last divider (which could look a bit off if present) * tap on segment control was fired twice, because there was a missing check, if selected segment changed * fixed changing width on selecting an other segment * fixed jumping width of sortSegmentControl items by setting the same width for all items by calculating the longest width before * [fix/index-bar] Make Index Bar available in general file list (#469) * #413 show index bar in general file list - Index bar is only visible, if sorting is "Sort by name" - Index bar is only visible, if more than one different letters are available - Index bar is only visible, if more items are available as visible * - adopt to new sort methods - reverse index list, if sort direction is descendant * Version Bump to 134 * Version Bump to 135 * Version Bump to 136 * Change behaviour of default expiration date (#476) * Enabling expiration date switch if default expiration date is configured Checking files_sharing.public.expire_date.expire_date.days server capability. If it is set then it is used as default date and the switch is enabled even if the expiration date is not enforced. * Fixed QA issue (1) * Fixed QA finding (2): limiting the date range if expiration date is enforced * Permission increasing UX in reshares (#467) * Not showing permissions which can’t be elevated Affecting items shared with the current users which are being reshared * Fixed an issue with wrong permissions displayed when creating public link - Checking if permissions can be elevated for the public link - Only viable options are displayed * Fixed a trailing whitespace warning * Fixed another compiler warning * Inhereting permissions from the share when creating public link * Fixed QA finding (4) * Fixed QA finding (1) * Fixed QA finding (2) * Version Bump to 137 * [feature/itempolicy] Item Policy / Available Offline support (#456) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Version Bump to 138 * [feature/reorder buttons] Reorder Navigation Bar Actions (#478) * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * #477 reorganize navigation bar with create and more button and moved select button to sort menu * added safeAreaLayoutGuide.rightAnchor * - Update SDK * changed "Select" image * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * addresses: - missing multiselection UI in toolbar - missing select all / deselect all button in navigation bar * set if selectButton should be shown * disable temporary some tests due to bitrise failures * using correct SDK * Version Bump to 139 * Feature/task scheduling (#484) * #386 Added relevant classes * First draft of the scheduled task manager * Just changed some comments * Implemented background fetch * Implemented background update fetch * Clean up of the code * Fixed merge problems from previous commit And eliminated some warnings * Small changes in background fetch operation * Some refactoring done * Added some user defaults for media upload * Available Offline Support - new MakeAvailableOfflineAction and MakeUnavailableOfflineAction actions - new Available Offline Library section - easy access to all files available offline via Available Offline - new ItemPolicyTableViewController and ItemPolicyCell prepared for reuse for presenting other item policies - Design: added SVG/TVG/PNG icons and cloud symbols for make available offline / make unavailable offline ClientItemCell changes - new ClientItemResolvingCell can resolve item from LocalID or path, new superclass of ShareClientItemCell and ItemPolicyCell - string to use as content for title and detail are now provided via titleLabelString(for:) and detailLabelString(for:), updated via updateLabels(with:), making customization easier - support for new available offline badge ProgressSummarizer changes - changed ProgressSummary from a struct to a class, addressing issue #451 ("Waiting for server response..." message sometimes not disappearing) * UI for bookmark / upload path selection * Added dummy implementation of media upload task * - Adopt OCClaim to ensure files aren't deleted while they are being used in - DisplayViewController - DownloadItemsHUDViewController - FileListTableViewController - File Provider - Add new Storage Settings to allow control over how long unused local copies are kept around - DisplayViewController now uses OCClaims to ensure files aren't deleted while they are being viewed - ClientItemCell uses new SDK APIs to - show "available offline" badges for folders - to indicate if an item has been made available offline directly (solid icon) or indirectly (dimmed icon) - keep the cloud status icon updated following changes - Change "+" icon to "•••" icon in file list, rename plusButton to folderAction everywhere - Action.provideAlertAction() now applies padding to images if necessary so their text aligns at the same horizontal offset - Fix typo in BookmarkInfoViewController and change wording from "Delete Offline Copies" to "Delete Local Copies" for consistency - Update swiftlint.yml to silence ownCloudScreenshotsTests warnings * - Update ios-sdk * - Make changes requested in code review, adding missing localizable strings and removing extraneous code * - Update SDK * Refactored current implementation of media upload * Added method to fetch photos from photo library * - Add downloadTriggerIdentifier and availableOfflinePolicyCoverage as properties that trigger the update of an item's visual representation (addressing finding 6 in #456) - Update make available offline action to use new convertExistingLocalDownloads option (addressing finding 2 in #456) - Update SDK (addressing finding 5 in #456) * Items in moved Available Offline folders should no longer be cleared locally and re-downloaded, following under the hood improvements in the ios-sdk * - Add initial support for cookies via SDK update * First draft of instant photo upload implementation * Calling task completion handler after upload was finished * - Temporarily disable some UI tests broken due to lack of database running * - Fix issue (11) in #456 by updating the lastUsed date of already downloaded items when they are viewed * - Added slider type to StaticTableViewRow - changed storage settings to allow picking a timeframe via slider rather than a list of time frames * - Change text from "Remove" to "Make unavailable offline" in Quick Access > Available Offline list (addressing issue (9)) * - Update BookmarkInfoViewController to provide users with the choice to also remove local copies of items marked as Available Offline, issuing a warning and giving the users a way to cancel, addressing issue (12) in #456. * Fixed QA finding (1) * Using PHAsset.creationDate to check which media files shall be instantly uploaded * Fixed naming of class / group * Moved Tasks folder * Fixed some review findings * - Fix issue where a busy bookmark alert is followed by duplicating the bookmark * Fixed some review findings * Improved background update fetching * Fixed some issues in media upload * Fixed a type in settings key * Improvements in the instant media upload task * Added missing returnCore in the selection of the instant upload path * Fixed QA finding (3) * Fixes for the correct core returning / requesting * Small cosmetic code changes * Fixed video and serialized photo / video uploads * Moved upload() method to UploadBaseAction class * Some refactoring in InstantMediaUploadTaskExtension * Improvements for bookmark / path selection for instant upload * Remove instant upload configuration if bookmark is not available * minor refactoring * Added possibility to cancel account selection * Small fix: reset the path if new account is selected * Using item tracking instead of OCQuery - Disabling instant photo upload if target directory is removed - Item tracking is potentially faster since it can use cached item information and is not always issuing PROPFIND * Made some ivars private * Detect in settings that instant upload path is gone In this case instant upload is disabled * - Update ios-sdk - Take advantage of the new key-value store and OCCoreSkipAvailableOfflineKey to make sure available offline files are not immediately downloaded again after clearing the vault from local copies of available offline files. - Logging into an account removes any previously set OCCoreSkipAvailableOfflineKey from the key value store - Move Bookmark locking to an OCBookmarkManager category (next stop: use the new key-value store and bake support for locking bookmarks directly into the SDK) * - Add missing localization * Update SDK with latest fixes * - Fix SettingsTests bugs * - Apply additional fixes to UI tests and disable the remaining failing ones * - Wait for connection initialization to complete before requesting sharing info - Use latest SDK with cookie filtering * - Turn off Request/Response log tags only filter in project file * - Disable cookie support * - Address (13) via SDK update * link against newest development SDK * Showing warning that instant upload was disabled * Fixes for UI testing: - use new SDK version that returns errors if the SQLite DB has not been opened - MockOCCore.state now returns .running to avoid queueing of PROPFINDs * - Fix unit tests * Added alert shown when in settings when instant upload is disabled * Added fetchUpdates() within which target folder tracking is started * - UI testing: make MockOCCore.state no longer return .running due to side effects - fixed conflicting constraints in two places * - force-delete bookmarks in UI tests if timeout has passed, log error but don't assert in that case - handle case where a swipe on table cells in DeleteBookmarkTests triggers deletion outright * - Further hardening of SettingsTests against timing differences local vs. CI * - make SettingsTest.testCheckMoreItems() more robust * Update to the latest changes in the SDK Those changes reduce memory consumption used for hash calculation * Change required to accomodate latest SDK changes * Less parallel photo uploads * - update SDK to address stuck sync action issues - no longer auto-open the last selected bookmark when launched by iOS in the background. Instead, the last selected bookmark is now auto-openend only when the app comes to the foreground. - update FileProviderInterfaceManager to better support the case where the app ships without File Provider. * Fixed compilation warning * - Add ATS exemption for File Provider * - Update SDK * - Update SDK + add colored debugging option to scheme * Addressed code review comments * Removed unneeded weak self * Removed another unwanted weak self * - Fix OCEvent-dropping issue via SDK update with fix - Make UploadMediaAction use OCBackgroundTask to protect while exporting * - Added missing OCBackgroundTask.start call in UploadMediaAction - Update SDK * Fixed missing copyright information and some warnings * Version Bump to 140 * Media upload pending flag added - Displaying a warning alert on the next launch, in case media upload couldn’t be completed due e.g. app being terminated in a backround or even due to an app crash. - Fixed an issue with DispatchGroup.notify() called prematurely in MediaUploadQueue.uploadAssets() * - sortbar was sometimes hidden - fixing layout issue * fixed location identifier for actions * Added more logs in PHAsset+Upload extension * removed unused string files for fastlane screenshot creation * Fixes (1) and (2) * disable beta warning * Fixes issue (3) with downloads being cancelled by preview * - Update SDK to address finding (4) in #461 * - Update SDK to solve remaining of finding (4) * fixed strange text encoding (Transifex error)= * - Update ios-sdk to include latest changes - Fix SwiftLint warning in DisplayHostViewController.swift * [tx] updated from transifex * updated changelog for version 1.1.0 * - change presentation of "Make unavailable offline" action to filled icon + "Available offline" + checkmark - optimize PNG images to save space - fix lack of OAuth2 renewed token propagation (#501: token renewal from Files App) via ios-sdk update * added missing localization string * - Update ios-sdk to satisfy new requirements for using ASAuthorization in iOS 13 * - Fix finding (11) in #461 via SDK update * Fixed issue (6) * - Add debug output to verify theory on stuck uploads issue * - Update SDK to add improved logging and allowing to enable use of OCCores after they were returned and stopped ("zombie core detection") * - Update SDK - Fix retain cycle in MediaUploadSettingsSection that could also lead to a zombie core * using the newest development SDK: - solving showing certificate error in document browser (file provider) - solved importFile on main thread issue * Version Bump to 141 * - new localization values from Transifex - changed wrong/too long localization strings - added missing localizations * added new localization language Basque * added localization files for fastlane screenshot generator * updated to newest SDK after localization changes * - added new var to get beta warning state - disabled beta warning, beta build - using the newest development SDK * set new build number * using master SDK version 1.1.0 * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * [tx] updated from transifex * new build number * fixed encoding hell UTF-8 / UTF-16 * - added arabic language files to project - fixed wrong encoding * - fixed wrong padding for label - localized "Feedback" button * removed Feedback button from server list view * Renamed instant upload to auto upload * Version Bump to 146 * added changelog for version 1.2.0 * Updated oC SDK to latest master * Fixed compilation issue * Fix infinite loop when duplicating a Folder in Files app (#577) - QueryFileListTableViewController: request only needed/used data (#577) - FileProviderExtension: return correct error for file collissions (previously commented out due to duplicate bug) on iOS 13.3 and later - FileProviderEnumerator: include the root item only for the root folder. If it's not included, no folder can be created in the root directory. If a non-root folder is included in a query result for its content, the Files Duplicate action will loop infinitely. * added missing delete bookmark confirmation * fixed german translation string * using ThemeWindow in SceneDelegate * fixed getting current system appearance * fixed broken badge generation for bitrise builds, reverting to an old pange version * Fixes jumping of progress indicator in activity view * Version Bump to 147 * - fix: add missing DispatchGroup.wait() to AVAsset+Extension without which videos might not be exported reliably - fix: make UploadMediaAction use the actual bookmark for uploading, not the last bookmark selected for connection (could send files to the wrong server on iPad, may not be functional elsewhere) - fix: make _needsSchedulingCount accounting dependant on the Bookmark UUID. Otherwise, scheduleUploads() might not be called as necessary if an item was added after another one was already being processed in scheduleUploads(). * - Update SDK to turn off SDK compiler optimization in Release build to fix issue (13) Co-authored-by: Matthias Hühne Co-authored-by: Michael Neuwert Co-authored-by: Jesús Recio Rincón --- .gitignore | 2 + CHANGELOG.md | 14 + Gemfile.lock | 54 +- .../libzip/libzip.xcodeproj/project.pbxproj | 1 + fastlane/Fastfile | 3 + fastlane/screenshots/ar/keyword.strings | Bin 0 -> 692 bytes fastlane/screenshots/ar/title.strings | Bin 0 -> 788 bytes fastlane/screenshots/en-GB/keyword.strings | Bin 0 -> 716 bytes fastlane/screenshots/en-GB/title.strings | Bin 0 -> 802 bytes fastlane/screenshots/gl/keyword.strings | Bin 762 -> 762 bytes fastlane/screenshots/lv/title.strings | Bin 0 -> 862 bytes fastlane/screenshots/zh-Hans/title.strings | Bin 0 -> 592 bytes ios-sdk | 2 +- .../FileProviderEnumerator.h | 2 +- .../FileProviderEnumerator.m | 18 +- .../FileProviderExtension.m | 164 ++- ownCloud File Provider/Info.plist | 2 +- .../OCItem+FileProviderItem.m | 125 +- ownCloud File ProviderUI/Info.plist | 2 +- ownCloud.xcodeproj/project.pbxproj | 141 ++- .../AVAsset+Extension.swift | 44 +- ownCloud/AppDelegate.swift | 28 +- .../BookmarkInfoViewController.swift | 2 +- ownCloud/Client/Actions/Action.swift | 17 +- .../CollaborateAction.swift | 54 + .../Actions+Extensions/CopyAction.swift | 4 +- .../CreateFolderAction.swift | 4 +- .../Actions+Extensions/DeleteAction.swift | 4 +- .../DiscardSceneAction.swift | 62 + .../Actions+Extensions/DuplicateAction.swift | 4 +- .../Actions+Extensions/FavoriteAction.swift | 64 + .../Actions+Extensions/LinksAction.swift | 54 + .../MakeAvailableOfflineAction.swift | 4 +- .../MakeUnavailableOfflineAction.swift | 4 +- .../Actions+Extensions/MoveAction.swift | 4 +- .../Actions+Extensions/OpenInAction.swift | 39 +- .../Actions+Extensions/OpenSceneAction.swift | 62 + .../Actions+Extensions/RenameAction.swift | 4 +- .../Actions+Extensions/UnfavoriteAction.swift | 64 + .../Actions+Extensions/UnshareAction.swift | 3 + .../Actions+Extensions/UploadFileAction.swift | 4 +- .../UploadMediaAction.swift | 16 +- .../ClientDirectoryPickerViewController.swift | 4 +- .../Client/Actions/NamingViewController.swift | 17 +- .../Scanner/FixedHeightImageView.swift | 44 + .../Client/Actions/Scanner/ScanAction.swift | 102 ++ .../Actions/Scanner/ScanViewController.swift | 487 ++++++++ ownCloud/Client/ClientActivityCell.swift | 2 - ownCloud/Client/ClientItemCell.swift | 40 +- .../Client/ClientQueryViewController.swift | 102 +- .../Client/ClientRootViewController.swift | 77 +- .../PhotoAlbumTableViewController.swift | 1 + .../Client/PhotoSelectionViewController.swift | 15 + .../PublicLinkEditTableViewController.swift | 4 +- .../PublicLinkTableViewController.swift | 16 - ownCloud/Client/SortMethod.swift | 49 +- ownCloud/Client/Viewer/DisplayExtension.swift | 8 + .../Viewer/DisplayHostViewController.swift | 48 +- .../Client/Viewer/DisplayViewController.swift | 151 +-- .../Image/ImageDisplayViewController.swift | 16 +- .../Media/MediaDisplayViewController.swift | 217 +++- .../Viewer/PDF/PDFViewerViewController.swift | 2 +- .../Client/Viewer/PreviewViewController.swift | 145 +++ .../WebViewDisplayViewController.swift | 2 +- .../FileListTableViewController.swift | 49 +- .../QueryFileListTableViewController.swift | 53 +- ownCloud/Import/ImportFilesController.swift | 6 +- ownCloud/Key Commands/KeyCommands.swift | 1070 +++++++++++++++++ .../MediaUploadActivity.swift | 60 + .../MediaUploadQueue.swift | 344 ++++-- .../MediaUploadStorage.swift | 151 +++ .../PhotoKit Extensions/PHAsset+Upload.swift | 149 ++- .../cloud-unavailable-offline@2x.png | Bin 678 -> 951 bytes ownCloud/Resources/Info.plist | 30 +- ownCloud/Resources/ar.lproj/InfoPlist.strings | 22 + .../Resources/ar.lproj/Localizable.strings | 495 ++++++++ .../Resources/cs.lproj/Localizable.strings | 1 - .../Resources/de-DE.lproj/Localizable.strings | 55 +- .../Resources/de.lproj/Localizable.strings | 185 +-- .../Resources/el.lproj/Localizable.strings | Bin 47418 -> 51242 bytes .../Resources/en-GB.lproj/Localizable.strings | 1 - .../Resources/en.lproj/Localizable.strings | 51 +- .../Resources/es.lproj/Localizable.strings | Bin 44466 -> 48090 bytes .../Resources/eu.lproj/Localizable.strings | Bin 44904 -> 24274 bytes ownCloud/Resources/gl.lproj/InfoPlist.strings | Bin 1848 -> 1850 bytes .../Resources/gl.lproj/Localizable.strings | Bin 45688 -> 49764 bytes .../Resources/he.lproj/Localizable.strings | Bin 38854 -> 42212 bytes .../Resources/ko.lproj/Localizable.strings | 1 - .../Resources/mk.lproj/Localizable.strings | 1 - .../Resources/nb-NO.lproj/Localizable.strings | 1 - .../Resources/nn-NO.lproj/Localizable.strings | 1 - .../Resources/pt-BR.lproj/Localizable.strings | 50 +- .../Resources/pt-PT.lproj/Localizable.strings | 1 - .../Resources/ru.lproj/Localizable.strings | 50 +- .../Resources/sq.lproj/Localizable.strings | 50 +- .../Resources/th-TH.lproj/Localizable.strings | 774 ++++++------ .../zh-Hans.lproj/Localizable.strings | Bin 18780 -> 18724 bytes .../SDK Extensions/OCCore+Extension.swift | 16 + .../SDK Extensions/OCItem+Extension.swift | 4 + ownCloud/SceneDelegate.swift | 77 ++ .../ServerListTableHeaderView.swift | 2 +- .../ServerListTableViewController.swift | 237 ++-- .../Settings/MediaUploadSettingsSection.swift | 15 +- .../Passcode/PasscodeViewController.swift | 57 +- .../InstantMediaUploadTaskExtension.swift | 108 +- .../PendingMediaUploadTaskExtension.swift | 48 + ownCloud/Tasks/ScheduledTaskManager.swift | 125 +- ownCloud/Theming/ThemeStyle+Extensions.swift | 6 +- .../CardPresentationController.swift | 2 +- ownCloud/UI Elements/StaticTableViewRow.swift | 44 +- .../UIApplication+Extension.swift | 26 + .../UIImageView+Thumbnails.swift | 109 ++ .../UIViewController+Extension.swift | 17 + ownCloud/Window/OpenItemUserActivity.swift | 42 + ownCloudAppFramework/Resources/Info.plist | 2 +- ownCloudAppFramework/Tools/NSData+Encoding.h | 29 + ownCloudAppFramework/Tools/NSData+Encoding.m | 28 + ownCloudAppFramework/ownCloudApp.h | 1 + ownCloudAppTests/Info.plist | 2 +- ownCloudScreenshotsTests/Info.plist | 2 +- ownCloudTests/Info.plist | 2 +- 121 files changed, 5968 insertions(+), 1308 deletions(-) create mode 100644 fastlane/screenshots/ar/keyword.strings create mode 100644 fastlane/screenshots/ar/title.strings create mode 100644 fastlane/screenshots/en-GB/keyword.strings create mode 100644 fastlane/screenshots/en-GB/title.strings create mode 100644 fastlane/screenshots/lv/title.strings create mode 100644 fastlane/screenshots/zh-Hans/title.strings create mode 100644 ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift create mode 100644 ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift create mode 100644 ownCloud/Client/Actions/Actions+Extensions/FavoriteAction.swift create mode 100644 ownCloud/Client/Actions/Actions+Extensions/LinksAction.swift create mode 100644 ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift create mode 100644 ownCloud/Client/Actions/Actions+Extensions/UnfavoriteAction.swift create mode 100644 ownCloud/Client/Actions/Scanner/FixedHeightImageView.swift create mode 100644 ownCloud/Client/Actions/Scanner/ScanAction.swift create mode 100644 ownCloud/Client/Actions/Scanner/ScanViewController.swift create mode 100644 ownCloud/Client/Viewer/PreviewViewController.swift create mode 100644 ownCloud/Key Commands/KeyCommands.swift create mode 100644 ownCloud/PhotoKit Extensions/MediaUploadActivity.swift create mode 100644 ownCloud/PhotoKit Extensions/MediaUploadStorage.swift create mode 100644 ownCloud/Resources/ar.lproj/InfoPlist.strings create mode 100644 ownCloud/Resources/ar.lproj/Localizable.strings create mode 100644 ownCloud/SceneDelegate.swift create mode 100644 ownCloud/Tasks/PendingMediaUploadTaskExtension.swift create mode 100644 ownCloud/UIKit Extensions/UIApplication+Extension.swift create mode 100644 ownCloud/UIKit Extensions/UIImageView+Thumbnails.swift create mode 100644 ownCloud/Window/OpenItemUserActivity.swift create mode 100644 ownCloudAppFramework/Tools/NSData+Encoding.h create mode 100644 ownCloudAppFramework/Tools/NSData+Encoding.m diff --git a/.gitignore b/.gitignore index 700a69e90..7989e2359 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,5 @@ playground.xcworkspace Pods/ # End of https://www.gitignore.io/api/xcode,swift,macos,objective-c +/fastlane/fastlane/Appfile +/Gemfile.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index ae23091d1..a00dbca1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # ChangeLog +## Release version 1.2.0 (December 2019) + +- Multiple Window Support (iPadOS) (#488) +- Keyboard Commands (iPadOS) (#282) +- Media Player Improvements (#59, #374) +- Better File Previews (#481) +- Arabic Language Support +- Fix: Sort alphabetically (PR #546) +- Fix: Share Sheet on iPad (#568) +- Fix: FileProvider File Type Issue (#557) +- Fix: FileProvider Offline Browsing (PR #547) +- Fix: FileProvider Saving from Microsoft Word (PR #574) +- Fix: Photo Upload (#504) + ## Release version 1.1.2 (October 2019) - Fix for long delays before starting a request on iOS 13.1 (PR #531) diff --git a/Gemfile.lock b/Gemfile.lock index 1ad667255..ac69b37a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,17 +1,17 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.0) - addressable (2.6.0) - public_suffix (>= 2.0.2, < 4.0) + CFPropertyList (3.0.1) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) atomos (0.1.3) - babosa (1.0.2) + babosa (1.0.3) badge (0.10.0) curb (~> 0.9) fastimage (>= 1.6) fastlane (>= 2.0) mini_magick (>= 4.5) - claide (1.0.2) + claide (1.0.3) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) @@ -20,20 +20,20 @@ GEM declarative (0.0.10) declarative-option (0.1.0) digest-crc (0.4.1) - domain_name (0.5.20180417) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.4) + dotenv (2.7.5) emoji_regex (1.0.1) - excon (0.64.0) - faraday (0.15.4) + excon (0.68.0) + faraday (0.17.0) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) http-cookie (~> 1.0.0) faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) - fastimage (2.1.5) - fastlane (2.126.0) + fastimage (2.1.7) + fastlane (2.135.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -43,9 +43,9 @@ GEM dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 2.0) excon (>= 0.45.0, < 1.0.0) - faraday (~> 0.9) + faraday (~> 0.17) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.9) + faraday_middleware (~> 0.13.1) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-api-client (>= 0.21.2, < 0.24.0) @@ -53,12 +53,12 @@ GEM highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) jwt (~> 2.1.0) - mini_magick (~> 4.5.1) + mini_magick (>= 4.9.4, < 5.0.0) multi_xml (~> 0.5) multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) public_suffix (~> 2.0.0) - rubyzip (>= 1.2.2, < 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) @@ -81,9 +81,9 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.9) - google-cloud-core (1.3.0) + google-cloud-core (1.4.1) google-cloud-env (~> 1.0) - google-cloud-env (1.2.0) + google-cloud-env (1.3.0) faraday (~> 0.11) google-cloud-storage (1.16.0) digest-crc (~> 0.4) @@ -103,12 +103,12 @@ GEM httpclient (2.8.3) json (2.2.0) jwt (2.1.0) - memoist (0.16.0) - mime-types (3.2.2) + memoist (0.16.1) + mime-types (3.3) mime-types-data (~> 3.2015) - mime-types-data (3.2019.0331) - mini_magick (4.5.1) - multi_json (1.13.1) + mime-types-data (3.2019.1009) + mini_magick (4.9.5) + multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.0.0) nanaimo (0.2.6) @@ -122,14 +122,14 @@ GEM uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) - rubyzip (1.2.3) + rubyzip (1.3.0) security (0.1.3) - signet (0.11.0) + signet (0.12.0) addressable (~> 2.3) faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.5) + simctl (1.6.6) CFPropertyList naturally slack-notifier (2.3.2) @@ -146,7 +146,7 @@ GEM unf_ext (0.0.7.6) unicode-display_width (1.6.0) word_wrap (1.0.0) - xcodeproj (1.10.0) + xcodeproj (1.13.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -165,4 +165,4 @@ DEPENDENCIES fastlane-plugin-badge BUNDLED WITH - 2.0.1 + 2.0.2 diff --git a/external/libzip/libzip.xcodeproj/project.pbxproj b/external/libzip/libzip.xcodeproj/project.pbxproj index 7cf933b83..f03b4736f 100644 --- a/external/libzip/libzip.xcodeproj/project.pbxproj +++ b/external/libzip/libzip.xcodeproj/project.pbxproj @@ -467,6 +467,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = DCE93FD921FCA42B000E14F2; productRefGroup = DCE93FE421FCA42C000E14F2 /* Products */; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 557ce862d..2d5f16159 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -157,6 +157,9 @@ platform :ios do short_hash = commit[:abbreviated_commit_hash] # short sha of commit sh "curl https://badgen.net/badge/Bitrise/" + short_hash + "/blue > badge.svg" sh "brew install librsvg" + sh "brew unlink pango" + sh "brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/7cf3b63be191cb2ce4cd86f4406915128ec97432/Formula/pango.rb" + sh "brew switch pango 1.42.4_1 " sh "rsvg-convert badge.svg > badge.png" sh "badge --custom badge.png --glob /../**/*.appiconset/*.{png,PNG}" diff --git a/fastlane/screenshots/ar/keyword.strings b/fastlane/screenshots/ar/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..282ef33d1e07d3634e321df89d6bb50db69f062e GIT binary patch literal 692 zcmaKq+fKqj6h-&bdGITmCZyN|v7i`Yd_apKebO%=M!*ocX;J=OT{GO;fTWq}bf(?w z?0x$C6DrZA`WkDlocoJjx#s%POiRDB(ohq>H)Yk=fO*Orp#t48LPZwawWSuQY8Mt+ z-MXyjTA}g4w@z)qd$&4cPPk6-Ce)I{K1Qmb9a&=M=+~^ty2^-o>N2-Et#h^|HA>D# z=uW8Jxje;)aMERTHp(5#(06+^&uzlbU==wb4#Mw8bA*uJZAn(!91l#B**22+~Qa$dva#hOegN}`<{T+?FBf!r|)Qq;C~HIPd!Yh4qn zg{*nM$9vXk$TPQc#vE~+Vomvu4DKOHSz{gOH6w;lVt!<;!h6yVmEEstlWzza=elJ! zcKpT=rWly(DOyJ(tbEsm&Ry3tkS?@9676x`ry8)Pmz4f8ZhcI9%OJNkZ(iMHq# z(YtbhY@`_5Ds!DWZ`jDCnrANM1J^I8BX%T}C3`VGnr?{6#l|V7e_!um`3#!+K)tnW z?PU7S9#61)qqF~iEMH4_-?6VZ7@A)Phh$Uvy)?g&d@Wh?^JGifcA)Y{Z|~4E+*U5x Uxx5~smRw?k literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/en-GB/keyword.strings b/fastlane/screenshots/en-GB/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..aba38418299a39e3a16b19b4c60895e427561d78 GIT binary patch literal 716 zcmaKq+fIW(7=-7#PeFKswlUrqFNmkBCK{98Hl_t7(gg+O?XBOxxCJU{HhW-qcIKa1 z4nMILnrow6rE2CEZE2;xHZW;aN5(;5gXp5!v^Nd5pzwg UjZew(veS$S9xCd5KEZa3zXj5Jr~m)} literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/en-GB/title.strings b/fastlane/screenshots/en-GB/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..547033c70b80a5ebb2b1fa5c9ae54c387c499e1e GIT binary patch literal 802 zcmaKq%}#?r6ot>)ryx9mCB}_$L8?t$Xib`!xFStUYa>4uXd7Q%{q6u3lr{urGQ&CF zJ$L5&%hyPkinLL#l5wsDPp+LZRc_^=SczLpnMGRiP5Hx@r(0fMA!|!!uTEDm%XL6M zMrTg8H$zou@4minbb)TfbB>%)RY?`YOsg|yzr;O+l=LbH|Kw?=;iCO?7s@99IbWze@|z6!>w@6U1o^F`Z+NWO<#>Jx^YnbokM7VB|Meqzmh){Je2mVedj?gwYT zLH!P=zE|~m4doq^C#5Pojmo9IbN+M4)8^i^)wi_k9<6jrt0<$kQ(cXzXV>)gNO**m O@viZ(TW)v1ru+a#mx_}B literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/gl/keyword.strings b/fastlane/screenshots/gl/keyword.strings index 7cb3afe8de62f7ea2c301a0416ae950d2a3d7df6..db8cff28348cf9a95dc80aba393e331101fede6c 100644 GIT binary patch delta 38 rcmeyx`iphL9VWR%1_g#hh608hhD?TJAS)S2e`d&IP-3uV;9>v(+&T#= delta 38 rcmeyx`iphL9VWSa1_g#hh608hhD?TJAgcsOmoelsC^1+wa4`S?+42a@ diff --git a/fastlane/screenshots/lv/title.strings b/fastlane/screenshots/lv/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..fec42a1f9c1443175035abee4b59bbf94070b31f GIT binary patch literal 862 zcmaKqKU0H15XJXeTfYE9o5o}u8@0%2XJciX!2~jdzXk%|Uj6MN0TeR~ce}TH``+8V zuTNi=aw8jwrI3m}kS$*!C&{I@lU71W?JUFE$c{B5!xv8;`F(lib|{UlIgx`|HL;3% z4YfQ@TPi+LNo(&c7Rr&Rt}#{a<%)I5cZp3(ZUwXdc_i}S5~n%JN?9ng%Dn8%Nn`3I z@KL3m-?LavdJ+*$$=Ci|!e2{CPJ-qEJHpf3Cn6n_5IpC4yT%?+$J)oJIu)&{T$RP& z8N3iHW#RVhL;t7fI#UnT0hy|1z+H`cKE^GXO+VR|oD$?{7XQsUn4{jgqXaH6q!2%q zYo~q=u4nJcGh3&!Cwt3&Al9?Fo%H^L8!_q=P`kIO%;$Tc?=ZCvdGORJ=*ugOQ9qYK h_1y%gLT7C~&S5oQ|Cw$SKiYoRZ@^1IUAXtNmT&)SmcRf2 literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/zh-Hans/title.strings b/fastlane/screenshots/zh-Hans/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..d9d2b6e1baafa3f6860be64147f739dd65a46992 GIT binary patch literal 592 zcmezWPl>^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$};_yC7&qLQ9zfP_!JV&l#v5WI_s1mN$K2P)3*%gEa#eP^BRd;DHcTfuw@eykzTy k6P+;A%uq}#2D+&P?BhI!bR4F1MrQ +@interface FileProviderEnumerator : NSObject { __weak FileProviderExtension *_fileProviderExtension; diff --git a/ownCloud File Provider/FileProviderEnumerator.m b/ownCloud File Provider/FileProviderEnumerator.m index a4a5d9d9c..7bf069997 100644 --- a/ownCloud File Provider/FileProviderEnumerator.m +++ b/ownCloud File Provider/FileProviderEnumerator.m @@ -178,7 +178,7 @@ - (void)_startQuery { // Start query self->_query = [OCQuery queryForPath:queryPath]; - self->_query.includeRootItem = YES; + self->_query.includeRootItem = queryPath.isRootPath; // Include the root item only for the root folder. If it's not included, no folder can be created in the root directory. If a non-root folder is included in a query result for its content, the Files Duplicate action will loop infinitely. self->_query.delegate = self; [DisplaySettings.sharedDisplaySettings updateQueryWithDisplaySettings:self->_query]; @@ -322,9 +322,11 @@ - (void)provideItemsForChangeObserverFromQuery:(OCQuery *)query - (void)queryHasChangesAvailable:(OCQuery *)query { - OCLogDebug(@"##### Query for %@ has changes. Query state: %lu", query.queryPath, (unsigned long)query.state); + OCLogDebug(@"##### Query for %@ has changes. Query state: %lu, SinceSyncAnchor: %@, Changes available: %d", query.queryPath, (unsigned long)query.state, query.querySinceSyncAnchor, query.hasChangesAvailable); - if ((query.state == OCQueryStateContentsFromCache) || ((query.state == OCQueryStateWaitingForServerReply) && (query.queryResults.count > 0)) || (query.state == OCQueryStateIdle)) + if ( (query.state == OCQueryStateContentsFromCache) || + ((query.state == OCQueryStateWaitingForServerReply) && (query.queryResults.count > 0)) || + (query.state == OCQueryStateIdle)) { dispatch_async(dispatch_get_main_queue(), ^{ @synchronized(self) @@ -422,4 +424,14 @@ - (void)currentSyncAnchorWithCompletionHandler:(void (^)(NSFileProviderSyncAncho // - (void)finishEnumeratingWithError:(NSError *)error; // } ++ (NSArray *)logTags +{ + return (@[ @"FPEnum" ]); +} + +- (NSArray *)logTags +{ + return (@[ @"FPEnum", OCLogTagInstance(self)]); +} + @end diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index a5747cadf..3bf6ba552 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -26,6 +26,9 @@ #import "NSError+MessageResolution.h" @interface FileProviderExtension () +{ + NSFileCoordinator *_fileCoordinator; +} @property (nonatomic, readonly, strong) NSFileManager *fileManager; @@ -40,6 +43,8 @@ - (instancetype)init { NSDictionary *bundleInfoDict = [[NSBundle bundleForClass:[FileProviderExtension class]] infoDictionary]; + _fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; + OCCoreManager.sharedCoreManager.memoryConfiguration = OCCoreMemoryConfigurationMinimum; OCAppIdentity.sharedAppIdentity.appIdentifierPrefix = bundleInfoDict[@"OCAppIdentifierPrefix"]; @@ -59,10 +64,15 @@ - (instancetype)init - (void)dealloc { + OCLogDebug(@"Deallocating FileProvider %@", self); + + [_fileCoordinator cancel]; + [self removeObserver:self forKeyPath:@"domain" context:(__bridge void *)self]; if (_core != nil) { + OCLogDebug(@"Returning OCCore for FileProvider %@", self); [[OCCoreManager sharedCoreManager] returnCoreForBookmark:self.bookmark completionHandler:nil]; } } @@ -130,13 +140,13 @@ - (NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier } }); - // OCLogDebug(@"-itemForIdentifier:error: %@ => %@", identifier, item); - if ((item == nil) && (returnError == nil)) { returnError = [NSError fileProviderErrorForNonExistentItemWithIdentifier:identifier]; } + // OCLogDebug(@"-itemForIdentifier:error: %@ => %@ / %@", identifier, item, returnError); + if (outError != NULL) { *outError = [returnError translatedError]; @@ -386,8 +396,14 @@ - (void)createDirectoryWithName:(NSString *)directoryName inParentItemIdentifier if ((existingItem = [self.core cachedItemInParent:parentItem withName:directoryName isDirectory:YES error:NULL]) != nil) { FPLogCmd(@"Completed with collission with existingItem=%@ (locally detected)", existingItem); - // completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); // This is what we should do according to docs - completionHandler(nil, [OCError(OCErrorItemAlreadyExists) translatedError]); // This is what we need to do to avoid users running into issues using the broken Files "Duplicate" action + if (@available(iOS 13.3, *)) + { + completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); // This is what we should do according to docs + } + else + { + completionHandler(nil, [OCError(OCErrorItemAlreadyExists) translatedError]); // This is what we need to do to avoid users running into issues using the broken Files "Duplicate" action + } return; } @@ -513,80 +529,105 @@ - (void)deleteItemWithIdentifier:(NSFileProviderItemIdentifier)itemIdentifier co - (void)importDocumentAtURL:(NSURL *)fileURL toParentItemIdentifier:(NSFileProviderItemIdentifier)parentItemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler { NSError *error = nil; - BOOL isImportingFromVault = NO; - BOOL importByCopying = NO; - NSString *importFileName = fileURL.lastPathComponent; - OCItem *parentItem; + BOOL stopAccess = NO; - FPLogCmdBegin(@"Import", @"Start of importDocumentAtURL=%@, toParentItemIdentifier=%@", fileURL, parentItemIdentifier); + if ([fileURL startAccessingSecurityScopedResource]) + { + stopAccess = YES; + } - // Detect import of documents from our own internal storage (=> used by Files.app for duplication of files) - isImportingFromVault = [fileURL.path hasPrefix:self.core.vault.filesRootURL.path]; + FPLogCmdBegin(@"Import", @"Start of importDocumentAtURL=%@, toParentItemIdentifier=%@, attributes=%@", fileURL, parentItemIdentifier, [NSFileManager.defaultManager attributesOfItemAtPath:fileURL.path error:nil]); - if (isImportingFromVault) - { - NSFileProviderItemIdentifier sourceItemIdentifier; - NSFileProviderItem sourceItem; + [_fileCoordinator coordinateReadingItemAtURL:fileURL options:NSFileCoordinatorReadingWithoutChanges|NSFileCoordinatorReadingForUploading error:&error byAccessor:^(NSURL * _Nonnull readURL) { + NSError *error = nil; + BOOL isImportingFromVault = NO; + BOOL importByCopying = NO; + NSString *importFileName = readURL.lastPathComponent; + OCItem *parentItem; - // Determine source item - if (((sourceItemIdentifier = [self persistentIdentifierForItemAtURL:fileURL]) != nil) && - ((sourceItem = [self itemForIdentifier:sourceItemIdentifier error:nil]) != nil)) - { - importByCopying = YES; - } - } + FPLogCmd(@"Coordinated read of readURL=%@, toParentItemIdentifier=%@, attributes=%@", readURL, parentItemIdentifier, [NSFileManager.defaultManager attributesOfItemAtPath:readURL.path error:nil]); - if ((parentItem = (OCItem *)[self itemForIdentifier:parentItemIdentifier error:&error]) != nil) - { - // Detect name collissions - OCItem *existingItem; + // Detect import of documents from our own internal storage (=> used by Files.app for duplication of files) + isImportingFromVault = [readURL.path hasPrefix:self.core.vault.filesRootURL.path]; - if ((existingItem = [self.core cachedItemInParent:parentItem withName:importFileName isDirectory:NO error:NULL]) != nil) + if (isImportingFromVault) { - // Return collission error - FPLogCmd(@"Completed with collission with existingItem=%@ (local)", existingItem); - completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); - return; + NSFileProviderItemIdentifier sourceItemIdentifier; + NSFileProviderItem sourceItem; + + // Determine source item + if (((sourceItemIdentifier = [self persistentIdentifierForItemAtURL:readURL]) != nil) && + ((sourceItem = [self itemForIdentifier:sourceItemIdentifier error:nil]) != nil)) + { + importByCopying = YES; + } } - FPLogCmd(@"Importing %@ at %@ fromURL %@", importFileName, parentItem, fileURL); + if ((parentItem = (OCItem *)[self itemForIdentifier:parentItemIdentifier error:&error]) != nil) + { + // Detect name collissions + OCItem *existingItem; - // Import item - [self.core importItemNamed:importFileName at:parentItem fromURL:fileURL isSecurityScoped:YES options:@{ - OCCoreOptionImportByCopying : @(importByCopying) - } placeholderCompletionHandler:^(NSError *error, OCItem *item) { - FPLogCmd(@"Completed with placeholderItem=%@, error=%@", item, error); - completionHandler(item, [error translatedError]); - } resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { - if ([error.domain isEqual:OCHTTPStatusErrorDomain] && (error.code == OCHTTPStatusCodePRECONDITION_FAILED)) + if ((existingItem = [self.core cachedItemInParent:parentItem withName:importFileName isDirectory:NO error:NULL]) != nil) { - // Collission: file already exists - if ((parameter != nil) && ([parameter isKindOfClass:[OCItem class]])) + // Return collission error + FPLogCmd(@"Completed with collission with existingItem=%@ (local)", existingItem); + completionHandler(nil, [NSError fileProviderErrorForCollisionWithItem:existingItem]); + return; + } + + FPLogCmd(@"Importing %@ at %@ readURL %@", importFileName, parentItem, readURL); + + // Import item + [self.core importItemNamed:importFileName at:parentItem fromURL:readURL isSecurityScoped:YES options:@{ + OCCoreOptionImportByCopying : @(importByCopying) + } placeholderCompletionHandler:^(NSError *error, OCItem *item) { + FPLogCmd(@"Completed with placeholderItem=%@, error=%@", item, error); + completionHandler(item, [error translatedError]); + } resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { + if ([error.domain isEqual:OCHTTPStatusErrorDomain] && (error.code == OCHTTPStatusCodePRECONDITION_FAILED)) { - OCItem *placeholderItem = (OCItem *)parameter; + // Collission: file already exists + if ((parameter != nil) && ([parameter isKindOfClass:[OCItem class]])) + { + OCItem *placeholderItem = (OCItem *)parameter; - // TODO (defunct): - // Upload errors (such as NSFileProviderErrorInsufficientQuota) should be handled - // with a subsequent update to the [placeholder] item, setting its uploadingError property. + // TODO (defunct): + // Upload errors (such as NSFileProviderErrorInsufficientQuota) should be handled + // with a subsequent update to the [placeholder] item, setting its uploadingError property. - // TODO (not yet implemented): - // Upload errors should not prevent creating or importing a document, because they - // can be resolved at a later date (for example, when the user has quota again.) + // TODO (not yet implemented): + // Upload errors should not prevent creating or importing a document, because they + // can be resolved at a later date (for example, when the user has quota again.) - if (placeholderItem.isPlaceholder) - { - FPLogCmd(@"Completed with fileAlreadyExistsAs=%@", placeholderItem); - [placeholderItem setUploadingError:[NSError fileProviderErrorForCollisionWithItem:placeholderItem]]; + if (placeholderItem.isPlaceholder) + { + FPLogCmd(@"Completed with fileAlreadyExistsAs=%@", placeholderItem); + [placeholderItem setUploadingError:[NSError fileProviderErrorForCollisionWithItem:placeholderItem]]; + } } } - } - }]; - } - else + }]; + } + else + { + FPLogCmd(@"Completed with parentItem=%@ not found, error=%@", parentItem, error); + completionHandler(nil, error); + } + + if (stopAccess) + { + [readURL stopAccessingSecurityScopedResource]; + } + }]; + + FPLogCmd(@"File Coordinator returned with error=%@", error); + + if (error != nil) { - FPLogCmd(@"Completed with parentItem=%@ not found, error=%@", parentItem, error); completionHandler(nil, error); } + } - (void)setFavoriteRank:(NSNumber *)favoriteRank forItemIdentifier:(NSFileProviderItemIdentifier)itemIdentifier completionHandler:(void (^)(NSFileProviderItem _Nullable, NSError * _Nullable))completionHandler @@ -851,6 +892,8 @@ - (OCCore *)core { if (self.bookmark != nil) { + OCLogDebug(@"Requesting OCCore for FileProvider %@", self); + OCSyncExec(waitForCore, { [[OCCoreManager sharedCoreManager] requestCoreForBookmark:self.bookmark setup:^(OCCore *core, NSError *error) { self->_core = core; @@ -896,3 +939,8 @@ - (void)core:(OCCore *)core handleError:(NSError *)error issue:(OCIssue *)issue @end OCClaimExplicitIdentifier OCClaimExplicitIdentifierFileProvider = @"fileProvider"; + +/* + Additional information: + - NSExtensionFileProviderSupportsPickingFolders: https://twitter.com/palmin/status/1177860144258076673 +*/ diff --git a/ownCloud File Provider/Info.plist b/ownCloud File Provider/Info.plist index cdda3cc10..0601c81eb 100644 --- a/ownCloud File Provider/Info.plist +++ b/ownCloud File Provider/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 144 + 147 NSExtension NSExtensionFileProviderDocumentGroup diff --git a/ownCloud File Provider/OCItem+FileProviderItem.m b/ownCloud File Provider/OCItem+FileProviderItem.m index 81396450f..e2c78b53b 100644 --- a/ownCloud File Provider/OCItem+FileProviderItem.m +++ b/ownCloud File Provider/OCItem+FileProviderItem.m @@ -55,16 +55,131 @@ - (NSString *)filename return (self.name); } ++ (NSDictionary *)overriddenUTIByMIMEType +{ + static dispatch_once_t onceToken; + static NSDictionary *utiByMIMEType; + + dispatch_once(&onceToken, ^{ + utiByMIMEType = @{ + @"application/vnd.oasis.opendocument.text" : @"org.oasis-open.opendocument.text", + @"application/vnd.oasis.opendocument.text-template" : @"org.oasis-open.opendocument.text-template", + + @"application/vnd.oasis.opendocument.graphics" : @"org.oasis-open.opendocument.graphics", + @"application/vnd.oasis.opendocument.graphics-template" : @"org.oasis-open.opendocument.graphics-template", + + @"application/vnd.oasis.opendocument.presentation" : @"org.oasis-open.opendocument.presentation", + @"application/vnd.oasis.opendocument.presentation-template" : @"org.oasis-open.opendocument.presentation-template", + + @"application/vnd.oasis.opendocument.spreadsheet" : @"org.oasis-open.opendocument.spreadsheet", + @"application/vnd.oasis.opendocument.spreadsheet-template" : @"org.oasis-open.opendocument.spreadsheet-template", + + @"application/vnd.oasis.opendocument.formula" : @"org.oasis-open.opendocument.formula", + @"application/vnd.oasis.opendocument.formula-template" : @"org.oasis-open.opendocument.formula-template" + }; + }); + + return (utiByMIMEType); +} + ++ (NSDictionary *)overriddenUTIBySuffix +{ + static dispatch_once_t onceToken; + static NSDictionary *utiBySuffix; + + dispatch_once(&onceToken, ^{ + utiBySuffix = @{ + // These suffix -> UTI mappings are currently not needed as the OC server + // already returns correct MIME-Types for these, so the MIMEType -> UTI + // mapping already takes care of it. Decided to let them still stay here + // as a reference and to document the thinking behind not including these + // five file types in this conversion dictionary. + + // @"odt" : @"org.oasis-open.opendocument.text", + // @"ott" : @"org.oasis-open.opendocument.text-template", + + // @"odg" : @"org.oasis-open.opendocument.graphics", + // @"otg" : @"org.oasis-open.opendocument.graphics-template", + + // @"odp" : @"org.oasis-open.opendocument.presentation", + // @"otp" : @"org.oasis-open.opendocument.presentation-template", + + // @"ods" : @"org.oasis-open.opendocument.spreadsheet", + // @"ots" : @"org.oasis-open.opendocument.spreadsheet-template", + + // @"odf" : @"org.oasis-open.opendocument.formula", + // @"otf" : @"org.oasis-open.opendocument.formula-template", + + + // OC server does not seem to return MIME Types for these types + // at the time of writing, so these entries take care of correctly + // mapping suffixes to UTIs + + @"odc" : @"org.oasis-open.opendocument.chart", + @"otc" : @"org.oasis-open.opendocument.chart-template", + + @"odi" : @"org.oasis-open.opendocument.image", + @"oti" : @"org.oasis-open.opendocument.image-template", + + @"odm" : @"org.oasis-open.opendocument.text-master", + @"oth" : @"org.oasis-open.opendocument.text-web" + }; + }); + + return (utiBySuffix); +} + - (NSString *)typeIdentifier { + NSString *uti = nil; + // Return special UTI type for folders if (self.type == OCItemTypeCollection) { return ((__bridge NSString *)kUTTypeFolder); } + // Workaround for broken MIMEType->UTI conversions + if (uti == nil) + { + // Override by MIMEType + if (self.mimeType != nil) + { + uti = OCItem.overriddenUTIByMIMEType[self.mimeType]; + + OCLogDebug(@"Mapped %@ MIMEType %@ to UTI %@", self.name, self.mimeType, uti); + } + } + + if (uti == nil) + { + NSString *suffix; + + // Override by suffix + if ((suffix = self.name.pathExtension.lowercaseString) != nil) + { + uti = OCItem.overriddenUTIBySuffix[suffix]; + + OCLogDebug(@"Mapped %@ suffix %@ to UTI %@", self.name, suffix, uti); + } + } + // Convert MIME type to UTI type identifier - return ((NSString *)CFBridgingRelease(UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)self.mimeType, NULL))); + if (uti == nil) + { + if (self.mimeType != nil) + { + uti = ((NSString *)CFBridgingRelease(UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)self.mimeType, NULL))); + } + else + { + uti = (__bridge NSString *)kUTTypeItem; + } + + OCLogDebug(@"Converted %@ MIMEType %@ to UTI %@", self.name, self.mimeType, uti); + } + + return (uti); } - (NSFileProviderItemCapabilities)capabilities @@ -123,6 +238,14 @@ - (BOOL)isDownloaded return (YES); } + if (self.type == OCItemTypeCollection) + { + // Needs to return YES for folders in order to allow browsing while offline + // Otherwise Files.app will bring up an alert "You're not connected to the Internet" + // (big thanks to @palmin who pointed me to this possibility) + return (YES); + } + return (NO); } diff --git a/ownCloud File ProviderUI/Info.plist b/ownCloud File ProviderUI/Info.plist index e090e8e0f..3b2181a05 100644 --- a/ownCloud File ProviderUI/Info.plist +++ b/ownCloud File ProviderUI/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 144 + 147 NSExtension NSExtensionFileProviderActions diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index ab3233937..4718cfbce 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -32,16 +32,24 @@ 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */; }; 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F6238020B587EF004FDE8B /* SortMethod.swift */; }; 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; + 3900348223A100D3000D8510 /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3900348123A100D3000D8510 /* UIApplication+Extension.swift */; }; 390B51E02292DBB100935E24 /* SharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */; }; 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39104E0A223991C8002FC02F /* UIButton+Extension.swift */; }; 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */; }; 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */; }; 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */; }; + 394E1FDA233E2D64009D2897 /* FavoriteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */; }; + 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */; }; + 394E1FFF233E43F5009D2897 /* LinksAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E1FFE233E43F5009D2897 /* LinksAction.swift */; }; + 394E200C233E477F009D2897 /* OpenItemUserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */; }; 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; + 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3961281522F8730A0087BD3A /* SceneDelegate.swift */; }; 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */; }; 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C92289500E00B254A9 /* RoundedLabel.swift */; }; + 396C82FB2319AFDD00938262 /* CollaborateAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396C82FA2319AFDD00938262 /* CollaborateAction.swift */; }; + 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */; }; 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; - 397328EF22D606AC006CFAA4 /* ImportFilesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */; }; + 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754F22327A33500119FCB /* OpenSceneAction.swift */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */; }; @@ -59,16 +67,20 @@ 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */; }; 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */; }; 39DE75CD22F960CF0064C1E2 /* SortMethodTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DE75C722F960CF0064C1E2 /* SortMethodTableViewController.swift */; }; + 39DED4932369879100CCC62C /* ImportFilesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DED4922369879100CCC62C /* ImportFilesController.swift */; }; 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; + 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */; }; 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */; }; 46B9D336BF7FE50321823888 /* Pods_ownCloudScreenshotsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */; }; + 4C05D8A5238708D40073EF50 /* MediaUploadStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C05D8A4238708D40073EF50 /* MediaUploadStorage.swift */; }; 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */; }; 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */; }; 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */; }; 4C16CBA7226F0F1A00D67BB6 /* FileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */; }; 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */; }; + 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3E17DA234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift */; }; 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE12187AF1400D30602 /* PDFThumbnailCollectionViewCell.swift */; }; 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE82187AF1400D30602 /* PDFTocTableViewController.swift */; }; 4C464BF12187AF1500D30602 /* PDFTocTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE92187AF1400D30602 /* PDFTocTableViewCell.swift */; }; @@ -80,11 +92,14 @@ 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51727522DE04BD001BC97F /* ScheduledTaskExtension.swift */; }; 4C51727E22DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */; }; 4C51727F22DE04BD001BC97F /* ScheduledTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */; }; + 4C63F0E5231A91230088E8CA /* UIImageView+Thumbnails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63F0E4231A91230088E8CA /* UIImageView+Thumbnails.swift */; }; 4C6B78102226B83300C5F3DB /* PhotoAlbumTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */; }; 4C6B78122226B86300C5F3DB /* PhotoAlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */; }; 4C7295D8228C384E00FA4E68 /* LogFilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7295D7228C384E00FA4E68 /* LogFilesViewController.swift */; }; 4C82D07022C9387300835F0B /* MediaDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82D06F22C9387300835F0B /* MediaDisplayViewController.swift */; }; 4C88041822E78D790016CBA9 /* MediaFilesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */; }; + 4C96463C238489E4003278B7 /* MediaUploadActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C96463B238489E4003278B7 /* MediaUploadActivity.swift */; }; + 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BFA2223158C3F0059CA3E /* PreviewViewController.swift */; }; 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */; }; 4CB8ADDE22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */; }; 4CB8ADE022DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADDF22DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift */; }; @@ -146,6 +161,9 @@ 6EE97DCA2204703C0062CCBC /* BackgroundImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 6EE97DC42204703C0062CCBC /* BackgroundImage.png */; }; 75AC0B4AD332C8CC785FE349 /* Pods_ownCloudTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56EA84D8AD331FFA604138B /* Pods_ownCloudTests.framework */; }; A45A8D98137C902524B84E6D /* EarlGrey.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */ = {isa = PBXBuildFile; fileRef = DC0030BF2350B1CE00BB8570 /* NSData+Encoding.m */; }; + DC0030C22350B1CE00BB8570 /* NSData+Encoding.h in Headers */ = {isa = PBXBuildFile; fileRef = DC0030C02350B1CE00BB8570 /* NSData+Encoding.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC0030CB2350B75000BB8570 /* ScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1AC7CF2319ADAE002B7892 /* ScanViewController.swift */; }; DC018F8320A0F56300135198 /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC018F8220A0F56300135198 /* UIView+Extension.swift */; }; DC018F8C20A1060A00135198 /* ProgressHUDViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC018F8B20A1060A00135198 /* ProgressHUDViewController.swift */; }; DC0196AB20F7690C00C41B78 /* OCBookmark+FileProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DC27A1A420CBEF85008ACB6C /* OCBookmark+FileProvider.m */; }; @@ -199,6 +217,7 @@ DC42244C207CAFBB0006A2A6 /* ThemeCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC42244B207CAFBB0006A2A6 /* ThemeCollection.swift */; }; DC422450207CB2500006A2A6 /* NSObject+ThemeApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC42244F207CB2500006A2A6 /* NSObject+ThemeApplication.swift */; }; DC434D1320D7A8F100740056 /* UIAlertController+OCIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */; }; + DC4C575D233958B70098BAE9 /* FixedHeightImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C575C233958B70098BAE9 /* FixedHeightImageView.swift */; }; DC4FEAE7209E3A7700D4476B /* OCIssue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4FEAE6209E3A7700D4476B /* OCIssue+Extension.swift */; }; DC4FEAEA209E48E800D4476B /* DispatchQueueTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4FEAE9209E48E800D4476B /* DispatchQueueTools.swift */; }; DC576EC022647A070087316D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DC576EC222647A070087316D /* Localizable.strings */; }; @@ -226,6 +245,7 @@ DC7DBA2B207F71E400E7337D /* VectorImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7DBA2A207F71E400E7337D /* VectorImageView.swift */; }; DC7DBA37207F84BF00E7337D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7DBA36207F84BF00E7337D /* main.swift */; }; DC7DBA54207FA80C00E7337D /* TVGImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7DBA53207FA80C00E7337D /* TVGImage.swift */; }; + DC82D6FA23171339001551C5 /* ScanAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC82D6F923171339001551C5 /* ScanAction.swift */; }; DC85493421831B0B00782BA8 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC85493321831B0B00782BA8 /* Tools.swift */; }; DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC854935218331CF00782BA8 /* UserInterfaceSettingsSection.swift */; }; DC8549382183B4CD00782BA8 /* ThemeStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8549372183B4CD00782BA8 /* ThemeStyle+Extensions.swift */; }; @@ -610,19 +630,29 @@ 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDisplayViewController.swift; sourceTree = ""; }; 23F6238020B587EF004FDE8B /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 23FA23E520BFD3D8009A6D73 /* SortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBar.swift; sourceTree = ""; }; + 3900348123A100D3000D8510 /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingTableViewController.swift; sourceTree = ""; }; 39104E0A223991C8002FC02F /* UIButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListTableViewController.swift; sourceTree = ""; }; 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryTableViewController.swift; sourceTree = ""; }; 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadCrumbTableViewController.swift; sourceTree = ""; }; + 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteAction.swift; sourceTree = ""; }; + 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnfavoriteAction.swift; sourceTree = ""; }; + 394E1FFE233E43F5009D2897 /* LinksAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinksAction.swift; sourceTree = ""; }; + 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenItemUserActivity.swift; sourceTree = ""; }; 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; + 3961281522F8730A0087BD3A /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingSharesTableViewController.swift; sourceTree = ""; }; 396BE4C92289500E00B254A9 /* RoundedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLabel.swift; sourceTree = ""; }; + 396C82FA2319AFDD00938262 /* CollaborateAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollaborateAction.swift; sourceTree = ""; }; + 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardSceneAction.swift; sourceTree = ""; }; 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; - 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportFilesController.swift; sourceTree = ""; }; + 397754F22327A33500119FCB /* OpenSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSceneAction.swift; sourceTree = ""; }; 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; - 39880BAA233B5236006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; + 39880BAA233B5236006EA539 /* eu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 39880BB0233B524B006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; + 39954DDF23A39549006B6DC0 /* ar */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + 39954DE023A39625006B6DC0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedInfoView.swift; sourceTree = ""; }; 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 3998F5D422411EDF00B66713 /* BorderedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedLabel.swift; sourceTree = ""; }; @@ -638,17 +668,21 @@ 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientItemCell.swift; sourceTree = ""; }; 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; 39DE75C722F960CF0064C1E2 /* SortMethodTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortMethodTableViewController.swift; sourceTree = ""; }; + 39DED4922369879100CCC62C /* ImportFilesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImportFilesController.swift; path = Import/ImportFilesController.swift; sourceTree = ""; }; 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableHeaderView.swift; sourceTree = ""; }; 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeRoundedButton.swift; sourceTree = ""; }; + 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCommands.swift; sourceTree = ""; }; 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; 3D753147564B1E4F47826109 /* Pods-ownCloud Screenshots Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.debug.xcconfig"; sourceTree = ""; }; 42866B2892DC9EDC65D844E7 /* Pods_ownCloud_Screenshots_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloud_Screenshots_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C05D8A4238708D40073EF50 /* MediaUploadStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadStorage.swift; sourceTree = ""; }; 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMediaUploadTaskExtension.swift; sourceTree = ""; }; 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewController.swift; sourceTree = ""; }; 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewCell.swift; sourceTree = ""; }; 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTests.swift; sourceTree = ""; }; 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; + 4C3E17DA234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingMediaUploadTaskExtension.swift; sourceTree = ""; }; 4C464BE12187AF1400D30602 /* PDFThumbnailCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFThumbnailCollectionViewCell.swift; sourceTree = ""; }; 4C464BE82187AF1400D30602 /* PDFTocTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocTableViewController.swift; sourceTree = ""; }; 4C464BE92187AF1400D30602 /* PDFTocTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocTableViewCell.swift; sourceTree = ""; }; @@ -660,11 +694,14 @@ 4C51727522DE04BD001BC97F /* ScheduledTaskExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledTaskExtension.swift; sourceTree = ""; }; 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundFetchUpdateTaskAction.swift; sourceTree = ""; }; 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledTaskManager.swift; sourceTree = ""; }; + 4C63F0E4231A91230088E8CA /* UIImageView+Thumbnails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Thumbnails.swift"; sourceTree = ""; }; 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoAlbumTableViewController.swift; sourceTree = ""; }; 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoAlbumTableViewCell.swift; sourceTree = ""; }; 4C7295D7228C384E00FA4E68 /* LogFilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFilesViewController.swift; sourceTree = ""; }; 4C82D06F22C9387300835F0B /* MediaDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDisplayViewController.swift; sourceTree = ""; }; 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFilesSettings.swift; sourceTree = ""; }; + 4C96463B238489E4003278B7 /* MediaUploadActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadActivity.swift; sourceTree = ""; }; + 4C9BFA2223158C3F0059CA3E /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Extension.swift"; sourceTree = ""; }; 4CB8ADDF22DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertViewController+SystemPermissions.swift"; sourceTree = ""; }; @@ -750,6 +787,8 @@ D0D9C062DD1E85A838608B0F /* EarlGrey.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EarlGrey.framework; path = Pods/EarlGrey/EarlGrey/EarlGrey.framework; sourceTree = SOURCE_ROOT; }; D6033BC23D1129172E6D6383 /* Pods-ownCloudTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloudTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloudTests/Pods-ownCloudTests.release.xcconfig"; sourceTree = ""; }; D7F3B3E74D4B04F9CAF95C09 /* EarlGrey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EarlGrey.swift; sourceTree = ""; }; + DC0030BF2350B1CE00BB8570 /* NSData+Encoding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Encoding.m"; sourceTree = ""; }; + DC0030C02350B1CE00BB8570 /* NSData+Encoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Encoding.h"; sourceTree = ""; }; DC018F8220A0F56300135198 /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; DC018F8B20A1060A00135198 /* ProgressHUDViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressHUDViewController.swift; sourceTree = ""; }; DC01CDCB212EDDF600FC8E38 /* TextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewController.swift; sourceTree = ""; }; @@ -757,6 +796,7 @@ DC0B37952051541C00189B9A /* ownCloud.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ownCloud.entitlements; sourceTree = ""; }; DC0B37962051681600189B9A /* ThemeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeButton.swift; sourceTree = ""; }; DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmark+Extension.swift"; sourceTree = ""; }; + DC1AC7CF2319ADAE002B7892 /* ScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewController.swift; sourceTree = ""; }; DC1B26FE209CF0D2004715E1 /* IssuesDismissalAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssuesDismissalAnimator.swift; sourceTree = ""; }; DC1B26FF209CF0D2004715E1 /* IssuesPresentationAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssuesPresentationAnimator.swift; sourceTree = ""; }; DC1B2705209CF0D3004715E1 /* CertificateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificateViewController.swift; sourceTree = ""; }; @@ -798,6 +838,7 @@ DC42244B207CAFBB0006A2A6 /* ThemeCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCollection.swift; sourceTree = ""; }; DC42244F207CB2500006A2A6 /* NSObject+ThemeApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+ThemeApplication.swift"; sourceTree = ""; }; DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+OCIssue.swift"; sourceTree = ""; }; + DC4C575C233958B70098BAE9 /* FixedHeightImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedHeightImageView.swift; sourceTree = ""; }; DC4FEAE6209E3A7700D4476B /* OCIssue+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCIssue+Extension.swift"; sourceTree = ""; }; DC4FEAE9209E48E800D4476B /* DispatchQueueTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueTools.swift; sourceTree = ""; }; DC576EC122647A070087316D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -825,6 +866,7 @@ DC7DBA36207F84BF00E7337D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; DC7DBA52207F8BD600E7337D /* img */ = {isa = PBXFileReference; lastKnownFileType = folder; path = img; sourceTree = SOURCE_ROOT; }; DC7DBA53207FA80C00E7337D /* TVGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVGImage.swift; sourceTree = ""; }; + DC82D6F923171339001551C5 /* ScanAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanAction.swift; sourceTree = ""; }; DC85493321831B0B00782BA8 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; DC854935218331CF00782BA8 /* UserInterfaceSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceSettingsSection.swift; sourceTree = ""; }; DC8549372183B4CD00782BA8 /* ThemeStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeStyle+Extensions.swift"; sourceTree = ""; }; @@ -1040,15 +1082,18 @@ 233BDE9E204FEFE500C06732 /* ownCloud */ = { isa = PBXGroup; children = ( + 39DED4922369879100CCC62C /* ImportFilesController.swift */, 233BDE9F204FEFE500C06732 /* AppDelegate.swift */, - 397328E822D6067B006CFAA4 /* Import */, + 3961281522F8730A0087BD3A /* SceneDelegate.swift */, DCF4F1612051925A00189B9A /* Bookmarks */, DC29F09122974F8000F77349 /* FileLists */, 4C51727422DE04BD001BC97F /* Tasks */, DC3BE0DB2077CC13002A0AC0 /* Client */, DC7DF17C205140F400189B9A /* Server List */, DCF4F1802051A91500189B9A /* Settings */, + 39E42D152315286300B82AC3 /* Key Commands */, DC422448207CAED60006A2A6 /* Theming */, + 394E2005233E4765009D2897 /* Window */, DCF4F1622051927200189B9A /* UI Elements */, 239F1314205A69240029F186 /* UIKit Extensions */, DCE5E8B62080D8B8005F60CE /* SDK Extensions */, @@ -1099,6 +1144,7 @@ 236735A421217C2300E5834A /* Actions */ = { isa = PBXGroup; children = ( + DC255E432319AD13007279B1 /* Scanner */, 6E586CF52199A70100F680C4 /* Actions+Extensions */, 2308F93C21467F6200CF0B91 /* ClientDirectoryPickerViewController.swift */, 236735A521217C3500E5834A /* MoreViewController.swift */, @@ -1142,6 +1188,8 @@ 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */, 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */, 39CC8AE5228C12100020253B /* Array+Extension.swift */, + 4C63F0E4231A91230088E8CA /* UIImageView+Thumbnails.swift */, + 3900348123A100D3000D8510 /* UIApplication+Extension.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -1157,6 +1205,7 @@ 4C82D06E22C9384700835F0B /* Media */, DCBF0A5F2280394B00465530 /* Image */, DCBF0A602280395F00465530 /* WebView */, + 4C9BFA2223158C3F0059CA3E /* PreviewViewController.swift */, ); path = Viewer; sourceTree = ""; @@ -1185,12 +1234,20 @@ path = Library; sourceTree = ""; }; - 397328E822D6067B006CFAA4 /* Import */ = { + 394E2005233E4765009D2897 /* Window */ = { isa = PBXGroup; children = ( - 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */, + 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */, ); - path = Import; + path = Window; + sourceTree = ""; + }; + 39E42D152315286300B82AC3 /* Key Commands */ = { + isa = PBXGroup; + children = ( + 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */, + ); + path = "Key Commands"; sourceTree = ""; }; 4C51727422DE04BD001BC97F /* Tasks */ = { @@ -1200,6 +1257,7 @@ 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */, 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */, 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */, + 4C3E17DA234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift */, ); path = Tasks; sourceTree = ""; @@ -1218,6 +1276,8 @@ 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */, 4CB8ADE222DF6BA700F1FEBC /* PHAsset+Upload.swift */, 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */, + 4C96463B238489E4003278B7 /* MediaUploadActivity.swift */, + 4C05D8A4238708D40073EF50 /* MediaUploadStorage.swift */, ); path = "PhotoKit Extensions"; sourceTree = ""; @@ -1325,6 +1385,12 @@ 6E586CF52199A70100F680C4 /* Actions+Extensions */ = { isa = PBXGroup; children = ( + 397754F22327A33500119FCB /* OpenSceneAction.swift */, + 394E1FFE233E43F5009D2897 /* LinksAction.swift */, + 394E1FDB233E3750009D2897 /* UnfavoriteAction.swift */, + 394E1FD9233E2D64009D2897 /* FavoriteAction.swift */, + 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */, + 396C82FA2319AFDD00938262 /* CollaborateAction.swift */, 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */, 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */, 6E586CFF2199A78E00F680C4 /* DeleteAction.swift */, @@ -1342,6 +1408,15 @@ path = "Actions+Extensions"; sourceTree = ""; }; + DC0030BE2350B1CE00BB8570 /* Tools */ = { + isa = PBXGroup; + children = ( + DC0030BF2350B1CE00BB8570 /* NSData+Encoding.m */, + DC0030C02350B1CE00BB8570 /* NSData+Encoding.h */, + ); + path = Tools; + sourceTree = ""; + }; DC1B26FD209CF0D2004715E1 /* Issues Animators */ = { isa = PBXGroup; children = ( @@ -1360,6 +1435,16 @@ path = "Issues Subclasses"; sourceTree = ""; }; + DC255E432319AD13007279B1 /* Scanner */ = { + isa = PBXGroup; + children = ( + DC82D6F923171339001551C5 /* ScanAction.swift */, + DC1AC7CF2319ADAE002B7892 /* ScanViewController.swift */, + DC4C575C233958B70098BAE9 /* FixedHeightImageView.swift */, + ); + path = Scanner; + sourceTree = ""; + }; DC27A19B20CAB5D7008ACB6C /* FileProvider Integration */ = { isa = PBXGroup; children = ( @@ -1651,6 +1736,7 @@ DC774E5A22F44E2A000B11A1 /* Display Settings */, DCC5E443232654C1002E5B84 /* Foundation Extensions */, DC774E5422F44DF6000B11A1 /* SDK Extensions */, + DC0030BE2350B1CE00BB8570 /* Tools */, DC774E5B22F44E4A000B11A1 /* ZIP Archive */, DC774E6522F44EA7000B11A1 /* Resources */, ); @@ -1858,6 +1944,7 @@ DCC085802293F490008CC05C /* DisplaySettings.h in Headers */, DC774E6322F44E6D000B11A1 /* OCCore+BundleImport.h in Headers */, DCC0856E2293F1FD008CC05C /* ownCloudApp.h in Headers */, + DC0030C22350B1CE00BB8570 /* NSData+Encoding.h in Headers */, DCC5E4472326564F002E5B84 /* NSObject+AnnotatedProperties.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2123,6 +2210,7 @@ "pt-PT", "th-TH", eu, + ar, ); mainGroup = 233BDE93204FEFE500C06732; productRefGroup = 233BDE9D204FEFE500C06732 /* Products */; @@ -2495,6 +2583,7 @@ DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */, 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */, 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */, + DC82D6FA23171339001551C5 /* ScanAction.swift in Sources */, 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */, DC018F8C20A1060A00135198 /* ProgressHUDViewController.swift in Sources */, 4CB8ADE922DF6DE200F1FEBC /* AVAsset+Extension.swift in Sources */, @@ -2513,6 +2602,7 @@ DC42244A207CAFAA0006A2A6 /* Theme.swift in Sources */, 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */, 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */, + 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */, DC4FEAEA209E48E800D4476B /* DispatchQueueTools.swift in Sources */, 6ED1B80B21A4004900E16C95 /* CreateFolderAction.swift in Sources */, DC1B2708209CF0D3004715E1 /* IssuesPresentationAnimator.swift in Sources */, @@ -2520,23 +2610,28 @@ DC3BE0DF2077CC14002A0AC0 /* ClientRootViewController.swift in Sources */, 4CB8ADE022DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift in Sources */, 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */, + 394E1FFF233E43F5009D2897 /* LinksAction.swift in Sources */, + 396C82FB2319AFDD00938262 /* CollaborateAction.swift in Sources */, DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */, + DC0030CB2350B75000BB8570 /* ScanViewController.swift in Sources */, 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */, + 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, DC1B2709209CF0D3004715E1 /* CertificateViewController.swift in Sources */, DC248C67213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift in Sources */, + 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */, 6E586CFC2199A72600F680C4 /* OpenInAction.swift in Sources */, DC136582208223F000FC0F60 /* OCBookmark+Extension.swift in Sources */, 23D77FCD212BFBD100DE76F1 /* NamingViewController.swift in Sources */, 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */, 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */, 390B51E02292DBB100935E24 /* SharingTableViewController.swift in Sources */, + DC4C575D233958B70098BAE9 /* FixedHeightImageView.swift in Sources */, 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */, 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */, DCAB9CC923417243009091B6 /* ThemedAlertController.swift in Sources */, 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */, 6E83C78420A33C180066EC23 /* LAContext+Extension.swift in Sources */, 593BAB46209AE1BC00023634 /* PasscodeViewController.swift in Sources */, - 397328EF22D606AC006CFAA4 /* ImportFilesController.swift in Sources */, DC6428D02081406800493A01 /* CollapsibleProgressBar.swift in Sources */, DCB44D87218718BA00DAA4CC /* VendorServices.swift in Sources */, 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */, @@ -2545,11 +2640,13 @@ 39AFC3D1225E72FB00A6D3AE /* GroupSharingTableViewController.swift in Sources */, DC9BFBBD20A1C37B007064B5 /* PasswordManagerAccess.swift in Sources */, 39A5135322608836002CF1AA /* OCShare+Extension.swift in Sources */, + 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */, 23D5241521491C670002C566 /* DisplayViewController.swift in Sources */, DC0B379420514E4700189B9A /* ServerListBookmarkCell.swift in Sources */, DC297965226E4D1100E01BC7 /* PushTransitionDelegate.swift in Sources */, 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */, 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */, + 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */, DCF4F18B2052BA4C00189B9A /* Log.swift in Sources */, DC8549382183B4CD00782BA8 /* ThemeStyle+Extensions.swift in Sources */, 4C82D07022C9387300835F0B /* MediaDisplayViewController.swift in Sources */, @@ -2561,11 +2658,13 @@ DC85572C20513B8C00189B9A /* ServerListTableViewController.swift in Sources */, 233BDEA0204FEFE500C06732 /* AppDelegate.swift in Sources */, 4C51727E22DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift in Sources */, + 4C05D8A5238708D40073EF50 /* MediaUploadStorage.swift in Sources */, 236735A621217C3500E5834A /* MoreViewController.swift in Sources */, 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */, 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */, 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */, 232B01F62126B10900366FA0 /* MoreStaticTableViewController.swift in Sources */, + 4C96463C238489E4003278B7 /* MediaUploadActivity.swift in Sources */, 6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */, 4CC4A21922FB4F4C00AE7E2C /* MediaUploadQueue.swift in Sources */, 593BAB97209F8A0500023634 /* AppLockManager.swift in Sources */, @@ -2576,6 +2675,7 @@ DC85493421831B0B00782BA8 /* Tools.swift in Sources */, DCFED972208095E200A2D984 /* ClientItemCell.swift in Sources */, 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */, + 4C63F0E5231A91230088E8CA /* UIImageView+Thumbnails.swift in Sources */, 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */, 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */, 23E22BB720C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift in Sources */, @@ -2603,11 +2703,15 @@ 6E37F48B2188B27D00CF16CA /* Action.swift in Sources */, DC3BE0DE2077CC14002A0AC0 /* ClientQueryViewController.swift in Sources */, 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */, + 39DED4932369879100CCC62C /* ImportFilesController.swift in Sources */, 23C56538212167BE00BD4B47 /* CardTransitionDelegate.swift in Sources */, + 3900348223A100D3000D8510 /* UIApplication+Extension.swift in Sources */, + 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */, 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */, 6E3A104D219D6F0100F90C96 /* DuplicateAction.swift in Sources */, DC1B270A209CF0D3004715E1 /* ConnectionIssueViewController.swift in Sources */, DC0B37972051681600189B9A /* ThemeButton.swift in Sources */, + 394E200C233E477F009D2897 /* OpenItemUserActivity.swift in Sources */, DCF4F17B20519F9D00189B9A /* StaticTableViewSection.swift in Sources */, DC243BFF2317B446004FBB5C /* ThemeWindow.swift in Sources */, 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */, @@ -2637,6 +2741,7 @@ DC33939622E0747400DD3DA4 /* MakeAvailableOfflineAction.swift in Sources */, 6E586CFE2199A75900F680C4 /* MoveAction.swift in Sources */, DC625148225CEB2C00736874 /* UploadFileAction.swift in Sources */, + 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */, DC4FEAE7209E3A7700D4476B /* OCIssue+Extension.swift in Sources */, DC434D1320D7A8F100740056 /* UIAlertController+OCIssue.swift in Sources */, 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */, @@ -2649,6 +2754,7 @@ 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */, 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */, DC422450207CB2500006A2A6 /* NSObject+ThemeApplication.swift in Sources */, + 394E1FDA233E2D64009D2897 /* FavoriteAction.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2702,6 +2808,7 @@ buildActionMask = 2147483647; files = ( DC774E6422F44E6D000B11A1 /* OCCore+BundleImport.m in Sources */, + DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */, DC774E5F22F44E57000B11A1 /* ZIPArchive.m in Sources */, DCC0857F2293F48D008CC05C /* DisplaySettings.m in Sources */, ); @@ -2868,6 +2975,7 @@ 59AAD98D21F786CC00D15F07 /* pt-PT */, 59AAD98F21F7870B00D15F07 /* th-TH */, 39880BAA233B5236006EA539 /* eu */, + 39954DDF23A39549006B6DC0 /* ar */, ); name = Localizable.strings; sourceTree = ""; @@ -2892,6 +3000,7 @@ 59AAD98C21F786CB00D15F07 /* pt-PT */, 59AAD98E21F7870B00D15F07 /* th-TH */, 39880BB0233B524B006EA539 /* eu */, + 39954DE023A39625006B6DC0 /* ar */, ); name = InfoPlist.strings; sourceTree = ""; @@ -2919,7 +3028,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_SHORT_VERSION = 1.1.2; + APP_SHORT_VERSION = 1.2.0; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2981,7 +3090,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_SHORT_VERSION = 1.1.2; + APP_SHORT_VERSION = 1.2.0; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -3040,7 +3149,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3068,7 +3177,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3214,11 +3323,11 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -3247,11 +3356,11 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 144; + CURRENT_PROJECT_VERSION = 147; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 144; + DYLIB_CURRENT_VERSION = 147; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/ownCloud/AVFoundation Extensions/AVAsset+Extension.swift b/ownCloud/AVFoundation Extensions/AVAsset+Extension.swift index a2f54c2dc..5058d8589 100644 --- a/ownCloud/AVFoundation Extensions/AVAsset+Extension.swift +++ b/ownCloud/AVFoundation Extensions/AVAsset+Extension.swift @@ -19,28 +19,42 @@ import AVFoundation extension AVAsset { - func exportVideo(targetURL:URL, type:AVFileType, completion:@escaping (_ success:Bool) -> Void) { + func exportVideo(targetURL:URL, type:AVFileType) -> Bool { if self.isExportable { - + let group = DispatchGroup() let preset = AVAssetExportPresetHighestQuality + var compatiblePreset = false + var exportSuccess = false + group.enter() AVAssetExportSession.determineCompatibility(ofExportPreset: preset, with: self, outputFileType: type, completionHandler: { (isCompatible) in - if !isCompatible { - completion(false) - }}) + compatiblePreset = isCompatible + group.leave() + }) - guard let export = AVAssetExportSession(asset: self, presetName: preset) else { - completion(false) - return - } + group.wait() + + if compatiblePreset { + guard let export = AVAssetExportSession(asset: self, presetName: preset) else { + return false + } + // Configure export session + export.outputFileType = type + export.outputURL = targetURL - export.outputFileType = type - export.outputURL = targetURL - export.exportAsynchronously { - completion( export.status == .completed ) + // Start export + group.enter() + export.exportAsynchronously { + exportSuccess = (export.status == .completed) + group.leave() + } + + group.wait() } - } else { - completion(false) + + return exportSuccess } + + return false } } diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 42186fc8b..8f7713d5f 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -58,8 +58,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Display Extensions OCExtensionManager.shared.addExtension(WebViewDisplayViewController.displayExtension) OCExtensionManager.shared.addExtension(PDFViewerViewController.displayExtension) - OCExtensionManager.shared.addExtension(ImageDisplayViewController.displayExtension) + OCExtensionManager.shared.addExtension(PreviewViewController.displayExtension) OCExtensionManager.shared.addExtension(MediaDisplayViewController.displayExtension) + OCExtensionManager.shared.addExtension(ImageDisplayViewController.displayExtension) // Action Extensions OCExtensionManager.shared.addExtension(OpenInAction.actionExtension) @@ -74,10 +75,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(UnshareAction.actionExtension) OCExtensionManager.shared.addExtension(MakeAvailableOfflineAction.actionExtension) OCExtensionManager.shared.addExtension(MakeUnavailableOfflineAction.actionExtension) + OCExtensionManager.shared.addExtension(CollaborateAction.actionExtension) + OCExtensionManager.shared.addExtension(LinksAction.actionExtension) + OCExtensionManager.shared.addExtension(FavoriteAction.actionExtension) + OCExtensionManager.shared.addExtension(UnfavoriteAction.actionExtension) + // OCExtensionManager.shared.addExtension(ScanAction.actionExtension) + + if #available(iOS 13.0, *), UIDevice.current.isIpad() { + OCExtensionManager.shared.addExtension(DiscardSceneAction.actionExtension) + OCExtensionManager.shared.addExtension(OpenSceneAction.actionExtension) + } + // Task extensions OCExtensionManager.shared.addExtension(BackgroundFetchUpdateTaskAction.taskExtension) OCExtensionManager.shared.addExtension(InstantMediaUploadTaskExtension.taskExtension) + OCExtensionManager.shared.addExtension(PendingMediaUploadTaskExtension.taskExtension) + // Theming Theme.shared.activeCollection = ThemeCollection(with: ThemeStyle.preferredStyle) // Licenses @@ -126,4 +140,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCCoreManager.shared.handleEvents(forBackgroundURLSession: identifier, completionHandler: completionHandler) } + + // MARK: UISceneSession Lifecycle + @available(iOS 13.0, *) + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + @available(iOS 13.0, *) + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + } } diff --git a/ownCloud/Bookmarks/BookmarkInfoViewController.swift b/ownCloud/Bookmarks/BookmarkInfoViewController.swift index 4de509d0e..4bca63f7b 100644 --- a/ownCloud/Bookmarks/BookmarkInfoViewController.swift +++ b/ownCloud/Bookmarks/BookmarkInfoViewController.swift @@ -58,7 +58,7 @@ class BookmarkInfoViewController: StaticTableViewController { self?.present(alertController, animated: true, completion: nil) } - }, title: "Include available offline files".localized, value: false, identifier: "row-include-available-offline") + }, title: "Include available offline files".localized, value: false, identifier: "row-include-available-offline") let deleteLocalFilesRow = StaticTableViewRow(buttonWithAction: { [weak self] (row, _) in if let bookmark = self?.bookmark { diff --git a/ownCloud/Client/Actions/Action.swift b/ownCloud/Client/Actions/Action.swift index f18a1fff3..3d09fbc3d 100644 --- a/ownCloud/Client/Actions/Action.swift +++ b/ownCloud/Client/Actions/Action.swift @@ -59,18 +59,23 @@ extension OCExtensionLocationIdentifier { static let moreFolder: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreFolder") //!< Present in "more" options for a whole folder static let toolbar: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("toolbar") //!< Present in a toolbar static let folderAction: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("folderAction") //!< Present in the alert sheet when the folder action bar button is pressed + static let keyboardShortcut: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("keyboardShortcut") //!< Currently used for UIKeyCommand } class ActionExtension: OCExtension { // MARK: - Custom Instance Properties. var name: String var category: ActionCategory + var keyCommand: String? + var keyModifierFlags: UIKeyModifierFlags? // MARK: - Init & Deinit - init(name: String, category: ActionCategory = .normal, identifier: OCExtensionIdentifier, locations: [OCExtensionLocationIdentifier]?, features: [String : Any]?, objectProvider: OCExtensionObjectProvider?, customMatcher: OCExtensionCustomContextMatcher?) { + init(name: String, category: ActionCategory = .normal, identifier: OCExtensionIdentifier, locations: [OCExtensionLocationIdentifier]?, features: [String : Any]?, objectProvider: OCExtensionObjectProvider?, customMatcher: OCExtensionCustomContextMatcher?, keyCommand: String?, keyModifierFlags: UIKeyModifierFlags?) { self.name = name self.category = category + self.keyCommand = keyCommand + self.keyModifierFlags = keyModifierFlags super.init(identifier: identifier, type: .action, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher) } @@ -82,14 +87,16 @@ class ActionContext: OCExtensionContext { weak var core: OCCore? weak var query: OCQuery? var items: [OCItem] + weak var sender: AnyObject? // MARK: - Init & Deinit. - init(viewController: UIViewController, core: OCCore, query: OCQuery? = nil, items: [OCItem], location: OCExtensionLocation, requirements: [String : Any]? = nil, preferences: [String : Any]? = nil) { + init(viewController: UIViewController, core: OCCore, query: OCQuery? = nil, items: [OCItem], location: OCExtensionLocation, sender: AnyObject? = nil, requirements: [String : Any]? = nil, preferences: [String : Any]? = nil) { self.items = items super.init() self.viewController = viewController + self.sender = sender self.core = core self.location = location @@ -104,6 +111,8 @@ class Action : NSObject { class var identifier : OCExtensionIdentifier? { return nil } class var category : ActionCategory? { return .normal } class var name : String? { return nil } + class var keyCommand : String? { return nil } + class var keyModifierFlags : UIKeyModifierFlags? { return nil } class var locations : [OCExtensionLocationIdentifier]? { return nil } class var features : [String : Any]? { return nil } @@ -134,7 +143,7 @@ class Action : NSObject { // Additional filtering (f.ex. via OCClassSettings, Settings) goes here } - return ActionExtension(name: name!, category: category!, identifier: identifier!, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher) + return ActionExtension(name: name!, category: category!, identifier: identifier!, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher, keyCommand: keyCommand, keyModifierFlags: keyModifierFlags) } // MARK: - Extension matching @@ -246,7 +255,7 @@ class Action : NSObject { var actionWillRunHandler: ActionWillRunHandler? // to be filled before calling run(), provideStaticRow(), provideContextualAction(), etc. if desired // MARK: - Action implementation - func perform() { + @objc func perform() { self.willRun({ OnMainThread { self.run() diff --git a/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift new file mode 100644 index 000000000..bbe166e18 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift @@ -0,0 +1,54 @@ +// +// CollaborateAction.swift +// ownCloud +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class CollaborateAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.collaborate") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Sharing".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.keyboardShortcut] } + override class var keyCommand : String? { return "S" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.count == 1, let core = forContext.core, core.connectionStatus == .online, core.connection.capabilities?.sharingAPIEnabled == 1 { + return .first + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard context.items.count == 1, let item = context.items.first, let viewController = context.viewController, let core = self.core else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + let groupSharingController = GroupSharingTableViewController(core: core, item: item) + let navigationController = ThemeNavigationController(rootViewController: groupSharingController) + viewController.present(navigationController, animated: true) + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(named: "group") + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift index 961670e98..c05100104 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift @@ -23,7 +23,9 @@ class CopyAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.copy") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Copy".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "C" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift index a90982706..a33002b96 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift @@ -22,7 +22,9 @@ class CreateFolderAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.createFolder") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Create folder".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [ .folderAction ] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction, .keyboardShortcut] } + override class var keyCommand : String? { return "N" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift index a985a34b0..eac336d01 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift @@ -22,7 +22,9 @@ class DeleteAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.delete") } override class var category : ActionCategory? { return .destructive } override class var name : String? { return "Delete".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .tableRow, .moreFolder, .toolbar] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .tableRow, .moreFolder, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "\u{08}" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift new file mode 100644 index 000000000..51dec0c9c --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift @@ -0,0 +1,62 @@ +// +// DiscardSceneAction.swift +// ownCloud +// +// Created by Matthias Hühne on 06.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 MobileCoreServices + +@available(iOS 13.0, *) +class DiscardSceneAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.discardscene") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Close Window".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreFolder, .keyboardShortcut] } + override class var keyCommand : String? { return "W" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + + if UIDevice.current.isIpad() { + if let viewController = forContext.viewController, viewController.view.window?.windowScene?.userActivity != nil { + return .first + } + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard let viewController = context.viewController else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + if UIDevice.current.isIpad() { + if let scene = viewController.view.window?.windowScene { + UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil) { (_) in + } + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(systemName: "xmark.square")?.tinted(with: Theme.shared.activeCollection.tintColor) + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift index ea384cbeb..060f8e181 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift @@ -24,7 +24,9 @@ class DuplicateAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.duplicate") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Duplicate".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "D" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/FavoriteAction.swift b/ownCloud/Client/Actions/Actions+Extensions/FavoriteAction.swift new file mode 100644 index 000000000..57736bca7 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/FavoriteAction.swift @@ -0,0 +1,64 @@ +// +// FavoriteAction.swift +// ownCloud +// +// Created by Matthias Hühne on 27/09/2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 Foundation +import ownCloudSDK +import MobileCoreServices + +class FavoriteAction : Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.favorite") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Favorite".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.keyboardShortcut] } + override class var keyCommand : String? { return "F" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .shift] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.filter({return $0.isRoot}).count > 0 { + return .none + } else if forContext.items.count > 0, let item = forContext.items.first, item.isFavorite == true { + return .none + } + + return .middle + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let item = context.items.first, let core = core else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + + item.isFavorite = true + + core.update(item, properties: [OCItemPropertyName.isFavorite], options: nil, resultHandler: { (error, _, _, _) in + if error == nil { + } + }) + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "star") + } + + return nil + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/LinksAction.swift b/ownCloud/Client/Actions/Actions+Extensions/LinksAction.swift new file mode 100644 index 000000000..e198b0258 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/LinksAction.swift @@ -0,0 +1,54 @@ +// +// LinksAction.swift +// ownCloud +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class LinksAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.links") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Links".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.keyboardShortcut] } + override class var keyCommand : String? { return "L" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.count == 1, let core = forContext.core, core.connectionStatus == .online { + return .first + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard context.items.count == 1, let item = context.items.first, let viewController = context.viewController, let core = self.core else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + let publicLinkController = PublicLinkTableViewController(core: core, item: item) + let navigationController = ThemeNavigationController(rootViewController: publicLinkController) + viewController.present(navigationController, animated: true) + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(named: "link") + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/MakeAvailableOfflineAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MakeAvailableOfflineAction.swift index e57589d78..240765b2c 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MakeAvailableOfflineAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MakeAvailableOfflineAction.swift @@ -23,7 +23,9 @@ class MakeAvailableOfflineAction: Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.makeAvailableOffline") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Make available offline".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .keyboardShortcut] } + override class var keyCommand : String? { return "O" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } // MARK: - Extension matching override class func applicablePosition(forContext context: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/MakeUnavailableOfflineAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MakeUnavailableOfflineAction.swift index 43c0f0323..535b5ac81 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MakeUnavailableOfflineAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MakeUnavailableOfflineAction.swift @@ -23,7 +23,9 @@ class MakeUnavailableOfflineAction: Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.makeUnavailableOffline") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Available Offline".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .keyboardShortcut] } + override class var keyCommand : String? { return "O" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } // MARK: - Extension matching override class func applicablePosition(forContext context: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift index c14ce4893..8b3682d4f 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift @@ -22,7 +22,9 @@ class MoveAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.move") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Move".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "V" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift index 9aaa2f340..77a841c8c 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift @@ -22,7 +22,9 @@ class OpenInAction: Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openin") } override class var category : ActionCategory? { return .normal } override class var name : String { return "Open in".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .toolbar] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "O" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } override class func applicablePosition(forContext: ActionContext) -> ActionPosition { if forContext.items.contains(where: {$0.type == .collection}) { @@ -52,7 +54,6 @@ class OpenInAction: Action { hostViewController?.present(alertController, animated: true) } else { guard let files = files, files.count > 0, let viewController = hostViewController else { return } - // UIDocumentInteractionController can only be used with a single file if files.count == 1 { if let fileURL = files.first?.url { @@ -68,7 +69,24 @@ class OpenInAction: Action { // Present UIDocumentInteractionController self.interactionController = UIDocumentInteractionController(url: fileURL) self.interactionController?.delegate = self - self.interactionController?.presentOptionsMenu(from: .zero, in: viewController.view, animated: true) + + if let sender = self.context.sender as? UITabBarController { + var sourceRect = sender.view.frame + sourceRect.origin.y = viewController.view.frame.size.height + sourceRect.size.width = 0.0 + sourceRect.size.height = 0.0 + + self.interactionController?.presentOptionsMenu(from: sourceRect, in: sender.view, animated: true) + } else if let barButtonItem = self.context.sender as? UIBarButtonItem { + self.interactionController?.presentOptionsMenu(from: barButtonItem, animated: true) + } else if let cell = self.context.sender as? UITableViewCell, let clientQueryViewController = viewController as? ClientQueryViewController { + if let indexPath = clientQueryViewController.tableView.indexPath(for: cell) { + let cellRect = clientQueryViewController.tableView.rectForRow(at: indexPath) + self.interactionController?.presentOptionsMenu(from: cellRect, in: clientQueryViewController.tableView, animated: true) + } + } else { + self.interactionController?.presentOptionsMenu(from: viewController.view.frame, in: viewController.view, animated: true) + } } } else { // Handle multiple files with a fallback solution @@ -78,7 +96,18 @@ class OpenInAction: Action { let activityController = UIActivityViewController(activityItems: urls, applicationActivities: nil) if UIDevice.current.isIpad() { - activityController.popoverPresentationController?.sourceView = viewController.view + if let sender = self.context.sender as? UITabBarController { + var sourceRect = sender.view.frame + sourceRect.origin.y = viewController.view.frame.size.height + sourceRect.size.width = 0.0 + sourceRect.size.height = 0.0 + + activityController.popoverPresentationController?.sourceView = sender.view + activityController.popoverPresentationController?.sourceRect = sourceRect + } else { + activityController.popoverPresentationController?.sourceView = viewController.view + activityController.popoverPresentationController?.sourceRect = viewController.view.frame + } } viewController.present(activityController, animated: true, completion: nil) @@ -92,7 +121,7 @@ class OpenInAction: Action { } override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { - if location == .moreItem { + if location == .moreItem || location == .moreFolder { return UIImage(named: "open-in") } diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift new file mode 100644 index 000000000..9d648455a --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift @@ -0,0 +1,62 @@ +// +// OpenSceneAction.swift +// ownCloud +// +// Created by Matthias Hühne on 10.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 MobileCoreServices + +@available(iOS 13.0, *) +class OpenSceneAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openscene") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Open in a new Window".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .keyboardShortcut] } + override class var keyCommand : String? { return "O" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .shift] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + + if UIDevice.current.isIpad() { + if forContext.items.count == 1 { + return .first + } + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard let viewController = context.viewController else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + if UIDevice.current.isIpad() { + if context.items.count == 1, let item = context.items.first, let tabBarController = viewController.tabBarController as? ClientRootViewController { + let activity = OpenItemUserActivity(detailItem: item, detailBookmark: tabBarController.bookmark) + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity.openItemUserActivity, options: nil) + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(systemName: "uiwindow.split.2x1")?.tinted(with: Theme.shared.activeCollection.tintColor) + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift b/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift index 6144931e8..81d58454b 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift @@ -22,7 +22,9 @@ class RenameAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.rename") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Rename".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder] } + override class var keyCommand : String? { return "\r" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .keyboardShortcut] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/UnfavoriteAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UnfavoriteAction.swift new file mode 100644 index 000000000..bd5057626 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/UnfavoriteAction.swift @@ -0,0 +1,64 @@ +// +// UnfavoriteAction.swift +// ownCloud +// +// Created by Matthias Hühne on 27/09/2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 Foundation +import ownCloudSDK +import MobileCoreServices + +class UnfavoriteAction : Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.unfavorite") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Unfavorite".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.keyboardShortcut] } + override class var keyCommand : String? { return "F" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .shift] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.filter({return $0.isRoot}).count > 0 { + return .none + } else if forContext.items.count > 0, let item = forContext.items.first, item.isFavorite == false { + return .none + } + + return .middle + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let item = context.items.first, let core = core else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + + item.isFavorite = false + + core.update(item, properties: [OCItemPropertyName.isFavorite], options: nil, resultHandler: { (error, _, _, _) in + if error == nil { + } + }) + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "star") + } + + return nil + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift index bbb37c596..21c6570b1 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift @@ -22,6 +22,9 @@ extension Array where Element: OCItem { var sharedWithUser : [OCItem] { return self.filter({ (item) -> Bool in return item.isSharedWithUser }) } + var isShared : [OCItem] { + return self.filter({ (item) -> Bool in return item.isShared }) + } } class UnshareAction : Action { diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift index d60b8e8e7..41ad2ee22 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift @@ -24,7 +24,9 @@ class UploadFileAction: UploadBaseAction { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.uploadfile") } override class var category : ActionCategory? { return .normal } override class var name : String { return "Upload file".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction, .keyboardShortcut] } + override class var keyCommand : String? { return "+" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } private struct AssociatedKeys { static var actionKey = "action" diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift index eb0d876ed..833f8b915 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift @@ -25,7 +25,9 @@ class UploadMediaAction: UploadBaseAction { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.uploadphotos") } override class var category : ActionCategory? { return .normal } override class var name : String { return "Upload from your photo library".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction, .keyboardShortcut] } + override class var keyCommand : String? { return "M" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } private struct AssociatedKeys { static var actionKey = "action" @@ -55,15 +57,11 @@ class UploadMediaAction: UploadBaseAction { photoAlbumViewController.selectionCallback = {(assets) in self.completed() - guard let rootItem = self.context.items.first else { return } + guard let path = self.context.items.first?.path else { return } - MediaUploadQueue.shared.uploadAssets(assets, with: self.core, at: rootItem, progressHandler: { (progress) in - if progress.isFinished || progress.isCancelled { - self.unpublish(progress: progress) - } else { - self.publish(progress: progress) - } - }) + guard let bookmark = self.core?.bookmark else { return } + + MediaUploadQueue.shared.addUploads(assets, for: bookmark, at: path) } let navigationController = ThemeNavigationController(rootViewController: photoAlbumViewController) diff --git a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift index d2ed69c35..5615d3058 100644 --- a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift +++ b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift @@ -27,7 +27,7 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { private let SELECT_BUTTON_HEIGHT: CGFloat = 44.0 // MARK: - Instance Properties - private var selectButton: UIBarButtonItem? + var selectButton: UIBarButtonItem? private var selectButtonTitle: String? private var cancelBarButton: UIBarButtonItem? @@ -219,7 +219,7 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { // Actions for Create Folder if let core = self.core, let rootItem = query.rootItem { let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .folderAction) - let actionContext = ActionContext(viewController: self, core: core, items: [rootItem], location: actionsLocation) + let actionContext = ActionContext(viewController: self, core: core, items: [rootItem], location: actionsLocation, sender: sender) let actions = Action.sortedApplicableActions(for: actionContext).filter { (action) -> Bool in if action.actionExtension.identifier == OCExtensionIdentifier("com.owncloud.action.createFolder") { diff --git a/ownCloud/Client/Actions/NamingViewController.swift b/ownCloud/Client/Actions/NamingViewController.swift index 927124620..c9432b067 100644 --- a/ownCloud/Client/Actions/NamingViewController.swift +++ b/ownCloud/Client/Actions/NamingViewController.swift @@ -112,23 +112,10 @@ class NamingViewController: UIViewController, Themeable { stackViewLeftAnchorConstraint = stackView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0) stackViewRightAnchorConstraint = stackView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0) - if let item = item { + if let item = item, let core = core { nameTextField.text = item.name thumbnailImageView.image = item.icon(fitInSize: thumbnailSize) - - if item.thumbnailAvailability != .none { - _ = core!.retrieveThumbnail(for: item, maximumSize: self.thumbnailSize, scale: 0, retrieveHandler: { (error, _, _, thumbnail, _, _) in - _ = thumbnail?.requestImage(for: self.thumbnailSize, scale: 0, withCompletionHandler: { [weak self] (thumbnail, error, _, image) in - if error == nil, - image != nil, - item.itemVersionIdentifier == thumbnail?.itemVersionIdentifier { - OnMainThread { - self?.thumbnailImageView.image = image - } - } - }) - }) - } + thumbnailImageView.setThumbnailImage(using: core, from: item, with: thumbnailSize) } else { nameTextField.text = defaultName thumbnailImageView.image = Theme.shared.image(for: "folder", size: thumbnailSize) diff --git a/ownCloud/Client/Actions/Scanner/FixedHeightImageView.swift b/ownCloud/Client/Actions/Scanner/FixedHeightImageView.swift new file mode 100644 index 000000000..c9aa097ba --- /dev/null +++ b/ownCloud/Client/Actions/Scanner/FixedHeightImageView.swift @@ -0,0 +1,44 @@ +// +// FixedHeightImageView.swift +// ownCloud +// +// Created by Felix Schwarz on 23.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class FixedHeightImageView : UIImageView { + var aspectHeight : CGFloat? { + didSet { + self.invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + if let aspectHeight = aspectHeight, let image = image { + let imageSize = image.size + var intrinsicSize : CGSize = CGSize( + width: imageSize.width * aspectHeight / imageSize.height, + height: aspectHeight + ) + + if intrinsicSize.width < 1 { intrinsicSize.width = 1 } + if intrinsicSize.height < 1 { intrinsicSize.height = 1 } + + return intrinsicSize + } + + return super.intrinsicContentSize + } +} diff --git a/ownCloud/Client/Actions/Scanner/ScanAction.swift b/ownCloud/Client/Actions/Scanner/ScanAction.swift new file mode 100644 index 000000000..a60334138 --- /dev/null +++ b/ownCloud/Client/Actions/Scanner/ScanAction.swift @@ -0,0 +1,102 @@ +// +// ScanAction.swift +// ownCloud +// +// Created by Felix Schwarz on 28.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 ownCloudSDK +import VisionKit + +class ScanAction: Action, VNDocumentCameraViewControllerDelegate { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.scan") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Scan document".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [ .folderAction, .keyboardShortcut ] } + override class var keyCommand : String? { return "S" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.count > 1 { + return .none + } + + if forContext.items.first?.type != OCItemType.collection { + return .none + } + + if #available(iOS 13.0, *) { + return .middle + } else { + return .none + } + } + + // MARK: - Action implementation + override func run() { + guard let viewController = context.viewController else { + return + } + + guard context.items.count > 0 else { + completed(with: NSError(ocError: .itemNotFound)) + return + } + + guard let targetFolderItem = context.items.first, let itemPath = targetFolderItem.path else { + completed(with: NSError(ocError: .itemNotFound)) + return + } + + guard let core = context.core else { + completed(with: NSError(ocError: .internal)) + return + } + + if #available(iOS 13.0, *) { + Scanner.scan(on: viewController) { [weak core] (_, _, scan) in + if let pageCount = scan?.pageCount, pageCount > 0, let scannedPages = scan?.scannedPages { + var filename : String? = scan?.title + + if filename?.count == 0 { + filename = nil + } + + core?.suggestUnusedNameBased(on: filename ?? "\("Scan".localized) \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .medium)).pdf", atPath: itemPath, isDirectory: true, using: .bracketed, filteredBy: nil, resultHandler: { (suggestedName, _) in + guard let suggestedName = suggestedName else { return } + + OnMainThread { + let navigationController = ThemeNavigationController(rootViewController: ScanViewController(with: scannedPages, core: core, fileName: suggestedName, targetFolder: targetFolderItem)) + viewController.present(navigationController, animated: true) + } + }) + } + + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .folderAction { + if #available(iOS 13.0, *) { + return UIImage(systemName: "doc.text.viewfinder", withConfiguration: UIImage.SymbolConfiguration(pointSize: 26, weight: .regular)) + } +// Theme.shared.add(tvgResourceFor: "application-pdf") +// return Theme.shared.image(for: "application-pdf", size: CGSize(width: 30.0, height: 30.0))!.withRenderingMode(.alwaysTemplate) + } + + return nil + } +} diff --git a/ownCloud/Client/Actions/Scanner/ScanViewController.swift b/ownCloud/Client/Actions/Scanner/ScanViewController.swift new file mode 100644 index 000000000..f5f0b42a4 --- /dev/null +++ b/ownCloud/Client/Actions/Scanner/ScanViewController.swift @@ -0,0 +1,487 @@ +// +// ScanViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 VisionKit +import ImageIO +import ownCloudSDK +import ownCloudApp + +class ScanPage { + var image : UIImage + + init(with image: UIImage) { + self.image = image + } +} + +class ScanPageCell : UICollectionViewCell, Themeable { + private var imageView : FixedHeightImageView? + + var page : ScanPage? { + didSet { + self.imageView?.image = page?.image + self.imageView?.invalidateIntrinsicContentSize() + } + } + + var aspectHeight : CGFloat = 0 { + didSet { + if aspectHeight != 0 { + imageView?.aspectHeight = aspectHeight + } + } + } + + private let cellShadowRadius : CGFloat = 6 + private let cellShadowOpacity : Float = 0.2 + private let cellShadowOffset : CGSize = CGSize(width: 0, height: 4) + + override init(frame: CGRect) { + super.init(frame: frame) + + imageView = FixedHeightImageView() + imageView?.translatesAutoresizingMaskIntoConstraints = false + imageView?.contentMode = .scaleAspectFill + imageView?.setContentCompressionResistancePriority(.required, for: .horizontal) + imageView?.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + guard let imageView = imageView else { return } + + self.contentView.addSubview(imageView) + + NSLayoutConstraint.activate([ + imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + imageView.topAnchor.constraint(equalTo: contentView.topAnchor), + imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowRadius = cellShadowRadius + self.layer.shadowOpacity = cellShadowOpacity + self.layer.shadowOffset = cellShadowOffset + + Theme.shared.register(client: self, applyImmediately: true) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + Theme.shared.unregister(client: self) + } + + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.backgroundColor = collection.tableRowColors.backgroundColor + } +} + +class ScanPagesCollectionViewController : UICollectionViewController, UICollectionViewDelegateFlowLayout, Themeable { + var flowLayout : UICollectionViewFlowLayout + + var pages : [ScanPage] = [] { + didSet { + self.collectionView.reloadData() + } + } + + private var height : CGFloat + private let verticalPadding : CGFloat = 25 + private let horizontalPadding : CGFloat = 20 + private let thumbnailAspectRatio : CGFloat = 0.6 + + init (height: CGFloat) { + self.height = height + + flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.estimatedItemSize = CGSize(width: floor(height * thumbnailAspectRatio), height: height) + flowLayout.sectionInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding) + flowLayout.minimumInteritemSpacing = 20 + + super.init(collectionViewLayout: flowLayout) + + self.collectionView.alwaysBounceHorizontal = true + self.collectionView.register(ScanPageCell.self, forCellWithReuseIdentifier: "page") + + Theme.shared.register(client: self, applyImmediately: true) + + } + + deinit { + Theme.shared.unregister(client: self) + } + + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.collectionView.backgroundColor = collection.tableBackgroundColor + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return pages.count + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "page", for: indexPath) + + if let pageCell = cell as? ScanPageCell { + pageCell.aspectHeight = height - (verticalPadding * 2) + pageCell.page = pages[indexPath.item] + } + + return cell + } +} + +extension CGRect { + func asData() -> NSData { + return NSData(from: self) + } +} + +struct ScanExportFormat { + var name : String + var suffix : String + var supportsMultiPage : Bool = false + var exporter : ((_ baseURL: URL, _ pages: [ScanPage]) -> ([URL]?))? +} + +@available(iOS 13.0, *) +class ScanViewController: StaticTableViewController { + weak var core : OCCore? + var targetFolderItem : OCItem? + + // Pages + var pagesSection : StaticTableViewSection + var pagesCollectionViewController : ScanPagesCollectionViewController? + + // Save + var saveSection : StaticTableViewSection + var fileNameRow : StaticTableViewRow? + + // Options + var optionsSection : StaticTableViewSection + var formatSegmentedControl : UISegmentedControl? + var oneFilePerPageRow : StaticTableViewRow? + + private static func exportAsImage(format: String, baseURL: URL, pages: [ScanPage]) -> [URL]? { + var files : [URL] = [] + + for page in pages { + let targetURL = baseURL.appendingPathComponent(UUID().uuidString) + var imageData : Data? + + switch format { + case "jpeg": + imageData = page.image.jpegData(compressionQuality: 0.75) + + case "png": + imageData = page.image.pngData() + + default: break + } + + if let imageData = imageData { + if (try? imageData.write(to: targetURL)) != nil { + // Writing successful + files.append(targetURL) + } else { + // Error writing + return nil + } + } else { + // Error + return nil + } + } + + return files.count > 0 ? files : nil + } + + private static func exportAsPDF(pages: [ScanPage], to url: URL) -> Bool { + // Using Quartz here to store the exported images JPEG-compressed - something that PDFKit currently doesn't support + // (a naive PDFKit implementation using PDFDocument and PDFPage produces ~ 11x larger files (31 MB vs. 2.7 MB for a two page scan)) + + if let pdfContext = CGContext(url as CFURL, mediaBox: nil, nil) { + defer { + pdfContext.closePDF() + } + + for page in pages { + if let jpegData = page.image.jpegData(compressionQuality: 0.75), + let dataProvider = CGDataProvider(data: jpegData as CFData), + let jpegImage = CGImage(jpegDataProviderSource: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) { + let mediaBoxRect = CGRect(x: 0, y: 0, width: page.image.size.width, height: page.image.size.height) + + pdfContext.beginPDFPage([ + kCGPDFContextMediaBox : mediaBoxRect.asData() + ] as CFDictionary) + + pdfContext.draw(jpegImage, in: mediaBoxRect) + + pdfContext.endPDFPage() + } else { + return false + } + } + + return true + } + + return false + } + + let availableExportFormats : [ScanExportFormat] = [ + ScanExportFormat(name: "PDF", suffix: "pdf", supportsMultiPage: true, exporter: { (baseURL, pages) in + let targetURL = baseURL.appendingPathComponent(UUID().uuidString) + + if ScanViewController.exportAsPDF(pages: pages, to: targetURL) { + // Writing successful + return [targetURL] + } else { + // Error writing + return nil + } + }), + ScanExportFormat(name: "JPEG", suffix: "jpg", exporter: { (baseURL, pages) in + return ScanViewController.exportAsImage(format: "jpeg", baseURL: baseURL, pages: pages) + }), + ScanExportFormat(name: "PNG", suffix: "png", exporter: { (baseURL, pages) in + return ScanViewController.exportAsImage(format: "png", baseURL: baseURL, pages: pages) + }) + ] + var exportFormat : ScanExportFormat? { + didSet { + if let fileName = fileNameRow?.value as? NSString { + fileNameRow?.value = fileName.deletingPathExtension + "." + (exportFormat?.suffix ?? "") + } + + guard let oneFilePerPageRow = oneFilePerPageRow else { return } + + if exportFormat?.supportsMultiPage == true { + if !oneFilePerPageRow.attached { + optionsSection.add(row: oneFilePerPageRow, animated: true) + } + } else { + if oneFilePerPageRow.attached { + optionsSection.remove(rows: [oneFilePerPageRow], animated: true) + } + } + } + } + + init(withImages images: [UIImage]? = nil, with scannedPages: [ScanPage]? = nil, core: OCCore?, fileName: String? = nil, targetFolder item: OCItem ) { + // Sections + pagesSection = StaticTableViewSection(headerTitle: "Scans".localized, identifier: "pages") + saveSection = StaticTableViewSection(headerTitle: "Save as".localized, identifier: "save") + optionsSection = StaticTableViewSection(headerTitle: "Options".localized, identifier: "options") + + // Init + super.init(style: .grouped) + + self.isModalInPresentation = true + self.navigationItem.title = "Scan".localized + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ScanViewController.cancel)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(ScanViewController.save)) + + self.core = core + self.targetFolderItem = item + + // Pages section + let pagesHeight : CGFloat = 200 + var pages : [ScanPage] = scannedPages ?? [] + + if let images = images { + for image in images { + pages.append(ScanPage(with: image)) + } + } + + pagesCollectionViewController = ScanPagesCollectionViewController(height: pagesHeight) + pagesCollectionViewController?.pages = pages + if let pagesCollectionView = pagesCollectionViewController?.view { + pagesSection.add(row: StaticTableViewRow(customView: pagesCollectionView, fixedHeight: pagesHeight)) + } + pagesSection.add(row: StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + guard let self = self else { return } + + Scanner.scan(on: self) { (_, _, scan) in + var pages = self.pagesCollectionViewController?.pages + + if let scan = scan, pages != nil { + pages!.append(contentsOf: scan.scannedPages) + + self.pagesCollectionViewController?.pages = pages! + } + } + }, title: "Scan additional".localized)) + self.addSection(pagesSection) + + // Save section + + // - Name + fileNameRow = StaticTableViewRow(textFieldWithAction: { [weak self] (row, _, _) in + self?.navigationItem.rightBarButtonItem?.isEnabled = ((row.value as? String)?.count ?? 0) > 0 + }, placeholder: "Name".localized, value: fileName ?? "", keyboardType: .default, autocorrectionType: .no, enablesReturnKeyAutomatically: true, returnKeyType: .default, identifier: "name", accessibilityLabel: "Name".localized) + saveSection.add(row: fileNameRow!) + self.addSection(saveSection) + + // Options section + // - Format + formatSegmentedControl = UISegmentedControl(items: availableExportFormats.map({ (format) in return format.name })) + formatSegmentedControl?.selectedSegmentIndex = 0 + formatSegmentedControl?.isUserInteractionEnabled = true + formatSegmentedControl?.addTarget(self, action: #selector(updateExportFormat), for: .valueChanged) + optionsSection.add(row: StaticTableViewRow(label: "File format".localized, alignment: .left, accessoryView: formatSegmentedControl, identifier: "format")) + + // - One file per page + oneFilePerPageRow = StaticTableViewRow(switchWithAction: nil, title: "Create one file per page".localized, value: false, identifier: "one-file-per-page") + optionsSection.add(row: oneFilePerPageRow!) + self.addSection(optionsSection) + + updateExportFormat() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func updateExportFormat() { + if let selectedFormatIndx = formatSegmentedControl?.selectedSegmentIndex { + self.exportFormat = availableExportFormats[selectedFormatIndx] + } + } + + @objc func cancel() { + self.dismiss(animated: true, completion: nil) + } + + @objc func save() { + if let fileName = fileNameRow?.value as? String, let pages = pagesCollectionViewController?.pages, let exporter = self.exportFormat?.exporter, let core = core { + let progressHUDViewController = ProgressHUDViewController(on: self, label: "Saving".localized) + + DispatchQueue.global(qos: .userInitiated).async { + let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + + try? FileManager.default.createDirectory(at: tmpURL, withIntermediateDirectories: true, attributes: nil) + + var exportedURLs : [URL]? + + if self.exportFormat?.supportsMultiPage == true, (self.oneFilePerPageRow?.value as? Bool) == true { + for page in pages { + if let exportedPageURLs = exporter(tmpURL, [page]) { + if exportedURLs == nil { + exportedURLs = [] + } + exportedURLs?.append(contentsOf: exportedPageURLs) + } else { + exportedURLs = nil + break + } + } + } else { + exportedURLs = exporter(tmpURL, pages) + } + + Log.log("ExportPDF=\(String(describing: exportedURLs))") + + if let targetItem = self.targetFolderItem, let exportedURLs = exportedURLs { + for exportedURL in exportedURLs { + core.importFileNamed(fileName, at: targetItem, from: exportedURL, isSecurityScoped: false, options: [ .automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue ], placeholderCompletionHandler: nil, resultHandler: nil) + } + } + + OnMainThread { + progressHUDViewController.dismiss(animated: true, completion: { + self.dismiss(animated: true) + try? FileManager.default.removeItem(at: tmpURL) + }) + } + } + } + } + +} + +@available(iOS 13.0, *) +extension VNDocumentCameraScan { + var scannedPages : [ScanPage] { + var pages : [ScanPage] = [] + + for pageIdx in 0 ..< self.pageCount { + let image = self.imageOfPage(at: pageIdx) + + pages.append(ScanPage(with: image)) + } + + return pages + } +} + +@available(iOS 13.0, *) +class Scanner : NSObject, VNDocumentCameraViewControllerDelegate { + typealias ScannerCompletionHandler = (_ scanner: Scanner, _ error: Error?, _ scan: VNDocumentCameraScan?) -> Void + + var completionHandler : ScannerCompletionHandler? + + static func scan(on viewController: UIViewController, completion: @escaping ScannerCompletionHandler) { + let scanner = Scanner() + + scanner.scan(on: viewController, completion: { (_, error, scan) in + completion(scanner, error, scan) + }) + } + + func scan(on viewController: UIViewController, completion: @escaping ScannerCompletionHandler) { + completionHandler = completion + + let documentCameraViewController = VNDocumentCameraViewController() + documentCameraViewController.delegate = self + viewController.present(documentCameraViewController, animated: true) + } + + func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { + controller.presentingViewController?.dismiss(animated: true, completion: { + self.completionHandler?(self, nil, nil) + self.completionHandler = nil + }) + } + + func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) { + Log.log("Failed with error=\(error)") + completionHandler?(self, error, nil) + completionHandler = nil + } + + func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { + Log.log("Finished with scan=\(scan)") + controller.presentingViewController?.dismiss(animated: true, completion: { + self.completionHandler?(self, nil, scan) + self.completionHandler = nil + }) + } +} diff --git a/ownCloud/Client/ClientActivityCell.swift b/ownCloud/Client/ClientActivityCell.swift index 322868265..a6782f34f 100644 --- a/ownCloud/Client/ClientActivityCell.swift +++ b/ownCloud/Client/ClientActivityCell.swift @@ -70,8 +70,6 @@ class ClientActivityCell: ThemeTableViewCell { statusLabel.rightAnchor.constraint(equalTo: statusCircle.leftAnchor, constant: -20), statusCircle.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), - statusCircle.topAnchor.constraint(equalTo: self.contentView.topAnchor), - statusCircle.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), statusCircle.widthAnchor.constraint(equalToConstant: 50), statusCircle.rightAnchor.constraint(equalTo: self.contentView.rightAnchor) ]) diff --git a/ownCloud/Client/ClientItemCell.swift b/ownCloud/Client/ClientItemCell.swift index efd752c92..524992611 100644 --- a/ownCloud/Client/ClientItemCell.swift +++ b/ownCloud/Client/ClientItemCell.swift @@ -236,38 +236,20 @@ class ClientItemCell: ThemeTableViewCell { activeThumbnailRequestProgress?.cancel() } + // Set the icon and initiate thumbnail generation iconImage = item.icon(fitInSize: iconSize) - showingIcon = true - - self.accessoryType = .none - - if item.thumbnailAvailability != .none { - let displayThumbnail = { (thumbnail: OCItemThumbnail?) in - _ = thumbnail?.requestImage(for: self.thumbnailSize, scale: 0, withCompletionHandler: { (thumbnail, error, _, image) in - if error == nil, - image != nil, - self.item?.itemVersionIdentifier == thumbnail?.itemVersionIdentifier { - OnMainThread { - self.showingIcon = false - self.iconView.image = image - } - } - }) - } - - if let thumbnail = item.thumbnail { - displayThumbnail(thumbnail) - } else { - activeThumbnailRequestProgress = core?.retrieveThumbnail(for: item, maximumSize: self.thumbnailSize, scale: 0, retrieveHandler: { [weak self] (_, _, _, thumbnail, _, progress) in - displayThumbnail(thumbnail) + self.iconView.image = iconImage - if self?.activeThumbnailRequestProgress === progress { - self?.activeThumbnailRequestProgress = nil - } - }) - } + if let core = core { + activeThumbnailRequestProgress = self.iconView.setThumbnailImage(using: core, from: item, with: thumbnailSize, progressHandler: { [weak self] (progress) in + if self?.activeThumbnailRequestProgress === progress { + self?.activeThumbnailRequestProgress = nil + } + }) } + self.accessoryType = .none + if item.isSharedWithUser || item.sharedByUserOrGroup { sharedStatusIconView.image = UIImage(named: "group") sharedStatusIconViewRightMarginConstraint?.constant = -horizontalSmallMargin @@ -285,8 +267,6 @@ class ClientItemCell: ThemeTableViewCell { self.updateCloudStatusIcon(with: item) - self.iconView.image = iconImage - self.updateLabels(with: item) self.iconView.alpha = item.isPlaceholder ? 0.5 : 1.0 diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index f08ed0b01..b9e7e0679 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -35,6 +35,11 @@ extension OCQueryState { } } +struct OCItemDraggingValue { + var item : OCItem + var bookmarkUUID : String +} + class ClientQueryViewController: QueryFileListTableViewController, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate { var selectedItemIds = Set() @@ -220,18 +225,20 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac for item in session.items { if item.localObject == nil, item.itemProvider.hasItemConformingToTypeIdentifier("public.folder") { return false + } else if let itemValues = item.localObject as? OCItemDraggingValue, let core = self.core, core.bookmark.uuid.uuidString != itemValues.bookmarkUUID, itemValues.item.type == .collection { + return false } } return true } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let core = self.core, let item : OCItem = itemAt(indexPath: indexPath) else { + guard let core = self.core, let item : OCItem = itemAt(indexPath: indexPath), let cell = tableView.cellForRow(at: indexPath) else { return nil } let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .tableRow) - let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) + let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation, sender: cell) let actions = Action.sortedApplicableActions(for: actionContext) actions.forEach({ $0.progressHandler = makeActionProgressHandler() @@ -259,11 +266,14 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac } } - func updateToolbarItemsForDropping(_ items: [OCItem]) { + func updateToolbarItemsForDropping(_ draggingValues: [OCItemDraggingValue]) { guard let tabBarController = self.tabBarController as? ClientRootViewController else { return } guard let toolbarItems = tabBarController.toolbar?.items else { return } if let core = self.core { + let items = draggingValues.map({(value: OCItemDraggingValue) -> OCItem in + return value.item + }) // Remove duplicates let uniqueItems = Array(Set(items)) // Get possible associated actions @@ -443,6 +453,7 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac // Find associated action if let action = self.actions?.first(where: {type(of:$0).identifier == sender.actionIdentifier}) { // Configure progress handler + action.context.sender = self.tabBarController action.progressHandler = makeActionProgressHandler() action.completionHandler = { [weak self] (_, _) in @@ -513,7 +524,7 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac // Actions for folderAction if let core = self.core, let rootItem = query.rootItem { let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .folderAction) - let actionContext = ActionContext(viewController: self, core: core, items: [rootItem], location: actionsLocation) + let actionContext = ActionContext(viewController: self, core: core, items: [rootItem], location: actionsLocation, sender: sender) let actions = Action.sortedApplicableActions(for: actionContext) @@ -542,7 +553,7 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac } let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreFolder) - let actionContext = ActionContext(viewController: self, core: core, query: query, items: [rootItem], location: actionsLocation) + let actionContext = ActionContext(viewController: self, core: core, query: query, items: [rootItem], location: actionsLocation, sender: sender) if let moreViewController = Action.cardViewController(for: rootItem, with: actionContext, progressHandler: makeActionProgressHandler()) { self.present(asCard: moreViewController, animated: true) @@ -622,6 +633,12 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac } } +extension OCBookmarkManager { + public func bookmark(for uuidString: String) -> OCBookmark? { + return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first + } +} + // MARK: - Drag & Drop delegates extension ClientQueryViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { @@ -629,11 +646,13 @@ extension ClientQueryViewController: UITableViewDropDelegate { for item in coordinator.items { if item.dragItem.localObject != nil { + var destinationItem: OCItem - guard let item = item.dragItem.localObject as? OCItem, let itemName = item.name else { + guard let itemValues = item.dragItem.localObject as? OCItemDraggingValue, let itemName = itemValues.item.name, let sourceBookmark = OCBookmarkManager.shared.bookmark(for: itemValues.bookmarkUUID) else { return } + let item = itemValues.item if coordinator.proposal.intent == .insertIntoDestinationIndexPath { @@ -663,13 +682,32 @@ extension ClientQueryViewController: UITableViewDropDelegate { } - if let progress = core.move(item, to: destinationItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in - if error != nil { - Log.log("Error \(String(describing: error)) moving \(String(describing: item.path))") + // Move Items in the same Account + if core.bookmark.uuid.uuidString == itemValues.bookmarkUUID { + if let progress = core.move(item, to: destinationItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in + if error != nil { + Log.log("Error \(String(describing: error)) moving \(String(describing: item.path))") + } + }) { + self.progressSummarizer?.startTracking(progress: progress) + } + // Copy Items between Accounts + } else { + OCCoreManager.shared.requestCore(for: sourceBookmark, setup: nil) { (srcCore, error) in + if error == nil { + srcCore?.downloadItem(item, options: nil, resultHandler: { (error, _, srcItem, _) in + if error == nil, let srcItem = srcItem, let localURL = srcCore?.localCopy(of: srcItem) { + core.importItemNamed(srcItem.name, at: destinationItem, from: localURL, isSecurityScoped: false, options: nil, placeholderCompletionHandler: nil) { (error, _, _, _) in + if error == nil { + + } + } + } + }) + } } - }) { - self.progressSummarizer?.startTracking(progress: progress) } + // Import Items from outside } else { guard let UTI = item.dragItem.itemProvider.registeredTypeIdentifiers.last else { return } item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: UTI) { (url, _ error) in @@ -689,28 +727,31 @@ extension ClientQueryViewController: UITableViewDragDelegate { self.populateToolbar() } - var selectedItems = [OCItem]() + var selectedItems = [OCItemDraggingValue]() // Add Items from Multiselection too if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { if selectedIndexPaths.count > 0 { for indexPath in selectedIndexPaths { - if let selectedItem : OCItem = itemAt(indexPath: indexPath) { - selectedItems.append(selectedItem) + if let selectedItem : OCItem = itemAt(indexPath: indexPath), let uuid = core?.bookmark.uuid.uuidString { + let draggingValue = OCItemDraggingValue(item: selectedItem, bookmarkUUID: uuid) + selectedItems.append(draggingValue) } } } } for dragItem in session.items { - guard let item = dragItem.localObject as? OCItem else { continue } - selectedItems.append(item) + guard let item = dragItem.localObject as? OCItem, let uuid = core?.bookmark.uuid.uuidString else { continue } + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) } - if let item: OCItem = itemAt(indexPath: indexPath) { - selectedItems.append(item) + if let item: OCItem = itemAt(indexPath: indexPath), let uuid = core?.bookmark.uuid.uuidString { + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) updateToolbarItemsForDropping(selectedItems) - guard let dragItem = itemForDragging(item: item) else { return [] } + guard let dragItem = itemForDragging(draggingValue: draggingValue) else { return [] } return [dragItem] } @@ -718,32 +759,35 @@ extension ClientQueryViewController: UITableViewDragDelegate { } func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { - var selectedItems = [OCItem]() + var selectedItems = [OCItemDraggingValue]() for dragItem in session.items { - guard let item = dragItem.localObject as? OCItem else { continue } - selectedItems.append(item) + guard let item = dragItem.localObject as? OCItem, let uuid = core?.bookmark.uuid.uuidString else { continue } + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) } - if let item: OCItem = itemAt(indexPath: indexPath) { - selectedItems.append(item) + if let item: OCItem = itemAt(indexPath: indexPath), let uuid = core?.bookmark.uuid.uuidString { + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) updateToolbarItemsForDropping(selectedItems) - guard let dragItem = itemForDragging(item: item) else { return [] } + guard let dragItem = itemForDragging(draggingValue: draggingValue) else { return [] } return [dragItem] } return [] } - func itemForDragging(item : OCItem) -> UIDragItem? { + func itemForDragging(draggingValue : OCItemDraggingValue) -> UIDragItem? { + let item = draggingValue.item if let core = self.core { switch item.type { case .collection: guard let data = item.serializedData() else { return nil } let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + dragItem.localObject = draggingValue return dragItem case .file: guard let itemMimeType = item.mimeType else { return nil } @@ -756,13 +800,13 @@ extension ClientQueryViewController: UITableViewDragDelegate { let itemProvider = NSItemProvider(item: fileData, typeIdentifier: rawUtiString) itemProvider.suggestedName = item.name let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + dragItem.localObject = draggingValue return dragItem } else { guard let data = item.serializedData() else { return nil } let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + dragItem.localObject = draggingValue return dragItem } } diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 55961cf79..286c9a03c 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -42,6 +42,8 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega var progressSummarizer : ProgressSummarizer? var toolbar : UIToolbar? + var pasteboardChangedCounter = 0 + weak var authDelegate : ClientRootViewControllerAuthenticationDelegate? var skipAuthorizationFailure : Bool = false @@ -137,7 +139,7 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega } // MARK: - Startup - func afterCoreStart(_ completionHandler: @escaping (() -> Void)) { + func afterCoreStart(_ lastVisibleItemId: String?, completionHandler: @escaping (() -> Void)) { OCCoreManager.shared.requestCore(for: bookmark, setup: { (core, _) in self.core = core core?.delegate = self @@ -146,7 +148,7 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) }, completionHandler: { (core, error) in if error == nil { - self.coreReady() + self.coreReady(lastVisibleItemId) } // Start showing connection status with a delay of 1 second, so "Offline" doesn't flash briefly @@ -172,14 +174,17 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega self.tabBar.isTranslucent = false + // Add tab bar icons + Theme.shared.add(tvgResourceFor: "folder") + Theme.shared.add(tvgResourceFor: "owncloud-logo") + Theme.shared.add(tvgResourceFor: "status-flash") + filesNavigationController = ThemeNavigationController() filesNavigationController?.delegate = self filesNavigationController?.navigationBar.isTranslucent = false filesNavigationController?.tabBarItem.title = "Browse".localized filesNavigationController?.tabBarItem.image = Theme.shared.image(for: "folder", size: folderButtonsSize) - Theme.shared.add(tvgResourceFor: "status-flash") - activityViewController = ClientActivityViewController() activityNavigationController = ThemeNavigationController(rootViewController: activityViewController!) activityNavigationController?.tabBarItem.title = "Status".localized @@ -222,16 +227,6 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if MediaUploadQueue.isMediaUploadPendingFlagSet(for: self.bookmark) { - let unfinishedUploadAlert = ThemedAlertController(with: "Warning".localized, - message: "Media upload in the previous session was incomplete since the application was terminated".localized) - self.present(unfinishedUploadAlert, animated: true, completion: nil) - MediaUploadQueue.resetUploadPendingFlag(for: self.bookmark) - } - } - var closeClientCompletionHandler : (() -> Void)? func closeClient(completion: (() -> Void)? = nil) { @@ -246,14 +241,17 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega }) } - func coreReady() { + func coreReady(_ lastVisibleItemId: String?) { OnMainThread { if let core = self.core { - let query = OCQuery(forPath: "/") - - let queryViewController = ClientQueryViewController(core: core, query: query) - // Because we have nested UINavigationControllers (first one from ServerListTableViewController and each item UITabBarController needs it own UINavigationController), we have to fake the UINavigationController logic. Here we insert the emptyViewController, because in the UI should appear a "Back" button if the root of the queryViewController is shown. Therefore we put at first the emptyViewController inside and at the same time the queryViewController. Now, the back button is shown and if the users push the "Back" button the ServerListTableViewController is shown. This logic can be found in navigationController(_: UINavigationController, willShow: UIViewController, animated: Bool) below. - self.filesNavigationController?.setViewControllers([self.emptyViewController, queryViewController], animated: false) + if let localItemId = lastVisibleItemId { + self.createFileListStack(for: localItemId) + } else { + let query = OCQuery(forPath: "/") + let queryViewController = ClientQueryViewController(core: core, query: query) + // Because we have nested UINavigationControllers (first one from ServerListTableViewController and each item UITabBarController needs it own UINavigationController), we have to fake the UINavigationController logic. Here we insert the emptyViewController, because in the UI should appear a "Back" button if the root of the queryViewController is shown. Therefore we put at first the emptyViewController inside and at the same time the queryViewController. Now, the back button is shown and if the users push the "Back" button the ServerListTableViewController is shown. This logic can be found in navigationController(_: UINavigationController, willShow: UIViewController, animated: Bool) below. + self.filesNavigationController?.setViewControllers([self.emptyViewController, queryViewController], animated: false) + } let emptyViewController = self.emptyViewController emptyViewController.navigationItem.title = "Accounts".localized @@ -308,6 +306,45 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega self.progressBar?.setNeedsLayout() // self.view.setNeedsLayout() } + + func createFileListStack(for itemLocalID: String) { + if let core = core { + // retrieve the item for the item id + core.retrieveItemFromDatabase(forLocalID: itemLocalID, completionHandler: { (error, _, item) in + if error == nil, let item = item { + OnMainThread { + // get all parent items for the item and rebuild all underlaying ClientQueryViewController for this items in the navigation stack + let parentItems = core.retrieveParentItems(for: item) + let query = OCQuery(forPath: "/") + let queryViewController = ClientQueryViewController(core: core, query: query) + + var subController = queryViewController + var newViewControllersStack : [UIViewController] = [] + for item in parentItems { + if let controller = self.open(item: item, in: subController) { + subController = controller + newViewControllersStack.append(controller) + } + } + + newViewControllersStack.insert(self.emptyViewController, at: 0) + self.filesNavigationController?.setViewControllers(newViewControllersStack, animated: false) + + // open the controller for the item + subController.open(item: item, animated: false) + } + } + }) + } + } + + func open(item: OCItem, in controller: ClientQueryViewController) -> ClientQueryViewController? { + if let subController = controller.open(item: item, animated: false, pushViewController: false) { + return subController + } + + return nil + } } extension ClientRootViewController : Themeable { diff --git a/ownCloud/Client/PhotoAlbumTableViewController.swift b/ownCloud/Client/PhotoAlbumTableViewController.swift index 06796d676..9c4531391 100644 --- a/ownCloud/Client/PhotoAlbumTableViewController.swift +++ b/ownCloud/Client/PhotoAlbumTableViewController.swift @@ -117,6 +117,7 @@ class PhotoAlbumTableViewController : UITableViewController, Themeable { cell?.titleLabel.text = album.name cell?.countLabel.text = album.countString cell?.thumbnailImageView.image = album.thumbnail + cell?.selectionStyle = .default return cell! } diff --git a/ownCloud/Client/PhotoSelectionViewController.swift b/ownCloud/Client/PhotoSelectionViewController.swift index 4c76471bd..49a8dbe50 100644 --- a/ownCloud/Client/PhotoSelectionViewController.swift +++ b/ownCloud/Client/PhotoSelectionViewController.swift @@ -54,6 +54,21 @@ class PhotoSelectionViewController: UICollectionViewController, Themeable { fileprivate lazy var durationFormatter = DateComponentsFormatter() fileprivate var uploadButtonItem: UIBarButtonItem? + public var focussedIndexPath: IndexPath? { + didSet { + guard let focussedIndexPath = focussedIndexPath, let focussedCell = collectionView.cellForItem(at: focussedIndexPath) else { return } + + UIView.animate(withDuration: 0.3, animations: { + focussedCell.alpha = 0.5 + }, completion: { _ in + UIView.animate(withDuration: 0.3, animations: { + focussedCell.alpha = 1.0 + }) + }) + + } + } + // MARK: - Init / deinit init() { layout.scrollDirection = .vertical diff --git a/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift b/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift index eb2f51e31..c6e82ba89 100644 --- a/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift +++ b/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift @@ -137,7 +137,9 @@ class PublicLinkEditTableViewController: StaticTableViewController { if error == nil { guard let changedShare = share else { return } self.share.name = changedShare.name - self.title = changedShare.name + OnMainThread { + self.title = changedShare.name + } } else { if let shareError = error { OnMainThread { diff --git a/ownCloud/Client/Sharing/PublicLinkTableViewController.swift b/ownCloud/Client/Sharing/PublicLinkTableViewController.swift index ea09236b9..e1b6f8ed2 100644 --- a/ownCloud/Client/Sharing/PublicLinkTableViewController.swift +++ b/ownCloud/Client/Sharing/PublicLinkTableViewController.swift @@ -192,22 +192,6 @@ class PublicLinkTableViewController: SharingTableViewController { } } - func retrievePrivateLink(for item: OCItem, in row: StaticTableViewRow) { - let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) - progressView.startAnimating() - row.cell?.accessoryView = progressView - - self.core?.retrievePrivateLink(for: item, completionHandler: { (error, url) in - OnMainThread { - row.cell?.accessoryView = nil - } - if error == nil { - guard let url = url else { return } - UIPasteboard.general.url = url - } - }) - } - // MARK: TableView Delegate override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { diff --git a/ownCloud/Client/SortMethod.swift b/ownCloud/Client/SortMethod.swift index 570a460ac..742b2f83f 100644 --- a/ownCloud/Client/SortMethod.swift +++ b/ownCloud/Client/SortMethod.swift @@ -56,7 +56,19 @@ public enum SortMethod: Int { } func comparator(direction: SortDirection) -> OCSort { - var comparator: OCSort + var comparator : OCSort + var combinedComparator: OCSort? + + let alphabeticComparator : OCSort = { (left, right) in + guard let leftName = (left as? OCItem)?.name, let rightName = (right as? OCItem)?.name else { + return .orderedSame + } + if direction == .descendant { + return rightName.caseInsensitiveCompare(leftName) + } + + return leftName.caseInsensitiveCompare(rightName) + } switch self { case .size: @@ -67,22 +79,14 @@ public enum SortMethod: Int { let leftSize = leftItem!.size as NSNumber let rightSize = rightItem!.size as NSNumber if direction == .descendant { - return (leftSize.compare(rightSize)) + return leftSize.compare(rightSize) } - return (rightSize.compare(leftSize)) + return rightSize.compare(leftSize) } case .alphabetically: - comparator = { (left, right) in - guard let leftName = (left as? OCItem)?.name, let rightName = (right as? OCItem)?.name else { - return .orderedSame - } - if direction == .descendant { - return (rightName.caseInsensitiveCompare(leftName)) - } - - return (leftName.caseInsensitiveCompare(rightName)) - } + comparator = alphabeticComparator + combinedComparator = alphabeticComparator case .type: comparator = { (left, right) in let leftItem = left as? OCItem @@ -145,12 +149,25 @@ public enum SortMethod: Int { return .orderedSame } if direction == .descendant { - return (leftLastModified.compare(rightLastModified)) + return leftLastModified.compare(rightLastModified) } - return (rightLastModified.compare(leftLastModified)) + return rightLastModified.compare(leftLastModified) } } - return comparator + + if combinedComparator == nil { + combinedComparator = { (left, right) in + var result : ComparisonResult = comparator(left, right) + + if result == .orderedSame { + result = alphabeticComparator(left, right) + } + + return result + } + } + + return combinedComparator ?? comparator } } diff --git a/ownCloud/Client/Viewer/DisplayExtension.swift b/ownCloud/Client/Viewer/DisplayExtension.swift index d2b6faf7f..69b5c2d90 100644 --- a/ownCloud/Client/Viewer/DisplayExtension.swift +++ b/ownCloud/Client/Viewer/DisplayExtension.swift @@ -18,6 +18,7 @@ import UIKit import ownCloudSDK +import MobileCoreServices protocol DisplayExtension where Self: DisplayViewController { @@ -48,6 +49,13 @@ extension DisplayExtension where Self: DisplayViewController { return displayExtension } + + static func mimeTypeConformsTo(mime: String, utTypeClass: CFString) -> Bool { + guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mime as CFString, nil) else { + return false + } + return UTTypeConformsTo(uti.takeUnretainedValue(), utTypeClass) + } } struct FeatureKeys { diff --git a/ownCloud/Client/Viewer/DisplayHostViewController.swift b/ownCloud/Client/Viewer/DisplayHostViewController.swift index 5d66b96c8..12dce8cf3 100644 --- a/ownCloud/Client/Viewer/DisplayHostViewController.swift +++ b/ownCloud/Client/Viewer/DisplayHostViewController.swift @@ -26,7 +26,7 @@ class DisplayHostViewController: UIPageViewController { } // MARK: - Constants - let imageFilterRegexp: String = "\\A((image/*))" // Filters all the mime types that are images (incluiding gif and svg) + let mediaFilterRegexp: String = "\\A(((image|audio|video)/*))" // Filters all the mime types that are images (incluiding gif and svg) // MARK: - Instance Variables weak private var core: OCCore? @@ -34,7 +34,7 @@ class DisplayHostViewController: UIPageViewController { private var initialItem: OCItem private var displayedIndex: Int? - private var items: [OCItem]? + public var items: [OCItem]? private var query: OCQuery private var queryStarted : Bool = false @@ -61,7 +61,7 @@ class DisplayHostViewController: UIPageViewController { query.requestChangeSet(withFlags: .onlyResults) { ( _, changeSet) in guard let changeSet = changeSet else { return } - if let queryResult = changeSet.queryResult, let newItems = self?.applyImageFilesFilter(items: queryResult) { + if let queryResult = changeSet.queryResult, let newItems = self?.applyMediaFilesFilter(items: queryResult) { let shallUpdateDatasource = self?.items?.count != newItems.count ? true : false self?.items = newItems @@ -105,6 +105,12 @@ class DisplayHostViewController: UIPageViewController { displayController.itemIndex = currentIndex } } + + NotificationCenter.default.addObserver(self, selector: #selector(handleMediaPlaybackFinished(notification:)), name: MediaDisplayViewController.MediaPlaybackFinishedNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(handlePlayNextMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackNextTrackNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(handlePlayPreviousMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) } override var childForHomeIndicatorAutoHidden : UIViewController? { @@ -163,9 +169,6 @@ class DisplayHostViewController: UIPageViewController { let foundIndex = self?.items?.firstIndex(where: {$0.localID == item.localID}) if foundIndex == nil { - - currentDisplayViewController.removeFromParent() - if index < itemCount { if let newIndex = self?.computeNewIndex(for: index, itemCount: itemCount, position: .after, indexFound: false), let newViewController = self?.viewControllerAtIndex(index: newIndex) { @@ -250,9 +253,9 @@ class DisplayHostViewController: UIPageViewController { } // MARK: - Filters - private func applyImageFilesFilter(items: [OCItem]) -> [OCItem] { - if initialItem.mimeType?.matches(regExp: imageFilterRegexp) ?? false { - let filteredItems = items.filter({$0.type != .collection && $0.mimeType?.matches(regExp: self.imageFilterRegexp) ?? false}) + private func applyMediaFilesFilter(items: [OCItem]) -> [OCItem] { + if initialItem.mimeType?.matches(regExp: mediaFilterRegexp) ?? false { + let filteredItems = items.filter({$0.type != .collection && $0.mimeType?.matches(regExp: self.mediaFilterRegexp) ?? false}) return filteredItems } else { let filteredItems = items.filter({$0.type != .collection && $0.fileID == self.initialItem.fileID}) @@ -326,3 +329,30 @@ extension DisplayHostViewController: Themeable { self.view.backgroundColor = .black } } + +extension DisplayHostViewController { + + @objc private func handleMediaPlaybackFinished(notification:Notification) { + if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { + if let vc = vendNewViewController(from: mediaController, .after) { + self.setViewControllers([vc], direction: .forward, animated: false, completion: nil) + } + } + } + + @objc private func handlePlayNextMedia(notification:Notification) { + if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { + if let vc = vendNewViewController(from: mediaController, .after) { + self.setViewControllers([vc], direction: .forward, animated: false, completion: nil) + } + } + } + + @objc private func handlePlayPreviousMedia(notification:Notification) { + if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { + if let vc = vendNewViewController(from: mediaController, .before) { + self.setViewControllers([vc], direction: .forward, animated: false, completion: nil) + } + } + } +} diff --git a/ownCloud/Client/Viewer/DisplayViewController.swift b/ownCloud/Client/Viewer/DisplayViewController.swift index d79c28a4a..cce799a2d 100644 --- a/ownCloud/Client/Viewer/DisplayViewController.swift +++ b/ownCloud/Client/Viewer/DisplayViewController.swift @@ -30,6 +30,7 @@ enum DisplayViewState { case noNetworkConnection case downloading(progress: Progress) case errorDownloading(error: Error?) + case downloadFinished case canceledDownload case notSupportedMimeType } @@ -79,16 +80,20 @@ class DisplayViewController: UIViewController, OCQueryDelegate { var source: URL? { didSet { - OnMainThread(inline: true) { - self.iconImageView?.isHidden = true - self.hideItemMetadataUIElements() - self.renderSpecificView(completion: { (success) in - if !success { - self.iconImageView?.isHidden = false - self.infoLabel?.text = "File couldn't be opened".localized - self.infoLabel?.isHidden = false + if self.source != oldValue && self.source != nil { + OnMainThread(inline: true) { + if self.shallShowPreview == true && self.canPreview(url: self.source!) { + self.iconImageView?.isHidden = true + self.hideItemMetadataUIElements() + self.renderSpecificView(completion: { (success) in + if !success { + self.iconImageView?.isHidden = false + self.infoLabel?.text = "File couldn't be opened".localized + self.infoLabel?.isHidden = false + } + }) } - }) + } } } } @@ -97,24 +102,27 @@ class DisplayViewController: UIViewController, OCQueryDelegate { var shallDisplayMoreButtonInToolbar = true + var shallShowPreview = true + private var state: DisplayViewState = .hasNetworkConnection { didSet { - OnMainThread(inline: true) { - switch self.state { - case .downloading(let progress): - self.downloadProgress = progress - - default: - self.downloadProgress = nil - } - self.render() + switch self.state { + case .downloading(let progress): + self.downloadProgress = progress + case .notSupportedMimeType: + shallShowPreview = false + default: + self.downloadProgress = nil } + self.render() } } public var downloadProgress : Progress? { didSet { - progressView?.observedProgress = downloadProgress + OnMainThread(inline: true) { + self.progressView?.observedProgress = self.downloadProgress + } } } @@ -232,31 +240,13 @@ class DisplayViewController: UIViewController, OCQueryDelegate { Theme.shared.register(client: self) - if let item = item { - iconImageView?.image = item.icon(fitInSize:iconImageSize) - - if item.thumbnailAvailability != .none { - let displayThumbnail = { (thumbnail: OCItemThumbnail?) in - _ = thumbnail?.requestImage(for: self.iconImageSize, scale: 0, withCompletionHandler: { (thumbnail, error, _, image) in - if error == nil, - image != nil, - item.itemVersionIdentifier == thumbnail?.itemVersionIdentifier { - OnMainThread { - if self.iconImageView?.isHidden == false { - self.iconImageView?.image = image - } - } - } - }) - } + if let item = item, let core = self.core { + if core.localCopy(of: item) == nil { + iconImageView?.setThumbnailImage(using: core, from: item, with: iconImageSize, avoidSystemThumbnails: true) + } - if let thumbnail = item.thumbnail { - displayThumbnail(thumbnail) - } else { - _ = core?.retrieveThumbnail(for: item, maximumSize: iconImageSize, scale: 0, retrieveHandler: { (_, _, _, thumbnail, _, _) in - displayThumbnail(thumbnail) - }) - } + if iconImageView?.image == nil { + iconImageView?.image = item.icon(fitInSize:iconImageSize) } } @@ -311,6 +301,8 @@ class DisplayViewController: UIViewController, OCQueryDelegate { } return } + self?.state = .downloadFinished + self?.item = latestItem self?.source = file?.url @@ -338,33 +330,44 @@ class DisplayViewController: UIViewController, OCQueryDelegate { } private func render() { - switch state { - case .hasNetworkConnection: - hideProgressIndicators() - - case .noNetworkConnection: - self.progressView?.isHidden = true - self.cancelButton?.isHidden = true - self.infoLabel?.isHidden = false - self.showPreviewButton?.isHidden = true - - case .errorDownloading, .canceledDownload: - if core?.connectionStatus == .online { - hideProgressIndicators() - } - - case .downloading(_): - self.progressView?.isHidden = false - self.cancelButton?.isHidden = false - self.infoLabel?.isHidden = true - self.showPreviewButton?.isHidden = true + OnMainThread(inline: true) { + switch self.state { + case .hasNetworkConnection: + self.hideProgressIndicators() + + case .noNetworkConnection: + self.progressView?.isHidden = true + self.cancelButton?.isHidden = true + self.infoLabel?.isHidden = false + self.showPreviewButton?.isHidden = true + + case .errorDownloading, .canceledDownload: + if self.core?.connectionStatus == .online { + self.hideProgressIndicators() + } - case .notSupportedMimeType: - self.progressView?.isHidden = true - self.cancelButton?.isHidden = true - self.infoLabel?.isHidden = true - self.showPreviewButton?.isHidden = true + case .downloading(_): + self.progressView?.isHidden = false + self.cancelButton?.isHidden = false + self.infoLabel?.isHidden = true + self.showPreviewButton?.isHidden = true + + case .notSupportedMimeType: + self.progressView?.isHidden = true + self.cancelButton?.isHidden = true + self.infoLabel?.isHidden = true + + if let item = self.item { + if self.core?.localCopy(of:item) == nil { + self.showPreviewButton?.isHidden = false + self.showPreviewButton?.setTitle("Download".localized, for: .normal) + } + } + case .downloadFinished: + self.cancelButton?.isHidden = true + self.progressView?.isHidden = true + } } } @@ -373,16 +376,16 @@ class DisplayViewController: UIViewController, OCQueryDelegate { self.progressView?.isHidden = true self.cancelButton?.isHidden = true self.infoLabel?.isHidden = true - self.showPreviewButton?.isHidden = false + //self.showPreviewButton?.isHidden = false } - @objc func optionsBarButtonPressed() { + @objc func optionsBarButtonPressed(_ sender: UIBarButtonItem) { guard let core = core, let item = item else { return } let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreItem) - let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) + let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation, sender: sender) if let moreViewController = Action.cardViewController(for: item, with: actionContext, completionHandler: nil) { self.present(asCard: moreViewController, animated: true) @@ -482,9 +485,13 @@ class DisplayViewController: UIViewController, OCQueryDelegate { } self.updateNavigationBarItems() - } } + + // Override in subclasses and implement specific checks if required + func canPreview(url:URL) -> Bool { + return true + } } // MARK: - Public API diff --git a/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift b/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift index 3b0a0c33b..50699c915 100644 --- a/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift +++ b/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift @@ -37,7 +37,7 @@ class ImageDisplayViewController : DisplayViewController { // MARK: - Gesture recognizers var tapToZoomGestureRecognizer : UITapGestureRecognizer! - var tapToHideBarsGestureRecognizer: UITapGestureRecognizer! + var showHideBarsTapGestureRecognizer: UITapGestureRecognizer! // MARK: - View controller lifecycle @@ -113,11 +113,11 @@ class ImageDisplayViewController : DisplayViewController { self.tapToZoomGestureRecognizer.numberOfTapsRequired = 2 self.scrollView?.addGestureRecognizer(self.tapToZoomGestureRecognizer) - self.tapToHideBarsGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapToHideBars)) - self.scrollView?.addGestureRecognizer(self.tapToHideBarsGestureRecognizer) + self.showHideBarsTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.showHideBars)) + self.scrollView?.addGestureRecognizer(self.showHideBarsTapGestureRecognizer) self.tapToZoomGestureRecognizer.delegate = self - self.tapToHideBarsGestureRecognizer.delegate = self + self.showHideBarsTapGestureRecognizer.delegate = self completion(true) } else { @@ -151,7 +151,7 @@ class ImageDisplayViewController : DisplayViewController { setNeedsUpdateOfHomeIndicatorAutoHidden() } - @objc func tapToHideBars() { + @objc func showHideBars() { guard let navigationController = navigationController else { return } @@ -209,7 +209,7 @@ extension ImageDisplayViewController: DisplayExtension { extension ImageDisplayViewController: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer === tapToZoomGestureRecognizer && otherGestureRecognizer === tapToHideBarsGestureRecognizer { + if gestureRecognizer === tapToZoomGestureRecognizer && otherGestureRecognizer === showHideBarsTapGestureRecognizer { return true } @@ -217,11 +217,11 @@ extension ImageDisplayViewController: UIGestureRecognizerDelegate { } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer === tapToZoomGestureRecognizer && otherGestureRecognizer === tapToHideBarsGestureRecognizer { + if gestureRecognizer === tapToZoomGestureRecognizer && otherGestureRecognizer === showHideBarsTapGestureRecognizer { return false } - if gestureRecognizer === tapToHideBarsGestureRecognizer && otherGestureRecognizer === tapToZoomGestureRecognizer { + if gestureRecognizer === showHideBarsTapGestureRecognizer && otherGestureRecognizer === tapToZoomGestureRecognizer { return false } diff --git a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift index 11ce1d167..21ee2d1ad 100644 --- a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift +++ b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift @@ -18,20 +18,36 @@ import UIKit import AVKit +import MediaPlayer import ownCloudSDK +import MobileCoreServices class MediaDisplayViewController : DisplayViewController { + static let MediaPlaybackFinishedNotification = NSNotification.Name("media_playback.finished") + static let MediaPlaybackNextTrackNotification = NSNotification.Name("media_playback.play_next") + static let MediaPlaybackPreviousTrackNotification = NSNotification.Name("media_playback.play_previous") + private var playerStatusObservation: NSKeyValueObservation? private var playerItemStatusObservation: NSKeyValueObservation? private var playerItem: AVPlayerItem? private var player: AVPlayer? private var playerViewController: AVPlayerViewController? + // Information for now playing + private var mediaItemArtwork: MPMediaItemArtwork? + private var mediaItemTitle: String? + private var mediaItemArtist: String? + deinit { playerStatusObservation?.invalidate() playerItemStatusObservation?.invalidate() - NotificationCenter.default.removeObserver(self) + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil) } override func viewDidLoad() { @@ -83,20 +99,61 @@ class MediaDisplayViewController : DisplayViewController { player = AVPlayer(playerItem: playerItem) player?.allowsExternalPlayback = true playerViewController = AVPlayerViewController() - playerViewController!.player = player + playerViewController!.updatesNowPlayingInfoCenter = false + + if UIApplication.shared.applicationState == .active { + playerViewController!.player = player + } addChild(playerViewController!) playerViewController!.view.frame = self.view.bounds self.view.addSubview(playerViewController!.view) playerViewController!.didMove(toParent: self) + // Add artwork to the player overlay if corresponding meta data item is available in the asset + if let artworkMetadataItem = asset.commonMetadata.filter({$0.commonKey == AVMetadataKey.commonKeyArtwork}).first, + let imageData = artworkMetadataItem.dataValue, + let overlayView = playerViewController?.contentOverlayView { + + if let artworkImage = UIImage(data: imageData) { + + // Construct image view overlay for AVPlayerViewController + let imageView = UIImageView(image: artworkImage) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .center + playerViewController?.contentOverlayView?.addSubview(imageView) + + NSLayoutConstraint.activate([ + imageView.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor), + imageView.centerXAnchor.constraint(equalTo: overlayView.centerXAnchor) + ]) + + // Create MPMediaItemArtwork to be shown in 'now playing' in the lock screen + mediaItemArtwork = MPMediaItemArtwork(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in + return artworkImage + }) + } + } + + // Extract title meta-data item + mediaItemTitle = asset.commonMetadata.filter({$0.commonKey == AVMetadataKey.commonKeyTitle}).first?.value as? String + + // Extract artist meta-data item + mediaItemArtist = asset.commonMetadata.filter({$0.commonKey == AVMetadataKey.commonKeyArtist}).first?.value as? String + + // Setup player status observation handler playerStatusObservation = player!.observe(\AVPlayer.status, options: [.initial, .new], changeHandler: { [weak self] (player, _) in if player.status == .readyToPlay { + self?.setupRemoteTransportControls() + try? AVAudioSession.sharedInstance().setCategory(.playback) try? AVAudioSession.sharedInstance().setActive(true) self?.player?.play() + + self?.updateNowPlayingInfoCenter() + } else if player.status == .failed { self?.present(error: self?.player?.error) } @@ -132,27 +189,157 @@ class MediaDisplayViewController : DisplayViewController { @objc private func handleAVPlayerItem(notification:Notification) { try? AVAudioSession.sharedInstance().setActive(false) + OnMainThread { + NotificationCenter.default.post(name: MediaDisplayViewController.MediaPlaybackFinishedNotification, object: self.item) + } + } + + private func setupRemoteTransportControls() { + // Get the shared MPRemoteCommandCenter + let commandCenter = MPRemoteCommandCenter.shared() + + // Add handler for Play Command + commandCenter.playCommand.addTarget { [weak self] _ in + if let player = self?.player { + if player.rate == 0.0 { + player.play() + self?.updateNowPlayingTimeline() + return .success + } + } + + return .commandFailed + } + + // Add handler for Pause Command + commandCenter.pauseCommand.addTarget { [weak self] _ in + if let player = self?.player { + if player.rate == 1.0 { + player.pause() + self?.updateNowPlayingTimeline() + return .success + } + } + + return .commandFailed + } + + // Add handler for skip forward command + commandCenter.skipForwardCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + if let player = self?.player { + let time = player.currentTime() + CMTime(seconds: 10.0, preferredTimescale: 1) + player.seek(to: time) { (finished) in + if finished { + self?.updateNowPlayingTimeline() + } + } + return .success + } + return .commandFailed + } + + // Add handler for skip backward command + commandCenter.skipBackwardCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + if let player = self?.player { + let time = player.currentTime() - CMTime(seconds: 10.0, preferredTimescale: 1) + player.seek(to: time) { (finished) in + if finished { + self?.updateNowPlayingTimeline() + } + } + return .success + } + return .commandFailed + } + + // TODO: Skip controls are useful for podcasts but not so much for music. + // Disable them for now but keep the implementation of command handlers + commandCenter.skipForwardCommand.isEnabled = false + commandCenter.skipBackwardCommand.isEnabled = false + + // Configure next / previous track buttons according to number of items to be played + var enableNextTrackCommand = false + var enablePreviousTrackCommand = false + + if let itemIndex = self.itemIndex { + if itemIndex > 0 { + enablePreviousTrackCommand = true + } + + if let displayHostController = self.parent as? DisplayHostViewController, let items = displayHostController.items { + enableNextTrackCommand = itemIndex < (items.count - 1) + } + } + + commandCenter.nextTrackCommand.isEnabled = enableNextTrackCommand + commandCenter.previousTrackCommand.isEnabled = enablePreviousTrackCommand + + // Add handler for seek forward command + commandCenter.nextTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + if let player = self?.player { + player.pause() + OnMainThread { + NotificationCenter.default.post(name: MediaDisplayViewController.MediaPlaybackNextTrackNotification, object: nil) + } + return .success + } + return .commandFailed + } + + // Add handler for seek backward command + commandCenter.previousTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + if let player = self?.player { + player.pause() + OnMainThread { + NotificationCenter.default.post(name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) + } + return .success + } + return .commandFailed + } + } + + private func updateNowPlayingTimeline() { + + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.playerItem?.currentTime().seconds + + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.player?.rate + } + + private func updateNowPlayingInfoCenter() { + guard let player = self.player else { return } + guard let playerItem = self.playerItem else { return } + + var nowPlayingInfo = [String : Any]() + + nowPlayingInfo[MPMediaItemPropertyTitle] = mediaItemTitle + nowPlayingInfo[MPMediaItemPropertyArtist] = mediaItemArtist + nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = source + nowPlayingInfo[MPNowPlayingInfoPropertyCurrentPlaybackDate] = playerItem.currentDate() + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playerItem.currentTime().seconds + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds + + if mediaItemArtwork != nil { + nowPlayingInfo[MPMediaItemPropertyArtwork] = mediaItemArtwork + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + updateNowPlayingTimeline() } } // MARK: - Display Extension. extension MediaDisplayViewController: DisplayExtension { static var customMatcher: OCExtensionCustomContextMatcher? = { (context, defaultPriority) in - do { - if let mimeType = context.location?.identifier?.rawValue { - let supportedFormatsRegex = try NSRegularExpression(pattern: "\\A((video/)|(audio/))", - options: .caseInsensitive) - let matches = supportedFormatsRegex.numberOfMatches(in: mimeType, options: .reportCompletion, range: NSRange(location: 0, length: mimeType.count)) - - if matches > 0 { - return OCExtensionPriority.locationMatch - } - } + if let mimeType = context.location?.identifier?.rawValue { - return OCExtensionPriority.noMatch - } catch { - return OCExtensionPriority.noMatch + if MediaDisplayViewController.mimeTypeConformsTo(mime: mimeType, utTypeClass: kUTTypeAudiovisualContent) { + return OCExtensionPriority.locationMatch + } } + return OCExtensionPriority.noMatch } static var displayExtensionIdentifier: String = "org.owncloud.media" static var supportedMimeTypes: [String]? diff --git a/ownCloud/Client/Viewer/PDF/PDFViewerViewController.swift b/ownCloud/Client/Viewer/PDF/PDFViewerViewController.swift index 0166a4b76..2eeb01982 100644 --- a/ownCloud/Client/Viewer/PDF/PDFViewerViewController.swift +++ b/ownCloud/Client/Viewer/PDF/PDFViewerViewController.swift @@ -51,7 +51,7 @@ class PDFViewerViewController: DisplayViewController, DisplayExtension { fileprivate let thumbnailViewHeightMultiplier: CGFloat = 0.1 fileprivate let thumbnailMargin: CGFloat = 32.0 fileprivate let filenameContainerTopMargin: CGFloat = 10.0 - fileprivate let pdfView = PDFView() + public let pdfView = PDFView() fileprivate let thumbnailView = PDFThumbnailView() fileprivate let containerView = UIStackView() diff --git a/ownCloud/Client/Viewer/PreviewViewController.swift b/ownCloud/Client/Viewer/PreviewViewController.swift new file mode 100644 index 000000000..5024cfe3c --- /dev/null +++ b/ownCloud/Client/Viewer/PreviewViewController.swift @@ -0,0 +1,145 @@ +// +// PreviewViewController.swift +// ownCloud +// +// Created by Michael Neuwert on 27.08.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 QuickLook + +class PreviewViewController : DisplayViewController, QLPreviewControllerDataSource, QLPreviewControllerDelegate { + + private var qlPreviewController: QLPreviewController? + var showHideBarsTapGestureRecognizer: UITapGestureRecognizer! + + override func viewDidLoad() { + super.viewDidLoad() + + qlPreviewController = QLPreviewController() + addChild(qlPreviewController!) + qlPreviewController!.view.frame = self.view.bounds + self.view.addSubview(qlPreviewController!.view) + qlPreviewController!.didMove(toParent: self) + + qlPreviewController?.view.isHidden = true + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + if let qlPreviewController = self.qlPreviewController { + qlPreviewController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + qlPreviewController.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), + qlPreviewController.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor), + qlPreviewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + qlPreviewController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) + ]) + } + + self.view.layoutIfNeeded() + } + + override func renderSpecificView(completion: @escaping (Bool) -> Void) { + if source != nil { + + self.showHideBarsTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.showHideBars)) + self.showHideBarsTapGestureRecognizer.delegate = self + self.showHideBarsTapGestureRecognizer.delaysTouchesBegan = true + self.qlPreviewController?.view.gestureRecognizers?.forEach({ $0.delegate = self }) + self.qlPreviewController?.view?.addGestureRecognizer(self.showHideBarsTapGestureRecognizer) + + self.qlPreviewController?.dataSource = self + self.qlPreviewController?.view.isHidden = false + + completion(true) + } else { + completion(false) + } + } + + @objc func showHideBars() { + guard let navigationController = navigationController else { + return + } + + if !navigationController.isNavigationBarHidden { + navigationController.setNavigationBarHidden(true, animated: true) + } else { + navigationController.setNavigationBarHidden(false, animated: true) + } + + setNeedsUpdateOfHomeIndicatorAutoHidden() + } + + // MARK: - QLPreviewControllerDataSource + func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + return source != nil ? 1 : 0 + } + + // MARK: - QLPreviewControllerDelegate + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + return source! as QLPreviewItem + } + + override func canPreview(url:URL) -> Bool { + return QLPreviewController.canPreview(url as QLPreviewItem) + } +} + +// MARK: - Gesture recognizer delegete. +extension PreviewViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + // Don't recognize a single tap until a double-tap fails. + if let otherTapGestureRecognizer = otherGestureRecognizer as? UITapGestureRecognizer { + if gestureRecognizer == self.showHideBarsTapGestureRecognizer && otherTapGestureRecognizer.numberOfTapsRequired == 2 { + return true + } + } + return false + } +} + +// MARK: - Display Extension. +extension PreviewViewController: DisplayExtension { + + static var customMatcher: OCExtensionCustomContextMatcher? = { (context, defaultPriority) in + do { + if let mimeType = context.location?.identifier?.rawValue { + let supportedFormatsRegex = try NSRegularExpression(pattern: "\\A((text/)|(image/svg)|(model/(vnd|usd))|(application/(rtf|x-rtf|doc))|(application/x-iwork*)|(application/(vnd.|ms))(?!(oasis|android))(ms|openxmlformats)?)", options: .caseInsensitive) + let matches = supportedFormatsRegex.numberOfMatches(in: mimeType, options: .reportCompletion, range: NSRange(location: 0, length: mimeType.count)) + + if matches > 0 { + return OCExtensionPriority.locationMatch + } + } + + return OCExtensionPriority.noMatch + } catch { + return OCExtensionPriority.noMatch + } + } + + static var supportedMimeTypes: [String]? + static var displayExtensionIdentifier: String = "org.owncloud.ql_preview" + static var features: [String : Any]? = [FeatureKeys.canEdit : false] +} diff --git a/ownCloud/Client/Viewer/WebView/WebViewDisplayViewController.swift b/ownCloud/Client/Viewer/WebView/WebViewDisplayViewController.swift index 5254c6754..b2d8f4191 100644 --- a/ownCloud/Client/Viewer/WebView/WebViewDisplayViewController.swift +++ b/ownCloud/Client/Viewer/WebView/WebViewDisplayViewController.swift @@ -119,7 +119,7 @@ extension WebViewDisplayViewController: DisplayExtension { static var customMatcher: OCExtensionCustomContextMatcher? = { (context, defaultPriority) in do { if let mimeType = context.location?.identifier?.rawValue { - let supportedFormatsRegex = try NSRegularExpression(pattern: "\\A((text/)|(application/javascript)|(application/json)|(application/octet-stream)|(application/x-php)|(image/(gif|svg))|(application/(vnd.|ms))(?!(oasis|android))(ms|openxmlformats)?)", options: .caseInsensitive) + let supportedFormatsRegex = try NSRegularExpression(pattern: "\\A((text/(html|css))|(image/gif)|(application/(javascript|json|x-php|octet-stream)))", options: .caseInsensitive) let matches = supportedFormatsRegex.numberOfMatches(in: mimeType, options: .reportCompletion, range: NSRange(location: 0, length: mimeType.count)) if matches > 0 { diff --git a/ownCloud/FileLists/FileListTableViewController.swift b/ownCloud/FileLists/FileListTableViewController.swift index 1a280510a..22836125c 100644 --- a/ownCloud/FileLists/FileListTableViewController.swift +++ b/ownCloud/FileLists/FileListTableViewController.swift @@ -72,7 +72,7 @@ class FileListTableViewController: UITableViewController, ClientItemCellDelegate } let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreItem) - let actionContext = ActionContext(viewController: self, core: core, query: query, items: [item], location: actionsLocation) + let actionContext = ActionContext(viewController: self, core: core, query: query, items: [item], location: actionsLocation, sender: cell) if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: makeActionProgressHandler()) { self.present(asCard: moreViewController, animated: true) @@ -222,26 +222,43 @@ class FileListTableViewController: UITableViewController, ClientItemCellDelegate return } - if let core = self.core { - switch rowItem.type { - case .collection: - if let path = rowItem.path { - self.navigationController?.pushViewController(ClientQueryViewController(core: core, query: OCQuery(forPath: path)), animated: true) - } - - case .file: - guard let query = self.query(forItem: rowItem) else { - return - } + open(item: rowItem, animated: true) + } + } - let itemViewController = DisplayHostViewController(core: core, selectedItem: rowItem, query: query) - itemViewController.hidesBottomBarWhenPushed = true - itemViewController.progressSummarizer = self.progressSummarizer - self.navigationController?.pushViewController(itemViewController, animated: true) + @discardableResult func open(item: OCItem, animated: Bool, pushViewController: Bool = true) -> ClientQueryViewController? { + if let core = self.core { + if #available(iOS 13.0, *) { + if let tabBarController = self.tabBarController as? ClientRootViewController { + let activity = OpenItemUserActivity(detailItem: item, detailBookmark: tabBarController.bookmark) + view.window?.windowScene?.userActivity = activity.openItemUserActivity } } + switch item.type { + case .collection: + if let path = item.path { + let clientQueryViewController = ClientQueryViewController(core: core, query: OCQuery(forPath: path)) + if pushViewController { + self.navigationController?.pushViewController(clientQueryViewController, animated: animated) + } + + return clientQueryViewController + } + + case .file: + guard let query = self.query(forItem: item) else { + return nil + } + + let itemViewController = DisplayHostViewController(core: core, selectedItem: item, query: query) + itemViewController.hidesBottomBarWhenPushed = true + itemViewController.progressSummarizer = self.progressSummarizer + self.navigationController?.pushViewController(itemViewController, animated: animated) + } } + + return nil } // MARK: - Themable diff --git a/ownCloud/FileLists/QueryFileListTableViewController.swift b/ownCloud/FileLists/QueryFileListTableViewController.swift index 950f4ffd3..61f53b34a 100644 --- a/ownCloud/FileLists/QueryFileListTableViewController.swift +++ b/ownCloud/FileLists/QueryFileListTableViewController.swift @@ -44,12 +44,6 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele if query.sortComparator == nil { query.sortComparator = self.sortMethod.comparator(direction: sortDirection) } - - core?.start(query) - - queryStateObservation = query.observe(\OCQuery.state, options: .initial, changeHandler: { [weak self] (_, _) in - self?.updateQueryProgressSummary() - }) } required init?(coder aDecoder: NSCoder) { @@ -58,10 +52,6 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele deinit { NotificationCenter.default.removeObserver(self, name: .DisplaySettingsChanged, object: nil) - - queryProgressSummary = nil - - core?.stop(query) } // MARK: - Display settings @@ -211,7 +201,7 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele func queryHasChangesAvailable(_ query: OCQuery) { queryRefreshRateLimiter.runRateLimitedBlock { - query.requestChangeSet(withFlags: OCQueryChangeSetRequestFlag(rawValue: 0)) { (query, changeSet) in + query.requestChangeSet(withFlags: .onlyResults) { (query, changeSet) in OnMainThread { if query.state.isFinal { OnMainThread { @@ -241,7 +231,9 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele self.messageView?.message(show: false) } + let indexPath = self.tableView.indexPathForSelectedRow self.tableView.reloadData() + self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) case .targetRemoved: self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) self.tableView.reloadData() @@ -298,12 +290,22 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + core?.start(query) + + queryStateObservation = query.observe(\OCQuery.state, options: .initial, changeHandler: { [weak self] (_, _) in + self?.updateQueryProgressSummary() + }) + updateQueryProgressSummary() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + core?.stop(query) + queryStateObservation?.invalidate() + queryStateObservation = nil + queryProgressSummary = nil searchController?.searchBar.text = "" @@ -381,4 +383,33 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele return 0 } + + @available(iOS 13.0, *) + override func tableView(_ tableView: UITableView, + contextMenuConfigurationForRowAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + if let item = itemAt(indexPath: indexPath), UIDevice.current.isIpad() { + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in + return self.makeContextMenu(for: indexPath, with: item) + }) + } + + return nil + } + + @available(iOS 13.0, *) + func makeContextMenu(for indexPath: IndexPath, with item: OCItem) -> UIMenu { + let openWindow = UIAction(title: "Open in a new Window".localized, image: UIImage(systemName: "uiwindow.split.2x1")) { _ in + self.openItemInWindow(at: indexPath) + } + return UIMenu(title: item.name ?? "", children: [openWindow]) + } + + @available(iOS 13.0, *) + func openItemInWindow(at indexPath: IndexPath) { + if let item = itemAt(indexPath: indexPath), let tabBarController = self.tabBarController as? ClientRootViewController { + let activity = OpenItemUserActivity(detailItem: item, detailBookmark: tabBarController.bookmark) + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity.openItemUserActivity, options: nil) + } + } } diff --git a/ownCloud/Import/ImportFilesController.swift b/ownCloud/Import/ImportFilesController.swift index af123892d..24cbdd85a 100644 --- a/ownCloud/Import/ImportFilesController.swift +++ b/ownCloud/Import/ImportFilesController.swift @@ -129,7 +129,7 @@ extension ImportFilesController { if bookmarks.count > 0 { let moreViewController = self.cardViewController(for: self.localCopyURL ?? self.url) - if let delegateWindow = UIApplication.shared.delegate?.window, let window = delegateWindow { + if let window = UIApplication.shared.currentWindow() { let viewController = window.rootViewController if let navigationController = viewController as? UINavigationController, let viewController = navigationController.visibleViewController { OnMainThread { @@ -159,8 +159,8 @@ extension ImportFilesController { let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) pickerNavigationController.modalPresentationStyle = .formSheet - if let window = UIApplication.shared.delegate?.window { - let viewController = window!.rootViewController + if let window = UIApplication.shared.currentWindow() { + let viewController = window.rootViewController if let navCon = viewController as? UINavigationController, let viewController = navCon.visibleViewController { viewController.present(pickerNavigationController, animated: true) } else { diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift new file mode 100644 index 000000000..6b299ec5d --- /dev/null +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -0,0 +1,1070 @@ +// +// KeyCommands.swift +// ownCloud +// +// Created by Matthias Hühne on 27.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 MobileCoreServices + +extension ServerListTableViewController { + override var keyCommands: [UIKeyCommand]? { + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + let addAccountCommand = UIKeyCommand(input: "+", modifierFlags: [.command], action: #selector(addBookmark), discoverabilityTitle: "Add account".localized.localized) + let openSettingsCommand = UIKeyCommand(input: ",", modifierFlags: [.command], action: #selector(settings), discoverabilityTitle: "Settings".localized.localized) + + let editSettingsCommand = UIKeyCommand(input: ",", modifierFlags: [.command, .shift], action: #selector(editBookmark), discoverabilityTitle: "Edit".localized) + let manageSettingsCommand = UIKeyCommand(input: "M", modifierFlags: [.command, .shift], action: #selector(manageBookmark), discoverabilityTitle: "Manage".localized) + let deleteSettingsCommand = UIKeyCommand(input: "\u{08}", modifierFlags: [.command, .shift], action: #selector(deleteBookmarkCommand), discoverabilityTitle: "Delete".localized) + + var shortcuts = [UIKeyCommand]() + if let selectedRow = self.tableView?.indexPathForSelectedRow?.row { + shortcuts.append(editSettingsCommand) + shortcuts.append(manageSettingsCommand) + shortcuts.append(deleteSettingsCommand) + if #available(iOS 13.0, *), UIDevice.current.isIpad() { + let openWindowCommand = UIKeyCommand(input: "W", modifierFlags: [.command, .shift], action: #selector(openSelectedBookmarkInWindow), discoverabilityTitle: "Open in new Window".localized) + shortcuts.append(openWindowCommand) + } + + if selectedRow < OCBookmarkManager.shared.bookmarks.count - 1 { + shortcuts.append(nextObjectCommand) + } + if selectedRow > 0 { + shortcuts.append(previousObjectCommand) + } + shortcuts.append(selectObjectCommand) + } else if self.tableView?.numberOfRows(inSection: 0) ?? 0 > 0 { + shortcuts.append(nextObjectCommand) + } + + for (index, bookmark) in OCBookmarkManager.shared.bookmarks.enumerated() { + let accountIndex = String(index + 1) + let selectAccountCommand = UIKeyCommand(input: accountIndex, modifierFlags: [.command, .shift], action: #selector(selectBookmark), discoverabilityTitle: bookmark.shortName) + shortcuts.append(selectAccountCommand) + } + + shortcuts.append(addAccountCommand) + shortcuts.append(openSettingsCommand) + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } + + @available(iOS 13.0, *) + @objc func openSelectedBookmarkInWindow() { + if let indexPath = self.tableView?.indexPathForSelectedRow { + openAccountInWindow(at: indexPath) + } + } + + @objc func selectBookmark(_ command : UIKeyCommand) { + for bookmark in OCBookmarkManager.shared.bookmarks { + if bookmark.shortName == command.discoverabilityTitle { + self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) + } + } + } + + @objc func editBookmark() { + if let indexPath = self.tableView?.indexPathForSelectedRow, let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { + showBookmarkUI(edit: bookmark) + } + } + + @objc func manageBookmark() { + if let indexPath = self.tableView?.indexPathForSelectedRow, let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { + showBookmarkInfoUI(bookmark) + } + } + + @objc func deleteBookmarkCommand() { + if let indexPath = self.tableView?.indexPathForSelectedRow, let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { + delete(bookmark: bookmark, at: indexPath) + } + } +} + +extension BookmarkViewController { + override var keyCommands: [UIKeyCommand]? { + return [ + UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Cancel".localized), + UIKeyCommand(input: "C", modifierFlags: [.command], action: #selector(handleContinue), discoverabilityTitle: "Continue".localized) + ] + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension IssuesViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + + if let buttons = buttons { + var counter = 1 + for button in buttons { + let command = UIKeyCommand(input: String(counter), modifierFlags: [.command], action: #selector(issueButtonPressed), discoverabilityTitle: button.title) + shortcuts.append(command) + counter += 1 + } + } + + return shortcuts + } + + @objc func issueButtonPressed(_ command : UIKeyCommand) { + guard let button = buttons?.first(where: {$0.title == command.discoverabilityTitle}) else { return } + + let buttonPressed: IssueButton = button + buttonPressed.action() + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension UIAlertController { + + typealias AlertHandler = @convention(block) (UIAlertAction) -> Void + + override open var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + var counter = 1 + for action in actions { + if let title = action.title { + let command = UIKeyCommand(input: String(counter), modifierFlags: [.command], action: #selector(tapActionButton), discoverabilityTitle: title) + shortcuts.append(command) + counter += 1 + } + } + + return shortcuts + } + + @objc func tapActionButton(_ command : UIKeyCommand) { + guard let action = actions.first(where: {$0.title == command.discoverabilityTitle}) else { return } + guard let block = action.value(forKey: "handler") else { + dismiss(animated: true, completion: nil) + return + } + + let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self) + dismiss(animated: true) { + handler(action) + } + } + + override open var canBecomeFirstResponder: Bool { + return true + } +} + +extension BookmarkInfoViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + + let doneCommand = UIKeyCommand(input: "D", modifierFlags: [.command], action: #selector(userActionDone), discoverabilityTitle: "Done".localized) + shortcuts.append(doneCommand) + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension ThemeNavigationController { + override var keyCommands: [UIKeyCommand]? { + + var shortcuts = [UIKeyCommand]() + + if self.viewControllers.count > 1 { + let backCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [.command], action: #selector(popViewControllerAnimated), discoverabilityTitle: "Back".localized) + shortcuts.append(backCommand) + } + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } + + @objc func popViewControllerAnimated() { + _ = popViewController(animated: true) + } +} + +extension NamingViewController { + override var keyCommands: [UIKeyCommand]? { + + var shortcuts = [UIKeyCommand]() + if let leftItem = self.navigationItem.leftBarButtonItem, let action = leftItem.action { + let dismissCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: action, discoverabilityTitle: "Cancel".localized) + shortcuts.append(dismissCommand) + } + if let rightItem = self.navigationItem.rightBarButtonItem, let action = rightItem.action { + let doneCommand = UIKeyCommand(input: "D", modifierFlags: [.command], action: action, discoverabilityTitle: "Done".localized) + shortcuts.append(doneCommand) + } + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension ClientRootViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + + let excludeViewControllers = [ThemedAlertController.self, SharingTableViewController.self, PublicLinkTableViewController.self, PublicLinkEditTableViewController.self, GroupSharingEditTableViewController.self] + + if let navigationController = self.selectedViewController as? ThemeNavigationController, let visibleController = navigationController.visibleViewController { + if excludeViewControllers.contains(where: {$0 == type(of: visibleController)}) { + return shortcuts + } + } + + let keyCommands = self.tabBar.items?.enumerated().map { (index, item) -> UIKeyCommand in + let tabIndex = String(index + 1) + return UIKeyCommand(input: tabIndex, modifierFlags: .command, action:#selector(selectTab), discoverabilityTitle: item.title ?? String(format: "Tab %@".localized, tabIndex)) + } + if let keyCommands = keyCommands { + shortcuts.append(contentsOf: keyCommands) + } + + if let navigationController = self.selectedViewController as? ThemeNavigationController, (navigationController.visibleViewController is ClientQueryViewController || navigationController.visibleViewController is GroupSharingTableViewController) { + let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) + shortcuts.append(cancelCommand) + } + + return shortcuts + } + + @objc func dismissSearch(sender: UIKeyCommand) { + if let navigationController = self.selectedViewController as? ThemeNavigationController { + if let clientQueryViewController = navigationController.visibleViewController as? ClientQueryViewController { + clientQueryViewController.searchController?.isActive = false + } else if let groupSharingViewController = navigationController.visibleViewController as? GroupSharingTableViewController { + groupSharingViewController.searchController?.isActive = false + } + } + } + + @objc func selectTab(sender: UIKeyCommand) { + if let newIndex = Int(sender.input!), newIndex >= 1 && newIndex <= (self.tabBar.items?.count ?? 0) { + self.selectedIndex = newIndex - 1 + } + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension UITableViewController { + + @objc func selectNext(sender: UIKeyCommand) { + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow, selectedIndexPath.row < (self.tableView?.numberOfRows(inSection: selectedIndexPath.section) ?? 0 ) - 1 { + self.tableView.selectRow(at: NSIndexPath(row: selectedIndexPath.row + 1, section: selectedIndexPath.section) as IndexPath, animated: true, scrollPosition: .middle) + } else if self.tableView?.numberOfRows(inSection: 0) ?? 0 > 0 { + self.tableView.selectRow(at: NSIndexPath(row: 0, section: 0) as IndexPath, animated: true, scrollPosition: .top) + } + } + + @objc func selectPrevious(sender: UIKeyCommand) { + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow, selectedIndexPath.row > 0 { + self.tableView.selectRow(at: NSIndexPath(row: selectedIndexPath.row - 1, section: selectedIndexPath.section) as IndexPath, animated: true, scrollPosition: .middle) + } + } + + @objc func selectCurrent(sender: UIKeyCommand) { + if let delegate = tableView.delegate, let tableView = tableView, let indexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: indexPath, animated: true) + delegate.tableView!(tableView, didSelectRowAt: indexPath) + } + } + + override open var canBecomeFirstResponder: Bool { + return true + } +} + +extension GroupSharingTableViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + let searchCommand = UIKeyCommand(input: "F", modifierFlags: [.command], action: #selector(enableSearch), discoverabilityTitle: "Search".localized) + let doneCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Done".localized) + shortcuts.append(searchCommand) + shortcuts.append(doneCommand) + + return shortcuts + } + + @objc func enableSearch() { + self.searchController?.isActive = true + self.searchController?.searchBar.becomeFirstResponder() + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension GroupSharingEditTableViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + let dismissCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Cancel".localized) + let createCommand = UIKeyCommand(input: "S", modifierFlags: [.command], action: #selector(createShareAndDismiss), discoverabilityTitle: "Save".localized) + shortcuts.append(dismissCommand) + shortcuts.append(createCommand) + + if createShare { + let showInfoObjectCommand = UIKeyCommand(input: "H", modifierFlags: [.command, .alternate], action: #selector(showInfoSubtitles), discoverabilityTitle: "Help".localized) + shortcuts.append(showInfoObjectCommand) + } + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension PublicLinkTableViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + let doneCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Done".localized) + shortcuts.append(doneCommand) + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension PublicLinkEditTableViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + let dismissCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Cancel".localized) + let createCommand = UIKeyCommand(input: "S", modifierFlags: [.command], action: #selector(createPublicLink), discoverabilityTitle: "Create".localized) + shortcuts.append(dismissCommand) + shortcuts.append(createCommand) + + if createLink { + let showInfoObjectCommand = UIKeyCommand(input: "H", modifierFlags: [.command, .alternate], action: #selector(showInfoSubtitles), discoverabilityTitle: "Help".localized) + shortcuts.append(showInfoObjectCommand) + } else { + let shareObjectCommand = UIKeyCommand(input: "S", modifierFlags: [.command, .alternate], action: #selector(shareLinkURL), discoverabilityTitle: "Share".localized) + shortcuts.append(shareObjectCommand) + } + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension StaticTableViewController { + + override var keyCommands: [UIKeyCommand]? { + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + + var shortcuts = [UIKeyCommand]() + + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow { + let selectedRow = selectedIndexPath.row + let selectedSection = selectedIndexPath.section + if selectedRow < sections[selectedSection].rows.count - 1 || sections.count > selectedSection { + shortcuts.append(nextObjectCommand) + } + if selectedRow > 0 || selectedSection > 0 { + shortcuts.append(previousObjectCommand) + } + if staticRowForIndexPath(selectedIndexPath).type == .slider { + let row = staticRowForIndexPath(selectedIndexPath) + let sliders = row.cell?.subviews.filter { $0 is UISlider } + if let slider = sliders?.first as? UISlider { + slider.thumbTintColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + } + let sliderDownCommand = UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(sliderDown), discoverabilityTitle: "Decrease Slider Value".localized) + let sliderUpCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(sliderUp), discoverabilityTitle: "Increase Slider Value".localized) + shortcuts.append(sliderDownCommand) + shortcuts.append(sliderUpCommand) + } else { + shortcuts.append(selectObjectCommand) + } + } else if self.tableView?.numberOfSections ?? 0 > 0, self.tableView?.numberOfRows(inSection: 0) ?? 0 > 0 { + shortcuts.append(nextObjectCommand) + } + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } + + @objc func sliderDown() { + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow { + let row = staticRowForIndexPath(selectedIndexPath) + let sliders = row.cell?.subviews.filter { $0 is UISlider } + if let slider = sliders?.first as? UISlider, slider.value > slider.minimumValue { + slider.value = (slider.value - 1.0) + slider.sendActions(for: .valueChanged) + } + } + } + + @objc func sliderUp() { + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow { + let row = staticRowForIndexPath(selectedIndexPath) + let sliders = row.cell?.subviews.filter { $0 is UISlider } + if let slider = sliders?.first as? UISlider, slider.value < slider.maximumValue { + slider.value = (slider.value + 1.0) + slider.sendActions(for: .valueChanged) + } + } + } + + @objc override func selectNext(sender: UIKeyCommand) { + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow { + let staticRow = staticRowForIndexPath(selectedIndexPath) + self.tableView.endEditing(true) + if staticRow.type == .switchButton, let switchButton = staticRow.cell?.accessoryView as? UISwitch { + switchButton.tintColor = .white + staticRow.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowColors.labelColor + } else if staticRow.type == .text || staticRow.type == .secureText, let textField = staticRow.textField { + textField.textColor = Theme.shared.activeCollection.tableRowColors.labelColor + } else if staticRow.type == .slider { + let sliders = staticRow.cell?.subviews.filter { $0 is UISlider } + if let slider = sliders?.first as? UISlider { + slider.thumbTintColor = .white + } + } + + if (selectedIndexPath.row + 1) < sections[selectedIndexPath.section].rows.count { + self.tableView.selectRow(at: NSIndexPath(row: selectedIndexPath.row + 1, section: selectedIndexPath.section) as IndexPath, animated: true, scrollPosition: .middle) + } else if (selectedIndexPath.section + 1) < sections.count { + // New Section + self.tableView.selectRow(at: NSIndexPath(row: 0, section: (selectedIndexPath.section + 1)) as IndexPath, animated: true, scrollPosition: .middle) + } + } else { + self.tableView.selectRow(at: NSIndexPath(row: 0, section: 0) as IndexPath, animated: true, scrollPosition: .top) + } + + if let selectedIndexPath = self.tableView?.indexPathForSelectedRow { + let staticRow = staticRowForIndexPath(selectedIndexPath) + if staticRow.type == .switchButton, let switchButon = staticRow.cell?.accessoryView as? UISwitch { + switchButon.tintColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + staticRow.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + } else if staticRow.type == .text || staticRow.type == .secureText, let textField = staticRow.textField { + textField.textColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + } + } + } + + @objc override func selectPrevious(sender: UIKeyCommand) { + if let indexPath = self.tableView?.indexPathForSelectedRow { + let staticRow = staticRowForIndexPath(indexPath) + self.tableView.endEditing(true) + if staticRow.type == .switchButton, let switchButon = staticRow.cell?.accessoryView as? UISwitch { + switchButon.tintColor = .white + staticRow.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowColors.labelColor + } else if staticRow.type == .text || staticRow.type == .secureText, let textField = staticRow.textField { + textField.textColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + } else if staticRow.type == .slider { + let sliders = staticRow.cell?.subviews.filter { $0 is UISlider } + if let slider = sliders?.first as? UISlider { + slider.thumbTintColor = .white + } + } + + if indexPath.row == 0, indexPath.section > 0 { + let sectionRows = sections[indexPath.section - 1] + self.tableView.selectRow(at: NSIndexPath(row: sectionRows.rows.count - 1, section: indexPath.section - 1) as IndexPath, animated: true, scrollPosition: .middle) + } else { + self.tableView.selectRow(at: NSIndexPath(row: indexPath.row - 1, section: indexPath.section) as IndexPath, animated: true, scrollPosition: .middle) + } + + if let indexPath = self.tableView?.indexPathForSelectedRow { + let staticRow = staticRowForIndexPath(indexPath) + if staticRow.type == .switchButton, let switchButon = staticRow.cell?.accessoryView as? UISwitch { + switchButon.tintColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + staticRow.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + } else if staticRow.type == .text || staticRow.type == .secureText, let textField = staticRow.textField { + textField.textColor = Theme.shared.activeCollection.tableRowHighlightColors.backgroundColor + } + } + } + } + + @objc override func selectCurrent(sender: UIKeyCommand) { + if let indexPath = self.tableView?.indexPathForSelectedRow { + let staticRow = staticRowForIndexPath(indexPath) + if staticRow.type == .switchButton, let switchButton = staticRow.cell?.accessoryView as? UISwitch { + if switchButton.isOn { + switchButton.setOn(false, animated: true) + staticRow.value = false + } else { + switchButton.setOn(true, animated: true) + staticRow.value = true + } + + if let action = staticRow.action { + action(staticRow, switchButton) + } + } else if staticRow.type == .text || staticRow.type == .secureText, let textField = staticRow.textField { + textField.becomeFirstResponder() + } else if let delegate = tableView.delegate, let tableView = tableView { + tableView.deselectRow(at: indexPath, animated: true) + delegate.tableView!(tableView, didSelectRowAt: indexPath) + } + } + } +} + +extension ClientQueryViewController { + + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + + if let selectedRow = self.tableView?.indexPathForSelectedRow?.row { + if selectedRow < self.items.count - 1 { + shortcuts.append(nextObjectCommand) + } + if selectedRow > 0 { + shortcuts.append(previousObjectCommand) + } + shortcuts.append(selectObjectCommand) + } else { + shortcuts.append(nextObjectCommand) + } + + if let core = core, let rootItem = query.rootItem { + var item = rootItem + if let indexPath = self.tableView?.indexPathForSelectedRow, let selectedItem = itemAt(indexPath: indexPath) { + item = selectedItem + } + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreFolder) + let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) + let actions = Action.sortedApplicableActions(for: actionContext) + + actions.forEach({ + if let keyCommand = $0.actionExtension.keyCommand, let keyModifierFlags = $0.actionExtension.keyModifierFlags { + let actionCommand = UIKeyCommand(input: keyCommand, modifierFlags: keyModifierFlags, action: #selector(performFolderAction), discoverabilityTitle: $0.actionExtension.name) + shortcuts.append(actionCommand) + } + }) + } + + return shortcuts + } + + @objc func performFolderAction(_ command : UIKeyCommand) { + if let core = core, let rootItem = query.rootItem { + var item = rootItem + if let indexPath = self.tableView?.indexPathForSelectedRow, let selectedItem = itemAt(indexPath: indexPath) { + item = selectedItem + } + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreFolder) + let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) + let actions = Action.sortedApplicableActions(for: actionContext) + actions.forEach({ + if command.discoverabilityTitle == $0.actionExtension.name { + $0.perform() + } + }) + } + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension LibrarySharesTableViewController { + + override var canBecomeFirstResponder: Bool { + return true + } + + override var keyCommands: [UIKeyCommand]? { + + var shortcuts = [UIKeyCommand]() + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + + if let selectedRow = self.tableView?.indexPathForSelectedRow?.row { + if selectedRow < self.shares.count - 1 { + shortcuts.append(nextObjectCommand) + } + if selectedRow > 0 { + shortcuts.append(previousObjectCommand) + } + shortcuts.append(selectObjectCommand) + } else { + shortcuts.append(nextObjectCommand) + } + + return shortcuts + } +} + +extension QueryFileListTableViewController { + + override var canBecomeFirstResponder: Bool { + return true + } + + override var keyCommands: [UIKeyCommand]? { + + var shortcuts = [UIKeyCommand]() + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let selectLastPageObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [.command], action: #selector(selectLastPageObject), discoverabilityTitle: "Select Last Item on Page".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + let scrollTopCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [.command, .shift], action: #selector(scrollToFirstRow), discoverabilityTitle: "Scroll to Top".localized) + let scrollBottomCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [.command, .shift], action: #selector(scrollToLastRow), discoverabilityTitle: "Scroll to Bottom".localized) + let toggleSortCommand = UIKeyCommand(input: "S", modifierFlags: [.command, .shift], action: #selector(toggleSortOrder), discoverabilityTitle: "Change Sort Order".localized) + let searchCommand = UIKeyCommand(input: "F", modifierFlags: [.command], action: #selector(enableSearch), discoverabilityTitle: "Search".localized) + // Add key commands for file name letters + if sortMethod == .alphabetically { + let indexTitles = Array( Set( self.items.map { String(( $0.name?.first!.uppercased())!) })).sorted() + for title in indexTitles { + let letterCommand = UIKeyCommand(input: title, modifierFlags: [], action: #selector(selectLetter)) + shortcuts.append(letterCommand) + } + } + + if let core = core, let rootItem = query.rootItem { + var item = rootItem + if let indexPath = self.tableView?.indexPathForSelectedRow, let selectedItem = itemAt(indexPath: indexPath) { + item = selectedItem + } + let actionsLocationCollaborate = OCExtensionLocation(ofType: .action, identifier: .keyboardShortcut) + let actionContextCollaborate = ActionContext(viewController: self, core: core, items: [item], location: actionsLocationCollaborate) + let actionsCollaborate = Action.sortedApplicableActions(for: actionContextCollaborate) + + actionsCollaborate.forEach({ + if let keyCommand = $0.actionExtension.keyCommand, let keyModifierFlags = $0.actionExtension.keyModifierFlags { + let actionCommand = UIKeyCommand(input: keyCommand, modifierFlags: keyModifierFlags, action: #selector(performMoreItemAction), discoverabilityTitle: $0.actionExtension.name) + shortcuts.append(actionCommand) + } + }) + } + + shortcuts.append(searchCommand) + shortcuts.append(toggleSortCommand) + + for (index, method) in SortMethod.all.enumerated() { + let sortTitle = String(format: "Sort by %@".localized, method.localizedName()) + let sortCommand = UIKeyCommand(input: String(index + 1), modifierFlags: [.command, .alternate], action: #selector(changeSortMethod), discoverabilityTitle: sortTitle) + shortcuts.append(sortCommand) + } + + if let selectedRow = self.tableView?.indexPathForSelectedRow?.row { + if selectedRow < self.items.count - 1 { + shortcuts.append(nextObjectCommand) + } + if selectedRow > 0 { + shortcuts.append(previousObjectCommand) + } + shortcuts.append(selectObjectCommand) + } else { + shortcuts.append(nextObjectCommand) + } + shortcuts.append(scrollTopCommand) + shortcuts.append(scrollBottomCommand) + if self.items.count > 0 { + shortcuts.append(selectLastPageObjectCommand) + } + + return shortcuts + } + + @objc func selectLetter(_ command : UIKeyCommand) { + if let title = command.input { + let firstItem = self.items.filter { (( $0.name?.uppercased().hasPrefix(title) ?? nil)! ) }.first + + if let firstItem = firstItem { + if let itemIndex = self.items.index(of: firstItem) { + let indexPath = IndexPath(row: itemIndex, section: 0) + tableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.middle, animated: false) + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle) + } + } + } + } + + @objc func performMoreItemAction(_ command : UIKeyCommand) { + if let core = core, let rootItem = query.rootItem { + var item = rootItem + if let indexPath = self.tableView?.indexPathForSelectedRow, let selectedItem = itemAt(indexPath: indexPath) { + item = selectedItem + } + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .keyboardShortcut) + let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) + let actions = Action.sortedApplicableActions(for: actionContext) + actions.forEach({ + if command.discoverabilityTitle == $0.actionExtension.name { + $0.perform() + } + }) + } + } + + @objc func enableSearch() { + self.searchController?.isActive = true + self.searchController?.searchBar.becomeFirstResponder() + } + + @objc func toggleSortOrder() { + self.sortBar?.sortMethod = self.sortMethod + } + + @objc func changeSortMethod(_ command : UIKeyCommand) { + for (_, method) in SortMethod.all.enumerated() { + let sortTitle = String(format: "Sort by %@".localized, method.localizedName()) + if command.discoverabilityTitle == sortTitle { + self.sortBar?.sortMethod = method + break + } + } + } + + @objc func selectLastPageObject() { + if self.items.count > 0, let lastCell = tableView.visibleCells.last, let indexPath = tableView.indexPath(for: lastCell) { + self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top) + } + } + + @objc func scrollToFirstRow() { + if self.items.count > 0 { + self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true) + tableView.selectRow(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .bottom) + } + } + + @objc func scrollToLastRow() { + if self.items.count > 0 { + self.tableView.scrollToRow(at: IndexPath(row: self.items.count - 1, section: 0), at: .bottom, animated: true) + tableView.selectRow(at: IndexPath(row: self.items.count - 1, section: 0), animated: true, scrollPosition: .bottom) + } + } +} + +extension ClientDirectoryPickerViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + shortcuts.append(nextObjectCommand) + shortcuts.append(previousObjectCommand) + shortcuts.append(selectObjectCommand) + + if let selectButtonTitle = selectButton?.title, let selector = selectButton?.action { + let doCommand = UIKeyCommand(input: "\r", modifierFlags: [.command], action: selector, discoverabilityTitle: selectButtonTitle) + shortcuts.append(doCommand) + } + + let createFolder = UIKeyCommand(input: "N", modifierFlags: [.command], action: #selector(createFolderButtonPressed), discoverabilityTitle: "Create Folder".localized) + shortcuts.append(createFolder) + let dismissCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Cancel".localized) + shortcuts.append(dismissCommand) + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension PhotoAlbumTableViewController { + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) + + if let selectedRow = self.tableView?.indexPathForSelectedRow?.row { + if selectedRow < self.albums.count - 1 { + shortcuts.append(nextObjectCommand) + } + if selectedRow > 0 { + shortcuts.append(previousObjectCommand) + } + shortcuts.append(selectObjectCommand) + } else { + shortcuts.append(nextObjectCommand) + } + + let dismissCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Cancel".localized) + shortcuts.append(dismissCommand) + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } +} + +extension PhotoSelectionViewController { + + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) + let selectObjectCommand = UIKeyCommand(input: " ", modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Select".localized) + let selectAllCommand = UIKeyCommand(input: "A", modifierFlags: [.command], action: #selector(selectAllItems), discoverabilityTitle: "Select All".localized) + let deselectAllCommand = UIKeyCommand(input: "D", modifierFlags: [.command], action: #selector(deselectAllItems), discoverabilityTitle: "Deselect All".localized) + let uploadCommand = UIKeyCommand(input: "U", modifierFlags: [.command], action: #selector(upload), discoverabilityTitle: "Upload".localized) + let dismissCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismiss), discoverabilityTitle: "Cancel".localized) + + shortcuts.append(nextObjectCommand) + shortcuts.append(previousObjectCommand) + shortcuts.append(selectObjectCommand) + shortcuts.append(selectAllCommand) + if collectionView?.indexPathsForSelectedItems?.count ?? 0 > 0 { + shortcuts.append(deselectAllCommand) + shortcuts.append(uploadCommand) + } + shortcuts.append(dismissCommand) + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } + + @objc func selectCurrent() { + guard let focussedIndexPath = focussedIndexPath else { return } + if let isSelected = collectionView?.indexPathsForSelectedItems?.contains(focussedIndexPath), isSelected { + collectionView.deselectItem(at: focussedIndexPath, animated: true) + } else { + collectionView.selectItem(at: focussedIndexPath, animated: true, scrollPosition: .top) + } + collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: focussedIndexPath) + } + + @objc func selectNext() { + guard let focussedIndexPath = focussedIndexPath else { + self.focussedIndexPath = firstIndexPath + return + } + + let numberOfItems = collectionView.numberOfItems(inSection: 0) + let focussedItem = focussedIndexPath.item + + guard focussedItem != (numberOfItems - 1) else { + self.focussedIndexPath = firstIndexPath + return + } + + self.focussedIndexPath = IndexPath(item: focussedItem + 1, section: 0) + } + + @objc func selectPrevious() { + guard let focussedIndexPath = focussedIndexPath else { + self.focussedIndexPath = lastIndexPath + return + } + + let focussedItem = focussedIndexPath.item + + guard focussedItem > 0 else { + self.focussedIndexPath = lastIndexPath + return + } + + self.focussedIndexPath = IndexPath(item: focussedItem - 1, section: 0) + } + + private var lastIndexPath: IndexPath { + return IndexPath(item: collectionView.numberOfItems(inSection: 0) - 1, section: 0) + } + + private var firstIndexPath: IndexPath { + return IndexPath(item: 0, section: 0) + } +} + +extension PasscodeViewController { + + override var keyCommands: [UIKeyCommand]? { + var keyCommands : [UIKeyCommand] = [] + for i in 0 ..< 10 { + keyCommands.append( + UIKeyCommand(input:String(i), + modifierFlags: [], + action: #selector(self.performKeyCommand(sender:)), + discoverabilityTitle: String(i)) + ) + } + + keyCommands.append( + UIKeyCommand(input: "\u{8}", + modifierFlags: [], + action: #selector(self.performKeyCommand(sender:)), + discoverabilityTitle: "Delete".localized) + ) + + if cancelButton?.isHidden == false { + keyCommands.append( + + UIKeyCommand(input: UIKeyCommand.inputEscape, + modifierFlags: [], + action: #selector(self.performKeyCommand(sender:)), + discoverabilityTitle: "Cancel".localized) + ) + } + + return keyCommands + } + + override open var canBecomeFirstResponder: Bool { + return true + } + + @objc func performKeyCommand(sender: UIKeyCommand) { + guard let key = sender.input else { + return + } + + switch key { + case "\u{8}": + deleteLastDigit() + case UIKeyCommand.inputEscape: + cancelHandler?(self) + default: + appendDigit(digit: key) + } + + } +} + +extension DisplayHostViewController { + + override var keyCommands: [UIKeyCommand]? { + var shortcuts = [UIKeyCommand]() + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) + } + + let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Next".localized) + let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Previous".localized) + + var showCommands = false + if (self.viewControllers?.first as? PDFViewerViewController) != nil { + showCommands = true + } else if items?.count ?? 0 > 1 { + showCommands = true + } + + if showCommands { + shortcuts.append(nextObjectCommand) + shortcuts.append(previousObjectCommand) + } + + return shortcuts + } + + override var canBecomeFirstResponder: Bool { + return true + } + + @objc func selectNext() { + guard let currentViewController = self.viewControllers?.first else { return } + + if let pdfController = currentViewController as? PDFViewerViewController { + pdfController.pdfView.goToNextPage(self) + } else { + guard let nextViewController = dataSource?.pageViewController( self, viewControllerAfter: currentViewController ) else { return } + + setViewControllers([nextViewController], direction: .forward, animated: false, completion: nil) + } + } + + @objc func selectPrevious() { + guard let currentViewController = self.viewControllers?.first else { return } + + if let pdfController = currentViewController as? PDFViewerViewController { + pdfController.pdfView.goToPreviousPage(self) + } else { + guard let previousViewController = dataSource?.pageViewController( self, viewControllerBefore: currentViewController ) else { return } + + setViewControllers([previousViewController], direction: .reverse, animated: false, completion: nil) + } + } +} diff --git a/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift b/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift new file mode 100644 index 000000000..f83b87da1 --- /dev/null +++ b/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift @@ -0,0 +1,60 @@ +// +// MediaUploadActivity.swift +// ownCloud +// +// Created by Michael Neuwert on 19.11.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 ownCloudSDK + +class MediaUploadActivity : OCActivity { + + var isCancelled: Bool { + if let progress = self.progress { + return progress.isCancelled + } + return false + } + + init(identifier: String, assetCount:Int) { + super.init(identifier: identifier) + self.isCancellable = true + self.localizedDescription = "Media import".localized + + if assetCount <= 0 { + self.progress = Progress.indeterminate() + } else { + self.progress = Progress(totalUnitCount: Int64(assetCount)) + self.updateStatusMessage() + } + } + + public func updateAfterSingleFinishedUpload() { + guard let progress = self.progress else { return } + guard progress.isIndeterminate == false else { return } + + self.progress?.completedUnitCount += 1 + self.updateStatusMessage() + } + + // MARK: - Private helper methods + + private func updateStatusMessage() { + if let progress = self.progress, progress.isIndeterminate == false { + let total = progress.totalUnitCount + let current = progress.completedUnitCount + self.localizedStatusMessage = String(format: "%@ of %@".localized, "\(current)", "\(total)") + } + } +} diff --git a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift index 1ad4b493e..c3efc2dcc 100644 --- a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift +++ b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift @@ -21,114 +21,306 @@ import ownCloudSDK import Photos import MobileCoreServices -class MediaUploadQueue { +class MediaUploadQueue : OCActivitySource { - private let uploadSerialQueue = DispatchQueue(label: "com.owncloud.upload.queue", target: DispatchQueue.global(qos: .background)) + private var uploadActivity: MediaUploadActivity? - static let shared = MediaUploadQueue() + static var shared = MediaUploadQueue() - static let UploadPendingKey = OCKeyValueStoreKey(rawValue: "com.owncloud.upload.queue.upload-pending-flag") + // MARK: - OCActivitySource protocol implementation - private static var uploadStarted = false + func provideActivity() -> OCActivity { + return self.uploadActivity! + } - func uploadAssets(_ assets:[PHAsset], with core:OCCore?, at rootItem:OCItem, progressHandler:((Progress) -> Void)? = nil, assetUploadCompletion:((_ asset:PHAsset?, _ finished:Bool) -> Void)? = nil ) { + var activityIdentifier: String { + if let activity = self.uploadActivity { + return activity.identifier + } else { + return "" + } + } - let backgroundTask = OCBackgroundTask(name: "UploadMediaAction", expirationHandler: { (bgTask) in - Log.warning("UploadMediaAction background task expired") - bgTask.end() - }).start() + // MARK: - Public interface - let queue = DispatchQueue.global(qos: .userInitiated) + func addUpload(_ asset:PHAsset, for bookmark:OCBookmark, at path:String) { - weak var weakCore = core + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + storage.addJob(with: asset.localIdentifier, targetPath: path) + return storage + } - if weakCore != nil { - let vault : OCVault = OCVault(bookmark: weakCore!.bookmark) - let flag = NSNumber(value: true) - vault.keyValueStore?.storeObject(flag, forKey: MediaUploadQueue.UploadPendingKey) + self.setNeedsScheduling(in: bookmark) + } - MediaUploadQueue.uploadStarted = true + func addUploads(_ assets:[PHAsset], for bookmark:OCBookmark, at path:String) { + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + for asset in assets { + storage.addJob(with: asset.localIdentifier, targetPath: path) + } + return storage } + self.setNeedsScheduling(in: bookmark) + } - queue.async { + private var _needsSchedulingCountByBookmarkUUID : [UUID : Int] = [:] + func setNeedsScheduling(in bookmark: OCBookmark) { + // Increment counter by one + _needsSchedulingCountByBookmarkUUID[bookmark.uuid] = (_needsSchedulingCountByBookmarkUUID[bookmark.uuid] ?? 0) + 1 - guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + // Schedule right away. If it's already busy, it'll return quickly. If not, it'll schedule. + self.scheduleUploads(in: bookmark) + } - var prefferedMediaOutputFormats = [String]() - if userDefaults.convertHeic { - prefferedMediaOutputFormats.append(String(kUTTypeJPEG)) - } - if userDefaults.convertVideosToMP4 { - prefferedMediaOutputFormats.append(String(kUTTypeMPEG4)) + func scheduleUploads(in bookmark:OCBookmark) { + + var uploadStorageAlreadyProcessing = false + var uploadStorageQueueEmpty = false + var needsSchedulingCountAtEntry = _needsSchedulingCountByBookmarkUUID[bookmark.uuid] + + // Avoid race conditions by performing checks and modifications atomically + bookmark.modifyMediaUploadStorage { (mediaUploadStorage) -> MediaUploadStorage in + // First check if there are any media upload jobs stored + if mediaUploadStorage.jobCount == 0 { + uploadStorageQueueEmpty = true + } else { + // Check if upload queue processing can be started and no-one else is processing it + if mediaUploadStorage.processing != nil { + // Found OCProcessSession instance -> check if it is valid though + if OCProcessManager.shared.isSessionValid(mediaUploadStorage.processing!, usingThoroughChecks: true) { + // If the process session is valid, may be it is being used by running extension --> bail out + uploadStorageAlreadyProcessing = true + } else { + // Remove invalid session + mediaUploadStorage.processing = nil + } + } + + if mediaUploadStorage.processing == nil { + // Mark the queue as being processed + mediaUploadStorage.processing = OCProcessManager.shared.processSession + } } - let uploadGroup = DispatchGroup() - var uploadFailed = false + return mediaUploadStorage + } - for asset in assets { - if uploadFailed == false { - uploadGroup.enter() - self.uploadSerialQueue.async { - if weakCore != nil { - weakCore!.perform(inRunningCore: { (runningCoreCompletion) in - asset.upload(with: weakCore!, at: rootItem, preferredFormats: prefferedMediaOutputFormats, completionHandler: { (item, _) in - if item == nil { - uploadFailed = true - } else { - assetUploadCompletion?(asset, false) + if uploadStorageAlreadyProcessing || uploadStorageQueueEmpty { + // Already processing or nothing to do + return + } + + // Request a core for the passed bookmark + OCCoreManager.shared.requestCore(for: bookmark, setup:nil, completionHandler: {(core, _) in + + if let core = core { + + // This method is called when media import is finished or cancelled either by published activity or through unrecoverable error + func finalizeImport() { + self.unpublishImportActivity(for: core) + + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) + + // Mark the media upload storage as not in use anymore + bookmark.modifyMediaUploadStorage { (mediaUploadStorage) in + mediaUploadStorage.processing = nil + return mediaUploadStorage + } + + // Check if .setNeedsScheduling() has been called since starting the scheduling: + // since any new entries added to the queue after scheduling has started will not be handled, + // it's important to start scheduling again if any change to the queue has been performed since + if needsSchedulingCountAtEntry != self._needsSchedulingCountByBookmarkUUID[bookmark.uuid] { + self.scheduleUploads(in: bookmark) + } + } + + // Publish activity including number of jobs to be processed + guard let uploadStorage = bookmark.mediaUploadStorage else { return } + + self.publishImportActivity(for: core, itemCount: uploadStorage.jobCount) + + // Create background task used to continue media upload in the background + let backgroundTask = OCBackgroundTask(name: "com.owncloud.media-upload-task", expirationHandler: { (bgTask) in + Log.warning("UploadMediaAction background task expired") + bgTask.end() + }).start() + + let queue = DispatchQueue.global(qos: .background) + let importGroup = DispatchGroup() + + queue.async { + // Make copies to avoid side effects of caching that KVS might perform + let assetIDQueue : [String] = uploadStorage.queue + let jobsByAssetID : [String : [MediaUploadJob]] = uploadStorage.jobs + + // Iterate over PHObject local asset IDs + for assetId in assetIDQueue { + + if let assetJobs = jobsByAssetID[assetId] { + // Iterate over jobs associated with asset ID + for job in assetJobs { + + // Check if the import activity has been cancelled + if self.isImportActivityCancelled() { + // Remove all stored jobs + bookmark.modifyMediaUploadStorage { (_) -> MediaUploadStorage in + return MediaUploadStorage() + } + + finalizeImport() + return + } + + // Skip jobs for which local item IDs are valid and known in the scope of the current bookmark + if let localID = job.scheduledUploadLocalID { + if let existingItem = self.findItem(in: core, for: localID as String) { + // If item is found and it's not a placeholder, upload was finished + if existingItem.isPlaceholder == false { + // Now upload is done and the job can be removed completely + if let itemPath = existingItem.path { + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + storage.removeJob(with: assetId, targetPath: itemPath) + return storage + } + } + } + // Otherwise if isPlaceholder property is true, then upload is still ongoing, just skip it here + continue + } + } + + // Found a job which requires an import operation + if let asset = self.fetchAsset(with: assetId), let path = job.targetPath { + + importGroup.enter() + + // Convert target path to the OCItem object + var tracking = core.trackItem(atPath: path) { (_, item, isInitial) in + + if isInitial == true { + + defer { + importGroup.leave() + } + + if let rootItem = item { + // Perform asset import + if let itemLocalId = self.importAsset(asset: asset, using: core, at: rootItem, uploadCompletion: { + + // Now upload is done and the job can be removed completely + bookmark.modifyMediaUploadStorage { (storage) -> MediaUploadStorage in + storage.removeJob(with: assetId, targetPath: path) + return storage + } + + })?.localID as OCLocalID? { + + // Update import activity + self.updateActivityAfterFinishedImport(for: core) + + // Update media upload storage object + bookmark.modifyMediaUploadStorage { (storage) in + storage.update(localItemID: itemLocalId, assetId: assetId, targetPath: path) + return storage + } + } + } + } } - runningCoreCompletion() - uploadGroup.leave() - }, progressHandler: { (progress) in - progressHandler?(progress) - }) - }, withDescription: "Uploading \(assets.count) photo assets") - - } else { - uploadGroup.leave() - // Core reference became nil - uploadFailed = true + } + + importGroup.wait() + } } } - // Avoid submitting to many jobs simultaneously to reduce memory pressure - _ = uploadGroup.wait() - } else { - // Escape on first failed download - break + // Wait until all to-be-uploaded assets are processed + importGroup.notify(queue: queue, execute: { + // Finish background task + backgroundTask?.end() + finalizeImport() + }) } } + }) + } - uploadGroup.notify(queue: queue, execute: { - if weakCore != nil { - MediaUploadQueue.resetUploadPendingFlag(for: weakCore!.bookmark) - } - MediaUploadQueue.uploadStarted = false - backgroundTask?.end() - assetUploadCompletion?(nil, true) - }) + // MARK: - Private helper methods + + private func findItem(in core:OCCore, for localID:String) -> OCItem? { + guard let database = core.vault.database else { return nil } + var foundItem: OCItem? + + let semaphore = DispatchSemaphore(value: 0) + + database.retrieveCacheItem(forLocalID: localID, completionHandler: { (_, _, _, item) in + foundItem = item + semaphore.signal() + }) + + semaphore.wait() + + return foundItem + } + + private func fetchAsset(with assetID:String) -> PHAsset? { + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil) + if fetchResult.count > 0 { + return fetchResult.object(at: 0) } + return nil } - class func isMediaUploadPendingFlagSet(for bookmark:OCBookmark) -> Bool { - let vault : OCVault = OCVault(bookmark: bookmark) - if vault.keyValueStore?.readObject(forKey: MediaUploadQueue.UploadPendingKey) != nil { - return true - } else { - return false + private func importAsset(asset:PHAsset, using core:OCCore, at rootItem:OCItem, uploadCompletion: @escaping () -> Void) -> OCItem? { + + // Determine the list of preferred media formats + var prefferedMediaOutputFormats = [String]() + + if let userDefaults = OCAppIdentity.shared.userDefaults { + if userDefaults.convertHeic { + prefferedMediaOutputFormats.append(String(kUTTypeJPEG)) + } + if userDefaults.convertVideosToMP4 { + prefferedMediaOutputFormats.append(String(kUTTypeMPEG4)) + } } + + if let result = asset.upload(with: core, at: rootItem, preferredFormats: prefferedMediaOutputFormats, progressHandler: nil, uploadCompleteHandler: { + uploadCompletion() + }) { + if let error = result.1 { + Log.error("Asset upload failed with error \(error)") + } + + return result.0 + } + + return nil } - class func resetUploadPendingFlag(for bookmark:OCBookmark) { - let vault : OCVault = OCVault(bookmark: bookmark) - vault.keyValueStore?.storeObject(nil, forKey: MediaUploadQueue.UploadPendingKey) + // MARK: - Activity management + + private func publishImportActivity(for core:OCCore, itemCount:Int) { + let activityId = "MediaUploadQueue:\(UUID())" + self.uploadActivity = MediaUploadActivity(identifier: activityId, assetCount: itemCount) + core.activityManager.update(OCActivityUpdate.publishingActivity(for: self)) } - class func shallShowUploadUnfinishedWarning(for bookmark:OCBookmark) -> Bool { - if !MediaUploadQueue.uploadStarted && isMediaUploadPendingFlagSet(for: bookmark) { - return true - } else { - return false + private func updateActivityAfterFinishedImport(for core:OCCore) { + self.uploadActivity?.updateAfterSingleFinishedUpload() + core.activityManager.update(OCActivityUpdate.updatingActivity(for: self)) + } + + private func unpublishImportActivity(for core:OCCore) { + core.activityManager.update(OCActivityUpdate.unpublishActivity(for: self)) + } + + private func isImportActivityCancelled() -> Bool { + if let activity = self.uploadActivity { + return activity.isCancelled } + return false } + } diff --git a/ownCloud/PhotoKit Extensions/MediaUploadStorage.swift b/ownCloud/PhotoKit Extensions/MediaUploadStorage.swift new file mode 100644 index 000000000..60d2a0c71 --- /dev/null +++ b/ownCloud/PhotoKit Extensions/MediaUploadStorage.swift @@ -0,0 +1,151 @@ +// +// File.swift +// ownCloud +// +// Created by Michael Neuwert on 21.11.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +import Foundation +import Photos +import ownCloudSDK + +class MediaUploadJob : NSObject, NSSecureCoding { + + var targetPath: String? + var scheduledUploadLocalID: OCLocalID? + + static var supportsSecureCoding: Bool { + return true + } + + func encode(with coder: NSCoder) { + coder.encode(targetPath, forKey: "targetPath") + coder.encode(scheduledUploadLocalID, forKey: "scheduledUploadLocalID") + } + + required init?(coder: NSCoder) { + self.targetPath = coder.decodeObject(forKey: "targetPath") as? String + self.scheduledUploadLocalID = coder.decodeObject(forKey: "scheduledUploadLocalID") as? OCLocalID + } + + init(_ path:String) { + self.targetPath = path + } +} + +class MediaUploadStorage : NSObject, NSSecureCoding { + + var queue: [String] + var jobs: [String : [MediaUploadJob]] + var processing: OCProcessSession? + + static var supportsSecureCoding: Bool { + return true + } + + var jobCount: Int { + jobs.reduce(0) {$0 + $1.value.count } + } + + func encode(with coder: NSCoder) { + coder.encode(queue, forKey: "queue") + coder.encode(jobs, forKey: "jobs") + coder.encode(processing, forKey: "processing") + } + + required init?(coder: NSCoder) { + let storedQueue = coder.decodeObject(forKey: "queue") as? [String] + self.queue = storedQueue != nil ? storedQueue! : [String]() + + let storedJobs = coder.decodeObject(forKey: "jobs") as? [String : [MediaUploadJob]] + jobs = storedJobs != nil ? storedJobs! : [String : [MediaUploadJob]]() + + processing = coder.decodeObject(forKey: "processing") as? OCProcessSession + } + + override init() { + queue = [String]() + jobs = [String : [MediaUploadJob]]() + } + + func addJob(with assetID:String, targetPath:String) { + if !queue.contains(assetID) { + self.queue.append(assetID) + } + var existingJobs: [MediaUploadJob] = jobs[assetID] != nil ? jobs[assetID]! : [MediaUploadJob]() + if existingJobs.filter({$0.targetPath == targetPath}).count == 0 { + existingJobs.append(MediaUploadJob(targetPath)) + } + jobs[assetID] = existingJobs + } + + func removeJob(with assetID:String, targetPath:String) { + if let remainingJobs = jobs[assetID]?.filter({$0.targetPath != targetPath}) { + jobs[assetID] = remainingJobs + if remainingJobs.count == 0 { + if let assetIdQueueIndex = queue.firstIndex(of: assetID) { + queue.remove(at: assetIdQueueIndex) + } + } + } + } + + func update(localItemID:OCLocalID, assetId:String, targetPath:String) { + jobs[assetId]?.filter({$0.targetPath == targetPath}).first?.scheduledUploadLocalID = localItemID + } +} + +typealias MediaUploadStorageModifier = (_ storage:MediaUploadStorage) -> MediaUploadStorage + +extension OCBookmark { + private static let MediaUploadStorageKey = OCKeyValueStoreKey(rawValue: "com.owncloud.media-upload-storage") + + // + // NOTE: Deriving KV store from bookmark rather than from OCCore since it simplifies adding upload jobs + // significantly without a need to initialize full blown core for that + // + + var kvStore : OCKeyValueStore? { + let vault : OCVault = OCVault(bookmark: self) + if let store = vault.keyValueStore { + + // Check if NSCoding-compatible classes are not yet registered? + if store.registeredClasses(forKey: OCBookmark.MediaUploadStorageKey) == nil { + + // This weird trickery is required since Set can't be created directly + if let classSet = NSSet(array: [MediaUploadStorage.self, + MediaUploadJob.self, + OCProcessSession.self, + NSArray.self, + NSDictionary.self, + NSString.self]) as? Set { + + store.registerClasses(classSet, forKey: OCBookmark.MediaUploadStorageKey) + } + } + return store + } + return nil + } + + func modifyMediaUploadStorage(with modifier: @escaping MediaUploadStorageModifier) { + self.kvStore?.updateObject(forKey: OCBookmark.MediaUploadStorageKey, usingModifier: { (value, changesMadePtr) -> Any? in + var storage = value as? MediaUploadStorage + + if storage == nil { + storage = MediaUploadStorage() + } + + storage = modifier(storage!) + + changesMadePtr.pointee = true + + return storage + }) + } + + var mediaUploadStorage : MediaUploadStorage? { + return self.kvStore?.readObject(forKey: OCBookmark.MediaUploadStorageKey) as? MediaUploadStorage + } +} diff --git a/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift b/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift index e7695f1dd..dcd8a1495 100644 --- a/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift +++ b/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift @@ -28,10 +28,11 @@ extension PHAsset { - parameter preferredFormats: Array of UTI identifiers describing desired output formats - 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, preferredFormats:[String]? = nil, completionHandler:@escaping (_ item:OCItem?, _ error:Error?) -> Void, progressHandler:((_ progress:Progress) -> Void)? = nil) { + func upload(with core:OCCore?, at rootItem:OCItem, preferredFormats:[String]? = nil, progressHandler:((_ progress:Progress) -> Void)? = nil, uploadCompleteHandler:(() -> Void)? = nil) -> (OCItem?, Error?)? { - func performUpload(sourceURL:URL, copySource:Bool) { + func performUpload(sourceURL:URL, copySource:Bool) -> (OCItem?, Error?)? { @discardableResult func removeSourceFile() -> Bool { do { @@ -43,6 +44,7 @@ extension PHAsset { } var uploadProgress: Progress? + var importResult:(OCItem?, Error?)? // Sometimes if the image was edited, the name is FullSizeRender.jpg but it is stored in the subfolder // in the PhotoLibrary which is named after original image @@ -58,6 +60,9 @@ extension PHAsset { } } + // Synchronously import media file into the OCCore and schedule upload + let importSemaphore = DispatchSemaphore(value: 0) + uploadProgress = sourceURL.upload(with: core, at: rootItem, alternativeName: fileName, @@ -72,17 +77,23 @@ extension PHAsset { } else { Log.debug(tagged: ["MEDIA_UPLOAD"], "Finished uploading asset ID \(self.localIdentifier)") } - completionHandler(item, error) + importResult = (item, error) + importSemaphore.signal() }, completionHandler: { (_, _) in if uploadProgress != nil { progressHandler?(uploadProgress!) } + uploadCompleteHandler?() }) if uploadProgress != nil { progressHandler?(uploadProgress!) } + + importSemaphore.wait() + + return importResult } Log.debug(tagged: ["MEDIA_UPLOAD"], "Prepare uploading asset ID \(self.localIdentifier), type:\(self.mediaType), subtypes:\(self.mediaSubtypes), sourceType:\(self.sourceType), creationDate:\(String(describing: self.creationDate)), modificationDate:\(String(describing: self.modificationDate)), favorite:\(self.isFavorite), hidden:\(self.isHidden)") @@ -95,23 +106,30 @@ extension PHAsset { let contentInputOptions = PHContentEditingInputRequestOptions() contentInputOptions.isNetworkAccessAllowed = true + var supportedConversionFormats = Set() + var assetUTI: String? + var assetURL: URL? + var avAsset:AVAsset? + var uploadResult:(OCItem?, Error?)? + + // Synchronously retrieve asset content + let semaphore = DispatchSemaphore(value: 0) _ = autoreleasepool { self.requestContentEditingInput(with: contentInputOptions) { (contentInput, requestInfo) in - - var supportedConversionFormats = Set() + if let error = requestInfo[PHContentEditingInputErrorKey] as? NSError { + Log.error(tagged: ["MEDIA_UPLOAD"], "Requesting content for asset ID \(self.localIdentifier) with error \(String(describing: error))") + } if let input = contentInput { - // Determine the correct source URL based on media type - var assetUTI: String? - var assetURL: URL? switch self.mediaType { case .image: assetURL = input.fullSizeImageURL assetUTI = input.uniformTypeIdentifier supportedConversionFormats.insert(String(kUTTypeJPEG)) case .video: - assetURL = (input.audiovisualAsset as? AVURLAsset)?.url + avAsset = input.audiovisualAsset + assetURL = (avAsset as? AVURLAsset)?.url assetUTI = PHAssetResource.assetResources(for: self).first?.uniformTypeIdentifier supportedConversionFormats.insert(String(kUTTypeMPEG4)) default: @@ -119,69 +137,72 @@ extension PHAsset { } Log.debug(tagged: ["MEDIA_UPLOAD"], "Preparing to export asset ID \(self.localIdentifier), URL: \(String(describing: assetURL)), UTI: \(String(describing: assetUTI))") + } - guard let url = assetURL else { return } - - let fileName = url.lastPathComponent - - // Check if the conversion was requested and current media format is not found in the list of requested formats - if let formats = preferredFormats, formats.count > 0 { - if assetUTI != nil, !formats.contains(assetUTI!) { - // Conversion is required - if let outputFormat = formats.first(where: { supportedConversionFormats.contains($0) }) { - - switch (self.mediaType, outputFormat) { - case (.video, String(kUTTypeMPEG4)): - if let avAsset = input.audiovisualAsset { - let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("mp4") - avAsset.exportVideo(targetURL: localURL, type: .mp4, completion: { (exportSuccess) in - if exportSuccess { - performUpload(sourceURL: localURL, copySource: false) - } else { - Log.error(tagged: ["MEDIA_UPLOAD"], "Video export failed for asset ID \(self.localIdentifier)") - completionHandler(nil, NSError(ocError: .internal)) - } - }) - } - case (.image, String(kUTTypeJPEG)): - let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("jpg") - var imageConverted = false - - if let image = CIImage(contentsOf: assetURL!) { - imageConverted = image.convert(targetURL: localURL, outputFormat: .JPEG) - } - - if imageConverted { - performUpload(sourceURL: localURL, copySource: false) - } else { - Log.error(tagged: ["MEDIA_UPLOAD"], "CIImage conversion failed for \(self.localIdentifier)") - completionHandler(nil, NSError(ocError: .internal)) - } - default: - break - } - + semaphore.signal() + } + } + semaphore.wait() + + guard let url = assetURL else { return (nil, nil) } + let fileName = url.lastPathComponent + var exportedAssetURL: URL? + var conversionError: Error? + + // Check if the conversion was requested and current media format is not found in the list of requested formats + if let formats = preferredFormats, formats.count > 0 { + if assetUTI != nil, !formats.contains(assetUTI!) { + // Conversion is required + if let outputFormat = formats.first(where: { supportedConversionFormats.contains($0) }) { + + switch (self.mediaType, outputFormat) { + case (.video, String(kUTTypeMPEG4)): + if let avAsset = avAsset { + // Export video + let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("mp4") + if avAsset.exportVideo(targetURL: localURL, type: .mp4) { + exportedAssetURL = localURL } else { - Log.error(tagged: ["MEDIA_UPLOAD"], "Format mismatch for asset ID \(self.localIdentifier)") - completionHandler(nil, NSError(ocError: .internal)) + Log.error(tagged: ["MEDIA_UPLOAD"], "Video export failed for asset ID \(self.localIdentifier)") + conversionError = NSError(ocError: .internal) } + } + case (.image, String(kUTTypeJPEG)): + // Convert image to JPEG format + let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("jpg") + var imageConverted = false + + if let image = CIImage(contentsOf: assetURL!) { + imageConverted = image.convert(targetURL: localURL, outputFormat: .JPEG) + } + + if imageConverted { + exportedAssetURL = localURL } else { - performUpload(sourceURL: url, copySource: true) + Log.error(tagged: ["MEDIA_UPLOAD"], "CIImage conversion failed for \(self.localIdentifier)") + conversionError = NSError(ocError: .internal) } - } else { - performUpload(sourceURL: url, copySource: true) + default: + break } + } else { + Log.debug(tagged: ["MEDIA_UPLOAD"], "No conversion format match for asset ID \(self.localIdentifier), URL: \(String(describing: assetURL)), UTI: \(String(describing: assetUTI))") + } + } + } - } else { - // If no content was returned check request info dictionary - let error = requestInfo[PHContentEditingInputErrorKey] as? NSError - - Log.error(tagged: ["MEDIA_UPLOAD"], "Requesting content for asset ID \(self.localIdentifier) with error \(String(describing: error))") + // Bail out if the conversion has failed + if conversionError != nil { + return (nil, conversionError) + } - completionHandler(nil, error) - } - } + // Perform actual upload + if exportedAssetURL != nil { + uploadResult = performUpload(sourceURL: exportedAssetURL!, copySource: false) + } else { + uploadResult = performUpload(sourceURL: url, copySource: true) } - } + return uploadResult + } } diff --git a/ownCloud/Resources/Assets.xcassets/cloud-unavailable-offline.imageset/cloud-unavailable-offline@2x.png b/ownCloud/Resources/Assets.xcassets/cloud-unavailable-offline.imageset/cloud-unavailable-offline@2x.png index 5e21db82d8169ebf63644438b6f09790e7c371ac..a2202c7aa0c97af914ac2e264f208858bb870244 100644 GIT binary patch delta 930 zcmV;T16};41-A!~BYyw^b5ch_0Itp)=>Px&aY;l$R9Fe^m|KWVVHC$_MlnVhiG&*D z&Wl?S<;g^mcyLb%59UQKDKR>Q7oy~8@(#Y-ipZaQ9 z?Y+<5`%E)OomIct>-*O2TYG=|a;Cbf`=pvw^Pv&uzyufxoqzBK+MpG#!xJbw8O)0I z!fW`Gk~{dVff^_a*~lv1L)r$s-+f|d_GTyjD1Nqt25dt=$+M$3nyIB|YfKYgnLh^` zU@{DZQLqRO!6UFuSp6in9+it!vkQ=}vw9qcK{1(!gKl`AL*wRR!QEwVidcZ2NON-( zSVxSmpl#?kR+%;1uN6+AV&8-jri$G*xWl~Vlz#zz&p(&b+gM_V61y(wVSs?+2m8Oljx_xHuBWj zGY@wY;m?jO5cIqF=}jJ_fD&wR&4ea`pJIZHLGg9<)PEZb(_u8&dJN-bJ*H5^9zQVO z0-InV%m8h@AlfQgyp8eA_$K5lAG^(Nxg0;-SnD_g3m{e|V)+hsF~1lehB_!FaSPRa zlFjBlz`)i|()HjOYJ-D*!6nhzS{aNfk5LQO$q;r?{%VEvOPrsLiPE3rKRyKOsLSge z_gRMwdVf?iHg{YTDlr_auXH_ho`K(H!B4h&=n{Q81cQ$4p!Y{!Q8WH3D6i&ZOOo># zkH9JzXB@S?*MgpAcCFG+L6_X-uQh6~mLjDzQmk%&94>=hudebW1-^9#{V2*SrEIOV zHPuWT*k%d}_WN&;CUZzs6IpKdEG4(%Y#X_!;C~lxQZJ&c64gYwv6F0B>jeJ>6x&FL z3^(1R|MZYglIYE=Mcc%oi4YgpjC~;n-!#WsSFck+&2Ca}*sM|@AT;8e2)TSSipeW_ zfrolJwky$8u#?i4%aBnD_{TL7QiW#ZTZ-%<*sl3nNOr>+J~CE=StZR@N#m1LcLc_F zV1FCg76N9Y_^;2;%<08cPNF6rfNes*ne0bw)j{VFob_mHf+^4kdRp|Ap{uwbv{S;m z!f_XK|40oZ|M3`{%k_ALHAntV(HTDh<+^aYE`7?^L89pYP>;a|mW!_KwH>sBQi&WF zLkm0v-Tb}Lj6QSp+v6&1fqJM&07*qoM6N<$ Ef@kx=-v9sr delta 655 zcmV;A0&xAe2c`v(BYy&INkl5Pl3&z*IG-JHm=j6S95b*N*Kq;cFcDSo6GE~#P!Go-ep-gSa3Lfr zjI)p!T7$n4v|hkaNE~pZ8Uo&nSOC#*4Z~3oKjJlff>daU&3_OtdLW>UgD7rv#(VHp zBFu(pXoP@N9-7R z2ub0!?{NcS@Sqi5hOhE?8^*_PW{n|!nt*o@t+9GT$PS(DFdo3i5Jc%97WU&?{S*nvAX?fs*D1RR4A#@|r9=Y)~9`!QvVJAfC zgZnK}KQsmfU=aIW!bIq^`~w`P)erxH0)A=P46_HKALaV`dmX9(-0XIhf*4lAaR}-~ zqzb^}*Rn4lhD&hFgZlKa>epDm5bb*rVz>oIBZy%!Y>NFSihbWhKWQC5LzFDAD3%|M z_SJ?MW`E#*bJY*$@iT0*|BScqL!5ya%He+hLVtZ;L~evqA7UNEZ~<@OaV;RwR--(A z!ZQeT&*K*~#CeEP!=Z9vEF_Fff)mkt2HhZGU;00>D%PDHLkV1itkEBpWe diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index 70227cdba..b454d582f 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -23,8 +23,6 @@ - LSSupportsOpeningDocumentsInPlace - CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -38,7 +36,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 144 + 147 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -47,6 +45,8 @@ LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -60,6 +60,13 @@ This permission is needed to upload photos and videos from your photo library. NSPhotoLibraryUsageDescription This permission is needed to upload photos and videos from your photo library. + NSCameraUsageDescription + This permission is needed to scan documents. + NSUserActivityTypes + + com.owncloud.ios-app.openAccount + com.owncloud.ios-app.openItem + OCAppGroupIdentifier group.$(PRODUCT_BUNDLE_IDENTIFIER) OCAppIdentifierPrefix @@ -68,6 +75,23 @@ OCKeychainAccessGroupIdentifier group.$(PRODUCT_BUNDLE_IDENTIFIER) + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_NAME).SceneDelegate + + + + UIBackgroundModes audio diff --git a/ownCloud/Resources/ar.lproj/InfoPlist.strings b/ownCloud/Resources/ar.lproj/InfoPlist.strings new file mode 100644 index 000000000..4735c62da --- /dev/null +++ b/ownCloud/Resources/ar.lproj/InfoPlist.strings @@ -0,0 +1,22 @@ +/* + InfoPlist.strings + ownCloud + + Created by Javier Gonzalez on 06/06/2018. + Copyright © 2018 ownCloud GmbH. All rights reserved. +*/ + +/* + * Copyright (C) 2018, 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 . + * + */ + +"NSFaceIDUsageDescription" = "إلغاء قفل التطبيق دون كتابة الرمز المروري."; +"NSAppleMusicUsageDescription" = "هذا الإذن مطلوب لتحميل ملفات الوسائط من الخادم."; +"NSPhotoLibraryAddUsageDescription" = "هذا الإذن مطلوب لتحميل الصور والفيديوهات من مكتبة الصور."; +"NSPhotoLibraryUsageDescription" = "هذا الإذن مطلوب لتحميل الصور والفيديوهات من مكتبة الصور."; diff --git a/ownCloud/Resources/ar.lproj/Localizable.strings b/ownCloud/Resources/ar.lproj/Localizable.strings new file mode 100644 index 000000000..ebbbe96e1 --- /dev/null +++ b/ownCloud/Resources/ar.lproj/Localizable.strings @@ -0,0 +1,495 @@ +/* + Localizable.strings + ownCloud + + Created by Pablo Carrascal on 13/03/2018. + Copyright © 2018 ownCloud GmbH. All rights reserved. +*/ + +/* + * Copyright (C) 2018, 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 . + * + */ + +/* Add / Edit Bookmark */ +"Edit account" = "تعديل الحساب"; +"Add account" = "إضافة حساب"; +"Server URL" = "عنوان URL الخادم"; +"https://example.com" = "https://example.com"; +"Continue" = "متابعة"; +"Name" = "الاسم"; +"Example Server" = "خادم المثال"; +"Show Certificate Details" = "عرض تفاصيل الشهادة"; +"Connect" = "اتصال"; +"Delete Authentication Data" = "حذف بيانات المصادقة"; +"Authentication" = "مصادقة"; +"Username" = "اسم المستخدم"; +"Password" = "كلمة السر"; +"Certificate Details" = "تفاصيل الشهادة"; +"Cancel" = "إلغاء"; +"Approve" = "موافقة"; +"Error" = "خطأ"; +"Review Connection" = "مراجعة الاتصال"; +"Authenticated via" = "تمت المصادقة عبر"; +"Authenticated as %@ via %@" = "تمت مصادقة %@ عبر %@"; +"Edit" = "تعديل"; +"Credentials" = "بيانات الاعتماد"; +"Rejected" = "مرفوض"; +"Passed" = "تم الاجتياز"; +"Accepted" = "مقبول"; +"Validation Error" = "خطأ في التحقق"; +"Certificate was rejected by user." = "لقد رفض المستخدم الشهادة."; +"Certificate has issues.\nOpen 'Certificate Details' for more informations." = "هناك مشكلة بالشهادة.\nافتح 'تفاصيل الشهادة' لمزيد من المعلومات."; +"No issues found. Certificate passed validation." = "لم يتم العثور على أي مشكلات. اجتازت الشهادة عملية التحقق."; +"Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations." = "قد تكون هناك مشكلة بالشهادة، ولكن وافق عليها المستخدم.\nافتح 'تفاصيل الشهادة' لمزيد من المعلومات."; +"If you 'Continue', you will be prompted to allow the '%@' App to open OAuth2 login where you can enter your credentials." = "إذا قررت 'الاستمرار'، سيُطلب منك السماح لتطبيق '%@' بفتح ملف تسجيل الدخول إلى OAuth2 حيث يُمكنك إدخال بيانات اعتمادك."; + +"Contacting server…" = "جارٍ الاتصال بالخادم…"; +"Authenticating…" = "جارٍ المصادقة…"; + +"Fetching user information…" = "جارٍ إحضار معلومات المستخدم…"; +"Updating connection…" = "جارٍ تحديث الاتصال…"; + +"Missing hostname" = "اسم المضيف مفقود"; +"The entered URL does not include a hostname." = "لا يتضمن عنوان URL المُدخل اسم المضيف."; +"Add account" = "إضافة حساب"; +"Server name" = "اسم الخادم"; +"Server Password" = "كلمة مرور الخادم"; +"Server Username" = "اسم مستخدم الخادم"; + +/* Client */ +"Browse" = "تصفح"; +"Disconnect" = "قطع الاتصال"; +"Connecting…" = "جارٍ الاتصال…"; +"Connected." = "متصل."; +"Select" = "اختيار"; +"Done" = "تم"; + +"Folder" = "مجلد"; + +"Stopped" = "تم الإيقاف"; +"Started…" = "تم البدء…"; +"Contents from cache." = "محتويات من ذاكرة التخزين المؤقتة."; +"Waiting for server response…" = "انتظار استجابة الخادم…"; +"This folder no longer exists." = "لم يعد هذا المجلد مفقودًا."; +"Everything up-to-date." = "كل الأشياء محدثة."; +"Please wait…" = "يُرجى الانتظار…"; +"Sort by %@" = "ترتيب حسب %@"; +"Sort by" = "ترتيب حسب"; + +"name" = "الاسم"; +"type" = "النوع"; +"size" = "الحجم"; +"date" = "التاريخ"; +"Search this folder" = "بحث في هذا المجلد"; +"Pending" = "معلق"; +"Show parent paths" = "عرض المسارات الأصلية"; + +"%@ of %@ used" = "%@ الخاصة %@ المستخدمة"; +"Total: %@" = "إجمالي: %@"; + +/* Client Messages */ +"Empty folder" = "مجلد فارغ"; +"This folder contains no files or folders." = "لا يحتوي هذا المجلد على ملفات أو مجلدات."; + +"Folder removed" = "المجلد محذوف"; +"This folder no longer exists on the server." = "لم يعد هذا المجلد موجود في الخادم."; +"Are you sure you want to delete this item from the server?" = "هل تريد بالتأكيد حذف هذا العنصر من الخادم؟"; +"Are you sure you want to delete these items from the server?" = "هل تريد بالتأكيد حذف هذه العناصر من الخادم؟"; +"Multiple items" = "عناصر متعددة"; +"Deleting '%@'" = "حذف'%@'"; +"Renaming to %@" = "إعادة تسمية لـ %@"; +"Upload from your photo library" = "تحميل من مكتبة الصور"; +"Missing permissions" = "الأذونات مفقودة"; +"This permission is needed to upload photos and videos from your photo library." = "هذا الإذن مطلوب لتحميل الصور والفيديوهات من مكتبة الصور."; +"Not now" = "ليس الآن"; +"Upload file" = "تحميل ملف"; +"Upload files" = "تحميل ملفات"; + +"No matches" = "لا يوجد تطابقات"; +"There are no results for this search term" = "لا توجد نتائج عن البحث بهذا المصطلح"; +"Status" = "الحالة"; + +"Authorization failed" = "تعذر إجراء المصادقة"; +"The account has been disabled." = "تم تعطيل الحساب."; +"The server declined access with the credentials stored for this connection." = "رفض الخادم الوصول باستخدام بيانات الاعتماد المخزنة لهذا الاتصال."; +"Access denied" = "الوصول محظور"; +"The connection's access token has expired or become invalid. Sign in again to re-gain access." = "انتهت صلاحية الوصول للاتصال أو أصبح الوصول غير صالح. أعد تسجيل الدخول للحصول على وصول."; +"No authentication data has been found for this connection." = "لم يتم العثور على بيانات مصادقة لهذا الاتصال."; +"Sign in" = "تسجيل الدخول"; +"Ignore" = "تجاهل"; +"Continue offline" = "الاستمرار دون اتصال بالإنترنت"; +"Media upload in the previous session was incomplete since the application was terminated" = "Media upload in the previous session was incomplete since the application was terminated"; + +/* Server List*/ +"Cancel" = "إلغاء"; +"OK" = "تم"; + +"'%@' is currently locked" = "'%@' مقفول حاليًا"; +"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "هناك عملية تُجرى حاليًا تحول دون الاتصال بـ '%@'. يُرجى المحاولة مجددًا."; +"Really delete '%@'?" = "هل تريد الحذف بالتأكيد '%@'؟"; +"This will also delete all locally stored file copies." = "سيحذف هذا جميع نسخ الملف المخزنة محليًا."; +"Delete" = "حذف"; +"Deletion of '%@' failed" = "تعذر حذف '%@'"; +"Accounts" = "حسابات"; + +"Help" = "المساعدة"; +"Feedback" = "ملاحظات"; +"Welcome" = "مرحباً"; +"Thanks for choosing %@! \n Start by adding your account." = "شكرًا على اختيارك %@! \n ابدأ بإضافة حسابك."; + +/* Settings Messages */ + +"Security" = "الأمان"; +"Passcode Lock" = "قفل رمز المرور"; +"Face ID" = "معرّف الوجه"; +"Touch ID" = "معرّف اللمسة"; +"Videos" = "فيديوهات"; +"Photos" = "صور"; +"Background uploads" = "تحميلات الخلفية"; +"Wifi only" = "Wifi فقط"; +"Settings" = "الإعدادات"; +"Lock application" = "تطبيق القفل"; +"Upload videos via WiFi only" = "تحميل فيديو عبر WiFi فقط"; +"Upload pictures via WiFi only" = "تحميل الصور عبر WiFi فقط"; +"More" = "المزيد"; +"Send feedback" = "إرسال التعليقات"; +"Recommend to a friend" = "توصية إلى صديق"; +"Privacy Policy" = "سياسة الخصوصية"; +"Acknowledgements" = "رسائل الإقرار"; +"Video upload path" = "مسار تحميل الفيديو"; +"Photo upload path" = "مسار تحميل الصورة"; +"Immediately" = "على الفور"; +"After 1 minute" = "بعد دقيقة واحدة"; +"After 5 minutes" = "بعد 5 دقائق"; +"After 30 minutes" = "بعد 30 دقيقة"; +"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "إذا اخترت \"Immediately\"، سيتم غلق التطبيق، فهو لم يعد موجودًا في المقدمة. الدخول إلى تطبيق \n\nAccess in Files غير متاح، إذا ما اخترت فترة الإقفال \"Immediately\".يُرجى \n اختيار مدة تأخير أخرى، إذا أردت الوصول إلى الملفات في تطبيق \"الملفات\" أو بواسطة منتقي المستندات."; +"Please configure an email account" = "يُرجى تكوين حساب البريد الإلكتروني"; +"You need to configure an email account first to be able to send emails." = "تحتاج إلى تكوين حساب البريد الإلكتروني أولًا حتى تتمكن من إرسال رسائل البريد الإلكتروني."; +"Do you want to open the following URL?" = "هل ترغب في فتح عنوان URL التالي؟"; + +"%@ %@ version %@ build %@ (app: %@, sdk: %@)" = "%@ %@ إصدار %@ إنشاء %@ (تطبيق: %@, sdk: %@)"; +"beta" = "بيتا"; +"release" = "إطلاق"; + + +/* User Interface Settings */ +"Theme" = "لون الخلفية"; +"User Interface" = "واجهة المستخدم"; +"Dark" = "داكن"; +"Light" = "ساطع"; +"Classic" = "كلاسيكي"; +"System" = "النظام"; +"System Appeareance" = "System Appeareance"; + +/* Log settings */ +"Log Files" = "ملفات السجل"; +"Browse log files" = "ملفات سجل التصفح"; +"Share" = "مشاركة"; +"Delete all log files?" = "هل تريد حذف جميع ملفات السجل؟"; +"Delete all" = "حذف الكل"; + +"Log Level" = "مستوى السجل"; +"Log Destinations" = "وجهات السجل"; +"Privacy" = "خصوصية"; + +"Logging" = "جارِ التسجيل"; +"Enable logging" = "تمكين التسجيل"; +"Options" = "خيارات"; + +"Off" = "إيقاف التشغيل"; +"Info" = "معلومات"; +"Default" = "افتراضي"; +"Warning" = "تحذير"; +"Error" = "خطأ"; + +"Share log file" = "ملف سجل المشاركة"; +"Reset log file" = "إعادة تعيين ملف السجل"; + +"When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to %@ servers. Sharing logs with others is sole user responsibility." = "عند التنشيط، قد تؤثر السجلات في الأداء وقد تشمل محتويات حساسة. ومع هذا، لا تخضع السجلات إلى الإرسال الآلي لخوادم %@ . إنَّ مشاركة السجلات مع مستخدمين آخرين هي مسؤولية المستخدم وحده."; + +"The last 10 archived logs are kept on the device - with each log covering up to 24 hours of usage. When sharing please bear in mind that logs may contain sensitive information such as server URLs and user-specific information." = "السجلات العشرة الأخيرة محفوظة على الجهاز - يحمل كل سجل تغطية ذاكرة تصل إلى 24 ساعة من الاستخدام. عند المشاركة، يُرجى الأخذ في الاعتبار أن السجلات قد تحتوي على معلومات حساسة مثل عناوين URL للخادم ومعلومات خاصة بالمستخدم."; + +"Mask private data" = "إخفاء البيانات الخاصة"; +"Enabling this option will attempt to mask private data, so it does not become part of any log. Since logging is a development and debugging feature, though, we can't guarantee that the log file will be free of any private data even with this option enabled. Therefore, please look through any log file and verify its free of any data you're not comfortable sharing before sharing it with anybody." = "سيعمل تمكين هذا الخيار على إخفاء البيانات الخاصة، كي لا تصبح جزءًا من أي سجل. نظرًا لأن التسجيل ميزة تطويرية ومتعلقة بالتصحيح، ورغم هذا، لا يمكن ضمان خلو ملف السجل من أي بيانات خاصة حتى بعد تمكين هذا الخيار. ولهذا، يُرجى فحص أي ملف سجل والتحقق من خلوه من أي بيانات لا ترغب في مشاركتها قبل مشاركة هذا مع أي شخص."; + +"No log file found" = "لم يتم العثور على أي ملف سجل"; +"The log file can't be shared because no log file could be found or the log file is empty." = "تعذر مشاركة ملف السجل لأنه لم يتم العثور على أي ملف سجل أو أن ملف السجل فارغ."; +"Enable log file" = "تمكين ملف السجل"; + +"Really reset log file?" = "هل تريد إعادة تعيين ملف السجل؟"; +"This action can't be undone." = "لا يمكن التراجع عن هذا الإجراء."; + +/* Storage settings */ +"Delete unused local copies" = "حذف نسخ محلية غير مستخدمة"; +"Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "الوقت المقدّر منذ تحميل الملف المعني أو تعديله أو تنزيله أو عرضه بواسطة هذا الجهاز. لا ينطبق على أي من الملفات المحملة بواسطة خاصية التوافر مع عدم الاتصال بالإنترنت. يمكن حذف النسخ المحلية قبل انتهاء المدة المحددة، على سبيل المثال، بعد أن تتوفر نسخة أحدث من الملف على الخادم - أو من خلال الحذف اليدوي للنسخ غير المتصلة بالإنترنت. وأيضًا، لا يمكن حذف النسخ المحلية قبل انتهاء المدة المحددة، على سبيل المثال، إذا تم تنفيذ إجراء ما عليها، أو أن الملف قيد الاستخدام - أو لم يتم بعد استخدام الحساب الذي يحتوي على الملف في التطبيق."; +"never" = "أبدًا"; +"after %@" = "بعد %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; + +/* Display settings */ +"Display settings" = "عرض الإعدادات"; +"Show hidden files and folders" = "عرض الملفات والمجلدات المختفية"; + +/* Passcode Messages */ + +"Enter code" = "إدخال الرمز"; +"Repeat code" = "تكرار الرمز"; +"Delete code" = "حذف الرمز"; +"The entered codes are different" = "الرموز المدخلة مختلفة"; +"Incorrect code" = "رمز غير صحيح"; +"Please try again in %@" = "يُرجى المحاولة مجددًا %@"; +"Unlock %@" = "إلغاء القفل %@"; +"Biometric authentication failed" = "تعذر إجراء المصادقة البيولوجية"; + +/* Certificate management */ + +"Certificates" = "شهادات"; +"User-approved certificates" = "شهادات المستخدم المعتمدة"; + +"Approved" = "معتمدة"; +"Auto-approved" = "معتمدة آليًا"; +"Revoke approval" = "Revoke approval"; + +/* Actions */ + +"Forbidden Characters" = "أحرف ممنوعة"; +"File name cannot contain / or \\" = "لا يمكن أن يحتوي اسم الملف على / أو \\"; +"New Folder" = "مجلد جديد"; +"Folder name" = "اسم المجلد"; +"Rename" ="Rename"; +"Create folder" ="انشئ مجلد"; +"Duplicate" = "تكرار"; +"Move" = "نقل"; +"Open in" = "فتح في"; +"Copy" = "نسخ"; +"Copy here" = "نسخ هنا"; +"Cannot connect to " = "تعذر الاتصال بـ "; +" couldn't download file(s)" = " تعذر تنزيل الملف (الملفات)"; +"Actions" = "إجراءات"; +"copy" = "نسخ"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; + +"Preparing…" = "جارٍ الإعداد…"; + +/* Directory Picker Messages */ +"Move here" = "نقل هنا"; + +/* Preview */ +"Open file" = "فتح الملف"; +"Network unavailable" = "الشبكة غير متوفرة"; +"Error" = "خطأ"; +"Could not get the picture" = "تعذر الحصول على الصورة"; +"Downloading" = "جارٍ التنزيل"; +"File couldn't be opened" = "تعذر فتح الملفات"; + +/* PDF Viewer */ +"Resume" = "استئناف"; +"Go to page" = "ذهاب إلى الصفحة"; +"Page" = "صفحة"; +"This document has %@ pages" = "يحتوي هذا المستند على عدد %@ صفحة"; +"%@ of %@" = "%@ من %@"; +"Invalid Page" = "صفحة غير صالحة"; +"The entered page number doesn't exist" = "رقم الصفحة المكتوب غير موجود"; +"Search PDF" = "بحث في ملف PDF"; +"Outline" = "مخطط تفصيلي"; + +/* Photo Upload */ +"Upload" = "تحميل"; +"Select All" = "تحديد الكل"; +"Deselect All" = "إلغاء تحديد الكل"; +"All Photos" = "جميع الصور"; +"Albums" = "الألبومات"; +"Importing from photo library" = "استيراد من مكتبة صورك"; + +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Saving"; +"File format" = "File format"; +"Name" = "الاسم"; +"Save as" = "Save as"; +"Options" = "خيارات"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; + +/* Sharing */ +"Searching Shares…" = "جارِ البحث في المشاركات…"; +"Recipient" = "مستلم"; +"Recipients" = "مستلمون"; +"Public Link" = "ارتباط عام"; +"Public Links" = "ارتباطات عامة"; +"Shared by %@" = "مشاركة بواسطة %@"; +"Invite Recipient" = "إرسال دعوة إلى مستلم"; +"Recipients" = "مستلمون"; +"Add email or name" = "إضافة بريد إلكتروني أو اسم"; +"Users" = "مستخدمون"; +"Groups" = "مجموعات"; +"Start typing to search users, groups and remote users." = "ابدأ الكتابة للبحث عن المستخدمين والمجموعات والمستخدمين عن بُعد."; +"(Group)" = "(مجموعة)"; +"Adding User to Share failed" = "تعذر إضافة مستخدم إلى المشاركة"; +"Permissions" = "أذونات"; +"Invited: %@" = "تمت الدعوة: %@"; +"Created: %@" = "تم الإنشاء: %@"; +"Allows the users you share with to re-share" = "يُتيح للمستخدمين الذين تشاركهم إعادة المشاركة"; +"Allows the users you share with to edit your shared files, and to collaborate" = "يُتيح للمستخدمين الذين تشاركهم تعديل الملفات التي تم مشاركتها والتعاون فيها"; +"Allows the users you share with to create new files and add them to the share" = "يُتيح للمستخدمين الذين تشاركهم إنشاء ملفات جديدة وإضافتهم للمشاركة"; +"Allows uploading a new version of a shared file and replacing it" = "يُتيح تحميل إصدار جديد من الملفات التي تم مشاركتها واستبدالها"; +"Allows the users you share with to delete shared files" = "يُتيح للمستخدمين الذين تشاركهم حذف الملفات التي تشاركها"; +"Setting permission failed" = "تعذر إعداد الإذن"; +"Shared with" = "تمت المشاركة مع"; +"Remove Recipient failed" = "تعذر إزالة المستلم"; +"Remove Recipient" = "إزالة المستلم"; +"Create" = "إنشاء"; +"Change" = "تغيير"; +"Recipients can view or download contents." = "يستطيع المستلمون عرض المحتويات أو تنزيلها."; +"Recipients can view, download, edit, delete and upload contents." = "يستطيع المستلمون عرض المحتويات أو تنزيلها أو تعديلها أو حذفها أو تحميلها."; +"Receive files from multiple recipients without revealing the contents of the folder." = "استلام ملفات من مستلمين متعددين دون إظهار محتويات المجلد."; +"Download / View" = "تنزيل / عرض"; +"Download / View / Upload" = "تنزيل / عرض/ تحميل"; +"Upload only (File Drop)" = "تحميل الملف فقط (إفلات الملف)"; +"Creating public link failed" = "تعذر إنشاء ارتباط عام"; +"Create Public Link" = "إنشاء ارتباط عام"; +"Links" = "ارتباطات"; +"Link" = "ارتباط"; +"Setting expiration date failed" = "تعذر إعداد تاريخ انتهاء الصلاحية"; +"Expiration date" = "تاريخ إنتهاء الصلاحية"; +"Copy Public Link" = "نسخ ارتباط عام"; +"Delete Public Link" = "حذف ارتباط عام"; +"Deleting Public Link failed" = "تعذر حذف ارتباط عام"; +"Deleting password failed" = "تعذر حذف كلمة المرور"; +"Setting password failed" = "تعذر تعيين كلمة المرور"; +"Type to update password" = "اكتب لتحديث كلمة المرور"; +"Cannot change permission" = "تعذر تغيير الإذن"; +"Before you can set the permission\n%@,\n you must enter a password." = "قبل تعيين الإذن\n%@,\n يجب إدخال كلمة المرور."; +"Password Protected" = "كلمة المرور محمية"; +"Pending Federated Invites" = "تعليق الدعوات الموحدة"; +"Pending Invites" = "تعليق الدعوات"; +"Shared with you" = "تمت المشاركة معك"; +"Shared with others" = "تمت المشاركة مع الآخرين"; +"Shares" = "المشاركات"; +"Copy Private Link" = "نسخ ارتباط خاص"; +"Only recipients can use this link. Use it as a permanent link to point to this resource" = "يستطيع المستلمون فقط استخدام هذا الارتباط. استخدم الارتباط كارتباط دائم للإشارة إلى هذا المصدر"; +"Accept Share failed" = "تعذر الموافقة على المشاركة"; +"Decline Share failed" = "تعذر رفض المشاركة"; +"Accept" = "قبول"; +"Decline" = "رفض"; +"Declined" = "رفض"; +"Decline Share" = "رفض المشاركة"; +"Unshare" = "إلغاء المشاركة"; +"Unshare failed" = "تعذر إلغاء المشاركة"; +"Are you sure you want to unshare these items?" = "هل تريد بالتأكيد إلغاء مشاركة هذه العناصر؟"; +"Are you sure you want to unshare this item?" = "هل تريد بالتأكيد إلغاء مشاركة هذا العنصر؟"; +"Share" = "مشاركة"; +"Read" = "قراءة"; +"Can Share" = "يمكن المشاركة"; +"Can Edit" = "يمكن التعديل"; +"Can Edit and Change" = "يمكن التعديل والتغيير"; +"Can Create" = "يمكن الإنشاء"; +"Can Change" = "يمكن التغيير"; +"Can Delete" = "يمكن الحذف"; +"Accept Invite %@" = "قبول الدعوة %@"; +"Decline Invite %@" = "رفض الدعوة %@"; +"Decline cannot be undone." = "تعذر التراجع عن الرفض."; +"Sharing" = "مشاركة"; +"You" = "أنت"; +"Share this file" = "مشاركة هذا الملف"; +"Share this folder" = "مشاركة هذا المجلد"; +"shared" = "مُشارك"; +"Owner" = "المالك"; +"Private Link" = "ارتباط خاص"; + +/* Quick Access view */ +"Quick Access" = "وصول سريع"; +"Collection" = "مجموعة"; +"Recents" = "الأخيرة"; +"Favorites"= "المفضلة"; +"Images" = "صور"; +"PDF Documents" = "مستندات PDF"; + +/* Media files settings */ +"Media Files" = "ملفات الوسائط"; +"Streaming Enabled" = "تم تمكين البث"; + +/* Photo upload settings */ +"Media Upload" = "تحميل الوسائط"; +"Convert HEIC to JPEG" = "تحويل HEIC إلى JPEG"; +"Convert videos to MP4" = "تحويل الفيديوهات إلى امتداد MP4"; +"Auto Upload Photos" = "Instant Upload Photos"; +"Auto Upload Videos" = "Instant Upload Videos"; +"Account" = "حساب"; +"Accounts" = "حسابات"; +"Select account" = "Select account"; +"Upload Path" = "Upload Path"; +"Select Upload Path" = "Select Upload Path"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto upload of media was disabled since configured account / folder was not found"; + +/* Progress summarizer */ +"Creating %ld folders…" = "إنشاء مجلدات %ld…"; +"Moving %ld items…" = "نقل عناصر %ld…"; +"Copying %ld items…" = "نسخ عناصر %ld…"; +"Deleting %ld items…" = "حذف عناصر %ld…"; +"Uploading %ld files…" = "تحميل ملفات %ld…"; +"Downloading %ld files…" = "تنزيل ملفات %ld…"; +"Updating %ld items…" = "تحميل عناصر %ld…"; + +/* Offline storage management */ +"Free on %@" = "مجاني في %@"; +"unknown" = "غير معروف"; +"Offline files use" = "استخدام الملفات دون اتصال"; +"Compacting of '%@' failed" = "تعذر ضغط '%@'"; +"Include available offline files" = "تضمين ملفات متوفرة دون اتصال"; +"Delete Local Copies" = "حذف نسخ محلية"; +"Manage" = "إدارة"; +"Storage" = "وحدة التخزين"; +"Compacting" = "Compacting"; + +"Really include available offline files?" = "هل تريد تضمين ملفات متوفرة دون اتصال؟"; +"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "ستصبح الملفات والمجلدات المُشار إليها بـ(متوفرة دون اتصال) غير متوفرة. سيتم إعادة تحميلها في المرة القادمة التي تسجل فيها الدخول إلى حسابك (الاتصال مطلوب)."; + +/* Available offline */ +"Root folder" = "المجلد الجذر"; +"at" = "في"; +"(no match)" = "(لا يوجد تطابق)"; + +"Make available offline" = "اجعل العنصر متوفر دون اتصال"; +"Make unavailable offline" = "اجعل العنصر غير متوفر دون اتصال"; + +"Available Offline" = "متوفر دون اتصال"; +"No items have been selected for offline availability." = "لم يتم اختيار أي عنصر للتوفر دون اتصال."; + +"Overview" = "نظرة عامة"; +"All Files" = "جميع الملفات"; + +/* Import File */ +"Save File" = "حفظ الملفات"; +"Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "اختر حسابًا ومجلدًا لاستيراد الملف إليه.\n\nيمكن استيراد ملف واحد في المرة الواحدة."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "بحث"; +"Back" = "رجوع إلى الخلف"; +"Save" = "حفظ"; +"Sort by %@" = "ترتيب حسب %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "التالي"; +"Previous" = "السابق"; +"Favorite" = "المفضلة"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/cs.lproj/Localizable.strings b/ownCloud/Resources/cs.lproj/Localizable.strings index c0996690d..9da3f9e60 100644 --- a/ownCloud/Resources/cs.lproj/Localizable.strings +++ b/ownCloud/Resources/cs.lproj/Localizable.strings @@ -98,7 +98,6 @@ "Passcode Lock" = "Přístupový kód"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Okamžitá odesílání"; "Videos" = "Videa"; "Photos" = "Fotky"; "Background uploads" = "Nahrávání na pozadí"; diff --git a/ownCloud/Resources/de-DE.lproj/Localizable.strings b/ownCloud/Resources/de-DE.lproj/Localizable.strings index 5f4329f1f..7b377aeb4 100644 --- a/ownCloud/Resources/de-DE.lproj/Localizable.strings +++ b/ownCloud/Resources/de-DE.lproj/Localizable.strings @@ -124,6 +124,7 @@ "Sign in" = "Einloggen"; "Ignore" = "Ignorieren"; "Continue offline" = "Offline fortfahren"; +"Media upload in the previous session was incomplete since the application was terminated" = "Der Medien-Upload in der vorherigen Sitzung war unvollständig, da die Anwendung beendet wurde."; /* Server List*/ "Cancel" = "Abbrechen"; @@ -148,7 +149,6 @@ "Passcode Lock" = "PIN-Sperre"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Sofortiges Hochladen"; "Videos" = "Videos"; "Photos" = "Fotos"; "Background uploads" = "Hochladen im Hintergrund"; @@ -160,7 +160,7 @@ "More" = "Mehr"; "Send feedback" = "Feedback senden"; "Recommend to a friend" = "Dies einem Freund empfehlen"; -"Privacy Policy" = "Datenschutzerkärung"; +"Privacy Policy" = "Datenschutzerklärung"; "Acknowledgements" = "Danksagungen"; "Video upload path" = "Pfad für den Video-Upload"; "Photo upload path" = "Upload Pfad für Fotos"; @@ -184,6 +184,8 @@ "Dark" = "Dunkel"; "Light" = "Hell"; "Classic" = "Klassik"; +"System" = "System"; +"System Appeareance" = "System Erscheinungsbild"; /* Log settings */ "Log Files" = "Protokolldateien"; @@ -228,6 +230,8 @@ "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Gemessene Zeit seit dem Hochladen, Bearbeiten, Herunterladen oder Anzeigen der jeweiligen Datei über dieses Gerät. Gilt nicht für Dateien, die über die Funktion Offline verfügbar heruntergeladen wurden. Lokale Kopien können vor Ablauf des angegebenen Zeitraums gelöscht werden, z.B. weil es eine neuere Version einer Datei auf dem Server gibt - oder durch das manuelle Löschen von Offline-Kopien. Außerdem dürfen lokale Kopien nach Ablauf der angegebenen Zeit nicht gelöscht werden, z.B. wenn eine Aktion an ihr durchgeführt wird, die Datei noch in Gebrauch ist - oder das Konto, das die Datei enthält, nicht in der App verwendet wurde."; "never" = "Niemals"; "after %@" = "nach %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; /* Display settings */ "Display settings" = "Anzeigeeinstellungen"; @@ -270,6 +274,9 @@ " couldn't download file(s)" = "Datei(en) konnte nicht geladen werden"; "Actions" = "Aktionen"; "copy" = "Kopie"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; "Preparing…" = "Vorbereiten..."; @@ -303,6 +310,18 @@ "Albums" = "Alben"; "Importing from photo library" = "Importieren aus der Fotobibliothek"; +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Speichern"; +"File format" = "File format"; +"Name" = "Name"; +"Save as" = "Speichern unter"; +"Options" = "Optionen"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; + /* Sharing */ "Searching Shares…" = "Suche nach Freigaben…"; "Recipient" = "Empfänger"; @@ -405,6 +424,14 @@ "Media Upload" = "Medienupload"; "Convert HEIC to JPEG" = "Konvertiere HEIC zu JPEG"; "Convert videos to MP4" = "Videos konvertieren zu MP4"; +"Auto Upload Photos" = "Sofortiges hochladen der Bilder "; +"Auto Upload Videos" = "Sofortiges hochladen der Videos"; +"Account" = "Benutzerkonto"; +"Accounts" = "Konten"; +"Select account" = "Account auswählen"; +"Upload Path" = "Upload Pfad"; +"Select Upload Path" = "Upload-Pfad auswählen"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto Upload von Medien wurde deaktiviert, da das konfigurierte Konto / Ordner nicht gefunden wurde."; /* Progress summarizer */ "Creating %ld folders…" = "Erstelle %ld Ordner…"; @@ -424,11 +451,10 @@ "Delete Local Copies" = "Lösche lokale Kopien"; "Manage" = "Verwalten"; "Storage" = "Speicher"; -"Proceed" = "Fortfahren"; "Compacting" = "Freigeben"; "Really include available offline files?" = "Offline-Dateien wirklich einbeziehen?"; -"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Dateien und Ordner, die als Offline Verfügbar markiert sind, sind nicht mehr vorhanden. Sie werden beim nächsten Login in Ihr Konto erneut heruntergeladen (Konnektivität erforderlich)."; +"Files and folders marked as Available Offline will become unavailable. They will be re-downloaded next time you log into your account (connectivity required)." = "Dateien und Ordner, die als Offline Verfügbar markiert sind, werden nicht mehr verfübar sein. Sie werden beim nächsten Login in Ihr Konto erneut heruntergeladen (Konnektivität erforderlich)."; /* Available offline */ "Root folder" = "Hauptverzeichnis"; @@ -443,8 +469,27 @@ "Overview" = "Übersicht"; "All Files" = "Alle Dateien"; -"Locations" = "Speicherorte"; /* Import File */ "Save File" = "Datei speichern"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Wählen Sie ein Konto und einen Ordner, in den die Datei importiert werden soll.\n\nEs kann nur eine Datei auf einmal importiert werden."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "Suche"; +"Back" = "Zurück"; +"Save" = "Speichern"; +"Sort by %@" = "Sortiert nach %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "Weiter"; +"Previous" = "Zurück"; +"Favorite" = "Favorit"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index db5c2bace..7b377aeb4 100644 --- a/ownCloud/Resources/de.lproj/Localizable.strings +++ b/ownCloud/Resources/de.lproj/Localizable.strings @@ -19,24 +19,24 @@ /* Add / Edit Bookmark */ "Edit account" = "Konto bearbeiten"; "Add account" = "Konto hinzufügen"; -"Server URL" = "Server-URL"; -"https://example.com" = "https://beispiel.com"; +"Server URL" = "Serveradresse"; +"https://example.com" = "https://example.com"; "Continue" = "Fortsetzen"; "Name" = "Name"; -"Example Server" = "Beispielserver"; -"Show Certificate Details" = "Zeige Zertifikatsdetails"; +"Example Server" = "Beispiel Server"; +"Show Certificate Details" = "Zertifikats-Details anzeigen"; "Connect" = "Verbinden"; -"Delete Authentication Data" = "Lösche Anmeldedaten"; +"Delete Authentication Data" = "Authentifizierungs-Daten löschen"; "Authentication" = "Authentifizierung"; "Username" = "Benutzername"; -"Password" = "Kennwort"; -"Certificate Details" = "Zertifikatsdetails"; +"Password" = "Passwort"; +"Certificate Details" = "Zertifikats-Details"; "Cancel" = "Abbrechen"; "Approve" = "Genehmigen"; "Error" = "Fehler"; -"Review Connection" = "Verbindung überprüfen"; -"Authenticated via" = "Angemeldet mit"; -"Authenticated as %@ via %@" = "Als %@ mit %@ angemeldet"; +"Review Connection" = "Verbindung prüfen"; +"Authenticated via" = "Authentifiziert über "; +"Authenticated as %@ via %@" = "Authentifiziert als %@ über %@"; "Edit" = "Bearbeiten"; "Credentials" = "Zugangsdaten"; "Rejected" = "Abgelehnt"; @@ -49,14 +49,14 @@ "Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations." = "Das Zertifikat hat möglicherweise Probleme, wurde aber vom Benutzer akzeptiert.\nÖffnen Sie 'Zertifikat-Details' für weitere Informationen."; "If you 'Continue', you will be prompted to allow the '%@' App to open OAuth2 login where you can enter your credentials." = "Wenn Sie auf 'Fortsetzen' klicken, werden Sie aufgefordert, der App '%@' zu erlauben, die Anmeldung bei OAuth2 zu öffnen, wo Sie Ihre Zugangsdaten eingeben können."; -"Contacting server…" = "Kontaktiere Server..."; -"Authenticating…" = "Authentifiziere"; +"Contacting server…" = "Verbinde zu Server ..."; +"Authenticating…" = "Anmeldung läuft"; "Fetching user information…" = "Nutzerinformationen abrufen ..."; "Updating connection…" = "Aktualisierung der Verbindung..."; "Missing hostname" = "Hostname fehlt"; -"The entered URL does not include a hostname." = "Eingegebene URL enthält keinen Hostnamen."; +"The entered URL does not include a hostname." = "Die eingegebene URL Adresse enthält keinen Hostname"; "Add account" = "Konto hinzufügen"; "Server name" = "Server Name"; "Server Password" = "Server Passwort"; @@ -65,28 +65,28 @@ /* Client */ "Browse" = "Durchsuchen"; "Disconnect" = "Trennen"; -"Connecting…" = "Verbinde..."; -"Connected." = "Verbunden."; +"Connecting…" = "Verbinde ..."; +"Connected." = "Verbunden"; "Select" = "Auswählen"; "Done" = "Erledigt"; "Folder" = "Ordner"; -"Stopped" = "Angehalten"; -"Started…" = "Gestartet..."; -"Contents from cache." = "Inhalte vom Cache."; -"Waiting for server response…" = "Warte auf Serverantwort..."; +"Stopped" = "Gestoppt"; +"Started…" = "Gestartet"; +"Contents from cache." = "Inhalte aus Zwischenspeicher."; +"Waiting for server response…" = "Warte auf Server-Antwort ..."; "This folder no longer exists." = "Dieser Ordner existiert nicht mehr."; -"Everything up-to-date." = "Alles aktuell."; -"Please wait…" = "Bitte warten..."; -"Sort by %@" = "Sortieren nach %@"; +"Everything up-to-date." = "Alles ist aktuell."; +"Please wait…" = "Bitte warten ..."; +"Sort by %@" = "Sortiert nach %@"; "Sort by" = "Sortieren nach"; "name" = "Name"; "type" = "Typ"; "size" = "Größe"; "date" = "Datum"; -"Search this folder" = "Diesen Ordner durchsuchen"; +"Search this folder" = "Ordner durchsuchen"; "Pending" = "Ausstehend"; "Show parent paths" = "Übergeordnete Pfade anzeigen"; @@ -95,18 +95,18 @@ /* Client Messages */ "Empty folder" = "Leerer Ordner"; -"This folder contains no files or folders." = "Dieser Ordner enthält keine Dateien oder Ordner."; +"This folder contains no files or folders." = "Dieser Ordner enthält keine Dateien oder andere Ordner"; -"Folder removed" = "Ordner entfernt"; -"This folder no longer exists on the server." = "Dieser Ordner existiert nicht mehr auf diesem Server."; -"Are you sure you want to delete this item from the server?" = "Sind Sie sich sicher dieses Element vom Server zu löschen?"; -"Are you sure you want to delete these items from the server?" = "Sind Sie sicher, dass sie diese Items vom Server löschen möchten?"; +"Folder removed" = "Ordner gelöscht"; +"This folder no longer exists on the server." = "Der Ordner existiert nicht mehr auf dem Server"; +"Are you sure you want to delete this item from the server?" = "Sind Sie sicher, dass Sie dieses Objekt vom Server löschen möchten?"; +"Are you sure you want to delete these items from the server?" = "Sind Sie sicher, dass Sie diese Objekte vom Server löschen möchten?"; "Multiple items" = "Mehrere Objekte"; -"Deleting '%@'" = "Lösche \"%@\""; -"Renaming to %@" = "Ändere Namen auf %@"; +"Deleting '%@'" = "Lösche '%@'"; +"Renaming to %@" = "Umbenennen in %@"; "Upload from your photo library" = "Hochladen aus Ihrer Fotobibliothek"; -"Missing permissions" = "Fehlende Berechtigung"; -"This permission is needed to upload photos and videos from your photo library." = "Diese Berechtigung wird benötigt, um Fotos und Videos aus Ihrer Fotobibliothek hochzuladen."; +"Missing permissions" = "Fehlende Berechtigungen"; +"This permission is needed to upload photos and videos from your photo library." = "Diese Berechtigung ist erforderlich, um Fotos und Videos aus deinem Fotoalbum hochzuladen."; "Not now" = "Nicht jetzt"; "Upload file" = "Datei hochladen"; "Upload files" = "Dateien hochladen"; @@ -124,17 +124,18 @@ "Sign in" = "Einloggen"; "Ignore" = "Ignorieren"; "Continue offline" = "Offline fortfahren"; +"Media upload in the previous session was incomplete since the application was terminated" = "Der Medien-Upload in der vorherigen Sitzung war unvollständig, da die Anwendung beendet wurde."; /* Server List*/ "Cancel" = "Abbrechen"; "OK" = "OK"; -"'%@' is currently locked" = "\"%@\" wird derzeit verwendet"; -"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "Ein Vorgang wird derzeit ausgeführt, der die Verbindung zu \"%@\" verhindert. Bitte versuchen Sie es später nochmal."; +"'%@' is currently locked" = "%@ ist gerade gesperrt"; +"An operation is currently performed that prevents connecting to '%@'. Please try again later." = "Die Verbindung zu %@ wird gerade durch eine laufende Operation verhindert. Bitte später probieren."; "Really delete '%@'?" = "\"%@\" wirklich löschen?"; -"This will also delete all locally stored file copies." = "Das wird auch alle lokale Kopien vom Gerät entfernen."; +"This will also delete all locally stored file copies." = "Dies löscht auch alle lokalen Datei-Kopien"; "Delete" = "Löschen"; -"Deletion of '%@' failed" = "\"%@\" konnte nicht gelöscht werden"; +"Deletion of '%@' failed" = "Löschen von 1%@ ist fehlgeschlagen"; "Accounts" = "Konten"; "Help" = "Hilfe"; @@ -147,8 +148,7 @@ "Security" = "Sicherheit"; "Passcode Lock" = "PIN-Sperre"; "Face ID" = "Face ID"; -"Touch ID" = "Fingerabdruck-ID"; -"Instant Uploads" = "Sofortiges Hochladen"; +"Touch ID" = "Touch ID"; "Videos" = "Videos"; "Photos" = "Fotos"; "Background uploads" = "Hochladen im Hintergrund"; @@ -159,18 +159,18 @@ "Upload pictures via WiFi only" = "Fotos nur über WiFi hochladen"; "More" = "Mehr"; "Send feedback" = "Feedback senden"; -"Recommend to a friend" = "Empfehle dies einem Freund"; -"Privacy Policy" = "Datenschutzerkärung"; +"Recommend to a friend" = "Dies einem Freund empfehlen"; +"Privacy Policy" = "Datenschutzerklärung"; "Acknowledgements" = "Danksagungen"; -"Video upload path" = "Pfad für Video-Upload"; -"Photo upload path" = "Foto-Upload-Pfad"; +"Video upload path" = "Pfad für den Video-Upload"; +"Photo upload path" = "Upload Pfad für Fotos"; "Immediately" = "Sofort"; -"After 1 minute" = "Nach 1 Minute"; -"After 5 minutes" = "Nach 5 Minuten"; -"After 30 minutes" = "Nach 30 Minuten"; -"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "Wenn du \"Sofort\" wählst, wird die App gesperrt, wenn sie nicht mehr im Vordergrund ist.\n\nDer Zugriff in Files.app ist nicht möglich, wenn du das Sperrintervall \"Sofort\" wählst.\nBitte wähle eine andere Verzögerung, wenn du auf deine Dateien in der Files.app oder über eine Dokumentauswahl, zugreifen möchtest."; -"Please configure an email account" = "Bitte richten Sie ein E-Mail-Konto ein"; -"You need to configure an email account first to be able to send emails." = "Sie müssen zuerst ein E-Mail-Konto einrichten, um E-Mails senden zu können."; +"After 1 minute" = "nach 1 Minute"; +"After 5 minutes" = "nach 5 Minuten"; +"After 30 minutes" = "nach 30 Minuten"; +"If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.\n\nAccess in Files.app is not possible, if you choose lock interval \"Immediately\".\nPlease choose an other delay, if you want to access your files in the Files.app or via a document picker." = "Wenn Sie \"Sofort\" wählen, wird die App gesperrt, wenn sie nicht mehr im Vordergrund ist.\n\nDer Zugriff in Files.app ist nicht möglich, wenn Sie das Sperrintervall \"Sofort\" wählen.\nBitte wählen Sie eine andere Verzögerung, wenn Sie auf Ihre Dateien in der Files.app oder über eine Dokumentauswahl, zugreifen möchten."; +"Please configure an email account" = "Bitte ein E-Mail Konto konfigurieren"; +"You need to configure an email account first to be able to send emails." = "Sie müssen zuerst ein E-Mail Konto konfigurieren, um Mails zu versenden"; "Do you want to open the following URL?" = "Möchten Sie die folgende URL öffnen?"; "%@ %@ version %@ build %@ (app: %@, sdk: %@)" = "%@ %@ Version %@ Build %@ (App: %@, SDK: %@)"; @@ -184,6 +184,8 @@ "Dark" = "Dunkel"; "Light" = "Hell"; "Classic" = "Klassik"; +"System" = "System"; +"System Appeareance" = "System Erscheinungsbild"; /* Log settings */ "Log Files" = "Protokolldateien"; @@ -194,7 +196,7 @@ "Log Level" = "Protokollierungsstufe"; "Log Destinations" = "Protokollierungsausgabe"; -"Privacy" = "Privatsphäre"; +"Privacy" = "Datenschutz"; "Logging" = "Protokollierung"; "Enable logging" = "Protokollierung aktivieren"; @@ -206,8 +208,8 @@ "Warning" = "Warnung"; "Error" = "Fehler"; -"Share log file" = "Logdatei teilen"; -"Reset log file" = "Logdatei zurücksetzen"; +"Share log file" = "Protokolldatei teilen"; +"Reset log file" = "Protokolldatei zurücksetzen"; "When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to %@ servers. Sharing logs with others is sole user responsibility." = "Wenn aktiviert, können Protokolle die Leistung beeinträchtigen und sensible Informationen enthalten. Die Protokolle unterliegen jedoch nicht der automatischen Übertragung an %@ Server. Die Weitergabe von Protokollen an andere liegt in der alleinigen Verantwortung des Benutzers."; @@ -221,13 +223,15 @@ "Enable log file" = "Protokolldatei aktivieren"; "Really reset log file?" = "Protokolldatei wirklich zurücksetzen?"; -"This action can't be undone." = "Diese Handlung kann nicht rückgängig gemacht werden."; +"This action can't be undone." = "Diese Aktion kann nicht rückgängig gemacht werden."; /* Storage settings */ "Delete unused local copies" = "Lösche ungenutzte lokale Kopien"; "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Gemessene Zeit seit dem Hochladen, Bearbeiten, Herunterladen oder Anzeigen der jeweiligen Datei über dieses Gerät. Gilt nicht für Dateien, die über die Funktion Offline verfügbar heruntergeladen wurden. Lokale Kopien können vor Ablauf des angegebenen Zeitraums gelöscht werden, z.B. weil es eine neuere Version einer Datei auf dem Server gibt - oder durch das manuelle Löschen von Offline-Kopien. Außerdem dürfen lokale Kopien nach Ablauf der angegebenen Zeit nicht gelöscht werden, z.B. wenn eine Aktion an ihr durchgeführt wird, die Datei noch in Gebrauch ist - oder das Konto, das die Datei enthält, nicht in der App verwendet wurde."; "never" = "Niemals"; "after %@" = "nach %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; /* Display settings */ "Display settings" = "Anzeigeeinstellungen"; @@ -238,11 +242,11 @@ "Enter code" = "Code eingeben"; "Repeat code" = "Code wiederholen"; "Delete code" = "Code löschen"; -"The entered codes are different" = "Die eingegebenen Codes sind verschieden"; +"The entered codes are different" = "Die eingegebenen Codes sind unterschiedlich"; "Incorrect code" = "Falscher Code"; -"Please try again in %@" = "Bitte erneut in %@ versuchen"; +"Please try again in %@" = "Bitte nochmal in %@ versuchen"; "Unlock %@" = "%@ entsperren"; -"Biometric authentication failed" = "Biometrische Authentifizierung fehlgeschlagen"; +"Biometric authentication failed" = "Biometrische Authentifizierung ist fehlgeschlagen"; /* Certificate management */ @@ -251,12 +255,12 @@ "Approved" = "Geprüft"; "Auto-approved" = "Automatisch genehmigt"; -"Revoke approval" = "Bestätigung widerrufen"; +"Revoke approval" = "Genehmigung widerrufen"; /* Actions */ "Forbidden Characters" = "Nicht erlaubte Zeichen"; -"File name cannot contain / or \\" = "Dateiname darf nicht / oder \\ enthalten"; +"File name cannot contain / or \\" = "Dateiname darf nicht / oder \\ enthalten."; "New Folder" = "Neuer Ordner"; "Folder name" = "Ordnername"; "Rename" ="Umbenennen"; @@ -270,28 +274,31 @@ " couldn't download file(s)" = "Datei(en) konnte nicht geladen werden"; "Actions" = "Aktionen"; "copy" = "Kopie"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; "Preparing…" = "Vorbereiten..."; /* Directory Picker Messages */ -"Move here" = "Hierhin verschieben"; +"Move here" = "Hierher bewegen"; /* Preview */ "Open file" = "Datei öffnen"; "Network unavailable" = "Netzwerk nicht verfügbar"; "Error" = "Fehler"; "Could not get the picture" = "Bild konnte nicht geladen werden"; -"Downloading" = "Lade herunter"; +"Downloading" = "Herunterladen"; "File couldn't be opened" = "Datei kann nicht geöffnet werden"; /* PDF Viewer */ "Resume" = "Fortsetzen"; -"Go to page" = "Gehe zu Seite"; +"Go to page" = "Gehe zur Seite"; "Page" = "Seite"; "This document has %@ pages" = "Dieses Dokument hat %@ Seiten"; "%@ of %@" = "%@ von %@"; "Invalid Page" = "Ungültige Seite"; -"The entered page number doesn't exist" = "Die angegebene Seitennummer existiert nicht."; +"The entered page number doesn't exist" = "Die eingegebene Seitennummer existiert nicht"; "Search PDF" = "PDF suchen"; "Outline" = "Gliederung"; @@ -303,6 +310,18 @@ "Albums" = "Alben"; "Importing from photo library" = "Importieren aus der Fotobibliothek"; +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Speichern"; +"File format" = "File format"; +"Name" = "Name"; +"Save as" = "Speichern unter"; +"Options" = "Optionen"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; + /* Sharing */ "Searching Shares…" = "Suche nach Freigaben…"; "Recipient" = "Empfänger"; @@ -337,7 +356,7 @@ "Receive files from multiple recipients without revealing the contents of the folder." = "Von mehreren Empfängern Dateien empfangen, ohne den Inhalt des Ordners preiszugeben."; "Download / View" = "Herunterladen / Ansehen"; "Download / View / Upload" = "Herunterladen / Ansehen / Hochladen"; -"Upload only (File Drop)" = "Nur Hochladen (File Drop)"; +"Upload only (File Drop)" = "Nur hochladen (Datei hineinziehen)"; "Creating public link failed" = "Öffentlichen Link erstellen fehlgeschlagen"; "Create Public Link" = "Öffentlichen Link erstellen"; "Links" = "Links"; @@ -355,15 +374,15 @@ "Password Protected" = "passwortgeschützt"; "Pending Federated Invites" = "Ausstehende Federated-Einladungen"; "Pending Invites" = "Ausstehende Einladungen"; -"Shared with you" = "Mit Dir geteilt"; -"Shared with others" = "Von Dir geteilt"; -"Shares" = "Freigaben"; +"Shared with you" = "Mit Ihnen geteilt"; +"Shared with others" = "Mit anderen geteilt"; +"Shares" = "Geteiltes"; "Copy Private Link" = "Privaten Link kopieren"; "Only recipients can use this link. Use it as a permanent link to point to this resource" = "Nur Empfänger können diesen Link verwenden. Verwenden Sie diesen als permanenten Link, um auf diese Ressource zu verweisen."; "Accept Share failed" = "Freigabe annehmen fehlgeschlagen"; "Decline Share failed" = "Freigabe ablehnen fehlgeschlagen"; "Accept" = "Akzeptieren"; -"Decline" = "Abgelehnt"; +"Decline" = "Ablehnen"; "Declined" = "Abgelehnt"; "Decline Share" = "Freigabe ablehnen"; "Unshare" = "Freigabe aufheben"; @@ -395,7 +414,7 @@ "Recents" = "Kürzlich"; "Favorites"= "Favoriten"; "Images" = "Bilder"; -"PDF Documents" = "PDF-Dokumente"; +"PDF Documents" = "PDF Dokumente"; /* Media files settings */ "Media Files" = "Medien Dateien"; @@ -403,8 +422,16 @@ /* Photo upload settings */ "Media Upload" = "Medienupload"; -"Convert HEIC to JPEG" = "HEIC in JPEG konvertieren"; +"Convert HEIC to JPEG" = "Konvertiere HEIC zu JPEG"; "Convert videos to MP4" = "Videos konvertieren zu MP4"; +"Auto Upload Photos" = "Sofortiges hochladen der Bilder "; +"Auto Upload Videos" = "Sofortiges hochladen der Videos"; +"Account" = "Benutzerkonto"; +"Accounts" = "Konten"; +"Select account" = "Account auswählen"; +"Upload Path" = "Upload Pfad"; +"Select Upload Path" = "Upload-Pfad auswählen"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto Upload von Medien wurde deaktiviert, da das konfigurierte Konto / Ordner nicht gefunden wurde."; /* Progress summarizer */ "Creating %ld folders…" = "Erstelle %ld Ordner…"; @@ -424,7 +451,6 @@ "Delete Local Copies" = "Lösche lokale Kopien"; "Manage" = "Verwalten"; "Storage" = "Speicher"; -"Proceed" = "Fortfahren"; "Compacting" = "Freigeben"; "Really include available offline files?" = "Offline-Dateien wirklich einbeziehen?"; @@ -443,8 +469,27 @@ "Overview" = "Übersicht"; "All Files" = "Alle Dateien"; -"Locations" = "Speicherorte"; /* Import File */ "Save File" = "Datei speichern"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Wählen Sie ein Konto und einen Ordner, in den die Datei importiert werden soll.\n\nEs kann nur eine Datei auf einmal importiert werden."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "Suche"; +"Back" = "Zurück"; +"Save" = "Speichern"; +"Sort by %@" = "Sortiert nach %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "Weiter"; +"Previous" = "Zurück"; +"Favorite" = "Favorit"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/el.lproj/Localizable.strings b/ownCloud/Resources/el.lproj/Localizable.strings index 5a182c4cf5252d8acc5ea47dcdc5cb54732d8c26..d0fb3cc4039235fe8d5675088210bbba24e3f6a3 100644 GIT binary patch delta 3222 zcmb_e-D^}w6rW^FlvGioF)8(9Tq-?xLan+IUA5il#+E_|Y_e;TcC!ilkwmFn z1Rs4+2OmTq1i=SEO}NA~YD`QDBIqAc`r3!mheBVP{?5$K-nrSk+iWQzd+(k3opXNY zbLROU^^b1whULG*tbJG4gFp!EWbqus z>IiT-tmN>W!yl~>avXBBPICQN8&=v<&_MEpqjh>WIui~cWO4`_2`sNvMWR_KB2n5W zsv#)&0{{O9_2HEUYv5ixb$5C1TpL0jXO|ET@@Clb<^g}tm-x?ohQH)fJj6O!6LxK^ zV8@iv6kiT5nNzJY->a-FhB)*_=F zh$@nru4SdOs+kHxRBP;(OepA5rBUgwSMTh;Isn zP4QDxWUw-hXRt<}gacS*kzf<6P+Ml4G*NMMsqBraa-hm}Ulk_TI@C(!LOTl=OGvNS zWnxw|r?_gr3_@3qp*25BAPpmsD4={PSb4uxd(nJ3$f!xEF_K{eGCH$UHcvF~xd?^S z=hWjKg-+x!4HJUd`*N~j&xSalV`>XC2WocJy_J8RKCGI{_)k-eCYu>6RL!1p<1=L} z2&c(Liv6Ky7|l6N#}K1(px7IQYORKD73>z|=b&$z&zI)PF&W$1DjN?p^LKnv z{<{Cm4`|k;RJ{9Tc+D;;&a4YRfvp#ia>3y>m@I+MJm!=si3@PtJ21NBosQiyba-pe zVhK^wi8Khd0~n}tsSIR? zkh_A)bW&dZWU~_nj!G_#%Fy{W`Z8E0M`*VIS^GGmp+#ezx{ombnd7QRep&-bci_?P z#seW10c$#y6X;&*3bddV{GeU;+T@l7*}K277P{vAlMuJ76tKC-h8szeLj7p zuHia=!KdUe@fNpfW?#M2gxS-M7@R@;F5#(lQ-f2?=$7D3r()5mn0*cW1m5gE4c<4y z#NO5O5O)+urN=+c0)Zu*R_9G~yAu7kj%pY)G|mnxPcaeTX&gqf%DWhzx}rS)BDh~s zR4QZTzU^CN&-QI$({;yH0)McAbRQkQgp?SB8jteaWQ**XSudNmosV@xrVnI3W=AbK z|7uW3&q9uhTCga7>Bb`9PDXCL+~U|V608Ce_%7%m_dIrV?V$EbV>gdC!MgD~YB8hn zTnSU;=>$NILrq?(w69h{%KS)^I3hvys!0)XeNpR!Ub=MI{q9gv|2tf(j;h8pg>X^% zMzO4JqN9X39^OM diff --git a/ownCloud/Resources/en-GB.lproj/Localizable.strings b/ownCloud/Resources/en-GB.lproj/Localizable.strings index 7ca777beb..0259dc7c1 100644 --- a/ownCloud/Resources/en-GB.lproj/Localizable.strings +++ b/ownCloud/Resources/en-GB.lproj/Localizable.strings @@ -141,7 +141,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 9ab0f5779..45b1f8c32 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* +/* Localizable.strings ownCloud @@ -149,7 +149,6 @@ "Passcode Lock" = "Passcode Lock"; "Face ID" = "Face ID"; "Touch ID" = "Touch ID"; -"Instant Uploads" = "Instant Uploads"; "Videos" = "Videos"; "Photos" = "Photos"; "Background uploads" = "Background uploads"; @@ -231,6 +230,8 @@ "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app." = "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app."; "never" = "never"; "after %@" = "after %@"; +"Decrease Slider Value" = "Decrease Slider Value"; +"Increase Slider Value" = "Increase Slider Value"; /* Display settings */ "Display settings" = "Display settings"; @@ -263,7 +264,7 @@ "New Folder" = "New Folder"; "Folder name" = "Folder name"; "Rename" ="Rename"; -"Create folder" =" Create folder"; +"Create folder" ="Create folder"; "Duplicate" = "Duplicate"; "Move" = "Move"; "Open in" = "Open in"; @@ -273,6 +274,9 @@ " couldn't download file(s)" = " couldn't download file(s)"; "Actions" = "Actions"; "copy" = "copy"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; "Preparing…" = "Preparing…"; @@ -305,6 +309,20 @@ "All Photos" = "All Photos"; "Albums" = "Albums"; "Importing from photo library" = "Importing from photo library"; +"Media import" = "Media import"; +"%@ of %@" = "%@ of %@"; + +/* Scan */ +"Scan" = "Scan"; +"Scans" = "Scans"; +"Scan document" = "Scan document"; +"Saving" = "Saving"; +"File format" = "File format"; +"Name" = "Name"; +"Save as" = "Save as"; +"Options" = "Options"; +"Scan additional" = "Scan additional"; +"Create one file per page" = "Create one file per page"; /* Sharing */ "Searching Shares…" = "Searching Shares…"; @@ -408,14 +426,15 @@ "Media Upload" = "Media Upload"; "Convert HEIC to JPEG" = "Convert HEIC to JPEG"; "Convert videos to MP4" = "Convert videos to MP4"; -"Instant Upload Photos" = "Instant Upload Photos"; -"Instant Upload Videos" = "Instant Upload Videos"; +"Auti Upload Photos" = "Auto Upload Photos"; +"Auto Upload Videos" = "Auto Upload Videos"; "Account" = "Account"; "Accounts" = "Accounts"; "Select account" = "Select account"; "Upload Path" = "Upload Path"; "Select Upload Path" = "Select Upload Path"; -"Instant upload of media was disabled since configured account / folder was not found" = "Instant upload of media was disabled since configured account / folder was not found"; +"Auto upload of media was disabled since configured account / folder was not found" = "Auto upload of media was disabled since configured account / folder was not found"; +"Importing %@ media files for upload" = "Importing %@ media files for upload"; /* Progress summarizer */ "Creating %ld folders…" = "Creating %ld folders…"; @@ -457,3 +476,23 @@ /* Import File */ "Save File" = "Save File"; "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once." = "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once."; + +/* Key Commands */ +"Select Next" = "Select Next"; +"Select Previous" = "Select Previous"; +"Open Selected" = "Open Selected"; +"Change Sort Order" = "Change Sort Order"; +"Search" = "Search"; +"Back" = "Back"; +"Save" = "Save"; +"Sort by %@" = "Sort by %@"; +"Tab %@" = "Tab %@"; +"Select Last Item on Page" = "Select Last Item on Page"; +"Scroll to Top" = "Scroll to Top"; +"Scroll to Bottom" = "Scroll to Bottom"; +"Copy to Pasteboard" = "Copy to Pasteboard"; +"Import from Pasteboard" = "Import from Pasteboard"; +"Next" = "Next"; +"Previous" = "Previous"; +"Favorite" = "Favorite"; +"Cut" = "Cut"; diff --git a/ownCloud/Resources/es.lproj/Localizable.strings b/ownCloud/Resources/es.lproj/Localizable.strings index 9ead55d4b020e313e166908922a200b00084c189..59f0042f24c5837aa8414cb7e8cb7aec3e80af70 100644 GIT binary patch delta 2984 zcmcIm&u?2r5Z<$ns}Yz^I-c&dkod z+kfuA^Y{LPj~>pbsavD>rjwj{UzJp>)>WjmYN?tkt16yjtkzWpzfH9UgvL%C|C^9j zfonk0z`B7CB}86@9_7?4LbhSpN?@Qqag_7faYl0BbSCkdMAo54WMuSWl>`0^zW>q5!> zE9s|txF}6~WMnRPMXk!+o7sE?RZ`vNnTO+LQyVLg@{00$dd9x{^3}na=>m|OMw8X$ zH7GA*hh?s9yO8|+t8eo<*i$fHF+S_CWLpe1LWW&%a^;K6B@JJhZ>?6g{pZ!9#TwXG z@$C8Xkn;}Mb~RM9eXlh(`CLxTp&K?~NUIM)>bvJ6`ev0o^2v<}^f+ERjeQnj13Jra zDK;w>C%Xbu*Hm|n%f34d;$JfQCD;hV8cGA3$MMX#t03-4r434Uj|ITJ;C*9z#`R zPov`em~w+Ao<$5Ah}*I;!kHCL8P1@JN&PC~&E&>%|9Co?0gh!{HaTH2IKqm_nB8qb zH=I8I!M*<89Yr))3Ip7m{ul1GAACP~NX~7I%k?d!XzNh>$dBJXb|!@`G6f6o!p=4P z=W(nJb7y%Zj%eR!!u*^A{&TsI%|5|wnU!kFg=RKcG~x5Z-`|?<4EN%8w#g9BjC!W+ z-ir?4y)qa%@MU_8(~G@_zMWTfIX01gcFkNG-s2s+Hns9~BO^aG#!tE~cNX}fx*S+t zGKIhnRfp7dP4q1J>*h1Ea`c^J7fpd_^@4gc&ItAVp%ylH7g?n2v wz1Y9<^cJ>zv6<^2>(HXxu88IlhfO1SoDvD~Ke(DFvj6}9 delta 98 zcmV-o0Gv0B!(t0CWIh0B!(u03ZNW zlUp?>lZ2%f0duqIG@B2z3Pm^|linN%vleE!3bVX%ng_GCd;TbscybK0MyM7BvzD%u E2<84H<^TWy diff --git a/ownCloud/Resources/eu.lproj/Localizable.strings b/ownCloud/Resources/eu.lproj/Localizable.strings index 0dee35594c5dcb0ac5875fdca5fcaf2266790280..2c1c5b0d1cb188691214fb1bcc7ed5385d71ee10 100644 GIT binary patch literal 24274 zcmcJX*>W7ob%yWx6e&bZkfR{Lkw%ViNVX-cB*X_osLJ)^S^l$Rz4-N$5Z+YNY*}ow$ud7^+PWy`4Nt4v z@_1QwvnTvMuJf$TXJK*|E@@VUix<#mD*7B$_58)ODt(;%MR9Sp z&%By*KmT!>uQm1yE%+~07n+-@Th78w_Bm(w)4X8!A!8fsyHH(+R(p!QL$<6~%Wcu# z`15sL&FgG+5YDedS+&n?Isc^fu$P6rf_UuVEomO4h?uCC2d#~ACmxY{`S=;UX;mMx5j$6FU zZ;G}H3u~wbS31@@e0=}LM))DGCq=u->LRQ2QjasWN~Fwx%vMnNU|OxL`9sfXe$4Pi z+2z*e&*DHP)j036l|MY+LUhUhjlod6_WyH8Lh=udQ;totYI%hm*X` zie+PcPU~#ZwM`LzD7w1M=0)7VzvVZyYPvF8Xbcus=F^^7`6AKjBwywXdDy{|tex@2 zXD3;k`S8db6xDmZzAhHorl>MC=zQeup1zOm#+v^lQdA}yu;xjzgxCcvTqJ^AW=(Tj z)iWD{KQaz%W2Aa)@57eE?sl_snlIxH@=e+-LQ_|t{U9#NtoE}xt?R1xw)&OZobU6` zMGo1WZqURUz-Rf7+W%2r*Ttf13TcxLH z34cxUO}5NJHV{%n@FF+T6&p<}Dez?vY>yh8>7cU69+g{5v%A%-Ln2;E_FqMS?wZT8_`}mhIMY4N}$}jB#MQ7)}#G z9&t#v;Lm=Y@Jm*PS$3cWj|aYC;G$`|ygB$(UaXPfr{mXAW}b#?bn1#z4n@h&m2IH8 zw}M5;>TFXnH?-{-@x0C#MR+qeUo>g||D&1-HJYZ3R z4g)1=$J-yq-<5iPS`y?%&YUdp-r-gzh+)%V0{(V7V|eZ`Vo#GMe)`=0zAZ56lRO~* zD=~9uD~#6?#VT9;^s7HTl||LlO007s5&dmgR`a3^w>Q{r8)XXXAzhda=BUGT5H>%s zr%~mj(w?%}S-0t)_k-`lOyhC0P#8~TH((U#)WRyF&um@x%i3K|tIfWm9a@98)ZV_doyfpDt~Ky)&bj$s(}sg9Brok&2>k z>crD6E0E6%9(b20duMq&z0pjP=_C;{C#m~b@0*p4B4`$c9e4G32_M(eE(|yAb#NM} zSHt3J}v6uCryEAy!f!A2@nDPWS9WHROKbZme!f?tb(?W4!UyOj1hfk@vB8m1r`^wl9{uVDdp}siD~BgrMsd_`%Ra`o zTQ+voc1-q=@CQ&f94`yDZ;0xsu5KGQjE5i!cuxd6DVj)pr=!}Ck1a+%USM5`gK;Xfj0K|s;$=RXqsN; zOL4xP>{ZK2<+H>uud)VU7+ISYiHnI+j_sP_7Ro1ixX2HQ7$5xFWon|Wix5j=k1SK4bofT z-^}t&q|H@Tw=x7h64_@dU^2!`l=T*NJQwi=Qc|*6EarsW+rjj_-NU=~ZtaJ~a5D1FWsV6f=ho^6hSjK70?=z6 zNXvii%?@&&qjwr8Z|@8xJ=p_l;p^}(y3DcC0KNpo_VczdVEuvJEPtN}_Ojc6z3fl8 zb`Y_*dDCQbw2OH!SfIPe3fsE@$lI>Ryn+CN_Dd zK{Ke$dn9$GHc+n@;^a;sowei<;fl>au9sCdbFvzwS>IIjTo#i$tM8n;I+U!QXB+S@ zFS;5KfZYd>7V{{|$>y%}dL{FVdbGYrhGuA)o5NykJ+x6!q|DJihQ>NyiY)+CTFQ}{ zyfu@a4eR4!guPre5EH0m|yu_Hcm@ z59+fGByu5!_U;_C)k~U*%&3Kv;4BHE&(0YU45$Kr2gW8*iF0nzt1#z?Rsx;qEXh44 z2Nsd}s6EcoM70RLZ6W33T?o2ktGK8g3r`$3IKf;I$;{2H6a*W z_<;I9J6#xWt8whMrzaU1)ito9DKPVm@dI!dEkK4(G0m-IM&vcc$>c!0e6bW4mH)TF z9Z*E9{|4A7EOGpBpzGqbQ)yN;rg6+rCry;uUTuKwNHG7{V$5qaa|Dy1DJ22M=Hhq((Q?*lFA3plBkyyZ{=0-LG^s;~ zBXGBHq>&<;Z9@lUvMhK&M6CxT3?c&BSVC;kov!`uMa(L?#ds%!l4Vv zKG}PfFV{BLn+*3M#$L}b(35Psuovew)&TdV0! zRhc{f)t`P9K9#{7GkF-v$l#*jE&;cP81<17<`Pr7JA=Bi0m504h>gEVt z>ahGQLj|0l*p}08`hKW7T$K2{|AQCb8Gy0oNBjh9809X7v7G#ej;s9n5oDg%iVjBI z6x%prSueA)f+9c|$l*soJRTgr^c{V#zc_TPle;E~47LFH2e&z91GLF7(qq=*5VaPg z`(PG3$cSbl5L1Zy`{HagrSMOJR^OkWo!cLqtwqEk*2NUvkMr{IAVuAwk{98TLEa{{ zuPhEStrNQf$A;?%xKoS32CB__nOYnI5BC9ciFde`HWrrwDUeLHKxr5s)Wzp)N)E9? z$2(mw$-ZIw;1qib3mV59P8UE#AkN%e4nHe}&3h`_@h3AQzq99rbc*J2?mGh-@QyaA zZyfm}9{Q;HIopC_Z5|K*2((BX(N}hjS`EJmpgy3wP5M?pJDyeL3|g8sJ+x;35S#h> z<6p$IcH4dX%~r$o=T^%Ydo`KQ!>7H`B0ue!?Np34g<{A(ylc%w84)H8y8*!{FqT1#v>3F~ zL+fGaLsz_AvY;5%hEyS8tIG7T#K+ZeZ_*Iqv>dHQcyv}Rj27mTE5 zNMPQv;lFe$Re4>Yqj4@62)|~TybR-L441tYt)hjGtw0bwV{MY>zYtB3k11RYCV6B; zoVdENhyy7C`7LVRNN?E0(W0N)BKqM4Rr)2EKWRaRuq5+Z%8#?=t9*lfjXG(BX(G%% zskQ>VDIF|@s z%XlV}WO1+$H)u}l^fDu7%0Sz#?bYch6X>#hodE^Z`h;=-YjeJgR{V{MI_wdO(}z4> zV%dwSJ$%jY0)AKLt2=nvo#!jCG>0`jl`P_5!=+PSpEurm>`3EHrMyZ+8^YsyU=m;x z`d={;XA^jw9au<6S!ygU*4;FJnwp!5zamWGYq*r)_JMg1j5!XCLphgd%3d6Iv86#; z2bIcx+2j#eqV-~^T_d~!#>dw<40DyBRN*Z$Xq!+Aasgm0^9Xzsr$a-4q;PpeJ8n^J zuECUQ9HSO-1P|8s{iW}0n$yxm5vuf!U=-=bekyBsVf8DKl%}&px+i-V*Vi`dIXY)a z{9gxrPSV#N?i381;1Hp6=YjO}(WlrzC8A-PApfxu-rmE8L*=kvKnXb3Fbg<`elafrT|xY*)pDXWEI zBOO9n5Ar1E%?|M(ysBw;IEu5CbZz4ZryY;*(2N z<&}|9N3kGHpQ7$Ff)$+VzDLM%5?97QQ^NU1`_59?zx^Ffd{>h(xQ6KD9}a@?OcPIt z$OdYgb!SS~3g1OHzUL%Nrjie`h4)zo9$;P~Ab#{ROCGq@0q-~Ke5!(_EhaOOINT^I z28n)*r;S&`jz?0xP$|Q*dMAXIAuwTjKp@=52fuLW0Yus5OOZhixvCk~R8Pg{%+3&!*- zYz9({dd2Ks?*v@eypD5^Ne~oAa_%1Q5y`cvGL@BxygY>Gsqk@0fgp3>KhI%G--^#= zxdi0m8qH{F-(gBoTLA27RCTzE4BDFANm4Sgc_@o?wFUuAsWHW*nkCI;W{2o#C(b1l zp>urj_~21-GVVo52AGI_uR#=lLPk~1Vc?x)(5{YQ@H)f4B-Y|}K>}*hFle@pcd(f- zIi@_{pxh`F1(MD!8iv;1GMwsTN-!;CykHBz*ishN^XRMC=G-P3ccV=xX`54>#e=ikP+n6dXkgkm^>+@&8_=<%g%OnuwdR@oI0xmYU~AfVEZlN4 zgY}o(NE9Q7oa!)Q+T+31WTV~3qk&gq&6jynO1$7#C_szai4Md7l5FF)2LzLyvV^5^8a*`}C+Pf(AT5boz|T_7_=v;_rNHxY0@scu z1KUh=nrc7sll_3t<3kf(D;8EysYQUJX{7{0&@sZ3;Waan<&$E136fq$-}sgU>;71f9*mDTYI!NYINkA1AmdHK*A z4_8pnM;hU7N7?Q?bQPPf70BZE5p64^x+!MPhN?vpw(`2H6a|Ra5qkQWg+~%pvj^q4 ztPt}}DE~g|vYn_oRlEqrGK7b#u>iY4H`?+2#X+#v4%O7fS zFY5^>MlK*FuT`5+ZTcuC9_NTRK4O%JV#gjtUHS+yj~M=_Abr8bo~hhc0s{RNj!{yO zvJSn7mpAZVy^HcuyVV?4vxczCb0exC#gBD1*iU$CtK!}dd?<)_bNf-g9|~0WEFkSL zRL>uSSF~WGI$Nu~a1dzsS3G<}5Y7fMe)RhY9>ZZfRX>HjwI@E7uY%hfoPZQLLu3}o z3VRo&?6~R)HvN?l?f1}(i`C|-G>&^f#^A?jW$X^IRGZ@@N2{53LPxyj)k56k)13(w zsN_*FNER>o%`aqwJ`rE%j?!7u+rU6pA$Ecls7QHX3iQ(_gNHiPcU(L4u%JOlHZO>RAvF8Nc8G%%y3Md&;SrNBGIw zZQ*7t@u)&Ty`VH8Tvj~cEEN1Qf7W<-+eqxF`&m(cJvQ0T7GD!Bz#+6uVdMdy((Q)D zUqPL|5yO>nBg+b1{XNl=SR%$H#Nh#pibQd^HE#Iei&2Nsn5k)eqy zO3y74b&&A%2`G39rY41Hxu>lh_?}L1I0HXM;Tvfxi2@_qwI_wLZ3QBx5vAwen0XNK z-{mbAwiZ;2WC_t)TNI~iHUx3Z!wA$?N_XT+Npj}6B*I`ZVSJwlJXH+P zgl#q6$$;CfF)9*f24Ns%dT+AXa|uwif@Ndn%NBF>7Him{k>unoe8*KVERv`~u$FX| z%_Pwh#?UM;D{<@^UFj46a3K11v=W!;`Gw$<+FM;opC%M?NA0V7R5nA>3|uAkZ=+#$ zeAD_{<;gY~e^8JNtkl5gURH)_jZ4v#)G(x?FrR!dews6s-3lXQfgy>K6{XwI22(l1 zrdQM26iAWc)CE@!ZOSVzM!Y;ZbF$#upi%TE>$&LKz833;g3ci4+EOq^T+4dL8Uc~l zf7&<{7ia*#BA_d|_Vg8IPQ#y|;q;p0#yvt|qy{{QO+YEugg4=S&4rC%>pvu0v2wcG zPuJ<^t2Hbpf5Gkv+%Jqxx%r)%*izjR_i2w;*aTx8{jfdQ4{ePHKCl0Zk2nWUANP%i zCu)*?W>xm?rByDD866LiNRPcV#*cr~?VTHhuy-!&W0chm-z+X5{-)u>=@eV6Rqe># zcawVW8NH)@fon|>%U>JB;?b9`eXe4NO^o17CGzWO-t;;TPLv%cw7v*OVl}oCZ!b{p zQOM`;{a*n7UD*2PzSma_;aO-q{{9kn>Dk0nqWFu^x;&g?6MRro71%;PYsr-VAdEe~8h3ks5Cz$~m(cXu>c+uHk$TRCffuLDj z{x~*mi+@SnbnP!cV8kP?@oFt@ZkXGwl(^l-z*nxD=Rw68Yj**3lYnj63wNDgsYZ*9 zSkYc`v~Z^tDAok#77!(*(ooLf)`CE&cCInR^~@IM2K||@?rZTc``5a(Eh%5)6Yh60 zhu8RAMpiL=iRXAan#4=SjEQ#ZnJ*tKVvkC}j&C;H_eiIe?tDc4A6gi^>ffS@xMyVs z&S+)He+x0jShrRG+5_VZD{oI6#A@wQDVR}y^=LQpUMEF8G=!vH(vBHMAJBYOm^S?x z3s3~Z%Uo<7_;*m0qTE0wM zCAEbj@k+}TmiRx>vxfr_cQ44E2r0{rO@vOIr7OSap`Y4FA9jq$a4QTqBDQS;u(!;n zV(@s&dyAdfZK@Mc)D!DHiq8nmD;i~Mm5JDx!c+uAw@all;UN>DpXNu8KBhRh@xm`< z&7NEWg^E3WfmE?VM0O+Pmq#M)_YgFS2PJmhSI@xDOqil-?JPB6>^8xfmA9##zqh+j zgCZU)jzJ&y>%H$O-y2~^mB+3lr^_0cKF|ViFS!9|&nRZUr|{)T5-^6ZQSQBOBfpXZ zOeMz2kYQ%xrN!WJM(KTbZUh;X_SW@4+jcaO)Plzep7+Ch^pd}0bqNCRENlMkm*@4_ zM)6NSY3D&|R`0Jv9~Oy1YY1zVZb8+_jQa(Uii_$hD+j_N43g(W1&e(1w>z`R?)b$i zjIkT}yqu%Chye}~vv4))KCe`%U-*1k?f@$PR&ILZYd;mMG-ocRPexAMovD1=Q zBPe^n3-4dMkm^*tDbWxT2hR+*oz&IZEnaMf?6O|PGRFwxl`8eyD5DD-vs=Fi4Xpw+7ZKQOh`To7`|5Q@3$h85rSanp! zpoBnsvcOI&7kW1f&Nx7!lzoo9hqapZ&4U%Ms=egy^BIb?+S{=2CCiAEpRbS>ickyQNt@1$jDY_z}AhF!dV0x z2JGQJ+^D6OSy|(w947{q!A11PI{AWoP~rIY*aRn*qU;DW!67e3@x+XKC>K&$BZV!a z0Vyyn$HvKm`KXN6JMrAQB1z*wjUG696fDK6%U8V$rzj_BKZm_a z*0@z8zhv&3u5LzzuOf(LLC^^=L%BQUpsWq5qbnEKQwkc5da5a>SjNb`Ep@$<=^^R| zl@ZAqc0txgXYfdOxKU+EgoCKMz87uZ^99(EU05jGb&HK_Pd|ERW1l#mJ2ymTaKizl zwvnx+SjTj*tbAC54fC-ysA;Vjr3Yt@i{jfywnGt`;NwF(fdrD$dv~}ypH6~pjKDo= zFEvo+AYR+%ij-;uIH|N6<}gqTJ0Y$dy4~{gYH`2HZ50TIKRqj41?^W4&uQH83+J@s z-lDPD!}odSCxbmAXX`hiP>#+X+wCLwnyI42DHzR>vJ1jjh+m^Gqy)`FPXtn+Gj!b<}6_V^_pq!=19u(dF&s?;^?l>jV6~-9>rJ=qHRL)aNB>9jsz> z3u5eAf;VbEx?H%^KE6?Vzv^j=ptd9$i}=)u?2nza9!Sqdonr7kJz~hTrgWW+C_M&2 zb+}(USha=Q=l6H|FFWEzx$kGqr7niL zKcm&(b<~t1;}e&o2qiNfBQRTTF%7H{~JNv`~uX9K~1+}d8+W@T276^gmmc9R|}4B;bgs@on9qHmdh2>w`;C2n_FWX|g1Rf!x zYOwji>mGF%*r0}xff02^-$8EzW(9gP@t$mYs^`VxaA6V8}lARKo=9EDr|(OLeC5sZRq01Rm_kWo+k)a2SL_2{P3L4`y* zt;*DM)o=`X*|dtZv(ZH;OS?T!4!pqAgMp%C(mx0N^4f zR7!jRT4yPj02BBZ0;Fd>0dCDaeXU)nlMUPV$$SvPtsU;!m5~ ej;eY))SFl*kUS<^arBr>FgjvB{{c7^M=pF%@~4c#%&b901ae3ei^Yk-!_W!lp*e?W-K01`&f^zcT0N!$C& zRZDwko>Nk(n+`h&pma{2%za(^l6gx1^WR@BzFcfA{wDrzE$%K(7Ke++i`R>ni%;?Y zllXMBxUo20ykESFYsZV{@mXU}79Zp1n=#^K@nP{auIl>D7v7D{Q?#|Xy?7Jre;spdEw+Qw*YSTof7*UJ3M!7`>i3OaR{Jt&fEy&k+F!=s zn=$*i`n9&4C9NmgL@8ZtWwyN~|B<6g7_KZHu z7NmQm_%{Av6wP?Vy7+y_^{bVf4r3-+&y7aS z^SFaFj$n2U;MY>o5 z^dH6C(*0K4kv7mYx_lP@B6sOUvi91HhI)KrD=hD28(GJ{jTKJfE|&hL+2CHA@i?x& zUtlek&Oe>WN7DHi^L>mbp2zGT;(s)SY8A! ztcZV+i$^~`qh_>9tO;GohHo@3-;~)m9WP$N4CI0Dd$o8VS6L@t@-VllUJgXk?}Nyft&B z^Jlgm9yLUG*Q5fv=JS#>`g|P}V%dB`Geqj%LWs4e@#kYu{Vw#Mv*^y_m4`8lSH7&? z=d`@FxJbK13J~{op$Q!v#otl0iZmc7=$t(4H|bzsi7H5kk?+d5yhb)+KJUj|--pbO zR@RP(+TLU*xst8!hZryY@5FPggk5|H3BNp>cisvVzge$Z-MlRrH}OQ)KuSsXdGn*~ zt;OFi{w@C1TKXwD(Y+@zCpu?dqSu_Qn+iPvw`76X8g^zWI$q=oz9RqdIOT#Mko=Lg zD|*~%eEL3qCh8Iq(AQt$FL;17>xz=nv!)6ArB}8O^v_h=sj?-W9R=!-Ys`=TW1|ve zWmTg~+FR1NQ$`9!h_$xWYM1N$?_MB`^HNYw<`FR<@C)og)jz{yevJ|o`XxwXSyb9`}a&DG&C+q)7FfHloT-5+G7OjdANE^xHNo)#w|NjCYjzl z^^E_&wx=pvzJ;to=U1UY@hJ&j?774+tXetqr{;510skNDSryM;!>Y;W@q4JnXO$CQ zZM^zERg|i9V%1jQ1yP-RT*ypyg$H5V)Lvw)u8Xg>2#6OdbMRQ|cA?IG+#%+@h>Cry zJx_j$^gqTm(2O-r3%Mir6uY?Vdwe=vw|WG+y6CffWogid$KVZU3QdocUyu7B z(>i*w2=Z#M1DqnCLn6j%WCf~drI#T;c#s{P#vR23>Ios;v zGg?3+%sR%G)GOLTw{zS-3NLUh5M_sf}^BdakJW*dMlr+%Q2PRcTEeuFUbF~l+0~wcR*d(`O0v9 zzV-EDKkxdwb$;G;mcAAFm#o(us0&NhLpCF?%%0RvXpATTVkAy%H~Tcs*@AYPUG`R= z&+NtwU?V;;*Jlt?Wu-WxhP~xH1nTF&eyjbBGtfm#i$dPi`+Z;J>li~s6QU=l;v%_Q zHq1)95LTVUjQB>an(xG1?^?uU)P(SHDM`ut)y zRw24Vf#ba~3arEaQ%8N8tmmkjKRX3@zb+No9CzN?J4k@ zKA31!hr_4^Jp|AZe(AJ|eaBo?e0s`WQFu&b<%W;zhX@m2G<4mEc!T4~n=}L}x5bHoIo>O-wuzy=!KHd9%a?I**tv*nXH3v;1_&*k#bA7j>Hs$ON zGj*!G1I_UhmR7q$%O>60Q^ux{I90PW;OFr3l)t)G+PBeX?vp2j2;^oX9`za17>~3!hhJ!CF70Zh z1XREiWtA68gY57;ghdwEIyMPLfjy28HS&B;^rg~bl|Gxp=MtB<<9@A9>8$lvKvN=+ z>t66KcSeNf=o$?jg}tF~N7p%S|F!dG?dd=%83%KtS>^}tSeMvB9jX{pwL)Ceo`dpd zVx)S|b9~GHSlv5@J)b{@bH$M49Xb45+!KqD&3Xx-0agE-OW|IV8M5*2yY*O+liUg~ zmp@E3WSQ=+bjN(E2Hk^5PxFYvB~qOKLeKLOhVj#_d&4}6wr){T>b3S ztaN$kmQ;PILjz*H4T}Sx<*ai@(MzSs#`{ zZdIf5Sl1H6-!;xv?J8@`b$6OBTCl>eS0huAx1Mw$6Jsz=_Mc^X>~@3ID>AJmiW630~M)LF14 zcteuJYUS;Up*>gF6?O?eBS-#M%;PiINq0q7p{Spcfmp{g&K70gDUoG(j=D1H>Vf@8 z7HW7-R_-j3J~sY?-P%(`2|lY#T2HdmFRWM92s&2osb0zZ&|^wWeLCh6iXb8H#nDBQ zK5OPzJyH@>XXi!pe`KPZh4@b$X{sbsbBs!H*JsdccMgqD#R7A35h?@5CsNF=4V-Lz(@FbAjA z;598xYnqdiUV8JP`Xq z#>5Jc;kfMH-H0m>;ukLYRr;bGX+SS| z9hm}gQ)r9?(HFS}R>(;=)x^ST{108Fwy|lr!8e8Lk_&Uzd|;$U^PwAI7?mLu$%nCq z)V6RGYO!Cq0~PvnK*}|k)p|(}#`?9_3i6K#d66e1Lc)Wx3Q<55K^EE}l??F$qb8$s z$8{60NS=H9NUPq9)sO(33agLLFpcO28Y9tsVmow(he)f!ZJve`C^sVBj3>pHcJZ*} zXYt2qlr#2wVEFquvewJ=D_m{qOe@&r6Nq&CHa|bKALkL14$i}o4Q2eu{8qWvVbg#- z;(c&JEl!?w*u1!+MLsF(CTbePu`NYM?UW>*fm|v7?f8r=l>PC%W7m3WG*(s#RwyUL zhSf=dcIss`1KNz%s!U&5qq@(Z{fY744y1&!0LS7({o&JTEkB8MexO4aP&Ry z2{Ml?5+6oZ^iYIDLm{~oBQ0XNLLFU>WZSo7MOI6v07P*RMAtO?hYZj``3-Px5YE=27T&-gPWN{ zus@9D5i`M+lmzFFlu2vv!m{|b&F$@$m==XO@CQf zKxO`mkO?P6h|Jh>?Wj|EySH0UKPbPjjHh)=NaUR;^$y`n!x3;oeR?HzDOy=Npt z^;G-s+8yN`7Qer7H$L@x)fnZhcsOS)lqJ>P?X9@R2}JfkcZ*ew*HtAwj8#6wczXK% z_~{)zu@`iM9M}Z0qSmd|!of762-c(Q4P;U!%pLf!jr}pC3rC8ojtM)ft7yEAK4`Xw zrPlSfW6tcz)bU6ON%HnYW@NSZ(a)2)AYCD7C%ci|sn#Cp|28NE>xja~@kG^o-osPD zOxv@$A1y3BQ<$cmH>@QkX6qns=`ET*GtD_T!9*^V86(K6K16zX)uK-RS|5217*6Ox z%yB8b5+|)msNGt88ydeAfAbD?|2j$ZR1hVoi^yj2xXV#Or9pRD70+xdTz^&%w#OB)LOZLc3=^iJAE>mA125vVf=U1f{#eqS5f^$Y$EGW_l}d9LLQdYY4tdLMqD1VqD* z$&-u}_%BGLY_4X3Si$H?vW#i=hdx0rYBY8ReSQ~A;12p{1>s`$XjNsa7sb8YMacLs zIZ_9xM&ub)ZQMzVneuS2UD%0X1Zhprh7v zwI+R7req0qZ&+3U4YGu+9!FmR5ujGNIUHKk+L7Qaw7kGIN@Db#pt(K`HFNUw?gn$A zAu?yiDatx7{3-5;3uO}S$g1~pIU2z=G9a|(d4!i#40x`uqG#nBNo8O6FT)-r^*QQ& zE$OoqSP$KSwds+pt!}Dnx?ELuhu_LpluIg4LI2&(rt1pG9;DD3poQ+LazXOmQM=4assNcA zZ%jYPD;j}J&@**z&7sHZjLs8IKKVpMlGzB-Jzfs^t%pyr^4NCkGR!11Iv?&dT&H*b6C$tE8}84JYG3K&IV6-uQ7m# z>#^hPSpo3QI5Mmnq}!ob=h;lnZj57H#UtZ%o%PJsfJ~v$-I(yGqHtHRZq`~P-@?}L#Fk$4% z)LdzAh0e*MW;o^;+V{hTpB0+-8ik5DLb&XC5iOFt^WRQKJK%&)NaRy!Ua=4B#mkfS zZ{ss>>yL8WZg@n$Fvm)^#H5@%uYtR1b?IAEAIYbu-bQvjm9@OS_aLu!CB8pHc_TT; z=#RP9&e_Z$ENc)mFS{E0`*tNwWoT%gstH_BEhrkO|HxqIdGH3?+xSdhiJY5NRRJ8w`8D7n5XI#deHKdvv<^; zp(*=iPhXyXcs7#Gfue=-2z5FX(V^A*pX77&-* zXEdwM)aLsQ`gRa{N%XvH^|l=dgG`KzcokX9oVJzqO?9HG$yqt^MHoR=ljmHV&nWYu zZXsG?S**=f@KSXIQFPQ??hWRgU^q5PwuA)nYw%X+POLtk7gbO6hoBmLvwmt4Iz*#% z20D(XU&NQ5&x}||O5N#i0qBulvNYG!6FSSHK3R>cLS!)YxKYU27@djWf36`v_pRXq z8t1Yk$t6!4;i1aSur=};Z2Z@)r-bFxr^xS1N?)~CAazc%t5cQ!gI4GzC@;Jo_tSpC zKIXM`Yd2FKXuGEqXy5wH;&<_xUJN{A?b;v2(y=q?J#pqV#zLRxqCem~7fJCsdrRu< z^?U2dr!h(O9iG9g_#3*|1Z9L6;urbqS$n;!8Ls0!&|we09;5NJ!&sA?=rHcF`W%ua za&Lz>pcPvRaiI5Yd6sm%-pAReA$KIEF&9;p&tzebTuZM(!*g@lGEZVIW$WxXNTZsM zz6PGow9Io#$}o$gG1??{ktuk0kE%&IoyIysh)R5enb3yD zIZs`N+(93rjcRrxmLe53z9X#0?}tylpL(W4@cWW>hfwQoaw zB(nfpjUw2F&;C4L#bBrwatR5X?>gE#x~M{cBPv_%wc3k8Kyvu!t4OE{3+zFn*cg-} z7jTWblb^{&z(Z!a+Nh-GB?+tE<~~m`k3MllbY{I*B@6%DVNE^>Jo_~pZ(5&I>}R%ZtOdMt)&qO15&Zy!kr3sWnVFU!~Q;@K)^)ETICT-vtX zd%D^vOB<1Wh*EsO_0AYekfr$n$@@qH##L!a#!rIBr<0s3`?>C(g0WyO_TdipWxP)~ zYD`Aj*fAKWj@TUP!7Cb89K|oFi=7jR8h%2*XmuHKL#KFzYhb(q%0U6`95MqlGcQ*a zk+{OMtjC;WT6#`sCUjBW#!0JBtIQ27<{5Gwyum09HkjTys=SV71bs(|$SRnz$2Ucn zUMffznL;)36~0n);zxCoIG61svz^sw@gtoehjrN=^stI}0xMs&m9ahKZnQ;;Q@-b` zKYYwOL7vPGowhGD1Nyp;Q~UJrCh61nMXJ4BS&CDsIZyBm=pJrrJo+Z&I)`6)8LQRZ z^yP{YB)=)|NfkUJb9i>75=zajUolt+YHtVOWP^-Rwk9N*p9enFG1ffE8CXmv3aZYd zxMHTGVqdo*4S7apHt1q=*)Z1eu$`i9&l08Xm zOnOvL!#fI73zNNr*!}zV%VxL4f9CH|$Joeqat?S^zGBI{2mK(P$KHhXszc^`(bUk? zOdtaF2xlCa7rRG>Q2shT6E%GbHT8#G5yEvW`jzjE^i_MQEJ~Fb zvyq9^425wum*HpWzgERW26n^xTvdBXI+LtUQFic=R!^@q;yruQx7~7jq@8(;x~R&> zMQzPQq2+GOJ*U^~%L%>U!M0%>%#6r1V1dpa)h)v68L#XUxsfx=mdVz^A^K$SM4WvV z$ibT{(8kIYYqbh`@S8Z`FS^3kID^LjScH0R&UmYqbjS3vZr0>fvx&^3E}@#-1Fw)D zq)hr1SvTL3==}x0wnLJ1?%;QwU#0xXz~8z76H7NP3nn zpj&DCI4H*J;L*~yP5VhQN$eF&!Q0#e*^0NEswR*;7z*xyN=SqHiRe0dX|6$zej1}y zPa!=aP-2?aG$?{^*~>yZHz(3`SH@7eHVAAQgZG;y809Z3u8pEPWl z>&D2&r@UnSf?UX$g)x}{`0S~o@t6zAp3yT@BUda*Z|7O+$Tp?U7)4m5D{G)SlunXY zILrDmvt0EtrJwoPSS#m~b;BuWL_A2TWDbnQ(fL3lekV{i zR%v@#a%Jp7Q7lnNav|4uK2$NE-H9Bly$Yy;7b4~H>M3V~yFm^8Wq2n~CIYU@2G1Xv zt1g>l>NokwBIZsMKWNC!H+qsC@!pAKiPXt2YDb)yqBj+Z>gTOD)fxJbO$%{}QnqpY zh}|CamV3_n4Jik4jeLYy18wKOQ?hLON1KZBJdt$jePh;} zcvv^jZj5neHa|~~j96_?$-~eEoT3Tk5{lxMfph{b@_Moxc-Htiev!0rAXif_;!{I8 zI{w%-qY_DM-+OU~wZ*^ogH21WcIQk()C|yre|nCCCKGfC)pKtzB(l*5>XN?f@L zT4G-aI-PlPra>N5J15)i8R<#8Nr^;YXuk@&l15zJV-u<>(USWo#*&+HmuTpHD(FGl ziaV+|vqP>EPwYj^KUY2cyO`6xD%M8_>8r?Bnfvv)$|*c@b#Rm1lMeorsyh2J?$^rH zk`wP}6s*VJ(vEBQY)TPaZ4Z>BrS13LR9LWA(~! zv{Fj;&gv;nBk&X+VN~Y3c4gJ-OsaOo^7(EC)kPo!9-+K&D}Gy!YllC#bJ))2nW@iF z$vM=kl;)9yPyt)lOwQVvOXFGF8vG`xC6*x%M=DF4-;{$qGV&yzBu>>QlgIUQB*mry z{i97}qvw1oU&zH>vX|5^9w<4>Uu&H83?uE-tMUo2yBji6b|WP6>R1~11)4x@U6*&a z@YCdvZ@j{v>Ztvdg(@e^vEKz3K?m(9eY68)3_FXR?ixz z71y<|g^b_DI98r!OOF@Vf{riZ9{cfVg9^v=^f*wttT}r6EFRfw1YSptfSCu)4%Lgs z^ZM`T)?K;;Sg&`ZZk*-I#wl`I>=|8PrRew|uKAsn^*J2-;fxIv0RlW&+3fYO-szv!Z zV_w$rUWtfE487>oqP)%f8b)Bw;*i6-lY*wvg}Ss!-+HfrcfS#;dUM2@z5^;-$(fZ? zk=wYpB5N^!8AY(o-m^euB8H+Oc%SpGr+ChXraDY3*^L;%@9R@<8EvGkfZ+7B6TyX{ zjKZ$vW#;8}(2A`27tcZZN%4dD2|QH{Bm(qV@i>0JtwDC>`Kbonw;taFQ%}vguUGHm zJekP&mXMLoPhx4%rrf`xv@2NN$eXKexyCma5noL4UigLH$)lpw*`Qu3=2GwFQ<2y2 zG4ed?>LBw;e19DuJK(rculnxvTW%=T}u$ac951!r&ZSWDC~`@h0aS_Z@rp) z>#cK37TUooMN9hMnVIs<8metfJ>Cq``DuCgc{6B4Kg-i$7OjeG%u%)5?T`exNN)hU zGz}?#p66r_zyC7%%&RqP<*ixX8bs5=D&he0eG_x%w>g4!oYKQb`|qCre46e^8S!Q$--~upeI~jzq-|b8{7zYLn5uuQ2d1-a zF7fS{4c_Y8f>{1sHFG;g5nY5*^$FKm)B47>ex-z1l&dzNJ&{x6@gN`dg=YiQ9a$wX~H_)cqy&qp`hFK<4~JW(baxA9~+r-3;xye}w$Cd+ zZWV{dOaGRawdD*Y?H>fR$JQKcM3wBiAOoE}o0A2Sw>80qI6J>{Ma6++`nNNxee5(E zRr^&7sowYW>>bavtEq2|-xl9j-E(t12t6n_m_AFKA@=fHP>`)UwrN*B;h3ir$!hBf zPVz;plJn(?1${1{Y<-GBl5PGzJF*#nf7^O1Q+9`)(E*Wf)wqGpX%|N)0^EDsDOA3wKb zr&Sy_i5#(CAxd_>Qu3pAN6hMx>8EN$eKzIY##wC1GG2e~B_&xcJ2e3wXfKd_i~bAQ zWX;dkqq97v@2KME@?A-Bey5z?7bCS_T`d})B5x-P$C8vy_BfGOYZvhI;3I42UfR6$ zYP9%y{C#R&Xd9`3JC@7kt%_Vf_j*D4X)Ih9rE6R?Ep>mF{uo|Bbeh{UqSA2G!4@5v z(!r(QXUP1(640Itep_QXzOYG#PoG)6>Fi%BbDwsbiF{OQs^9u=A(_e(Eh14o z6n#pTbTi0UsIQ&x*M74izA8JNY|(jG#eXE->la%aIUr__)N{=-RQIcwJ7cdXs}B56 z<+t)yX^^QZGm1R{W{;T`$=QH!(8$)5M%U1tc$2X&V{wi#~7|>9;H~KT{gv@(e9iV37@b& z)*wP<)w3Hs)HA`_{d2!IeUQG;^laW&G3VZyb=y17pHnN$ch5FiMbF=9GIS*M%5_$> zqi??4id~*tAy*>io6M=gnq$+NZ1balVwQ{;U)J zM?%6>&yW9P9+#Rt3^`nWjhcC-?sE;>`usZUwA9;rK5NeEs+-&Ulq#}GPPAzsQFXwa z?~&=VJE8s@ap}kAM57h|bP6tKs}EZT51UtANn}r-8L6qZt5I~L)d!i%gEtXfX{!=J zU$(pPlmHP<+1Y%yoog>6yT^{GzmUE9ALxK+S$iZzWYL+nxD)nH)Fo=Wvyr(G-OGM| zcAEF|jhghOe;MaoSlhoJo&150IkupDh>i(#jdHA!erKRrw(LDW@_ar+6;ZxlTl4mI zn7d}G&y_iVY18iz9Ic%ldi7!1eIhqVsUHNLy^h(fgL<+9_~wkLwM*|%nF%`O)DN>M zyU@(~el&OKYo*xt2gz2DazBI4qw2TsVGHZl-HUshuAx|lMb#PSYcXp*<>UI+H5t0W z7v0IL(pfhrve${5Di17HJ2A3we5qD{o2^u5M3*CN8GmcUA(zSTKe#v^FkU$;{MNJa z*oU>mPL|%_n$NIZsihmNh){Sl!_{W)GanCe$q~RdUNcfZskOuetbnwVpf$|MT7pS zDwSMub2%h6kAkDqRpnT%ShT!dahv#5`$he;m(vbjFTdM77uyb7t?}6Jtr}f^-xCStl8e><|md+>*5-#l#}=z zC%&&!rgTv8r<&9E@qru=938c+cFvAJ%m7>~rV*=T=bWq^=}#>1R)juh=jmSB?TNUb)bxb`wOV86YSts{`LeL$XvaM4i zsK!TP_!n2KnfipgzjgKT{SJ2uFX}R z$pnAuDbvmPs{QV?mde?kSUY!|t$VDET3U9(_rfltkAE}xQbqNL=ol~KJXFq^BYcl% z(FWBGd!Sfc;>frHo&!em8zsETy;$XUWeu!cW6(cuFK~bTDk()e9h^tQie(je3f78s zlkt%$+A=c!%4&!%co&wHIM<&6^@twdptAPnLH>MC!*5xPt2ob_U%@#n9jNSMk2AZj zzx>=txUc1r&p7{XTvL{C`4yO7S(@^Rx%sQ*jw_^p*HgIGfaN1iXa{I+g>e_^$E||2 z`h730Wlw?+WqsrPn`xPhf$B%zq|R!u3tRbqSZDoq6*>-}w-EXKw@-PW5br=Uc>b7A z(F@`-Tj}-(x}}y=GHd zDJ5cho**VNV>9#LrGZ?qSuh#vP_CgGmRt{;hNr1iGwzPRTi|Z2NY?y1o)h+ProG0~ zDvWp`zxVDDQg-(Xt5#=* z-X@>X>ooegPwSPCaqj`j2|+924zx0Yv-7Tt9nTEs>S0=LyiVZkb@E9O7Nj?S0ZbCA9nMiJ0Wr{q72t1am99WOEN$>o;K)+BZ^{SUm@w4Wsgv z&69Jux)#Q0-+0u=s59-{jK4!-85(!zNBO92LH!PVShaWW8y-bWhBjB9HUTL<5d$b`(kSHzw}K z73B@ev+8K?XG(u{Zbws4g6HVXpttqk!~wsoZtAcaa%%MD%tCfF9~azTv%Mt;=ogIE ko+I<2Q|1=pKWjh#5I;G0@<^e{9A)aitCseGLo$$60;J0r@;67b+-Cv+v~US( diff --git a/ownCloud/Resources/gl.lproj/Localizable.strings b/ownCloud/Resources/gl.lproj/Localizable.strings index 8e412a79a3dbbab9b7f594b9c3ea175fcb55dcff..36e830b4457ed680907c2a6047f583709062bd24 100644 GIT binary patch delta 3027 zcmbVO-A`L(7=Lktj&8WYp&Sas9B^tRW@zdHiB9bbO~~dT12>HqP}*)*+f!&MFfUf8 zUQCS_=Gl##2JvFic*D{=qtQjLP5cvxTzKP1Tw;tD>hJfyoSwtiVocNX@x0IH@A*1^ zu0OE;?!Lx<{vJ^Mw?iMc+9~Czl$up}l~ZXvrE$Nf+}c;I*{*7zdQoLn1|+goG+PnR z8NB6`i|f344IBn@+MaYp(xin}6tt|@;lqVnQeD-vv#=+uy zHorp^MN3db(Uew{QGHNw68}CmhHDll3+gwRbKrDV;@51s`&oOHZw$#esUyZ)3fx8~ zoi1tlGAJoEqq%pwP94w-J-j+<4#AsncqJqSNX|oB1@CpN<-53DYkQ)pIkZWVZ{xPS z71}TEu|u`wiC>%c%*)8LKQtjP6Y4UI(%C{F05g}rF z9_sTNC*SBW&P^UG$QHSnu?5X(GGSbp1)V_~*X}`v1ylmn3wBhWm8v*kz5O@yakxWO zOh04)zvjaVZ3XFJRlAb?DU3WN5NdJLj|&LW1q?;>f*<-g?55F0#x4O}!m|r%9(OOS zF=U1Qd3{Gd@+{)MyWY5(2_ux84#$;^tC3fOijVK9dQWw}HaND`&EvI^*=nf30YV@9 zfrF_$L-C^>0Tza4D$t8>Vv|}9AQR*zhTC|hcixKbsTrpc%a=&$+2pEAva%Y9n=E*z zNr03GmX5lJ+;E_B&gI=dKeR)};&ASk((*h;hqKml6-7GJZM%Sq2Y|; zh}}JH;~Kj9o);wSal*KDaeA`;)Y8e9kHV2w%q8eBV~O2Vmehjv7^k8qmC(}=y$Ub5 zJlK;=&KRnC!;VA?(7?UK^srQT&JuRzPSTQVl|!=H-SL#C?G$*@>Wx6B#i6aF)_{x0 zqdQoZSEKTMSBG5hdU|RSQZvx~hWMrYU zXMtObqyB;>R!jkTp6g{7qtns3c9m0uYb}o-zley`B(AdoZ%ozk%PXbc#GiO@*6^Lk zl<>_O3YV-qo$ZoDq<`&MI3xnqd-hgIdHHei4-xuzsZ1<%=Ot--`xGJ=`MjKe^#@a uck7jH-4T$8@9dCHy&{tcjst_KGHvI`#=K}!}Bk^K2kFP delta 172 zcmaFT!u;b26Z`*v`V3kO3L9B_1;z6j6c`d23K()2G8vMAtP&ty#*n{xrC_Sm<~h0? zoVZj>PE--!e97zu8!iQ#FWMev+brk$O%a!h$=d1ho7*yOaBYq&sbJrnQ1?b_v%=aq Lw#~aXXK?}mYJ@YC diff --git a/ownCloud/Resources/he.lproj/Localizable.strings b/ownCloud/Resources/he.lproj/Localizable.strings index 9551bb9c1d551f1ce75307496b81c88bd4a433f9..f0225e2e9cfb4d9ddab5fb4f5a981bfaf5fc08f7 100644 GIT binary patch delta 2688 zcmb7GOK)366ds}uX$nFG*SJ+Bbn7TV4GK{akVO=U)Kb-m0!1$Ef`@P&=OO&Ud9+<1 z3yDow5LqL!>V{u{kbgkc4N`Y;e2smsp5Hm=JO1(e zq5gv*`S)K_V&aG4t?_bPydyGVL9B?Bu*AA3imWK$c>%vxL>`}OVi^(^Zc6yxz-a+; zt2kN3?^V2bLdjdu<2m)xILm2k8Cak`<#^6#&t^(}`D*lRph?LR^eCC4p4!?xldxr1 z)}3Cmex4k;F)lhWX%+yuk=1BAlB z8SG(OCj^QBFIPmN`pV3oXVo8H9WN7OqDYjIsKp8@K};-h9p_mfp9zI$%RSkWyEmqC{VbbqyoO#{^YKb;Rr3?Lf^^K*OQFU_irKc=anT)Bso6oAdQ_np^JRD652Wn(; z47VSf+j9(|g*y&;NXkcePSxIwjTqWsFQzA^RX=l5y?*TUa1Ff%oK@vmEc$^aFOLeZ zYJ8VfWivL+adTu%buzK?6Qx3cxE6KyyLyPrMtY>Epq?C2vJabV4WJw*;sUB{=~nWH z+NIjpbS+k)c2lobp)ZPbc9e4t0Q3&M18LJl&rWaC{_^mioPz_SlOo*4@P-NV~>YG~@= zbahy^YVPX~r5gX{#YoU-bzP~Rw5#{GRn@3m;81sW{pf@Gk`dgh`gjL@E*Pm_Xl>k`#4t)?DWw71AMbGn(bfkH2 z>~nZB{|Y!d%q_?wjCddJ9ZgdVW}q`6_qg-#qeKQO!D1FK{(mUo^BO*jwEK~}vv$yy z4&J7XIAh_=9yf3i2G+E(@XkMMrKM80qh;feshIx=H=)U$A1*Sd`arJ@5*RmK6A;;m zp$@RvD8ZtbM{^co!<2ear$)$*@KAOW1ns~Kr>Xld4j%7w-P=B%NB6he(Z1v*Bzk#6 e$4c4{7m^hH5ZOKQg%co+@Nn(OpHPvY!~Ox&vZWvZ delta 69 zcmV-L0J{I=$pXf&0tf&8F90e4Ah89HCX;?y2(#8Ff)umNJpLQAepzM-v%X`(2D7eg bDkHN9lUN0l)=m diff --git a/ownCloudAppFramework/Tools/NSData+Encoding.h b/ownCloudAppFramework/Tools/NSData+Encoding.h new file mode 100644 index 000000000..4ff269edb --- /dev/null +++ b/ownCloudAppFramework/Tools/NSData+Encoding.h @@ -0,0 +1,29 @@ +// +// NSData+Encoding.h +// ownCloudApp +// +// Created by Felix Schwarz on 02.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (OCEncoding) + ++ (NSData *)dataFromCGRect:(CGRect)rect; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Tools/NSData+Encoding.m b/ownCloudAppFramework/Tools/NSData+Encoding.m new file mode 100644 index 000000000..c47d4e7ef --- /dev/null +++ b/ownCloudAppFramework/Tools/NSData+Encoding.m @@ -0,0 +1,28 @@ +// +// NSData+Encoding.m +// ownCloudApp +// +// Created by Felix Schwarz on 02.09.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "NSData+Encoding.h" + +@implementation NSData (OCEncoding) + ++ (NSData *)dataFromCGRect:(CGRect)rect +{ + return ([NSData dataWithBytes:&rect length:sizeof(CGRect)]); +} + +@end diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 3dd4cd22a..5bb1d638b 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -27,6 +27,7 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import +#import #import #import #import diff --git a/ownCloudAppTests/Info.plist b/ownCloudAppTests/Info.plist index 16016759a..338c8baed 100644 --- a/ownCloudAppTests/Info.plist +++ b/ownCloudAppTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 141 + 147 diff --git a/ownCloudScreenshotsTests/Info.plist b/ownCloudScreenshotsTests/Info.plist index 26a93fab7..338c8baed 100644 --- a/ownCloudScreenshotsTests/Info.plist +++ b/ownCloudScreenshotsTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 144 + 147 diff --git a/ownCloudTests/Info.plist b/ownCloudTests/Info.plist index 26a93fab7..338c8baed 100644 --- a/ownCloudTests/Info.plist +++ b/ownCloudTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 144 + 147 From 10f4078476ff3f9949312115fbb5e25a62a8769d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Fri, 20 Dec 2019 16:22:00 +0100 Subject: [PATCH 6/9] Increment build number for AppStore Connect upload --- ownCloud File Provider/Info.plist | 2 +- ownCloud File ProviderUI/Info.plist | 2 +- ownCloud.xcodeproj/project.pbxproj | 12 ++++++------ ownCloud/Resources/Info.plist | 2 +- ownCloudAppFramework/Resources/Info.plist | 2 +- ownCloudAppTests/Info.plist | 2 +- ownCloudScreenshotsTests/Info.plist | 2 +- ownCloudTests/Info.plist | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ownCloud File Provider/Info.plist b/ownCloud File Provider/Info.plist index 0601c81eb..a711fe078 100644 --- a/ownCloud File Provider/Info.plist +++ b/ownCloud File Provider/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 147 + 148 NSExtension NSExtensionFileProviderDocumentGroup diff --git a/ownCloud File ProviderUI/Info.plist b/ownCloud File ProviderUI/Info.plist index 3b2181a05..92a6f4250 100644 --- a/ownCloud File ProviderUI/Info.plist +++ b/ownCloud File ProviderUI/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 147 + 148 NSExtension NSExtensionFileProviderActions diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 4718cfbce..170af0ef8 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -3149,7 +3149,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3177,7 +3177,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3323,11 +3323,11 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 147; + DYLIB_CURRENT_VERSION = 148; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -3356,11 +3356,11 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 147; + DYLIB_CURRENT_VERSION = 148; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index b454d582f..042ff9a1a 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -36,7 +36,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 147 + 148 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ownCloudAppFramework/Resources/Info.plist b/ownCloudAppFramework/Resources/Info.plist index 51fc143fc..be0918391 100644 --- a/ownCloudAppFramework/Resources/Info.plist +++ b/ownCloudAppFramework/Resources/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 147 + 148 diff --git a/ownCloudAppTests/Info.plist b/ownCloudAppTests/Info.plist index 338c8baed..2b2be950c 100644 --- a/ownCloudAppTests/Info.plist +++ b/ownCloudAppTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 147 + 148 diff --git a/ownCloudScreenshotsTests/Info.plist b/ownCloudScreenshotsTests/Info.plist index 338c8baed..2b2be950c 100644 --- a/ownCloudScreenshotsTests/Info.plist +++ b/ownCloudScreenshotsTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 147 + 148 diff --git a/ownCloudTests/Info.plist b/ownCloudTests/Info.plist index 338c8baed..2b2be950c 100644 --- a/ownCloudTests/Info.plist +++ b/ownCloudTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 147 + 148 From 9955956567b780e79834f9bb200740ecdbb9e833 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2020 09:28:14 +0100 Subject: [PATCH 7/9] Bump handlebars from 4.1.2 to 4.5.3 in /docs (#584) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.5.3. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2...v4.5.3) Signed-off-by: dependabot[bot] --- docs/yarn.lock | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 499de6493..ea742b059 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -482,11 +482,16 @@ combined-stream2@^1.0.2: debug "^2.1.1" stream-length "^1.0.1" -commander@^2.12.2, commander@^2.19.0, commander@~2.20.0: +commander@^2.12.2, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -897,9 +902,9 @@ gulp-vinyl-zip@^2.1.2: yazl "^2.2.1" handlebars@^4.0.12: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -1960,11 +1965,11 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= uglify-js@^3.1.4: - version "3.5.15" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.15.tgz#fe2b5378fd0b09e116864041437bff889105ce24" - integrity sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg== + version "3.7.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a" + integrity sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg== dependencies: - commander "~2.20.0" + commander "~2.20.3" source-map "~0.6.1" unc-path-regex@^0.1.2: From 9341d98d88c3df30593114afd4652b4a1407ef56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20H=C3=BChne?= Date: Thu, 9 Jan 2020 09:21:43 +0100 Subject: [PATCH 8/9] [release/1.2.1] Release 1.2.1 (#593) * - ThemeWindow: add tracking of live ThemeWindows (#591) - AppLockManager: add support for multiple UIWindowScenes - fixes iOS 13 lock issue * new build and version number for bugfix release 1.2.1 * updated changelog for release 1.2.1 * Added more logs for instant photo upload Co-authored-by: Felix Schwarz --- CHANGELOG.md | 4 + ownCloud File Provider/Info.plist | 2 +- ownCloud File ProviderUI/Info.plist | 2 +- ownCloud.xcodeproj/project.pbxproj | 16 +- ownCloud/AppDelegate.swift | 3 +- ownCloud/Resources/Info.plist | 2 +- ownCloud/SceneDelegate.swift | 4 +- .../Settings/Passcode/AppLockManager.swift | 204 ++++++++++++------ .../InstantMediaUploadTaskExtension.swift | 18 +- ownCloud/Theming/UI/ThemeWindow.swift | 51 +++++ ownCloudAppFramework/Resources/Info.plist | 2 +- ownCloudScreenshotsTests/Info.plist | 2 +- ownCloudTests/Info.plist | 2 +- 13 files changed, 233 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a00dbca1c..2d071e6f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # ChangeLog +## Release version 1.2.1 (January 2020) + +- Fix: Passcode Lock Screen on iOS 13 (#582) + ## Release version 1.2.0 (December 2019) - Multiple Window Support (iPadOS) (#488) diff --git a/ownCloud File Provider/Info.plist b/ownCloud File Provider/Info.plist index a711fe078..7b04cd34b 100644 --- a/ownCloud File Provider/Info.plist +++ b/ownCloud File Provider/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 148 + 149 NSExtension NSExtensionFileProviderDocumentGroup diff --git a/ownCloud File ProviderUI/Info.plist b/ownCloud File ProviderUI/Info.plist index 92a6f4250..58850a279 100644 --- a/ownCloud File ProviderUI/Info.plist +++ b/ownCloud File ProviderUI/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 148 + 149 NSExtension NSExtensionFileProviderActions diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 170af0ef8..dc7e95b32 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -3028,7 +3028,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_SHORT_VERSION = 1.2.0; + APP_SHORT_VERSION = 1.2.1; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -3090,7 +3090,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_SHORT_VERSION = 1.2.0; + APP_SHORT_VERSION = 1.2.1; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -3149,7 +3149,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 149; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3177,7 +3177,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 149; DEVELOPMENT_TEAM = 4AP2STM4H5; FRAMEWORK_SEARCH_PATHS = "${TARGET_BUILD_DIR}"; GCC_WARN_UNUSED_LABEL = YES; @@ -3323,11 +3323,11 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 149; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 148; + DYLIB_CURRENT_VERSION = 149; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -3356,11 +3356,11 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 148; + CURRENT_PROJECT_VERSION = 149; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 148; + DYLIB_CURRENT_VERSION = 149; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ownCloudAppFramework/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 8f7713d5f..28cddd34f 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -26,7 +26,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var serverListTableViewController: ServerListTableViewController? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. var navigationController: UINavigationController? // Set up logging (incl. stderr redirection) and log launch time, app version, build number and commit @@ -37,7 +36,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ThemeStyle.registerDefaultStyles() - serverListTableViewController = ServerListTableViewController(style: UITableView.Style.plain) + serverListTableViewController = ServerListTableViewController(style: .plain) navigationController = ThemeNavigationController(rootViewController: serverListTableViewController!) diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index 042ff9a1a..71c80ad1a 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -36,7 +36,7 @@ CFBundleShortVersionString $(APP_SHORT_VERSION) CFBundleVersion - 148 + 149 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ownCloud/SceneDelegate.swift b/ownCloud/SceneDelegate.swift index 2118b9caa..496b19230 100644 --- a/ownCloud/SceneDelegate.swift +++ b/ownCloud/SceneDelegate.swift @@ -27,8 +27,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene { window = ThemeWindow(windowScene: windowScene) - let serverListTableViewController = ServerListTableViewController(style: UITableView.Style.plain) + + let serverListTableViewController = ServerListTableViewController(style: .plain) serverListTableViewController.restorationIdentifier = "ServerListTableViewController" + let navigationController = ThemeNavigationController(rootViewController: serverListTableViewController) window?.rootViewController = navigationController window?.addSubview((navigationController.view)!) diff --git a/ownCloud/Settings/Passcode/AppLockManager.swift b/ownCloud/Settings/Passcode/AppLockManager.swift index 4e14575e3..c225a5a9e 100644 --- a/ownCloud/Settings/Passcode/AppLockManager.swift +++ b/ownCloud/Settings/Passcode/AppLockManager.swift @@ -23,9 +23,6 @@ import LocalAuthentication class AppLockManager: NSObject { // MARK: - UI - private var window: AppLockWindow? - private var passcodeViewController: PasscodeViewController? - private var userDefaults: UserDefaults // MARK: - State @@ -137,65 +134,122 @@ class AppLockManager: NSObject { NotificationCenter.default.addObserver(self, selector: #selector(self.appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.updateLockscreens), name: ThemeWindow.themeWindowListChangedNotification, object: nil) } deinit { NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: ThemeWindow.themeWindowListChangedNotification, object: nil) } // MARK: - Show / Dismiss Passcode View func showLockscreenIfNeeded(forceShow: Bool = false, context: LAContext = LAContext()) { if self.shouldDisplayLockscreen || forceShow { - if passcodeViewController == nil { - passcodeViewController = PasscodeViewController(completionHandler: { (_, passcode: String) in - self.attemptUnlock(with: passcode) - }) - - passcodeViewController?.message = "Enter code".localized - passcodeViewController?.cancelButtonHidden = false - - passcodeViewController?.screenBlurringEnabled = forceShow && !self.shouldDisplayLockscreen - - window = AppLockWindow(frame: UIScreen.main.bounds) - /* - Workaround to the lack of status bar animation when returning true for prefersStatusBarHidden in - PasscodeViewController. - - The documentation notes that "The ordering of windows within a given window level is not guaranteed.", - so that with a future iOS update this might break and the status bar be displayed regardless. In that - case, implement prefersStatusBarHidden in PasscodeViewController to return true and remove the dismiss - animation (the re-appearance of the status bar will lead to a jump in the UI otherwise). - */ - window?.windowLevel = UIWindow.Level.statusBar - window?.rootViewController = passcodeViewController! - window?.makeKeyAndVisible() - - self.startLockCountdown() - } else { - passcodeViewController?.screenBlurringEnabled = forceShow - } + lockscreenOpenForced = forceShow + lockscreenOpen = true - // Show biometrical - if !forceShow, !self.shouldDisplayCountdown { - showBiometricalAuthenticationInterface(context: context) - } + // Show biometrical + if !forceShow, !self.shouldDisplayCountdown { + showBiometricalAuthenticationInterface(context: context) + } } } func dismissLockscreen(animated:Bool) { - let hideWindow = { - self.window?.isHidden = true - self.passcodeViewController = nil - self.window = nil + if animated { + let animationGroup = DispatchGroup() + + for themeWindow in ThemeWindow.themeWindows { + if let appLockWindow = applockWindowByWindow.object(forKey: themeWindow) { + animationGroup.enter() + + appLockWindow.hideWindowAnimation { + appLockWindow.isHidden = true + animationGroup.leave() + } + } + } + + animationGroup.notify(queue: .main) { + self.lockscreenOpen = false + } + } else { + self.lockscreenOpen = false } + } - if animated { - self.window?.hideWindowAnimation { - hideWindow() + // MARK: - Lock window management + private var lockscreenOpenForced : Bool = false + private var lockscreenOpen : Bool = false { + didSet { + updateLockscreens() + } + } + + private var passcodeControllerByWindow : NSMapTable = NSMapTable.weakToStrongObjects() + private var applockWindowByWindow : NSMapTable = NSMapTable.weakToStrongObjects() + + @objc func updateLockscreens() { + if lockscreenOpen { + for themeWindow in ThemeWindow.themeWindows { + if let passcodeViewController = passcodeControllerByWindow.object(forKey: themeWindow) { + passcodeViewController.screenBlurringEnabled = lockscreenOpenForced + } else { + var appLockWindow : AppLockWindow + var passcodeViewController : PasscodeViewController + + passcodeViewController = PasscodeViewController(completionHandler: { (viewController: PasscodeViewController, passcode: String) in + self.attemptUnlock(with: passcode, passcodeViewController: viewController) + }) + + passcodeViewController.message = "Enter code".localized + passcodeViewController.cancelButtonHidden = false + + passcodeViewController.screenBlurringEnabled = lockscreenOpenForced && !self.shouldDisplayLockscreen + + if #available(iOS 13, *) { + if let windowScene = themeWindow.windowScene { + appLockWindow = AppLockWindow(windowScene: windowScene) + } else { + appLockWindow = AppLockWindow(frame: UIScreen.main.bounds) + } + } else { + appLockWindow = AppLockWindow(frame: UIScreen.main.bounds) + } + /* + Workaround to the lack of status bar animation when returning true for prefersStatusBarHidden in + PasscodeViewController. + + The documentation notes that "The ordering of windows within a given window level is not guaranteed.", + so that with a future iOS update this might break and the status bar be displayed regardless. In that + case, implement prefersStatusBarHidden in PasscodeViewController to return true and remove the dismiss + animation (the re-appearance of the status bar will lead to a jump in the UI otherwise). + */ + appLockWindow.windowLevel = UIWindow.Level.statusBar + appLockWindow.rootViewController = passcodeViewController + appLockWindow.makeKeyAndVisible() + + passcodeControllerByWindow.setObject(passcodeViewController, forKey: themeWindow) + applockWindowByWindow.setObject(appLockWindow, forKey: themeWindow) + + self.startLockCountdown() + + if self.shouldDisplayCountdown { + passcodeViewController.keypadButtonsHidden = true + updateLockCountdown() + } + } } } else { - hideWindow() + for themeWindow in ThemeWindow.themeWindows { + if let appLockWindow = applockWindowByWindow.object(forKey: themeWindow) { + appLockWindow.isHidden = true + + passcodeControllerByWindow.removeObject(forKey: themeWindow) + applockWindowByWindow.removeObject(forKey: themeWindow) + } + } } } @@ -215,7 +269,7 @@ class AppLockManager: NSObject { } // MARK: - Unlock - func attemptUnlock(with testPasscode: String?, customErrorMessage: String? = nil) { + func attemptUnlock(with testPasscode: String?, customErrorMessage: String? = nil, passcodeViewController: PasscodeViewController? = nil) { if testPasscode == self.passcode { unlocked = true lastApplicationBackgroundedDate = nil @@ -266,7 +320,9 @@ class AppLockManager: NSObject { // MARK: - Countdown display private func startLockCountdown() { if self.shouldDisplayCountdown { - passcodeViewController?.keypadButtonsHidden = true + performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.keypadButtonsHidden = true + } updateLockCountdown() lockTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateLockCountdown), userInfo: nil, repeats: true) @@ -288,41 +344,64 @@ class AppLockManager: NSObject { } let timeoutMessage:String = NSString(format: "Please try again in %@".localized as NSString, dateFormatted!) as String - self.passcodeViewController?.timeoutMessage = timeoutMessage + + performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.timeoutMessage = timeoutMessage + } if date <= Date() { // Time elapsed, allow entering passcode again self.lockTimer?.invalidate() - self.passcodeViewController?.keypadButtonsHidden = false - self.passcodeViewController?.timeoutMessage = nil - self.passcodeViewController?.errorMessage = nil + performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.keypadButtonsHidden = false + passcodeViewController.timeoutMessage = nil + passcodeViewController.errorMessage = nil + } } } } - // MARK: - Biometrical Unlock - func showBiometricalAuthenticationInterface(context: LAContext) { + private func performPasscodeViewControllerUpdates(_ updateHandler: (_: PasscodeViewController) -> Void) { + for themeWindow in ThemeWindow.themeWindows { + if let passcodeViewController = passcodeControllerByWindow.object(forKey: themeWindow) { + updateHandler(passcodeViewController) + } + } + } - if shouldDisplayLockscreen, biometricalSecurityEnabled { + // MARK: - Biometrical Unlock + private var biometricalAuthenticationInterfaceShown : Bool = false + func showBiometricalAuthenticationInterface(context: LAContext) { + if shouldDisplayLockscreen, biometricalSecurityEnabled, !biometricalAuthenticationInterfaceShown { var evaluationError: NSError? // Check if the device can evaluate the policy. if context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &evaluationError) { let reason = NSString.init(format: "Unlock %@".localized as NSString, OCAppIdentity.shared.appName!) as String - self.passcodeViewController?.errorMessage = nil + performPasscodeViewControllerUpdates { (passcodeViewController) in + OnMainThread { + passcodeViewController.errorMessage = nil + } + } context.localizedCancelTitle = "Enter code".localized context.localizedFallbackTitle = "" + self.biometricalAuthenticationInterfaceShown = true + context.evaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { (success, error) in + self.biometricalAuthenticationInterfaceShown = false + if success { - //Fill the passcode dots + // Fill the passcode dots OnMainThread { - self.passcodeViewController?.passcode = self.passcode + self.performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.passcode = self.passcode + } } - //Remove the passcode after small delay to give user feedback after use the biometrical unlock + // Remove the passcode after small delay to give user feedback after use the biometrical unlock OnMainThread(after: 0.3) { self.attemptUnlock(with: self.passcode) } @@ -331,7 +410,9 @@ class AppLockManager: NSObject { switch error { case LAError.biometryLockout: OnMainThread { - self.passcodeViewController?.errorMessage = error.localizedDescription + self.performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.errorMessage = error.localizedDescription + } } case LAError.authenticationFailed: @@ -345,9 +426,12 @@ class AppLockManager: NSObject { } } } else { - if let error = evaluationError, - self.biometricalSecurityEnabled { - self.passcodeViewController?.errorMessage = error.localizedDescription + if let error = evaluationError, biometricalSecurityEnabled { + OnMainThread { + self.performPasscodeViewControllerUpdates { (passcodeViewController) in + passcodeViewController.errorMessage = error.localizedDescription + } + } } } } diff --git a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift index 9140fdbaf..e92ce158a 100644 --- a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift +++ b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift @@ -37,9 +37,15 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { guard userDefaults.instantUploadPhotos == true || userDefaults.instantUploadVideos == true else { return } - guard let bookmarkUUID = userDefaults.instantUploadBookmarkUUID else { return } + guard let bookmarkUUID = userDefaults.instantUploadBookmarkUUID else { + Log.warning(tagged: ["INSTANT_MEDIA_UPLOAD"], "Instant media upload enabled, but bookmark not configured") + return + } - guard let path = userDefaults.instantUploadPath else { return } + guard let path = userDefaults.instantUploadPath else { + Log.warning(tagged: ["INSTANT_MEDIA_UPLOAD"], "Instant media upload enabled, but path not configured") + return + } if let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { uploadMediaAssets(for: bookmark, at: path) @@ -50,6 +56,8 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } var photoAssets = [PHAsset]() + + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching images created after \(String(describing: userDefaults.instantUploadPhotosAfter))") // Add photo assets if let uploadPhotosAfter = userDefaults.instantUploadPhotosAfter { @@ -66,9 +74,12 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { if photoAssets.count > 0 { MediaUploadQueue.shared.addUploads(Array(photoAssets), for: bookmark, at: path) userDefaults.instantUploadPhotosAfter = photoAssets.last?.modificationDate + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Last added photo asset modification date: \(String(describing: userDefaults.instantUploadPhotosAfter))") } var videoAssets = [PHAsset]() + + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching videos created after \(String(describing: userDefaults.instantUploadVideosAfter))") // Add video assets if let uploadVideosAfter = userDefaults.instantUploadVideosAfter { @@ -85,6 +96,7 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { if videoAssets.count > 0 { MediaUploadQueue.shared.addUploads(videoAssets, for: bookmark, at: path) userDefaults.instantUploadVideosAfter = videoAssets.last?.modificationDate + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Last added video asset modification date: \(String(describing: userDefaults.instantUploadPhotosAfter))") } } @@ -125,6 +137,8 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { let sort = NSSortDescriptor(key: "modificationDate", ascending: true) fetchOptions.sortDescriptors = [sort] + + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching assets with options \(fetchOptions.debugDescription)") return PHAsset.fetchAssets(in: cameraRoll, options: fetchOptions) } diff --git a/ownCloud/Theming/UI/ThemeWindow.swift b/ownCloud/Theming/UI/ThemeWindow.swift index 8d8647dac..f2f0c88ed 100644 --- a/ownCloud/Theming/UI/ThemeWindow.swift +++ b/ownCloud/Theming/UI/ThemeWindow.swift @@ -19,6 +19,57 @@ import UIKit class ThemeWindow : UIWindow { + // MARK: - Theme window list + static let themeWindowListChangedNotification: NSNotification.Name = NSNotification.Name(rawValue: "ThemeWindowListChanged") + static private let _themeWindows : NSHashTable = NSHashTable.weakObjects() + + private static func addThemeWindow(_ window: ThemeWindow) { + OCSynchronized(self) { + _themeWindows.add(window) + } + NotificationCenter.default.post(name: ThemeWindow.themeWindowListChangedNotification, object: nil, userInfo: nil) + } + + private static func removeThemeWindow(_ window: ThemeWindow) { + OCSynchronized(self) { + _themeWindows.remove(window) + } + NotificationCenter.default.post(name: ThemeWindow.themeWindowListChangedNotification, object: nil, userInfo: nil) + } + + static var themeWindows : [ThemeWindow] { + var themeWindows : [ThemeWindow] = [] + + OCSynchronized(self) { + themeWindows = _themeWindows.allObjects + } + + return themeWindows + } + + // MARK: - Lifecycle + override init(frame: CGRect) { + super.init(frame: frame) + + ThemeWindow.addThemeWindow(self) + } + + @available(iOS 13, *) + override init(windowScene: UIWindowScene) { + super.init(windowScene: windowScene) + + ThemeWindow.addThemeWindow(self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + ThemeWindow.removeThemeWindow(self) + } + + // MARK: - Theme change detection override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) diff --git a/ownCloudAppFramework/Resources/Info.plist b/ownCloudAppFramework/Resources/Info.plist index be0918391..674373422 100644 --- a/ownCloudAppFramework/Resources/Info.plist +++ b/ownCloudAppFramework/Resources/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 148 + 149 diff --git a/ownCloudScreenshotsTests/Info.plist b/ownCloudScreenshotsTests/Info.plist index 2b2be950c..8b1229f4f 100644 --- a/ownCloudScreenshotsTests/Info.plist +++ b/ownCloudScreenshotsTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 148 + 149 diff --git a/ownCloudTests/Info.plist b/ownCloudTests/Info.plist index 2b2be950c..8b1229f4f 100644 --- a/ownCloudTests/Info.plist +++ b/ownCloudTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 148 + 149 From 6904fcf99b8a9ddb4904a9cb84b0611799e6234a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Mon, 13 Jan 2020 09:13:49 +0100 Subject: [PATCH 9/9] fixed merge issues with master branch --- ios-sdk | 2 +- ownCloud/Client/ClientRootViewController.swift | 11 ----------- .../Server List/ServerListTableViewController.swift | 12 ------------ 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/ios-sdk b/ios-sdk index ac411087b..4047a738e 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit ac411087bbe1fb342dec5c3edd3b86844ebd4bec +Subproject commit 4047a738ef01f43ca7a43b747fd8e29bb4c99e1e diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 3f81dc3ca..286c9a03c 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -227,17 +227,6 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if MediaUploadQueue.isMediaUploadPendingFlagSet(for: self.bookmark) { - let unfinishedUploadAlert = ThemedAlertController(with: "Warning".localized, - message: "Media upload in the previous session was incomplete since the application was terminated".localized) - self.present(unfinishedUploadAlert, animated: true, completion: nil) - MediaUploadQueue.resetUploadPendingFlag(for: self.bookmark) - } - } - var closeClientCompletionHandler : (() -> Void)? func closeClient(completion: (() -> Void)? = nil) { diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift index 8334dca0f..eb2ba9883 100644 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ b/ownCloud/Server List/ServerListTableViewController.swift @@ -326,15 +326,8 @@ class ServerListTableViewController: UITableViewController, Themeable { alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) alertController.addAction(UIAlertAction(title: "Delete".localized, style: .destructive, handler: { (_) in - let vault : OCVault = OCVault(bookmark: bookmark) OCBookmarkManager.lock(bookmark: bookmark) - OnMainThread { - if error != nil { - // Inform user if vault couldn't be erased - let alertController = ThemedAlertController(title: NSString(format: "Deletion of '%@' failed".localized as NSString, bookmark.shortName as NSString) as String, - message: error?.localizedDescription, - preferredStyle: .alert) OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in let vault : OCVault = OCVault(bookmark: bookmark) @@ -350,13 +343,11 @@ class ServerListTableViewController: UITableViewController, Themeable { alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) self.present(alertController, animated: true, completion: nil) - self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) } else { // Success! We can now remove the bookmark self.ignoreServerListChanges = true OCBookmarkManager.shared.removeBookmark(bookmark) - } self.tableView.performBatchUpdates({ self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) @@ -366,9 +357,6 @@ class ServerListTableViewController: UITableViewController, Themeable { self.updateNoServerMessageVisibility() } - }) - }, for: bookmark) - } OCBookmarkManager.unlock(bookmark: bookmark)