diff --git a/.tx/config b/.tx/config index 91d9ed4da..4c4590ee1 100644 --- a/.tx/config +++ b/.tx/config @@ -28,4 +28,3 @@ lang_map = cs_CZ: cs, de_DE: de-DE, en_GB: en-GB, nb_NO: nb-NO, nn_NO: nn-NO, pt source_file = fastlane/screenshots/en/title.strings source_lang = en type = STRINGS - diff --git a/.xcode-version b/.xcode-version index c27905ac3..c32eced3a 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -13.4.1 +14.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index e06254fa1..940436e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +Changelog for ownCloud iOS Client [11.11.0] (2022-09-26) +======================================= +The following sections list the changes in ownCloud iOS Client 11.11.0 relevant to +ownCloud admins and users. + +[11.11.0]: https://github.com/owncloud/ios-app/compare/milestone/11.10.1...milestone/11.11.0 + +Summary +------- + +* Bugfix - Respect privateLinks capability: [#1138](https://github.com/owncloud/ios-app/issues/1138) +* Bugfix - Enabling Markup Mode, Showing Video Controls on iOS 16, Updating Theme: [#1141](https://github.com/owncloud/ios-app/issues/1141) +* Bugfix - Share Extension Passcode Lock Interval: [#1156](https://github.com/owncloud/ios-app/issues/1156) +* Bugfix - Video Metadata Image: [#5296](https://github.com/owncloud/enterprise/issues/5296) +* Change - New Dark Mode Themes: [#1146](https://github.com/owncloud/ios-app/issues/1146) + +Details +------- + +* Bugfix - Respect privateLinks capability: [#1138](https://github.com/owncloud/ios-app/issues/1138) + + Respect files.privateLinks capability and do not offer to create private links when + privateLinks are not supported. + + https://github.com/owncloud/ios-app/issues/1138 + +* Bugfix - Enabling Markup Mode, Showing Video Controls on iOS 16, Updating Theme: [#1141](https://github.com/owncloud/ios-app/issues/1141) + + Enabling markup mode was broken on iOS 16 because of rearranged navigation bar and toolbar + items. Video player controls were not showing on iOS 16. Furthermore when a new theme was + chosen, this causes that the UITabBar and UIToolbar does not updates colours. + + https://github.com/owncloud/ios-app/issues/1141 + +* Bugfix - Share Extension Passcode Lock Interval: [#1156](https://github.com/owncloud/ios-app/issues/1156) + + The passcode lock interval was not taken into use in the share extension. + + https://github.com/owncloud/ios-app/issues/1156 + +* Bugfix - Video Metadata Image: [#5296](https://github.com/owncloud/enterprise/issues/5296) + + If a video file includes a metadata image, the video file was not visible, because the metadata + image was overlaying. + + https://github.com/owncloud/enterprise/issues/5296 + +* Change - New Dark Mode Themes: [#1146](https://github.com/owncloud/ios-app/issues/1146) + + Adds a new dark mode theme which is mostly equal to the web UI dark mode theme. Furthermore it adds + a black dark mode theme. + + https://github.com/owncloud/ios-app/issues/1146 + Changelog for ownCloud iOS Client [11.10.1] (2022-08-02) ======================================= The following sections list the changes in ownCloud iOS Client 11.10.1 relevant to @@ -130,11 +184,11 @@ Summary * Bugfix - Fix WebDAV endpoint URL for media playback after restoration: [#1093](https://github.com/owncloud/ios-app/pull/1093) * Bugfix - OAuth token renewal race condition: [#1105](https://github.com/owncloud/ios-app/pull/1105) +* Change - Infinite PROPFIND support: [#950](https://github.com/owncloud/ios-app/issues/950) +* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972) * Change - Biometrical Authentication Button: [#1004](https://github.com/owncloud/ios-app/issues/1004) * Change - Poll for changes efficiency enhancements: [#1043](https://github.com/owncloud/ios-app/pull/1043) * Change - Webfinger / server location: [#1059](https://github.com/owncloud/ios-app/pull/1059) -* Change - Infinite PROPFIND support: [#950](https://github.com/owncloud/ios-app/issues/950) -* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972) Details ------- @@ -153,6 +207,20 @@ Details https://github.com/owncloud/ios-app/pull/1105 +* Change - Infinite PROPFIND support: [#950](https://github.com/owncloud/ios-app/issues/950) + + Added support for prepopulation of newly created account bookmarks via infinite PROPFINDs, + which speeds up the initial scan + + https://github.com/owncloud/ios-app/issues/950 + +* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972) + + Check if only the account name was changed in edit mode: save and dismiss without + re-authentication + + https://github.com/owncloud/ios-app/issues/972 + * Change - Biometrical Authentication Button: [#1004](https://github.com/owncloud/ios-app/issues/1004) Added biometrical authentication button to provide a fallback for the fileprovider or app, if @@ -174,20 +242,6 @@ Details https://github.com/owncloud/ios-app/pull/1059 -* Change - Infinite PROPFIND support: [#950](https://github.com/owncloud/ios-app/issues/950) - - Added support for prepopulation of newly created account bookmarks via infinite PROPFINDs, - which speeds up the initial scan - - https://github.com/owncloud/ios-app/issues/950 - -* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972) - - Check if only the account name was changed in edit mode: save and dismiss without - re-authentication - - https://github.com/owncloud/ios-app/issues/972 - Changelog for ownCloud iOS Client [11.8.2] (2022-01-17) ======================================= The following sections list the changes in ownCloud iOS Client 11.8.2 relevant to @@ -234,12 +288,19 @@ ownCloud admins and users. Summary ------- -* Change - Fallback on OIDC Dynamic Client Registration: [#1068](https://github.com/owncloud/ios-app/pull/1068) * Change - Localized Sort Order: [#975](https://github.com/owncloud/ios-app/issues/975) +* Change - Fallback on OIDC Dynamic Client Registration: [#1068](https://github.com/owncloud/ios-app/pull/1068) Details ------- +* Change - Localized Sort Order: [#975](https://github.com/owncloud/ios-app/issues/975) + + Improved sorting results and localized sorting across query results and database queries, + via the SDK's new OCLOCALIZED collation and sort comparator. + + https://github.com/owncloud/ios-app/issues/975 + * Change - Fallback on OIDC Dynamic Client Registration: [#1068](https://github.com/owncloud/ios-app/pull/1068) Adds authentication-oauth2.oidc-fallback-on-client-registration-failure - @@ -249,13 +310,6 @@ Details https://github.com/owncloud/ios-app/pull/1068 -* Change - Localized Sort Order: [#975](https://github.com/owncloud/ios-app/issues/975) - - Improved sorting results and localized sorting across query results and database queries, - via the SDK's new OCLOCALIZED collation and sort comparator. - - https://github.com/owncloud/ios-app/issues/975 - Changelog for ownCloud iOS Client [11.8.0] (2021-12-01) ======================================= The following sections list the changes in ownCloud iOS Client 11.8.0 relevant to @@ -390,13 +444,13 @@ ownCloud admins and users. Summary ------- +* Bugfix - (PDF-Viewer) Keyboard does not disappear: [#894](https://github.com/owncloud/ios-app/issues/894) * Bugfix - Enabling Markup Edit Mode on iOS 15: [#1012](https://github.com/owncloud/ios-app/issues/1012) * Bugfix - Automatic photo upload crash on iOS 15: [#1017](https://github.com/owncloud/ios-app/pull/1017) * Bugfix - Open Private Link in Branded Client: [#1031](https://github.com/owncloud/ios-app/issues/1031) -* Bugfix - (PDF-Viewer) "Go to page" action does not open last page: [#1033](https://github.com/owncloud/ios-app/issues/1033) * Bugfix - Open Private Link in Branded App: [#1031](https://github.com/owncloud/ios-app/issues/1031) +* Bugfix - (PDF-Viewer) "Go to page" action does not open last page: [#1033](https://github.com/owncloud/ios-app/issues/1033) * Bugfix - (Branding) iOS 12 crash when entering Settings: [#4701](https://github.com/owncloud/enterprise/issues/4701) -* Bugfix - (PDF-Viewer) Keyboard does not disappear: [#894](https://github.com/owncloud/ios-app/issues/894) * Change - (Branding) Add build flags support: [#1026](https://github.com/owncloud/ios-app/pull/1026) * Change - Added associated domains to resign script: [#1028](https://github.com/owncloud/ios-app/pull/1028) * Change - (Branding) Send Feedback via URL: [#1035](https://github.com/owncloud/ios-app/pull/1035) @@ -408,6 +462,12 @@ Summary Details ------- +* Bugfix - (PDF-Viewer) Keyboard does not disappear: [#894](https://github.com/owncloud/ios-app/issues/894) + + Keyboard does not disappear when using the "Go to page" action on the iPad. + + https://github.com/owncloud/ios-app/issues/894 + * Bugfix - Enabling Markup Edit Mode on iOS 15: [#1012](https://github.com/owncloud/ios-app/issues/1012) Auto-enabling the markup edit mode on iOS 15 was broken. @@ -430,29 +490,23 @@ Details https://github.com/owncloud/ios-app/issues/1031 -* Bugfix - (PDF-Viewer) "Go to page" action does not open last page: [#1033](https://github.com/owncloud/ios-app/issues/1033) - - The last page of a PDF file could not be opened with the "Go to page" action. - - https://github.com/owncloud/ios-app/issues/1033 - * Bugfix - Open Private Link in Branded App: [#1031](https://github.com/owncloud/ios-app/issues/1031) Private links will now be opened in detail view, if the app client is branded. https://github.com/owncloud/ios-app/issues/1031 -* Bugfix - (Branding) iOS 12 crash when entering Settings: [#4701](https://github.com/owncloud/enterprise/issues/4701) +* Bugfix - (PDF-Viewer) "Go to page" action does not open last page: [#1033](https://github.com/owncloud/ios-app/issues/1033) - Addresses an issue where a branded build of the app crashes on iOS 12 upon entering Settings. + The last page of a PDF file could not be opened with the "Go to page" action. - https://github.com/owncloud/enterprise/issues/4701 + https://github.com/owncloud/ios-app/issues/1033 -* Bugfix - (PDF-Viewer) Keyboard does not disappear: [#894](https://github.com/owncloud/ios-app/issues/894) +* Bugfix - (Branding) iOS 12 crash when entering Settings: [#4701](https://github.com/owncloud/enterprise/issues/4701) - Keyboard does not disappear when using the "Go to page" action on the iPad. + Addresses an issue where a branded build of the app crashes on iOS 12 upon entering Settings. - https://github.com/owncloud/ios-app/issues/894 + https://github.com/owncloud/enterprise/issues/4701 * Change - (Branding) Add build flags support: [#1026](https://github.com/owncloud/ios-app/pull/1026) @@ -558,14 +612,20 @@ ownCloud admins and users. Summary ------- +* Bugfix - FileProvider UI on iOS 12: [#986](https://github.com/owncloud/ios-app/issues/986) * Bugfix - In some cases, background media upload worked not as expected: [#4547](https://github.com/owncloud/enterprise/issues/4547) * Bugfix - Fixed misleading warnings at let's encrypt cert renewal: [#4558](https://github.com/owncloud/enterprise/issues/4558) -* Bugfix - FileProvider UI on iOS 12: [#986](https://github.com/owncloud/ios-app/issues/986) * Change - Additional URL Scheme: [#979](https://github.com/owncloud/ios-app/issues/979) Details ------- +* Bugfix - FileProvider UI on iOS 12: [#986](https://github.com/owncloud/ios-app/issues/986) + + Views in FileProvider UI (public links, share with user) could not be dismissed on iOS 12 + + https://github.com/owncloud/ios-app/issues/986 + * Bugfix - In some cases, background media upload worked not as expected: [#4547](https://github.com/owncloud/enterprise/issues/4547) https://github.com/owncloud/enterprise/issues/4547 @@ -574,12 +634,6 @@ Details https://github.com/owncloud/enterprise/issues/4558 -* Bugfix - FileProvider UI on iOS 12: [#986](https://github.com/owncloud/ios-app/issues/986) - - Views in FileProvider UI (public links, share with user) could not be dismissed on iOS 12 - - https://github.com/owncloud/ios-app/issues/986 - * Change - Additional URL Scheme: [#979](https://github.com/owncloud/ios-app/issues/979) Added an additional URL scheme to open a specific app, if more than one ownCloud apps are @@ -597,9 +651,6 @@ ownCloud admins and users. Summary ------- -* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) -* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) -* Bugfix - Enabling Markup Mode: [#4468](https://github.com/owncloud/enterprise/issues/4468) * Bugfix - Improved AppStore Review Request Time: [#845](https://github.com/owncloud/ios-app/pull/845) * Bugfix - Changed wording in documentation: [#867](https://github.com/owncloud/ios-app/pull/867) * Bugfix - Fix bookmark name editing: [#877](https://github.com/owncloud/ios-app/pull/877) @@ -612,11 +663,11 @@ Summary * Bugfix - Disable Markup Action for Mime-Type Gif: [#952](https://github.com/owncloud/ios-app/issues/952) * Bugfix - UI refinements in action card: [#956](https://github.com/owncloud/ios-app/issues/956) * Bugfix - State Restoration for Branded Login: [#957](https://github.com/owncloud/ios-app/issues/957) -* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) -* Change - Full Screen PDF View: [#428](https://github.com/owncloud/ios-app/issues/428) -* Change - "Go to Page" reallocated in PDF previews: [#4448](https://github.com/owncloud/enterprise/issues/4448) -* Change - French Localization: [#4450](https://github.com/owncloud/enterprise/issues/4450) +* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) +* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) +* Bugfix - Enabling Markup Mode: [#4468](https://github.com/owncloud/enterprise/issues/4468) * Change - Local account-wide search using custom queries: [#53](https://github.com/owncloud/ios-app/issues/53) +* Change - Full Screen PDF View: [#428](https://github.com/owncloud/ios-app/issues/428) * Change - Unified Branding with MDM support: [#697](https://github.com/owncloud/ios-app/issues/697) * Change - Presentation Mode: [#704](https://github.com/owncloud/ios-app/issues/704) * Change - Class Settings Metadata Support: [#831](https://github.com/owncloud/ios-app/issues/831) @@ -633,30 +684,13 @@ Summary * Change - File Provider Passcode Protection: [#880](https://github.com/owncloud/ios-app/issues/880) * Change - Updated Keyboard Shortcuts: [#902](https://github.com/owncloud/ios-app/issues/902) * Change - Added Actions to File Provider: Sharing & Public Links: [#910](https://github.com/owncloud/ios-app/pull/910) +* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) +* Change - "Go to Page" reallocated in PDF previews: [#4448](https://github.com/owncloud/enterprise/issues/4448) +* Change - French Localization: [#4450](https://github.com/owncloud/enterprise/issues/4450) Details ------- -* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) - - - adds a paragraph on top of the Acknowledgements to provide additional context - adds - PLCrashReporter license to acknowledgements - - https://github.com/owncloud/enterprise/issues/4284 - -* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) - - - UI fix for branded login on the iPad - Fill color for branded button was not used - - https://github.com/owncloud/enterprise/issues/4367 - https://github.com/owncloud/enterprise/issues/4366 - -* Bugfix - Enabling Markup Mode: [#4468](https://github.com/owncloud/enterprise/issues/4468) - - In some cases enabling markup mode failed. - - https://github.com/owncloud/enterprise/issues/4468 - * Bugfix - Improved AppStore Review Request Time: [#845](https://github.com/owncloud/ios-app/pull/845) Changed request time for In-App review and fixed storing the first launch date @@ -741,33 +775,25 @@ Details https://github.com/owncloud/ios-app/issues/957 -* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) - - - Passcode lock enforcement via class setting. User can be forced to set-up a passcode when he - first starts the app - Auto-generated MDM documentation - - https://github.com/owncloud/enterprise/issues/4104 - -* Change - Full Screen PDF View: [#428](https://github.com/owncloud/ios-app/issues/428) +* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) - - A PDF file can be opened in fullscreen view and hides unnecessary UI elements. (Tap to trigger - full screen view) - Thumbnails positioned based on vertical size class after rotating the - device to give the displayed document more screen real estate. + - adds a paragraph on top of the Acknowledgements to provide additional context - adds + PLCrashReporter license to acknowledgements - https://github.com/owncloud/ios-app/issues/428 + https://github.com/owncloud/enterprise/issues/4284 -* Change - "Go to Page" reallocated in PDF previews: [#4448](https://github.com/owncloud/enterprise/issues/4448) +* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) - The "Go to Page" option for PDF files has been reallocated to the Actions menu, and is also - available by tapping on the page label. + - UI fix for branded login on the iPad - Fill color for branded button was not used - https://github.com/owncloud/enterprise/issues/4448 + https://github.com/owncloud/enterprise/issues/4367 + https://github.com/owncloud/enterprise/issues/4366 -* Change - French Localization: [#4450](https://github.com/owncloud/enterprise/issues/4450) +* Bugfix - Enabling Markup Mode: [#4468](https://github.com/owncloud/enterprise/issues/4468) - Added french localization. + In some cases enabling markup mode failed. - https://github.com/owncloud/enterprise/issues/4450 + https://github.com/owncloud/enterprise/issues/4468 * Change - Local account-wide search using custom queries: [#53](https://github.com/owncloud/ios-app/issues/53) @@ -776,6 +802,14 @@ Details https://github.com/owncloud/ios-app/issues/53 +* Change - Full Screen PDF View: [#428](https://github.com/owncloud/ios-app/issues/428) + + - A PDF file can be opened in fullscreen view and hides unnecessary UI elements. (Tap to trigger + full screen view) - Thumbnails positioned based on vertical size class after rotating the + device to give the displayed document more screen real estate. + + https://github.com/owncloud/ios-app/issues/428 + * Change - Unified Branding with MDM support: [#697](https://github.com/owncloud/ios-app/issues/697) Refactored Branding, introducing a new Branding class, unifying branding support with class @@ -895,6 +929,26 @@ Details https://github.com/owncloud/ios-app/pull/910 +* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) + + - Passcode lock enforcement via class setting. User can be forced to set-up a passcode when he + first starts the app - Auto-generated MDM documentation + + https://github.com/owncloud/enterprise/issues/4104 + +* Change - "Go to Page" reallocated in PDF previews: [#4448](https://github.com/owncloud/enterprise/issues/4448) + + The "Go to Page" option for PDF files has been reallocated to the Actions menu, and is also + available by tapping on the page label. + + https://github.com/owncloud/enterprise/issues/4448 + +* Change - French Localization: [#4450](https://github.com/owncloud/enterprise/issues/4450) + + Added french localization. + + https://github.com/owncloud/enterprise/issues/4450 + Changelog for ownCloud iOS Client [11.5.2] (2021-03-03) ======================================= The following sections list the changes in ownCloud iOS Client 11.5.2 relevant to @@ -905,19 +959,13 @@ ownCloud admins and users. Summary ------- -* Bugfix - Accessing hyperlinks in PDF documents: [#4432](https://github.com/owncloud/enterprise/issues/4432) * Bugfix - PDF thumbnail view position on the iPad: [#905](https://github.com/owncloud/ios-app/pull/905) * Bugfix - Misplaced Collapsible Progress Bar in detail view: [#906](https://github.com/owncloud/ios-app/issues/906) +* Bugfix - Accessing hyperlinks in PDF documents: [#4432](https://github.com/owncloud/enterprise/issues/4432) Details ------- -* Bugfix - Accessing hyperlinks in PDF documents: [#4432](https://github.com/owncloud/enterprise/issues/4432) - - Tap on hyperlinks in PDF documents opens the link. - - https://github.com/owncloud/enterprise/issues/4432 - * Bugfix - PDF thumbnail view position on the iPad: [#905](https://github.com/owncloud/ios-app/pull/905) Fixed the position of the PDF thumbnail view on the iPad from the bottom to the right position to @@ -932,6 +980,12 @@ Details https://github.com/owncloud/ios-app/issues/906 +* Bugfix - Accessing hyperlinks in PDF documents: [#4432](https://github.com/owncloud/enterprise/issues/4432) + + Tap on hyperlinks in PDF documents opens the link. + + https://github.com/owncloud/enterprise/issues/4432 + Changelog for ownCloud iOS Client [11.5.1] (2021-02-17) ======================================= The following sections list the changes in ownCloud iOS Client 11.5.1 relevant to @@ -963,13 +1017,12 @@ ownCloud admins and users. Summary ------- -* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) -* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) * Bugfix - Improved AppStore Review Request Time: [#845](https://github.com/owncloud/ios-app/pull/845) * Bugfix - Changed wording in documentation: [#867](https://github.com/owncloud/ios-app/pull/867) * Bugfix - Fix bookmark name editing: [#877](https://github.com/owncloud/ios-app/pull/877) * Bugfix - Media Player Behaviour: [#884](https://github.com/owncloud/ios-app/pull/884) -* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) +* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) +* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) * Change - Full Screen PDF View: [#428](https://github.com/owncloud/ios-app/issues/428) * Change - Unified Branding with MDM support: [#697](https://github.com/owncloud/ios-app/issues/697) * Change - Class Settings Metadata Support: [#831](https://github.com/owncloud/ios-app/issues/831) @@ -982,24 +1035,11 @@ Summary * Change - TLS certificate comparison: [#872](https://github.com/owncloud/ios-app/pull/872) * Change - New Issue view / presentation: [#874](https://github.com/owncloud/ios-app/pull/874) * Change - Automated Calens Changelog Creation: [#879](https://github.com/owncloud/ios-app/pull/879) +* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) Details ------- -* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) - - - adds a paragraph on top of the Acknowledgements to provide additional context - adds - PLCrashReporter license to acknowledgements - - https://github.com/owncloud/enterprise/issues/4284 - -* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) - - - UI fix for branded login on the iPad - Fill color for branded button was not used - - https://github.com/owncloud/enterprise/issues/4367 - https://github.com/owncloud/enterprise/issues/4366 - * Bugfix - Improved AppStore Review Request Time: [#845](https://github.com/owncloud/ios-app/pull/845) Changed request time for In-App review and fixed storing the first launch date @@ -1029,12 +1069,19 @@ Details https://github.com/owncloud/ios-app/pull/884 -* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) +* Bugfix - Added paragraph on top of Acknowledgements page: [#4284](https://github.com/owncloud/enterprise/issues/4284) - - Passcode lock enforcement via class setting. User can be forced to set-up a passcode when he - first starts the app - Auto-generated MDM documentation + - adds a paragraph on top of the Acknowledgements to provide additional context - adds + PLCrashReporter license to acknowledgements - https://github.com/owncloud/enterprise/issues/4104 + https://github.com/owncloud/enterprise/issues/4284 + +* Bugfix - Fixed Branded UI on iPad: [#4367](https://github.com/owncloud/enterprise/issues/4367) + + - UI fix for branded login on the iPad - Fill color for branded button was not used + + https://github.com/owncloud/enterprise/issues/4367 + https://github.com/owncloud/enterprise/issues/4366 * Change - Full Screen PDF View: [#428](https://github.com/owncloud/ios-app/issues/428) @@ -1127,6 +1174,13 @@ Details https://github.com/owncloud/ios-app/pull/879 +* Change - MDM Enhancements: [#4104](https://github.com/owncloud/enterprise/issues/4104) + + - Passcode lock enforcement via class setting. User can be forced to set-up a passcode when he + first starts the app - Auto-generated MDM documentation + + https://github.com/owncloud/enterprise/issues/4104 + ## Release version 11.4.5 (January 2021) - Fix: Crash in Detail View (#855) diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 576976d90..e144ef90f 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,4 +1,4 @@ -# Known issues in version 12.0 alpha 2 +# Known issues ## WARNING @@ -9,42 +9,87 @@ It should only be used with dedicated test servers, test data - and test devices - in the new browsing experience, some features are not yet available: - a grid view - breadcrumb title - - item / folder / usage info at the bottom of lists - spaces do not yet show a member count or provide access to a list of members - subscription of spaces can't be turned on/off yet -- the root of spaces-based accounts is not yet shown as hierarchic sidebar -- support for sharing is widely untested and/or unavailable in the alpha -- inactivated state of spaces is not yet represented in the UI -- Copy & Paste allows copying a folder into a subfolder of its own / itself, leading to an infinite cycle - handling of detached drives with user data in them (see OCVault.detachedDrives) -- sync actions that are actually complete are not always cleared from the Status tab until a logout/login -- dropping an item into its source/origin folder (same view controller) triggers a MOVE that fails +- [x] clicking a file in favorite view doesn't open the viewer (due to lack of context.query - the viewer clases need to be updated to use data sources rather than queries) +- support for OC10 sharing is incomplete: + - lack of actions for accepted shares + - federated shares are not yet included in "Shared with me" view +- spaces support for Shortcuts + +Missing: +- [x] quick access +- [x] proper iPhone support +- [ ] static login/branded login UI +- [x] state restoration +- [ ] full inline progress reporting when account databases are updated on first login +- [ ] progress reporting in active connections +- [x] migration from the Legacy app clarified: the feature was removed +- [x] iPadOS: opening an account in a new window + - [x] by context menu (openAccountInWindow) + - [x] by drag and drop (see ServerListTableViewController: UITableViewDragDelegate) +- [x] account auto connect (also account.auto-connect in ServerListTableViewController) -> no longer necessary, handled by state restoration +- [x] opening private links (display(itemWithID…:…)) +- [x] account issue handling +- [x] functional share extension +- [ ] full themeing/branding support +- [ ] reinstate Key Commands + +Jesus: +- [ ] Presentation view after installing is missing +- [x] The icon to hide/show the sidebar is missing in portrait mode. -> resolved by BrowserNavigation replacement of UINavigationController +- [x] Adding an oCIS account with existing custom spaces makes the app freezes and then crashes +- [x] If an space is browsed and new space image is added in the web client, app crashes +- [x] "Open in new window" option does not work. It does nothing after clicking +- [x] I miss the option to "Select All" and "Deselect All" in multiselection +- [x] "Copy" and "Move" operations show empty folder picker. No way to consolidate. +- [ ] "Cut"/"Paste" only working in space scope +- [ ] Upper bar (time, hour, battery level, and so on) is black under dark themes, not visible (fixable?) + +Matthias: +- [x] Selecting an OC10 account's root folder twice results in an empty list -> not reproducible in latest builds + +Michael: +- [x] Account deletion by swipe doesn't work +- [x] Crash searching for accounts to share with +- [x] Certificate warning when an account refers to a mix of hostnames +- [ ] UI rendering picking an account for photo uploads on iPhone: prompt full length, button super-compressed. ## File Provider - dragging an entire space on top of another starts a full copy of the space, which eventually fails halfway through ## SDK -- local storage consumed by spaces that are then deleted or inactivated is not reclaimed - pre-population of accounts using infinite PROPFIND is not supported # Evolution roadmap -- collection views - - support sidebars / hierarchies, including expanded state, with dynamic updates from data sources - -- location picker replaces folder picker - - supports picking - - accounts - - spaces - - folders - - returns an OCLocation +- [ ] collection views + - [x] support sidebars / hierarchies, including expanded state, with dynamic updates from data sources + - [x] ItemListCell: replace manual composition of info line below name with SegmentView + - [x] allows to show different content there, f.ex. Space and Folder in search + - [ ] sticky sort / multiselect bar in file lists + +- [x] location picker replaces folder picker + - [x] supports picking + - [x] accounts + - [x] spaces + - [x] folders + - [x] returns an OCLocation - allow passing "quick locations" to present on top in a group - track and re-offer last-picked / recent locations (via account's KVS) - quick access to personal and other spaces - integrate favorites as group + - [x] use for preferences and share extension + +- improved bookmark setup / editing + - browsing UI for ALL certificates stored in a bookmark's store, not just the primary certificate - account list - allow grouping accounts (i.e. Home / Work) - - replace simple list with modern CollectionViewController-based UI + - [x] replace simple list with modern CollectionViewController-based UI + +- available offline + - allow creating available offline item policies from smart searches - or directly from the search UI - make sync smarter, f.ex.: - a file that is updated locally multiple times only should be uploaded once, not once for every update @@ -69,4 +114,6 @@ It should only be used with dedicated test servers, test data - and test devices - other errors - report to user, drop silently, retry (how often/long?)? -- more expressive "Empty folder" message display, based on new .message item type +- [x] more expressive "Empty folder" message display, based on new .message item type + +- show spinner while recreating a scene via "Open in new window" diff --git a/Podfile b/Podfile deleted file mode 100644 index 3c5337fd8..000000000 --- a/Podfile +++ /dev/null @@ -1,17 +0,0 @@ -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -target 'ownCloudTests' do - project 'ownCloud' - - use_frameworks! # Required for Swift Test Targets only - inherit! :search_paths # Required for not double-linking libraries in the app and test targets. - pod 'EarlGrey', '~> 1.16' -end - -target 'ownCloudScreenshotsTests' do - project 'ownCloud' - - use_frameworks! # Required for Swift Test Targets only - inherit! :search_paths # Required for not double-linking libraries in the app and test targets. - pod 'EarlGrey', '~> 1.16' -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 6700af4b7..000000000 --- a/Podfile.lock +++ /dev/null @@ -1,16 +0,0 @@ -PODS: - - EarlGrey (1.16.0) - -DEPENDENCIES: - - EarlGrey (~> 1.16) - -SPEC REPOS: - trunk: - - EarlGrey - -SPEC CHECKSUMS: - EarlGrey: 455e5597ae5ccaca92cd46b81d8b25cacec060a1 - -PODFILE CHECKSUM: 9075b8f92281024401a0039c3477c9a16650b092 - -COCOAPODS: 1.10.0 diff --git a/changelog/11.11.0_2022-09-26/1138 b/changelog/11.11.0_2022-09-26/1138 new file mode 100644 index 000000000..b5d61f2b2 --- /dev/null +++ b/changelog/11.11.0_2022-09-26/1138 @@ -0,0 +1,5 @@ +Bugfix: Respect privateLinks capability + +Respect files.privateLinks capability and do not offer to create private links when privateLinks are not supported. + +https://github.com/owncloud/ios-app/issues/1138 diff --git a/changelog/11.11.0_2022-09-26/1141 b/changelog/11.11.0_2022-09-26/1141 new file mode 100644 index 000000000..5e7f8ffee --- /dev/null +++ b/changelog/11.11.0_2022-09-26/1141 @@ -0,0 +1,7 @@ +Bugfix: Enabling Markup Mode, Showing Video Controls on iOS 16, Updating Theme + +Enabling markup mode was broken on iOS 16 because of rearranged navigation bar and toolbar items. +Video player controls were not showing on iOS 16. +Furthermore when a new theme was chosen, this causes that the UITabBar and UIToolbar does not updates colours. + +https://github.com/owncloud/ios-app/issues/1141 diff --git a/changelog/11.11.0_2022-09-26/1146 b/changelog/11.11.0_2022-09-26/1146 new file mode 100644 index 000000000..bf5bdb0b6 --- /dev/null +++ b/changelog/11.11.0_2022-09-26/1146 @@ -0,0 +1,5 @@ +Change: New Dark Mode Themes + +Adds a new dark mode theme which is mostly equal to the web UI dark mode theme. Furthermore it adds a black dark mode theme. + +https://github.com/owncloud/ios-app/issues/1146 diff --git a/changelog/11.11.0_2022-09-26/1156 b/changelog/11.11.0_2022-09-26/1156 new file mode 100644 index 000000000..b330c6dc0 --- /dev/null +++ b/changelog/11.11.0_2022-09-26/1156 @@ -0,0 +1,5 @@ +Bugfix: Share Extension Passcode Lock Interval + +The passcode lock interval was not taken into use in the share extension. + +https://github.com/owncloud/ios-app/issues/1156 diff --git a/changelog/11.11.0_2022-09-26/5296 b/changelog/11.11.0_2022-09-26/5296 new file mode 100644 index 000000000..826e39c72 --- /dev/null +++ b/changelog/11.11.0_2022-09-26/5296 @@ -0,0 +1,5 @@ +Bugfix: Video Metadata Image + +If a video file includes a metadata image, the video file was not visible, because the metadata image was overlaying. + +https://github.com/owncloud/enterprise/issues/5296 diff --git a/doc/BUILD_CUSTOMIZATION.md b/doc/BUILD_CUSTOMIZATION.md new file mode 100644 index 000000000..ce413fb34 --- /dev/null +++ b/doc/BUILD_CUSTOMIZATION.md @@ -0,0 +1,38 @@ +# Build Flags + +## Description + +Build Flags can be used to control the inclusion or exclusion of certain functionality or features at compile time. + +## Usage in Branding + +A **space-separated** list of flags can be specified in the `Branding.plist` with the key `build.flags`, f.ex.: + +```xml +build.flags +DISABLE_BACKGROUND_LOCATION +``` + +## Flags + +The following options can be used as `build.flags`: + +### `DISABLE_BACKGROUND_LOCATION` + +Removes the following from the app: +- the option for location-triggered background uploads from Settings +- the location description keys from the app's `Info.plist` + +Not used by default. + +### `DISABLE_APPSTORE_LICENSING` + +Removes the following from the app: +- App Store integration for OCLicense +- App Store related view controllers and settings section + +### `DISABLE_PLAIN_HTTP` + +Removes the following from the app: +- the `NSAppTransportSecurity` dictionary from the app's `Info.plist` +- including the `NSAllowsArbitraryLoads` key that's needed to allow plain/unsecured HTTP connections diff --git a/doc/CONFIGURATION.json b/doc/CONFIGURATION.json index 631d8df97..7725deea3 100644 --- a/doc/CONFIGURATION.json +++ b/doc/CONFIGURATION.json @@ -1,18 +1,4 @@ [ - { - "autoExpansion" : "none", - "category" : "Account", - "categoryTag" : "account", - "classIdentifier" : "account", - "className" : "ownCloud.ServerListTableViewController", - "defaultValue" : false, - "description" : "Skip \"Account\" screen / automatically open \"Files\" screen after login", - "flatIdentifier" : "account.auto-connect", - "key" : "auto-connect", - "label" : "account.auto-connect", - "status" : "supported", - "type" : "bool" - }, { "autoExpansion" : "none", "category" : "Actions", @@ -35,6 +21,10 @@ "description" : "Copy", "value" : "com.owncloud.action.copy" }, + { + "description" : "New document", + "value" : "com.owncloud.action.createDocument" + }, { "description" : "Create folder", "value" : "com.owncloud.action.createFolder" @@ -47,6 +37,10 @@ "description" : "Delete", "value" : "com.owncloud.action.delete" }, + { + "description" : "Close Window", + "value" : "com.owncloud.action.discardscene" + }, { "description" : "Duplicate", "value" : "com.owncloud.action.duplicate" @@ -83,6 +77,10 @@ "description" : "Open in", "value" : "com.owncloud.action.openin" }, + { + "description" : "Open in a new Window", + "value" : "com.owncloud.action.openscene" + }, { "description" : "Go to page", "value" : "com.owncloud.action.pdfpage" @@ -149,6 +147,10 @@ "description" : "Copy", "value" : "com.owncloud.action.copy" }, + { + "description" : "New document", + "value" : "com.owncloud.action.createDocument" + }, { "description" : "Create folder", "value" : "com.owncloud.action.createFolder" @@ -161,6 +163,10 @@ "description" : "Delete", "value" : "com.owncloud.action.delete" }, + { + "description" : "Close Window", + "value" : "com.owncloud.action.discardscene" + }, { "description" : "Duplicate", "value" : "com.owncloud.action.duplicate" @@ -197,6 +203,10 @@ "description" : "Open in", "value" : "com.owncloud.action.openin" }, + { + "description" : "Open in a new Window", + "value" : "com.owncloud.action.openscene" + }, { "description" : "Go to page", "value" : "com.owncloud.action.pdfpage" @@ -241,6 +251,61 @@ "status" : "advanced", "type" : "stringArray" }, + { + "autoExpansion" : "none", + "category" : "Actions", + "categoryTag" : "actions", + "classIdentifier" : "action", + "className" : "ownCloudAppShared.Action", + "description" : "List of all operating system activities that should be excluded from OS share sheets in actions such as Open In.", + "flatIdentifier" : "action.excludedSystemActivities", + "key" : "excludedSystemActivities", + "label" : "action.excludedSystemActivities", + "possibleValues" : [ + { + "description" : "Add to reading list", + "value" : "com.apple.UIKit.activity.AddToReadingList" + }, + { + "description" : "AirDrop", + "value" : "com.apple.UIKit.activity.AirDrop" + }, + { + "description" : "Assign to contact", + "value" : "com.apple.UIKit.activity.AssignToContact" + }, + { + "description" : "Copy to pasteboard", + "value" : "com.apple.UIKit.activity.CopyToPasteboard" + }, + { + "description" : "Mail", + "value" : "com.apple.UIKit.activity.Mail" + }, + { + "description" : "Markup as PDF", + "value" : "com.apple.UIKit.activity.MarkupAsPDF" + }, + { + "description" : "Message", + "value" : "com.apple.UIKit.activity.Message" + }, + { + "description" : "Open in (i)Books", + "value" : "com.apple.UIKit.activity.OpenInIBooks" + }, + { + "description" : "Print", + "value" : "com.apple.UIKit.activity.Print" + }, + { + "description" : "Save to camera roll", + "value" : "com.apple.UIKit.activity.SaveToCameraRoll" + } + ], + "status" : "advanced", + "type" : "stringArray" + }, { "autoExpansion" : "none", "category" : "App", @@ -289,7 +354,7 @@ "categoryTag" : "app", "classIdentifier" : "app", "className" : "ownCloudAppShared.VendorServices", - "defaultValue" : false, + "defaultValue" : true, "description" : "Controls if the app is built for beta or release purposes.", "flatIdentifier" : "app.is-beta-build", "key" : "is-beta-build", @@ -317,7 +382,7 @@ "categoryTag" : "app", "classIdentifier" : "app", "className" : "ownCloudAppShared.VendorServices", - "defaultValue" : false, + "defaultValue" : true, "description" : "Controls whether a warning should be shown on the first run of a beta version.", "flatIdentifier" : "app.show-beta-warning", "key" : "show-beta-warning", @@ -408,6 +473,20 @@ "status" : "advanced", "type" : "string" }, + { + "autoExpansion" : "none", + "category" : "OIDC", + "categoryTag" : "oidc", + "classIdentifier" : "authentication-oauth2", + "className" : "OCAuthenticationMethodOAuth2", + "defaultValue" : true, + "description" : "If client registration is enabled, but registration fails, controls if the error should be ignored and the default client ID and secret should be used instead.", + "flatIdentifier" : "authentication-oauth2.oidc-fallback-on-client-registration-failure", + "key" : "oidc-fallback-on-client-registration-failure", + "label" : "authentication-oauth2.oidc-fallback-on-client-registration-failure", + "status" : "supported", + "type" : "bool" + }, { "autoExpansion" : "none", "category" : "OIDC", @@ -824,6 +903,19 @@ "status" : "advanced", "type" : "string" }, + { + "autoExpansion" : "none", + "category" : "Build", + "categoryTag" : "build", + "classIdentifier" : "build", + "className" : "BuildOptions", + "description" : "Set a custom app group identifier via Branding.plist parameter. This value will be set by fastlane. Changes OCAppGroupIdentifier, OCKeychainAccessGroupIdentifier and updates other, directly signing-relevant parts of the Info.plist. With this value set, fastlane needs the provisioning profiles and certificate with the app group identifier. This is needed, if a customer is using an own resigning script which does not handle setting the app group identifier.", + "flatIdentifier" : "build.app-group-identifier", + "key" : "app-group-identifier", + "label" : "build.app-group-identifier", + "status" : "supported", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "Build", @@ -831,7 +923,7 @@ "classIdentifier" : "build", "className" : "BuildOptions", "defaultValue" : "owncloud", - "description" : "Name of the URL scheme to use for private links. Must be provided in Branding.plist at build time. For documentation, please see doc/BUILD_CUSTOMIZATION.md.", + "description" : "Name of the URL scheme to use for private links. Must be provided in Branding.plist at build time. For documentation, please see https://github.com/owncloud/ios-app/blob/master/doc/BUILD_CUSTOMIZATION.md.", "flatIdentifier" : "build.custom-app-scheme", "key" : "custom-app-scheme", "label" : "build.custom-app-scheme", @@ -845,7 +937,7 @@ "classIdentifier" : "build", "className" : "BuildOptions", "defaultValue" : "oc", - "description" : "Name of the URL scheme to use for OAuth2/OIDC authentication. Must be provided in Branding.plist at build time. The authentication redirect URI parameters must also be changed accordingly in Branding.plist and on the server side. For documentation, please see doc/BUILD_CUSTOMIZATION.md.", + "description" : "Name of the URL scheme to use for OAuth2/OIDC authentication. Must be provided in Branding.plist at build time. The authentication redirect URI parameters must also be changed accordingly in Branding.plist and on the server side. For documentation, please see https://github.com/owncloud/ios-app/blob/master/doc/BUILD_CUSTOMIZATION.md.", "flatIdentifier" : "build.custom-auth-scheme", "key" : "custom-auth-scheme", "label" : "build.custom-auth-scheme", @@ -858,13 +950,26 @@ "categoryTag" : "build", "classIdentifier" : "build", "className" : "BuildOptions", - "description" : "A set of space separated flags to customize the build. Must be provided in Branding.plist at build time. For documentation, please see doc/BUILD_CUSTOMIZATION.md.", + "description" : "A set of space separated flags to customize the build. Must be provided in Branding.plist at build time. For documentation, please see https://github.com/owncloud/ios-app/blob/master/doc/BUILD_CUSTOMIZATION.md.", "flatIdentifier" : "build.flags", "key" : "flags", "label" : "build.flags", "status" : "supported", "type" : "string" }, + { + "autoExpansion" : "none", + "category" : "Build", + "categoryTag" : "build", + "classIdentifier" : "build", + "className" : "BuildOptions", + "description" : "Set a custom app group identifier via Branding.plist parameter. This value will be set by fastlane. Changes OCAppGroupIdentifier, OCKeychainAccessGroupIdentifier only. Fastlane does not need the provisioning profile and certificate with the given app group identifer. Needs resigning with the correct provisioning profile and certificate. This is needed, if a customer is using an own resigning script which does not handle setting the app group identifier.", + "flatIdentifier" : "build.oc-app-group-identifier", + "key" : "oc-app-group-identifier", + "label" : "build.oc-app-group-identifier", + "status" : "supported", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "Connection", @@ -938,6 +1043,20 @@ "status" : "advanced", "type" : "bool" }, + { + "autoExpansion" : "none", + "category" : "Security", + "categoryTag" : "security", + "classIdentifier" : "connection", + "className" : "OCConnection", + "description" : "Rule that defines the criteria that need to be met by a hostname other than a bookmark's hostname for the associated certificate to be added to the bookmark, tracked for changes and validated by the same rules as the bookmark's primary certificate. No value (default) or a value of `(0 == 1)` disables this feature. A value of `$hostname like \"*.mycompany.com\"` tracks the certificates for all hosts ending with mycompany.com.", + "flags" : 4, + "flatIdentifier" : "connection.associated-certificates-tracking-rule", + "key" : "associated-certificates-tracking-rule", + "label" : "connection.associated-certificates-tracking-rule", + "status" : "advanced", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "Security", @@ -1028,21 +1147,6 @@ "status" : "advanced", "type" : "string" }, - { - "autoExpansion" : "none", - "category" : "Endpoints", - "categoryTag" : "endpoints", - "classIdentifier" : "connection", - "className" : "OCConnection", - "defaultValue" : "index.php/apps/files/api/v1/thumbnail", - "description" : "Path of the thumbnail endpoint.", - "flags" : 4, - "flatIdentifier" : "connection.endpoint-thumbnail", - "key" : "endpoint-thumbnail", - "label" : "connection.endpoint-thumbnail", - "status" : "advanced", - "type" : "string" - }, { "autoExpansion" : "none", "category" : "Endpoints", @@ -1261,8 +1365,8 @@ }, { "autoExpansion" : "none", - "category" : "Privacy", - "categoryTag" : "privacy", + "category" : "Connection", + "categoryTag" : "connection", "classIdentifier" : "core", "className" : "OCCore", "defaultValue" : true, @@ -1398,6 +1502,242 @@ "status" : "advanced", "type" : "bool" }, + { + "autoExpansion" : "none", + "category" : "Extensions", + "categoryTag" : "extensions", + "classIdentifier" : "extensions", + "className" : "OCExtensionManager", + "defaultValue" : [ + + ], + "description" : "List of all disallowed extensions. If provided, extensions not listed here are allowed.", + "flatIdentifier" : "extensions.disallowed", + "key" : "disallowed", + "label" : "extensions.disallowed", + "possibleValues" : [ + { + "description" : "Extension with the identifier action-timeout-simulator.", + "value" : "action-timeout-simulator" + }, + { + "description" : "Extension with the identifier auth-race-condition.", + "value" : "auth-race-condition" + }, + { + "description" : "Extension with the identifier com.owncloud.action.background_update.", + "value" : "com.owncloud.action.background_update" + }, + { + "description" : "Extension with the identifier com.owncloud.action.collaborate.", + "value" : "com.owncloud.action.collaborate" + }, + { + "description" : "Extension with the identifier com.owncloud.action.copy.", + "value" : "com.owncloud.action.copy" + }, + { + "description" : "Extension with the identifier com.owncloud.action.createDocument.", + "value" : "com.owncloud.action.createDocument" + }, + { + "description" : "Extension with the identifier com.owncloud.action.createFolder.", + "value" : "com.owncloud.action.createFolder" + }, + { + "description" : "Extension with the identifier com.owncloud.action.cutpasteboard.", + "value" : "com.owncloud.action.cutpasteboard" + }, + { + "description" : "Extension with the identifier com.owncloud.action.delete.", + "value" : "com.owncloud.action.delete" + }, + { + "description" : "Extension with the identifier com.owncloud.action.discardscene.", + "value" : "com.owncloud.action.discardscene" + }, + { + "description" : "Extension with the identifier com.owncloud.action.duplicate.", + "value" : "com.owncloud.action.duplicate" + }, + { + "description" : "Extension with the identifier com.owncloud.action.favorite.", + "value" : "com.owncloud.action.favorite" + }, + { + "description" : "Extension with the identifier com.owncloud.action.importpasteboard.", + "value" : "com.owncloud.action.importpasteboard" + }, + { + "description" : "Extension with the identifier com.owncloud.action.instant_media_upload.", + "value" : "com.owncloud.action.instant_media_upload" + }, + { + "description" : "Extension with the identifier com.owncloud.action.links.", + "value" : "com.owncloud.action.links" + }, + { + "description" : "Extension with the identifier com.owncloud.action.makeAvailableOffline.", + "value" : "com.owncloud.action.makeAvailableOffline" + }, + { + "description" : "Extension with the identifier com.owncloud.action.makeUnavailableOffline.", + "value" : "com.owncloud.action.makeUnavailableOffline" + }, + { + "description" : "Extension with the identifier com.owncloud.action.markup.", + "value" : "com.owncloud.action.markup" + }, + { + "description" : "Extension with the identifier com.owncloud.action.move.", + "value" : "com.owncloud.action.move" + }, + { + "description" : "Extension with the identifier com.owncloud.action.openin.", + "value" : "com.owncloud.action.openin" + }, + { + "description" : "Extension with the identifier com.owncloud.action.openscene.", + "value" : "com.owncloud.action.openscene" + }, + { + "description" : "Extension with the identifier com.owncloud.action.pdfpage.", + "value" : "com.owncloud.action.pdfpage" + }, + { + "description" : "Extension with the identifier com.owncloud.action.pending_media_upload.", + "value" : "com.owncloud.action.pending_media_upload" + }, + { + "description" : "Extension with the identifier com.owncloud.action.presentationmode.", + "value" : "com.owncloud.action.presentationmode" + }, + { + "description" : "Extension with the identifier com.owncloud.action.rename.", + "value" : "com.owncloud.action.rename" + }, + { + "description" : "Extension with the identifier com.owncloud.action.scan.", + "value" : "com.owncloud.action.scan" + }, + { + "description" : "Extension with the identifier com.owncloud.action.show-exif.", + "value" : "com.owncloud.action.show-exif" + }, + { + "description" : "Extension with the identifier com.owncloud.action.unfavorite.", + "value" : "com.owncloud.action.unfavorite" + }, + { + "description" : "Extension with the identifier com.owncloud.action.unshare.", + "value" : "com.owncloud.action.unshare" + }, + { + "description" : "Extension with the identifier com.owncloud.action.upload.camera_media.", + "value" : "com.owncloud.action.upload.camera_media" + }, + { + "description" : "Extension with the identifier com.owncloud.action.uploadfile.", + "value" : "com.owncloud.action.uploadfile" + }, + { + "description" : "Extension with the identifier com.owncloud.action.uploadphotos.", + "value" : "com.owncloud.action.uploadphotos" + }, + { + "description" : "Extension with the identifier com.owncloud.light.", + "value" : "com.owncloud.light" + }, + { + "description" : "Extension with the identifier five-seconds-of-404.", + "value" : "five-seconds-of-404" + }, + { + "description" : "Extension with the identifier license.Down.", + "value" : "license.Down" + }, + { + "description" : "Extension with the identifier license.ISRunLoopThread.", + "value" : "license.ISRunLoopThread" + }, + { + "description" : "Extension with the identifier license.PocketSVG.", + "value" : "license.PocketSVG" + }, + { + "description" : "Extension with the identifier license.libzip.", + "value" : "license.libzip" + }, + { + "description" : "Extension with the identifier license.openssl.", + "value" : "license.openssl" + }, + { + "description" : "Extension with the identifier license.plcrashreporter.", + "value" : "license.plcrashreporter" + }, + { + "description" : "Extension with the identifier lookup-table.", + "value" : "lookup-table" + }, + { + "description" : "Extension with the identifier only-404.", + "value" : "only-404" + }, + { + "description" : "Extension with the identifier org.owncloud.image.", + "value" : "org.owncloud.image" + }, + { + "description" : "Extension with the identifier org.owncloud.media.", + "value" : "org.owncloud.media" + }, + { + "description" : "Extension with the identifier org.owncloud.pdfViewer.default.", + "value" : "org.owncloud.pdfViewer.default" + }, + { + "description" : "Extension with the identifier org.owncloud.ql_preview.", + "value" : "org.owncloud.ql_preview" + }, + { + "description" : "Extension with the identifier org.owncloud.webview.", + "value" : "org.owncloud.webview" + }, + { + "description" : "Extension with the identifier recovering-apm.", + "value" : "recovering-apm" + }, + { + "description" : "Extension with the identifier reject-downloads-500.", + "value" : "reject-downloads-500" + }, + { + "description" : "Extension with the identifier simple-apm.", + "value" : "simple-apm" + }, + { + "description" : "Extension with the identifier web-finger.", + "value" : "web-finger" + } + ], + "status" : "advanced", + "type" : "stringArray" + }, + { + "autoExpansion" : "none", + "category" : "FileProvider", + "categoryTag" : "fileprovider", + "classIdentifier" : "fileprovider", + "className" : "OCFileProviderSettings", + "defaultValue" : true, + "description" : "Controls whether the account content is available to other apps via File Provider / Files.app.", + "flatIdentifier" : "fileprovider.browseable", + "key" : "browseable", + "label" : "fileprovider.browseable", + "status" : "supported", + "type" : "bool" + }, { "autoExpansion" : "none", "category" : "Connection", @@ -1412,6 +1752,14 @@ "key" : "active-simulations", "label" : "host-simulator.active-simulations", "possibleValues" : [ + { + "description" : "Lets all MOVE/COPY/DELETE/PUT requests fail with a timeout error.", + "value" : "action-timeout-simulator" + }, + { + "description" : "Responds to all .well-known/webfinger requests with server-instance responses.", + "value" : "auth-race-condition" + }, { "description" : "Return status code 404 for every request for the first five seconds.", "value" : "five-seconds-of-404" @@ -1431,6 +1779,10 @@ { "description" : "Redirect any request without cookies to a cookie-setting endpoint, where cookies are set - and then redirect back.", "value" : "simple-apm" + }, + { + "description" : "Responds to all .well-known/webfinger requests with server-instance responses.", + "value" : "web-finger" } ], "status" : "debugOnly", @@ -1490,7 +1842,7 @@ "key" : "vacuum-sync-anchor-ttl", "label" : "item-policy.vacuum-sync-anchor-ttl", "status" : "debugOnly", - "type" : "bool" + "type" : "int" }, { "autoExpansion" : "none", @@ -1765,6 +2117,21 @@ "classIdentifier" : "log", "className" : "OCLogger", "defaultValue" : true, + "description" : "Controls whether messages spanning more than one line should be logged as a single line, after replacing new line characters with \"\\n\".", + "flags" : 4, + "flatIdentifier" : "log.replace-newline", + "key" : "replace-newline", + "label" : "log.replace-newline", + "status" : "advanced", + "type" : "bool" + }, + { + "autoExpansion" : "none", + "category" : "Logging", + "categoryTag" : "logging", + "classIdentifier" : "log", + "className" : "OCLogger", + "defaultValue" : false, "description" : "Controls whether messages spanning more than one line should be broken into their individual lines and each be logged with the complete lead-in/lead-out sequence.", "flags" : 4, "flatIdentifier" : "log.single-lined", @@ -1858,6 +2225,27 @@ "status" : "advanced", "type" : "int" }, + { + "autoExpansion" : "none", + "category" : "Passcode", + "categoryTag" : "passcode", + "classIdentifier" : "passcode", + "className" : "AppLockSettings", + "defaultValue" : { + "com.air-watch.boxer" : { + "allow" : false + }, + "default" : { + "allow" : true + } + }, + "description" : "Controls the biometrical unlock availability in the share sheet, with per-app level control.", + "flatIdentifier" : "passcode.share-sheet-biometrical-unlock-by-app", + "key" : "share-sheet-biometrical-unlock-by-app", + "label" : "passcode.share-sheet-biometrical-unlock-by-app", + "status" : "advanced", + "type" : "dictionary" + }, { "autoExpansion" : "none", "category" : "Passcode", @@ -1872,6 +2260,22 @@ "status" : "advanced", "type" : "bool" }, + { + "autoExpansion" : "none", + "category" : "Security", + "categoryTag" : "security", + "classIdentifier" : "post-build", + "className" : "OCClassSettingsFlatSourcePostBuild", + "defaultValue" : [ + + ], + "description" : "List of settings (as flat identifiers) that are allowed to be changed post-build via the app's URL scheme. Including a value of \"*\" allows any setting to be changed. Defaults to an empty array (equalling not allowed). ", + "flatIdentifier" : "post-build.allowed-settings", + "key" : "allowed-settings", + "label" : "post-build.allowed-settings", + "status" : "advanced", + "type" : "stringArray" + }, { "autoExpansion" : "none", "category" : "Release Notes", @@ -1898,6 +2302,42 @@ "status" : "debugOnly", "type" : "string" }, + { + "autoExpansion" : "none", + "category" : "Connection", + "categoryTag" : "connection", + "classIdentifier" : "server-locator", + "className" : "OCServerLocator", + "description" : "Lookup table that maps users to server URLs", + "flatIdentifier" : "server-locator.lookup-table", + "key" : "lookup-table", + "label" : "server-locator.lookup-table", + "status" : "advanced", + "type" : "dictionary" + }, + { + "autoExpansion" : "none", + "category" : "Connection", + "categoryTag" : "connection", + "classIdentifier" : "server-locator", + "className" : "OCServerLocator", + "description" : "Use Server Locator", + "flatIdentifier" : "server-locator.use", + "key" : "use", + "label" : "server-locator.use", + "possibleValues" : [ + { + "description" : "Locate server via lookup table. Keys can match against the beginning (f.ex. \"begins:bob@\"), end (f.ex. \"ends:@owncloud.org\") or regular expression (f.ex. \"regexp:\")", + "value" : "lookup-table" + }, + { + "description" : "Locate server via Webfinger service-instance relation (http://webfinger.owncloud/rel/server-instance) using the entered/provided server URL", + "value" : "web-finger" + } + ], + "status" : "advanced", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "Security", diff --git a/doc/images/el/keyword.strings b/doc/images/el/keyword.strings index f60e7bc49..8a3dbb69f 100644 Binary files a/doc/images/el/keyword.strings and b/doc/images/el/keyword.strings differ diff --git a/doc/images/el/title.strings b/doc/images/el/title.strings index 57713faf8..2f4549f36 100644 Binary files a/doc/images/el/title.strings and b/doc/images/el/title.strings differ diff --git a/doc/images/ko/keyword.strings b/doc/images/ko/keyword.strings new file mode 100644 index 000000000..aa725dfd0 Binary files /dev/null and b/doc/images/ko/keyword.strings differ diff --git a/doc/images/ko/title.strings b/doc/images/ko/title.strings new file mode 100644 index 000000000..c332ce3eb Binary files /dev/null and b/doc/images/ko/title.strings differ diff --git a/enterprise/resign/resignOwncloudApp b/enterprise/resign/resignOwncloudApp index 23bd551e0..2f8870c83 100755 --- a/enterprise/resign/resignOwncloudApp +++ b/enterprise/resign/resignOwncloudApp @@ -7,7 +7,7 @@ # 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 . -VERSION="1.2.0" +VERSION="1.2.1" #Define output formats BOLD="$(tput bold)" @@ -81,7 +81,7 @@ if [ ! -f "$UNSIGNED_IPA" ]; then fi # Get certificate SHA-1 -CERT_SHA_1=`security find-certificate -c "$IDENTITY" -Z | grep "^SHA-1" | cut -d: -f 2 | xargs` +CERT_SHA_1=`security find-certificate -c "$IDENTITY" -Z | grep "^SHA-256" | cut -d: -f 2 | xargs` # Create temp directory mkdir $APPTEMP @@ -96,7 +96,7 @@ else # Convert provisioning profile to plist security cms -D -i "$a" > "$APPTEMP/tmp.plist" # Get provisioning SHA-1 - PROV_SHA_1=`/usr/libexec/PlistBuddy -c "Print :DeveloperCertificates:0" $APPTEMP/tmp.plist | openssl x509 -inform der -fingerprint | grep "^SHA1" | cut -d= -f 2 | ruby -ne 'puts $_.split(":").join'` + PROV_SHA_1=`/usr/libexec/PlistBuddy -c "Print :DeveloperCertificates:0" $APPTEMP/tmp.plist | openssl x509 -inform der -fingerprint | grep "^SHA256" | cut -d= -f 2 | ruby -ne 'puts $_.split(":").join'` rm -f "$APPTEMP/tmp.plist" if [ "$CERT_SHA_1" = "$PROV_SHA_1" ]; then diff --git a/fastlane/Fastfile b/fastlane/Fastfile index b070b2cd3..53eeba7fb 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -28,7 +28,7 @@ platform :ios do lane :build_ipa_ad_hoc do #Create the build gym( - workspace: "ownCloud.xcworkspace", + project: "ownCloud.xcodeproj", scheme: "ownCloud", codesigning_identity: "Apple Distribution: ownCloud GmbH (4AP2STM4H5)", export_method: "ad-hoc", @@ -463,8 +463,7 @@ end if File.exist?(xcode_version_paths) required_version = File.read(xcode_version_paths).strip puts "Found required Xcode version: " + required_version - xcversion(version: required_version) - ensure_xcode_version() + ensure_xcode_version(version: required_version) end appName = "ownCloud" @@ -547,6 +546,12 @@ end sh "mv ../ownCloud/Resources/Info.plist.mod ../ownCloud/Resources/Info.plist" end + # Special handling for app build flag DISABLE_PLAIN_HTTP (see above why this is needed) + if appBuildFlags.include? "DISABLE_PLAIN_HTTP" + sh "sed '/#ifndef DISABLE_PLAIN_HTTP/,/#endif/d' ../ownCloud/Resources/Info.plist >../ownCloud/Resources/Info.plist.mod" + sh "mv ../ownCloud/Resources/Info.plist.mod ../ownCloud/Resources/Info.plist" + end + # update_url_schemes can't seem to reach the second URL scheme ("oc") for authentication # so using sed and a XML property instead if !appCustomAppScheme.empty? @@ -824,13 +829,13 @@ end #Create the build build_app( - workspace: "ownCloud.xcworkspace", + project: "ownCloud.xcodeproj", scheme: "ownCloud", configuration: CONFIGURATION, codesigning_identity: ENTERPRISE_IDENTITY, output_name: ipaName, export_method: EXPORT_METHOD, - xcargs: "APP_BUILD_FLAGS='" + appBuildFlags + "'", + xcargs: "CODE_SIGN_STYLE=Manual APP_BUILD_FLAGS='" + appBuildFlags + "'", export_options: { method: EXPORT_METHOD, provisioningProfiles: { diff --git a/fastlane/metadata-emm/en-US/release_notes.txt b/fastlane/metadata-emm/en-US/release_notes.txt index 5da11c0dc..88613836b 100644 --- a/fastlane/metadata-emm/en-US/release_notes.txt +++ b/fastlane/metadata-emm/en-US/release_notes.txt @@ -1,9 +1,15 @@ -• UI fixes on iOS 15 -This version fixes some UI problems on iOS 15. +• New Dark Mode Themes +Two new dark mode themes are available. -• Shortcuts Action -The shortcuts action Delete Path Item did not provided configured accounts. +• iOS 16: Markup Mode +Markup mode was not enabled automatically on iOS 16. -• Increased Timeout for Copy Action -Timeout for Copy Action was increased up to 10 minutes. +• iOS 16: Video Player +Video player controls were not showing on iOS 16. + +• Video Player +Metadata image could overlay the video player canvas. + +• Passcode Interval +The passcode lock interval was not taken into use in the share extension. diff --git a/fastlane/metadata-owncloud-online/en-US/release_notes.txt b/fastlane/metadata-owncloud-online/en-US/release_notes.txt index 5da11c0dc..88613836b 100644 --- a/fastlane/metadata-owncloud-online/en-US/release_notes.txt +++ b/fastlane/metadata-owncloud-online/en-US/release_notes.txt @@ -1,9 +1,15 @@ -• UI fixes on iOS 15 -This version fixes some UI problems on iOS 15. +• New Dark Mode Themes +Two new dark mode themes are available. -• Shortcuts Action -The shortcuts action Delete Path Item did not provided configured accounts. +• iOS 16: Markup Mode +Markup mode was not enabled automatically on iOS 16. -• Increased Timeout for Copy Action -Timeout for Copy Action was increased up to 10 minutes. +• iOS 16: Video Player +Video player controls were not showing on iOS 16. + +• Video Player +Metadata image could overlay the video player canvas. + +• Passcode Interval +The passcode lock interval was not taken into use in the share extension. diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 5da11c0dc..88613836b 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,9 +1,15 @@ -• UI fixes on iOS 15 -This version fixes some UI problems on iOS 15. +• New Dark Mode Themes +Two new dark mode themes are available. -• Shortcuts Action -The shortcuts action Delete Path Item did not provided configured accounts. +• iOS 16: Markup Mode +Markup mode was not enabled automatically on iOS 16. -• Increased Timeout for Copy Action -Timeout for Copy Action was increased up to 10 minutes. +• iOS 16: Video Player +Video player controls were not showing on iOS 16. + +• Video Player +Metadata image could overlay the video player canvas. + +• Passcode Interval +The passcode lock interval was not taken into use in the share extension. diff --git a/fastlane/screenshots/ar/keyword.strings b/fastlane/screenshots/ar/keyword.strings new file mode 100644 index 000000000..57ed3deb4 Binary files /dev/null and b/fastlane/screenshots/ar/keyword.strings differ diff --git a/fastlane/screenshots/ar/title.strings b/fastlane/screenshots/ar/title.strings new file mode 100644 index 000000000..57ed3deb4 Binary files /dev/null and b/fastlane/screenshots/ar/title.strings differ diff --git a/fastlane/screenshots/ca/keyword.strings b/fastlane/screenshots/ca/keyword.strings new file mode 100644 index 000000000..8d6072935 Binary files /dev/null and b/fastlane/screenshots/ca/keyword.strings differ diff --git a/fastlane/screenshots/de-DE/keyword.strings b/fastlane/screenshots/de-DE/keyword.strings new file mode 100644 index 000000000..fe237c7fe Binary files /dev/null and b/fastlane/screenshots/de-DE/keyword.strings differ diff --git a/fastlane/screenshots/de-DE/title.strings b/fastlane/screenshots/de-DE/title.strings new file mode 100644 index 000000000..0a9465ab8 Binary files /dev/null and b/fastlane/screenshots/de-DE/title.strings differ diff --git a/fastlane/screenshots/de/keyword.strings b/fastlane/screenshots/de/keyword.strings new file mode 100644 index 000000000..fe237c7fe Binary files /dev/null and b/fastlane/screenshots/de/keyword.strings differ diff --git a/fastlane/screenshots/de/title.strings b/fastlane/screenshots/de/title.strings new file mode 100644 index 000000000..0a9465ab8 Binary files /dev/null and b/fastlane/screenshots/de/title.strings differ diff --git a/fastlane/screenshots/de_CH/keyword.strings b/fastlane/screenshots/de_CH/keyword.strings new file mode 100644 index 000000000..fe237c7fe Binary files /dev/null and b/fastlane/screenshots/de_CH/keyword.strings differ diff --git a/fastlane/screenshots/de_CH/title.strings b/fastlane/screenshots/de_CH/title.strings new file mode 100644 index 000000000..0a9465ab8 Binary files /dev/null and b/fastlane/screenshots/de_CH/title.strings differ diff --git a/fastlane/screenshots/el/keyword.strings b/fastlane/screenshots/el/keyword.strings new file mode 100644 index 000000000..8a3dbb69f Binary files /dev/null and b/fastlane/screenshots/el/keyword.strings differ diff --git a/fastlane/screenshots/el/title.strings b/fastlane/screenshots/el/title.strings new file mode 100644 index 000000000..2f4549f36 Binary files /dev/null and b/fastlane/screenshots/el/title.strings differ diff --git a/fastlane/screenshots/en-GB/keyword.strings b/fastlane/screenshots/en-GB/keyword.strings new file mode 100644 index 000000000..aba384182 Binary files /dev/null and b/fastlane/screenshots/en-GB/keyword.strings differ diff --git a/fastlane/screenshots/en-GB/title.strings b/fastlane/screenshots/en-GB/title.strings new file mode 100644 index 000000000..547033c70 Binary files /dev/null and b/fastlane/screenshots/en-GB/title.strings differ diff --git a/fastlane/screenshots/es/keyword.strings b/fastlane/screenshots/es/keyword.strings new file mode 100644 index 000000000..ad1dc0f49 Binary files /dev/null and b/fastlane/screenshots/es/keyword.strings differ diff --git a/fastlane/screenshots/es/title.strings b/fastlane/screenshots/es/title.strings new file mode 100644 index 000000000..90b256dbb Binary files /dev/null and b/fastlane/screenshots/es/title.strings differ diff --git a/fastlane/screenshots/et_EE/title.strings b/fastlane/screenshots/et_EE/title.strings new file mode 100644 index 000000000..5086a9bf9 Binary files /dev/null and b/fastlane/screenshots/et_EE/title.strings differ diff --git a/fastlane/screenshots/eu/keyword.strings b/fastlane/screenshots/eu/keyword.strings new file mode 100644 index 000000000..bc56f9a28 Binary files /dev/null and b/fastlane/screenshots/eu/keyword.strings differ diff --git a/fastlane/screenshots/eu/title.strings b/fastlane/screenshots/eu/title.strings new file mode 100644 index 000000000..3a670b7a7 Binary files /dev/null and b/fastlane/screenshots/eu/title.strings differ diff --git a/fastlane/screenshots/fr/title.strings b/fastlane/screenshots/fr/title.strings new file mode 100644 index 000000000..c64d79caf Binary files /dev/null and b/fastlane/screenshots/fr/title.strings differ diff --git a/fastlane/screenshots/gl/keyword.strings b/fastlane/screenshots/gl/keyword.strings new file mode 100644 index 000000000..db8cff283 Binary files /dev/null and b/fastlane/screenshots/gl/keyword.strings differ diff --git a/fastlane/screenshots/gl/title.strings b/fastlane/screenshots/gl/title.strings new file mode 100644 index 000000000..4e0cabde7 Binary files /dev/null and b/fastlane/screenshots/gl/title.strings differ diff --git a/fastlane/screenshots/he/keyword.strings b/fastlane/screenshots/he/keyword.strings new file mode 100644 index 000000000..05b8a398a Binary files /dev/null and b/fastlane/screenshots/he/keyword.strings differ diff --git a/fastlane/screenshots/he/title.strings b/fastlane/screenshots/he/title.strings new file mode 100644 index 000000000..311f4c0fc Binary files /dev/null and b/fastlane/screenshots/he/title.strings differ diff --git a/fastlane/screenshots/km/keyword.strings b/fastlane/screenshots/km/keyword.strings new file mode 100644 index 000000000..9e578cb74 Binary files /dev/null and b/fastlane/screenshots/km/keyword.strings differ diff --git a/fastlane/screenshots/ko/keyword.strings b/fastlane/screenshots/ko/keyword.strings new file mode 100644 index 000000000..aa725dfd0 Binary files /dev/null and b/fastlane/screenshots/ko/keyword.strings differ diff --git a/fastlane/screenshots/ko/title.strings b/fastlane/screenshots/ko/title.strings new file mode 100644 index 000000000..c332ce3eb Binary files /dev/null and b/fastlane/screenshots/ko/title.strings differ diff --git a/fastlane/screenshots/lv/title.strings b/fastlane/screenshots/lv/title.strings new file mode 100644 index 000000000..fec42a1f9 Binary files /dev/null and b/fastlane/screenshots/lv/title.strings differ diff --git a/fastlane/screenshots/pt-BR/keyword.strings b/fastlane/screenshots/pt-BR/keyword.strings new file mode 100644 index 000000000..37766ce90 Binary files /dev/null and b/fastlane/screenshots/pt-BR/keyword.strings differ diff --git a/fastlane/screenshots/pt-BR/title.strings b/fastlane/screenshots/pt-BR/title.strings new file mode 100644 index 000000000..bd1d1bd72 Binary files /dev/null and b/fastlane/screenshots/pt-BR/title.strings differ diff --git a/fastlane/screenshots/ru/keyword.strings b/fastlane/screenshots/ru/keyword.strings new file mode 100644 index 000000000..a004b1a7b Binary files /dev/null and b/fastlane/screenshots/ru/keyword.strings differ diff --git a/fastlane/screenshots/ru/title.strings b/fastlane/screenshots/ru/title.strings new file mode 100644 index 000000000..8684188dd Binary files /dev/null and b/fastlane/screenshots/ru/title.strings differ diff --git a/fastlane/screenshots/sq/keyword.strings b/fastlane/screenshots/sq/keyword.strings new file mode 100644 index 000000000..893314b78 Binary files /dev/null and b/fastlane/screenshots/sq/keyword.strings differ diff --git a/fastlane/screenshots/sq/title.strings b/fastlane/screenshots/sq/title.strings new file mode 100644 index 000000000..aeab3e0d3 Binary files /dev/null and b/fastlane/screenshots/sq/title.strings differ diff --git a/fastlane/screenshots/th-TH/keyword.strings b/fastlane/screenshots/th-TH/keyword.strings new file mode 100644 index 000000000..fa36c4461 Binary files /dev/null and b/fastlane/screenshots/th-TH/keyword.strings differ diff --git a/fastlane/screenshots/th-TH/title.strings b/fastlane/screenshots/th-TH/title.strings new file mode 100644 index 000000000..9e8b8b3be Binary files /dev/null and b/fastlane/screenshots/th-TH/title.strings differ diff --git a/fastlane/screenshots/tr/keyword.strings b/fastlane/screenshots/tr/keyword.strings new file mode 100644 index 000000000..c8d9b7ab5 Binary files /dev/null and b/fastlane/screenshots/tr/keyword.strings differ diff --git a/fastlane/screenshots/tr/title.strings b/fastlane/screenshots/tr/title.strings new file mode 100644 index 000000000..d6f3387f7 Binary files /dev/null and b/fastlane/screenshots/tr/title.strings differ diff --git a/fastlane/screenshots/ur_PK/keyword.strings b/fastlane/screenshots/ur_PK/keyword.strings new file mode 100644 index 000000000..0ffb50a7a Binary files /dev/null and b/fastlane/screenshots/ur_PK/keyword.strings differ diff --git a/fastlane/screenshots/zh-Hans/keyword.strings b/fastlane/screenshots/zh-Hans/keyword.strings new file mode 100644 index 000000000..384e9c30f Binary files /dev/null and b/fastlane/screenshots/zh-Hans/keyword.strings differ diff --git a/fastlane/screenshots/zh-Hans/title.strings b/fastlane/screenshots/zh-Hans/title.strings new file mode 100644 index 000000000..d9d2b6e1b Binary files /dev/null and b/fastlane/screenshots/zh-Hans/title.strings differ diff --git a/fastlane/screenshots/zh_TW/keyword.strings b/fastlane/screenshots/zh_TW/keyword.strings new file mode 100644 index 000000000..c6185c38f Binary files /dev/null and b/fastlane/screenshots/zh_TW/keyword.strings differ diff --git a/fastlane/screenshots/zh_TW/title.strings b/fastlane/screenshots/zh_TW/title.strings new file mode 100644 index 000000000..9676e63d1 Binary files /dev/null and b/fastlane/screenshots/zh_TW/title.strings differ diff --git a/ios-sdk b/ios-sdk index 93cfe6b31..5d81d1e01 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 93cfe6b31beb2f63624c85766f03f5ca723ad64e +Subproject commit 5d81d1e017b2a34b0a9cdc0d2677dc0edd148bb1 diff --git a/ownCloud File Provider UI/DocumentActionViewController.swift b/ownCloud File Provider UI/DocumentActionViewController.swift index 9ff1fe6af..b7b60a746 100644 --- a/ownCloud File Provider UI/DocumentActionViewController.swift +++ b/ownCloud File Provider UI/DocumentActionViewController.swift @@ -61,6 +61,16 @@ class DocumentActionViewController: FPUIActionExtensionViewController { } } + func prepareNavigationController() { + if themeNavigationController == nil { + themeNavigationController = ThemeNavigationController() + if let themeNavigationController = themeNavigationController { + view.addSubview(themeNavigationController.view) + addChild(themeNavigationController) + } + } + } + override func prepare(forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]) { guard let identifier = itemIdentifiers.first else { @@ -71,11 +81,7 @@ class DocumentActionViewController: FPUIActionExtensionViewController { let collection = Theme.shared.activeCollection self.view.backgroundColor = collection.toolbarColors.backgroundColor - themeNavigationController = ThemeNavigationController() - if let themeNavigationController = themeNavigationController { - view.addSubview(themeNavigationController.view) - addChild(themeNavigationController) - } + prepareNavigationController() showCancelLabel(with: "Connecting…".localized) @@ -151,6 +157,12 @@ class DocumentActionViewController: FPUIActionExtensionViewController { } override func prepare(forError error: Error) { + if !OCFileProviderSettings.browseable { + prepareNavigationController() + showCancelLabel(with: "File Provider access has been disabled by the administrator.\n\nPlease use the app to access your files.".localized) + return + } + if AppLockManager.supportedOnDevice { AppLockManager.shared.passwordViewHostViewController = self AppLockManager.shared.biometricCancelLabel = "Cancel".localized @@ -163,6 +175,7 @@ class DocumentActionViewController: FPUIActionExtensionViewController { AppLockManager.shared.showLockscreenIfNeeded() } else { + prepareNavigationController() showCancelLabel(with: "Passcode protection is not supported on this device.\nPlease disable passcode lock in the app settings.".localized) } } diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index c78f3545f..135dafc03 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -492,6 +492,12 @@ - (void)createDirectoryWithName:(NSString *)directoryName inParentItemIdentifier NSError *error = nil; OCItem *parentItem; + if (!OCFileProviderSettings.browseable) + { + completionHandler(nil, [OCErrorWithDescription(OCErrorInternal, OCLocalized(@"File Provider access has been disabled by the administrator. Please use the app to create new folders.")) translatedError]); + return; + } + FPLogCmdBegin(@"CreateDir", @"Start of createDirectoryWithName=%@, inParentItemIdentifier=%@", directoryName, parentItemIdentifier); if ((parentItem = [self ocItemForIdentifier:parentItemIdentifier vfsNode:NULL error:&error]) != nil) @@ -674,6 +680,12 @@ - (void)importDocumentAtURL:(NSURL *)fileURL toParentItemIdentifier:(NSFileProvi NSError *error = nil; BOOL stopAccess = NO; + if (!OCFileProviderSettings.browseable) + { + completionHandler(nil, [OCErrorWithDescription(OCErrorInternal, OCLocalized(@"File Provider access has been disabled by the administrator. Please use the share extension to import files.")) translatedError]); + return; + } + if (![Branding.sharedBranding isImportMethodAllowed:BrandingFileImportMethodFileProvider]) { completionHandler(nil, [OCErrorWithDescription(OCErrorInternal, OCLocalized(@"Importing files through the File Provider is not allowed on this device.")) translatedError]); @@ -898,6 +910,16 @@ - (void)setLastUsedDate:(NSDate *)lastUsedDate forItemIdentifier:(NSFileProvider #pragma mark - Enumeration - (nullable id)enumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)containerItemIdentifier error:(NSError **)error { + if (!OCFileProviderSettings.browseable) + { + if (error != NULL) + { + *error = [NSError errorWithDomain:NSFileProviderErrorDomain code:NSFileProviderErrorNotAuthenticated userInfo:nil]; + } + + return (nil); + } + if (AppLockSettings.sharedAppLockSettings.lockEnabled) { NSData *lockedDateData = [[[OCAppIdentity sharedAppIdentity] keychain] readDataFromKeychainItemForAccount:@"app.passcode" path:@"lockedDate"]; @@ -1011,9 +1033,11 @@ - (NSProgress *)fetchThumbnailsForItemIdentifiers:(NSArrayNSExtensionPointIdentifier com.apple.share-services NSExtensionPrincipalClass - ShareNavigationController + ShareExtensionViewController OCAppGroupIdentifier group.com.owncloud.ios-app OCAppIdentifierPrefix $(AppIdentifierPrefix) + OCAppComponentIdentifier + shareExtension OCHasFileProvider OCKeychainAccessGroupIdentifier diff --git a/ownCloud Share Extension/ShareViewController.swift b/ownCloud Share Extension/ShareExtensionViewController.swift similarity index 52% rename from ownCloud Share Extension/ShareViewController.swift rename to ownCloud Share Extension/ShareExtensionViewController.swift index d31799d76..13324b063 100644 --- a/ownCloud Share Extension/ShareViewController.swift +++ b/ownCloud Share Extension/ShareExtensionViewController.swift @@ -1,275 +1,120 @@ // -// ShareViewController.swift +// ShareExtensionViewController.swift // ownCloud Share Extension // -// Created by Matthias Hühne on 10.03.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. +// Created by Felix Schwarz on 07.12.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. // /* -* Copyright (C) 2020, ownCloud GmbH. -* -* This code is covered by the GNU Public License Version 3. -* -* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ -* You should have received a copy of this license along with this program. If not, see . -* -*/ + * Copyright (C) 2022, 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 UniformTypeIdentifiers import ownCloudSDK import ownCloudApp import ownCloudAppShared -import CoreServices -import UniformTypeIdentifiers extension NSErrorDomain { - static let ShareViewErrorDomain = "ShareViewErrorDomain" + static let ShareErrorDomain = "ShareErrorDomain" } -class ShareViewController: MoreStaticTableViewController { +@objc(ShareExtensionViewController) +class ShareExtensionViewController: EmbeddingViewController, Themeable { + // MARK: - Initialization + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + + ThemeStyle.registerDefaultStyles() + ShareExtensionViewController.shared = self - var willAppearInitial = false - var didAppearInitial = false + CollectionViewCellProvider.registerStandardImplementations() + CollectionViewSupplementaryCellProvider.registerStandardImplementations() + } + + @available(*, unavailable) + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + // MARK: - Entry point override func viewDidLoad() { super.viewDidLoad() + // Setup + setupServices() + + // Register for theme + Theme.shared.register(client: self, applyImmediately: true) + } + + func setupServices() { OCCoreManager.shared.memoryConfiguration = .minimum // Limit memory usage OCHTTPPipelineManager.setupPersistentPipelines() // Set up HTTP pipelines if AppLockManager.supportedOnDevice { AppLockManager.shared.passwordViewHostViewController = self AppLockManager.shared.cancelAction = { [weak self] in - self?.returnCores(completion: { - self?.extensionContext?.cancelRequest(withError: NSError(domain: NSErrorDomain.ShareViewErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "Canceled by user"])) - }) + self?.cancel() } } OCExtensionManager.shared.addExtension(CreateFolderAction.actionExtension) OCItem.registerIcons() - setupNavigationBar() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if Branding.shared.isImportMethodAllowed(.shareExtension) { - // Share extension allowed - if !willAppearInitial { - willAppearInitial = true - - if AppLockManager.supportedOnDevice { - AppLockManager.shared.showLockscreenIfNeeded() - } - - if let appexNavigationController = self.navigationController as? AppExtensionNavigationController { - appexNavigationController.dismissalAction = { [weak self] (_) in - self?.returnCores(completion: { - Log.debug("Returned all cores (share sheet was closed / dismissed)") - }) - } - } - setupAccountSelection() - } - } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !Branding.shared.isImportMethodAllowed(.shareExtension) { - // Share extension disabled, alert user - let alertController = ThemedAlertController(title: "Share Extension disabled".localized, message: "Importing files through the Share Extension is not allowed on this device.".localized, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: { [weak self] _ in - self?.extensionContext?.cancelRequest(withError: NSError(domain: NSErrorDomain.ShareViewErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "Canceled by user"])) - })) - self.navigationController?.present(alertController, animated: true, completion: nil) - } else { - // Share extension allowed - if didAppearInitial { - self.returnCores(completion: { - Log.debug("Returned all cores (back to server list)") - }) + func showLocationPicker() { + let locationPicker = ClientLocationPicker(location: .accounts, selectButtonTitle: "Save here".localized, avoidConflictsWith: nil, choiceHandler: { [weak self] folderItem, folderLocation, _, cancelled in + if cancelled { + self?.cancel() + return } - didAppearInitial = true - } - } - - private var requestedCoreBookmarks : [OCBookmark] = [] - - func requestCore(for bookmark: OCBookmark, completionHandler: @escaping (OCCore?, Error?) -> Void) { - requestedCoreBookmarks.append(bookmark) - - OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { (core, error) in - if error != nil { - // Remove only one entry, not all for that bookmark - if let index = self.requestedCoreBookmarks.firstIndex(of: bookmark) { - self.requestedCoreBookmarks.remove(at: index) - } - } - - if let core = core { - core.vault.resourceManager?.add(ResourceSourceItemIcons(core: core)) - } - - completionHandler(core, error) + self?.importTo(selectedFolder: folderItem, location: folderLocation) }) - } - - func returnCore(for bookmark: OCBookmark, completionHandler: @escaping () -> Void) { - OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { - // Remove only one entry, not all for that bookmark - if let index = self.requestedCoreBookmarks.firstIndex(of: bookmark) { - self.requestedCoreBookmarks.remove(at: index) - } - completionHandler() - }) + contentViewController = locationPicker.pickerViewControllerForPresentation() } - func returnCores(completion: (() -> Void)?) { - let waitGroup = DispatchGroup() - let returnBookmarks = requestedCoreBookmarks - - requestedCoreBookmarks = [] - - for bookmark in returnBookmarks { - waitGroup.enter() - - OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { - waitGroup.leave() - }) - } - - waitGroup.notify(queue: .main, execute: { - OnMainThread { - completion?() - } - }) - } - - @objc private func cancelAction () { - self.returnCores(completion: { - let error = NSError(domain: NSErrorDomain.ShareViewErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "Canceled by user"]) - self.extensionContext?.cancelRequest(withError: error) - }) - } - - private func setupNavigationBar() { - self.navigationItem.title = VendorServices.shared.appName - - let itemCancel = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelAction)) - self.navigationItem.setRightBarButton(itemCancel, animated: false) - } - - func setupAccountSelection() { - let title = NSAttributedString(string: "Save File".localized, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .heavy)]) - - var actionsRows: [StaticTableViewRow] = [] - OCBookmarkManager.shared.loadBookmarks() - let bookmarks : [OCBookmark] = OCBookmarkManager.shared.bookmarks as [OCBookmark] - if bookmarks.count > 0 { - if bookmarks.count > 1 { - let rowDescription = StaticTableViewRow(label: "Choose an account and folder to import into.".localized, alignment: .center) - actionsRows.append(rowDescription) - - for (bookmark) in bookmarks { - let row = StaticTableViewRow(buttonWithAction: { (_ row, _ sender) in - self.openDirectoryPicker(for: bookmark, withBackButton: true) - }, title: bookmark.shortName, style: .plain, image: UIImage(named: "bookmark-icon")?.scaledImageFitting(in: CGSize(width: 25.0, height: 25.0)), imageWidth: 25, alignment: .left) - actionsRows.append(row) - } - } else if let bookmark = bookmarks.first { - self.openDirectoryPicker(for: bookmark, withBackButton: false) - } - } else { - let rowDescription = StaticTableViewRow(label: "No account configured.\nSetup an new account in the app to save to.".localized, alignment: .center) - actionsRows.append(rowDescription) - } + // MARK: - Import + var fpServiceSession : OCFileProviderServiceSession? + var asyncQueue : OCAsyncSequentialQueue = OCAsyncSequentialQueue() - self.addSection(MoreStaticTableViewSection(headerAttributedTitle: title, identifier: "actions-section", rows: actionsRows)) - } + func importTo(selectedFolder: OCItem?, location: OCLocation?) { + if let targetFolder = selectedFolder, let bookmarkUUID = targetFolder.bookmarkUUID { + if let bookmark = OCBookmarkManager.shared.bookmark(forUUIDString: bookmarkUUID) { + let vault = OCVault(bookmark: bookmark) + self.fpServiceSession = OCFileProviderServiceSession(vault: vault) - func openDirectoryPicker(for bookmark: OCBookmark, withBackButton: Bool) { - self.requestCore(for: bookmark, completionHandler: { (core, error) in - if let core = core, error == nil { OnMainThread { - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, location: .legacyRoot, selectButtonTitle: "Save here".localized, avoidConflictsWith: [], choiceHandler: { [weak core] (selectedDirectory, _) in - if let targetDirectory = selectedDirectory { - if let vault = core?.vault { - self.fpServiceSession = OCFileProviderServiceSession(vault: vault) + let progressViewController = ProgressIndicatorViewController(initialProgressLabel: "Preparing…".localized, progress: nil, cancelHandler: {}) - self.returnCores(completion: { - OnMainThread { - self.navigationController?.popToViewController(self, animated: false) - - let progressViewController = ProgressIndicatorViewController(initialProgressLabel: "Preparing…".localized, progress: nil, cancelHandler: {}) - - self.present(progressViewController, animated: false) + self.contentViewController = progressViewController - if let fpServiceSession = self.fpServiceSession { - self.importFiles(to: targetDirectory, serviceSession: fpServiceSession, progressViewController: progressViewController, completion: { [weak self] (error) in - OnMainThread { - if let error = error { - self?.extensionContext?.cancelRequest(withError: error) - progressViewController.dismiss(animated: false) - } else { - self?.extensionContext?.completeRequest(returningItems: [], completionHandler: { (_) in - OnMainThread { - progressViewController.dismiss(animated: false) - } - }) - } - } - }) + AccountConnectionPool.shared.disconnectAll { + OnMainThread { + if let fpServiceSession = self.fpServiceSession { + self.importFiles(to: targetFolder, serviceSession: fpServiceSession, progressViewController: progressViewController, completion: { [weak self] (error) in + OnMainThread { + if let error = error { + self?.extensionContext?.cancelRequest(withError: error) + } else { + self?.extensionContext?.completeRequest(returningItems: []) } } }) } } - }) - - directoryPickerViewController.cancelAction = { [weak self] in - self?.returnCores(completion: { - OnMainThread { - self?.extensionContext?.cancelRequest(withError: NSError(domain: NSErrorDomain.ShareViewErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "Canceled by user"])) - } - }) } - if !withBackButton { - directoryPickerViewController.navigationItem.setHidesBackButton(true, animated: false) - directoryPickerViewController.navigationItem.title = VendorServices.shared.appName - } - self.navigationController?.pushViewController(directoryPickerViewController, animated: withBackButton) } } - }) - } - - var fpServiceSession : OCFileProviderServiceSession? - var asyncQueue : OCAsyncSequentialQueue = OCAsyncSequentialQueue() - - func showAlert(title: String?, message: String? = nil, error: Error? = nil, decisionHandler: @escaping ((_ continue: Bool) -> Void)) { - OnMainThread { - let message = message ?? ((error != nil) ? error?.localizedDescription : nil) - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: { (_) in - decisionHandler(false) - })) - - if let nsError = error as NSError?, nsError.domain == NSCocoaErrorDomain, nsError.code == NSXPCConnectionInvalid || nsError.code == NSXPCConnectionInterrupted { - Log.error("XPC connection error: \(String(describing: error))") - } else { - alert.addAction(UIAlertAction(title: "Continue".localized, style: .default, handler: { (_) in - decisionHandler(true) - })) - } - - (self.presentedViewController ?? self).present(alert, animated: true, completion: nil) } } @@ -319,7 +164,6 @@ class ShareViewController: MoreStaticTableViewController { } attachment.loadItem(forTypeIdentifier: type, options: nil, completionHandler: { (item, error) -> Void in - if error == nil { var data : Data? var tempFilePath : String? @@ -449,4 +293,158 @@ class ShareViewController: MoreStaticTableViewController { }) } } + + // MARK: - Events + var willAppearDidInitialRun: Bool = false + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if Branding.shared.isImportMethodAllowed(.shareExtension) { + // Share extension allowed + if !willAppearDidInitialRun { + willAppearDidInitialRun = true + + if AppLockManager.supportedOnDevice { + AppLockManager.shared.showLockscreenIfNeeded() + } + + // Check for show stoppers + if !Branding.shared.isImportMethodAllowed(.shareExtension) { + // Share extension disabled, alert user + showErrorMessage(title: "Share Extension disabled".localized, message: "Importing files through the Share Extension is not allowed on this device.".localized) + return + } + + if !OCVault.hostHasFileProvider { + // No file provider -> share extension unavailable + showErrorMessage(title: "Share Extension unavailable".localized, message: "The {{app.name}} share extension is not available on this system.".localized) + return + } + + if OCBookmarkManager.shared.bookmarks.count == 0 { + // No account configured + showErrorMessage(title: "No account configured".localized, message: "Setup a new account in the app to save to.".localized) + return + } + + // Show location picker + showLocationPicker() + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + AppLockManager.shared.appDidEnterBackground() + } + + // MARK: - Error message view + var messageView: UIView? { + didSet { + if let messageView { + let viewController = UIViewController() + + messageView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + messageView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + messageView.widthAnchor.constraint(greaterThanOrEqualToConstant: 320).isActive = true + + viewController.view.embed(centered: messageView) + + contentViewController = viewController + } + } + } + + func showErrorMessage(title: String, message: String) { + let errorMessageView = ComposedMessageView(elements: [ + .text(title, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered), + .spacing(5), + .text(message, style: .systemSecondary(textStyle: .body), alignment: .centered), + .spacing(15), + .button("OK".localized, action: UIAction(handler: { [weak self] action in + self?.cancel() + })) + ]) + + messageView = errorMessageView + } + + // MARK: - Alert view + func showAlert(title: String?, message: String? = nil, error: Error? = nil, decisionHandler: @escaping ((_ continue: Bool) -> Void)) { + OnMainThread { + let message = message ?? ((error != nil) ? error?.localizedDescription : nil) + let alert = ThemedAlertController(title: title, message: message, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: { (_) in + decisionHandler(false) + })) + + if let nsError = error as NSError?, nsError.domain == NSCocoaErrorDomain, nsError.code == NSXPCConnectionInvalid || nsError.code == NSXPCConnectionInterrupted { + Log.error("XPC connection error: \(String(describing: error))") + } else { + alert.addAction(UIAlertAction(title: "Continue".localized, style: .default, handler: { (_) in + decisionHandler(true) + })) + } + + (self.presentedViewController ?? self).present(alert, animated: true, completion: nil) + } + } + + // MARK: - Actions + func completed() { + AppLockManager.shared.appDidEnterBackground() + + AccountConnectionPool.shared.disconnectAll { + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } + } + + func cancel() { + AppLockManager.shared.appDidEnterBackground() + + AccountConnectionPool.shared.disconnectAll { + self.extensionContext?.cancelRequest(withError: NSError(domain: NSErrorDomain.ShareErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "Canceled by user"])) + } + } + + // MARK: - Themeable + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + view.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + } + + // MARK: - UserInterfaceContext glue + public static weak var shared: ShareExtensionViewController? { + didSet { + ThemeStyle.considerAppearanceUpdate() + } + } + + // MARK: - Theme change detection + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + ThemeStyle.considerAppearanceUpdate() + } + } + + // MARK: - Host App Bundle ID + override func willMove(toParent parent: UIViewController?) { + super.willMove(toParent: parent) + + OCAppIdentity.shared.hostAppBundleIdentifier = parent?.oc_hostAppBundleIdentifier + + Log.debug("Extension Host App Bundle ID: \(OCAppIdentity.shared.hostAppBundleIdentifier ?? "nil")") + } +} + +extension UserInterfaceContext : UserInterfaceContextProvider { + public func provideRootView() -> UIView? { + return ShareExtensionViewController.shared?.view + } + + public func provideCurrentWindow() -> UIWindow? { + return ShareExtensionViewController.shared?.view.window + } } diff --git a/ownCloud Share Extension/ShareNavigationController.swift b/ownCloud Share Extension/ShareNavigationController.swift deleted file mode 100644 index 84d5e9fd2..000000000 --- a/ownCloud Share Extension/ShareNavigationController.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ShareNavigationController.swift -// ownCloud Share Extension -// -// Created by Matthias Hühne on 10.03.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudSDK -import ownCloudAppShared - -@objc(ShareNavigationController) -class ShareNavigationController: AppExtensionNavigationController { - override func setupViewControllers() { - if OCVault.hostHasFileProvider { - self.setViewControllers([ShareViewController(style: .grouped)], animated: false) - } else { - let viewController = StaticTableViewController(style: .grouped) - viewController.addSection(StaticTableViewSection(headerTitle: nil, rows: [ - StaticTableViewRow(message: "The share extension is not available on this system.".localized, title: "Share Extension unavailable".localized, icon: nil, tintIcon: false, style: .warning, titleMessageSpacing: 10, imageSpacing: 0, padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), identifier: "error-message") - ])) - viewController.navigationItem.title = OCAppIdentity.shared.appDisplayName - - self.setViewControllers([viewController], animated: false) - } - } -} - -extension UserInterfaceContext : UserInterfaceContextProvider { - public func provideRootView() -> UIView? { - return AppExtensionNavigationController.mainNavigationController?.view - } - - public func provideCurrentWindow() -> UIWindow? { - return AppExtensionNavigationController.mainNavigationController?.view.window - } -} diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index c21538e2a..6b5e65b58 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -3,11 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 02072E6023E46022006548A7 /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02072E5F23E46022006548A7 /* UIWindow+Extension.swift */; }; 0233F45E246E9D960095A799 /* UploadCameraMediaAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */; }; 0234EF0E2515138B00AE921A /* PasscodeSetupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0234EF072515138A00AE921A /* PasscodeSetupCoordinator.swift */; }; 024F3A2124A3AB410083E11E /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 024F3A2024A3AB410083E11E /* CrashReporter */; }; @@ -20,18 +19,13 @@ 025FC742247D5004009307A7 /* MediaUploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025FC741247D5004009307A7 /* MediaUploadOperation.swift */; }; 025FC745247EF0F1009307A7 /* BackgroundUploadsSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025FC744247EF0F1009307A7 /* BackgroundUploadsSettingsSection.swift */; }; 02633EFF2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02633EFE2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift */; }; - 0269F589244DED02002E9D99 /* UIAlertController+UniversalLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0269F588244DED02002E9D99 /* UIAlertController+UniversalLinks.swift */; }; 0287DD7D249131E000C912CA /* AppStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287DD7C249131E000C912CA /* AppStatistics.swift */; }; 02AE32E424D2FA8B00A19476 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 02AE32E324D2FA8B00A19476 /* CrashReporter */; }; 02D4C82A255208E60000E299 /* PDFSearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D4C829255208E60000E299 /* PDFSearchResultsView.swift */; }; 02DC7C9024CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DC7C8F24CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift */; }; - 02F2891424BFAF0100E3D35C /* MigrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2891024BFAF0100E3D35C /* MigrationViewController.swift */; }; - 02F2891524BFAF0100E3D35C /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2891124BFAF0100E3D35C /* Migration.swift */; }; - 02F2891624BFAF0100E3D35C /* MigrationActivityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2891224BFAF0100E3D35C /* MigrationActivityCell.swift */; }; 232F7CAF2097260400EE22E4 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232F7CAE2097260400EE22E4 /* SettingsViewController.swift */; }; 233BDEA0204FEFE500C06732 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233BDE9F204FEFE500C06732 /* AppDelegate.swift */; }; 233BDEA7204FEFE500C06732 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 233BDEA6204FEFE500C06732 /* Assets.xcassets */; }; - 233BDEB5204FEFE500C06732 /* OwnCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233BDEB4204FEFE500C06732 /* OwnCloudTests.swift */; }; 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E0FD72099F11D00C3D8D5 /* SecuritySettingsSection.swift */; }; 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23957A6C209AFFE8003C8537 /* MoreSettingsSection.swift */; }; 23D5241521491C670002C566 /* DisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D5241421491C670002C566 /* DisplayViewController.swift */; }; @@ -42,11 +36,8 @@ 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, ); }; }; 3912208223436EB80026C290 /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3912208123436EB80026C290 /* SortMethod.swift */; }; - 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */; }; 391C79A824E186DC00CB6333 /* OCBookmark+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */; }; - 391C79AE24E187C400CB6333 /* LegacyCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2890F24BFAF0100E3D35C /* LegacyCredentials.swift */; }; 392378FF24EBD1A1006E86DE /* branding-splashscreen.png in Resources */ = {isa = PBXBuildFile; fileRef = 39F48A6624D848550000E3F9 /* branding-splashscreen.png */; }; 3923790524EBD1A5006E86DE /* branding-splashscreen-background.png in Resources */ = {isa = PBXBuildFile; fileRef = 39F48A6724D848550000E3F9 /* branding-splashscreen-background.png */; }; 392CFEB72705831700631D2B /* LAContext+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392CFEB62705831700631D2B /* LAContext+Extension.swift */; }; @@ -68,11 +59,9 @@ 3968C881239C54AC00AC28AC /* ReleaseNotesHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3968C879239C54AC00AC28AC /* ReleaseNotesHostViewController.swift */; }; 3968C882239C54AD00AC28AC /* ReleaseNotesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3968C87A239C54AC00AC28AC /* ReleaseNotesTableViewController.swift */; }; 3968C883239C54AD00AC28AC /* ReleaseNotes.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3968C87B239C54AC00AC28AC /* ReleaseNotes.plist */; }; - 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 */; }; 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754F22327A33500119FCB /* OpenSceneAction.swift */; }; - 397E276A23D04D7100117B07 /* StaticLoginSingleAccountServerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397E276923D04D7100117B07 /* StaticLoginSingleAccountServerListViewController.swift */; }; 397E276C23D05A5400117B07 /* ServerListToolCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397E276B23D05A5400117B07 /* ServerListToolCell.swift */; }; 398393BE246D63B0001A212B /* branding-login-background.png in Resources */ = {isa = PBXBuildFile; fileRef = 398393BC246D63B0001A212B /* branding-login-background.png */; }; 398393BF246D63B0001A212B /* branding-login-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 398393BD246D63B0001A212B /* branding-login-logo.png */; }; @@ -86,19 +75,15 @@ 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */; }; 399DD7C722A691BC00B45EB2 /* UnshareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */; }; 399EA6F525E6544100B6FF11 /* GroupSharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA6EE25E6544000B6FF11 /* GroupSharingTableViewController.swift */; }; - 399EA6F625E6544100B6FF11 /* ShareClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA6EF25E6544000B6FF11 /* ShareClientItemCell.swift */; }; 399EA6F725E6544100B6FF11 /* PublicLinkEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA6F025E6544000B6FF11 /* PublicLinkEditTableViewController.swift */; }; 399EA6F825E6544100B6FF11 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA6F125E6544000B6FF11 /* PublicLinkTableViewController.swift */; }; 399EA6F925E6544100B6FF11 /* SharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA6F225E6544000B6FF11 /* SharingTableViewController.swift */; }; 399EA6FA25E6544100B6FF11 /* GroupSharingEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA6F325E6544000B6FF11 /* GroupSharingEditTableViewController.swift */; }; - 399EA70725E654B400B6FF11 /* PendingSharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA70625E654B400B6FF11 /* PendingSharesTableViewController.swift */; }; 399EA71B25E6561E00B6FF11 /* OCShare+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA71A25E6561D00B6FF11 /* OCShare+Extension.swift */; }; 399EA72625E6565900B6FF11 /* OCCore+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA72525E6565900B6FF11 /* OCCore+Extension.swift */; }; 399EA73A25E656A900B6FF11 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA73925E656A900B6FF11 /* UITableView+Extension.swift */; }; 399EA74625E6575B00B6FF11 /* NotificationHUDViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA74525E6575A00B6FF11 /* NotificationHUDViewController.swift */; }; - 399EA75A25E66DB000B6FF11 /* ClientItemResolvingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399EA75925E66DB000B6FF11 /* ClientItemResolvingCell.swift */; }; - 39A243C424BDD9E100F4441F /* StaticLoginBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A243C324BDD9E100F4441F /* StaticLoginBundle.swift */; }; - 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 39A7138022E79C6700089423 /* ownCloud Intents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 39A7138022E79C6700089423 /* ownCloud Intents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 39BC9C3023DB831F0097C52D /* DocumentEditingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CD755123D787E400193950 /* DocumentEditingAction.swift */; }; 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BE385C23435AFE0062A2FE /* String+Extension.swift */; }; 39BF674C25E7FE020039663F /* CancelLabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BF674B25E7FE020039663F /* CancelLabelViewController.swift */; }; @@ -109,23 +94,19 @@ 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */; }; 39DC7CD025C2E1570001E08C /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DC7CCF25C2E1570001E08C /* DocumentActionViewController.swift */; }; 39DC7CD325C2E1570001E08C /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 39DC7CD125C2E1570001E08C /* MainInterface.storyboard */; }; - 39DC7CD725C2E1570001E08C /* ownCloud File Provider UI.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 39DC7CCD25C2E1570001E08C /* ownCloud File Provider UI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 39DC7CD725C2E1570001E08C /* ownCloud File Provider UI.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 39DC7CCD25C2E1570001E08C /* ownCloud File Provider UI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 39DC7CE725C305E40001E08C /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; 39DC7CF425C305E80001E08C /* ownCloudAppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; }; 39DF77AB24EA7BBC0066E8F0 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DCE0FC4923E42ACB0037B4AD /* Localizable.strings */; }; 39DF77D524EA854C0066E8F0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 39DF77BF24EA842C0066E8F0 /* LaunchScreen.storyboard */; }; 39E104CA24C585C30085FDDD /* (null) in Resources */ = {isa = PBXBuildFile; }; - 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; - 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */; }; 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */; }; 39EF06B325D6C3FC001E1E19 /* PresentationModeAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EF06AF25D6C3FC001E1E19 /* PresentationModeAction.swift */; }; 39F48A6A24D89D7E0000E3F9 /* branding-bookmark-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 39F48A6524D847D70000E3F9 /* branding-bookmark-icon.png */; }; - 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 */; }; 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 */; }; @@ -153,36 +134,8 @@ 4CC46D212284C677009E938F /* BookmarkInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */; }; 4CC4A21222FA20AD00AE7E2C /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC4A21122FA20AD00AE7E2C /* URL+Extensions.swift */; }; 4CC4A21922FB4F4C00AE7E2C /* MediaUploadQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */; }; - 59056CAD22414F3C00A18A22 /* ownCloudScreenshotsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59056CAC22414F3C00A18A22 /* ownCloudScreenshotsTests.swift */; }; - 59056CB422414F8000A18A22 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59371D7B224103D300C6BC5B /* SnapshotHelper.swift */; }; - 5917244E20D3DC2100809B38 /* BiometricalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917244D20D3DC2100809B38 /* BiometricalTests.swift */; }; - 59296652224CD1DB0078F13D /* OCBookmarkManager+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB05E21F9FB370067E147 /* OCBookmarkManager+Tools.swift */; }; 593A821120C7D4C5000E2A90 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; - 59538A0321E4A9C2005E543B /* CreateFolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59538A0221E4A9C2005E543B /* CreateFolderTests.swift */; }; - 59538A0B21E4C301005E543B /* MockOCQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59538A0A21E4C300005E543B /* MockOCQuery.swift */; }; - 59538A1C21E77FB7005E543B /* MockOCCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59538A1B21E77FB6005E543B /* MockOCCore.swift */; }; - 5958C9BE20C000A700E0E567 /* PasscodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5958C9BD20C000A700E0E567 /* PasscodeTests.swift */; }; - 595E2C9F21EE4BF300F0E95D /* PropfindResponseNewFolder.xml in Resources */ = {isa = PBXBuildFile; fileRef = 595E2C9E21EE4BF300F0E95D /* PropfindResponseNewFolder.xml */; }; - 595E2CA721EE4FC400F0E95D /* PropfindResponse.xml in Resources */ = {isa = PBXBuildFile; fileRef = 59B09E5C21AD61DD007827B8 /* PropfindResponse.xml */; }; - 595E2CA821EE501400F0E95D /* PropfindResponseNewFolder.xml in Resources */ = {isa = PBXBuildFile; fileRef = 595E2C9E21EE4BF300F0E95D /* PropfindResponseNewFolder.xml */; }; - 5971CF3A22046F530052FE9A /* MockClientRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5971CF3922046F530052FE9A /* MockClientRootViewController.swift */; }; - 59799F7222415758007E8008 /* ownCloudMocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; - 59799F7322415758007E8008 /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; - 59799F7422415758007E8008 /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; }; - 59799F7D22415804007E8008 /* EarlGrey.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 59799F7E22415819007E8008 /* ownCloudMocking.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; - 59799F7F22415878007E8008 /* EarlGrey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; }; - 59799F80224158FD007E8008 /* EarlGrey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F3B3E74D4B04F9CAF95C09 /* EarlGrey.swift */; }; - 59799F8122415956007E8008 /* EarlGrey+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB06021F9FC510067E147 /* EarlGrey+Tools.swift */; }; - 59B09E6421AD61DD007827B8 /* PropfindResponse.xml in Resources */ = {isa = PBXBuildFile; fileRef = 59B09E5C21AD61DD007827B8 /* PropfindResponse.xml */; }; - 59B09E6521AD61DD007827B8 /* test_certificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = 59B09E5D21AD61DD007827B8 /* test_certificate.cer */; }; - 59B09E6621AD61DD007827B8 /* test_certificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = 59B09E5D21AD61DD007827B8 /* test_certificate.cer */; }; - 59B09E6D21AD61F4007827B8 /* FileListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B09E6821AD61F4007827B8 /* FileListTests.swift */; }; - 59B09E6E21AD61F4007827B8 /* EditBookmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B09E6A21AD61F4007827B8 /* EditBookmarkTests.swift */; }; - 59B09E6F21AD61F4007827B8 /* CreateBookmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B09E6B21AD61F4007827B8 /* CreateBookmarkTests.swift */; }; - 59B09E7021AD61F4007827B8 /* UtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B09E6C21AD61F4007827B8 /* UtilsTests.swift */; }; 59D4895220C83F2E00369C2E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 59D4895420C83F2E00369C2E /* InfoPlist.strings */; }; - 6D107AA0B21417432C72755A /* EarlGrey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F3B3E74D4B04F9CAF95C09 /* EarlGrey.swift */; }; 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3A103D219D5BBA00F90C96 /* RenameAction.swift */; }; 6E3A104D219D6F0100F90C96 /* DuplicateAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3A104C219D6F0100F90C96 /* DuplicateAction.swift */; }; 6E4F1734217749910049A71B /* ImageDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4F1733217749910049A71B /* ImageDisplayViewController.swift */; }; @@ -192,8 +145,6 @@ 6E5FC172221590B000F60846 /* DisplayHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5FC171221590B000F60846 /* DisplayHostViewController.swift */; }; 6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */; }; 6EA78B8F2179B55400A5216A /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA78B8E2179B55400A5216A /* ImageScrollView.swift */; }; - 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 */; }; 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 */; }; @@ -210,7 +161,8 @@ DC080CF1238C8D850044C5D2 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC080CF0238C8D850044C5D2 /* StoreKit.framework */; }; DC080CF2238C8DF70044C5D2 /* OCLicenseAppStoreItem.h in Headers */ = {isa = PBXBuildFile; fileRef = DC080CE7238BD71F0044C5D2 /* OCLicenseAppStoreItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC080CF3238C92480044C5D2 /* OCLicenseAppStoreItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DC080CE8238BD71F0044C5D2 /* OCLicenseAppStoreItem.m */; }; - DC0A355324C0E2C200FB58FC /* ClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFED971208095E200A2D984 /* ClientItemCell.swift */; }; + DC081C89299B8E5800BFF393 /* AppStateActionRevealItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC081C88299B8E5800BFF393 /* AppStateActionRevealItem.swift */; }; + DC081C8B299B9B9000BFF393 /* AppStateActionGoToPersonalFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC081C8A299B9B9000BFF393 /* AppStateActionGoToPersonalFolder.swift */; }; DC0A355424C0E2C200FB58FC /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; DC0A355624C0E33A00FB58FC /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F18A2052BA4C00189B9A /* Log.swift */; }; DC0A355724C0E35B00FB58FC /* UIDevice+UIUserInterfaceIdiom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E22BB220C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift */; }; @@ -233,9 +185,6 @@ DC0A356F24C0E42700FB58FC /* StaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F17820519F8C00189B9A /* StaticTableViewController.swift */; }; DC0A357024C0E42700FB58FC /* StaticTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F17A20519F9D00189B9A /* StaticTableViewSection.swift */; }; DC0A357124C0E42700FB58FC /* StaticTableViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F17E2051A0D000189B9A /* StaticTableViewRow.swift */; }; - DC0A357224C0E42D00FB58FC /* PushPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC297966226E4D3100E01BC7 /* PushPresentationController.swift */; }; - DC0A357324C0E42D00FB58FC /* PushTransitionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC297964226E4D1100E01BC7 /* PushTransitionDelegate.swift */; }; - DC0A357424C0E42D00FB58FC /* PushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC297968226E52E600E01BC7 /* PushTransition.swift */; }; DC0A357524C0E43200FB58FC /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC63208621FCEE5D007EC0A8 /* ProgressView.swift */; }; DC0A357624C0E43200FB58FC /* ProgressHUDViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC018F8B20A1060A00135198 /* ProgressHUDViewController.swift */; }; DC0A357724C0E43200FB58FC /* ProgressSummarizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45C20860B5D0044BCAE /* ProgressSummarizer.swift */; }; @@ -273,12 +222,9 @@ DC0A359F24C0EBE500FB58FC /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; DC0A35A124C1091400FB58FC /* UserInterfaceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0A35A024C1091400FB58FC /* UserInterfaceContext.swift */; }; DC0A5C432550C70800E6674B /* class-settings-sdk in Resources */ = {isa = PBXBuildFile; fileRef = DC0A5C422550C70800E6674B /* class-settings-sdk */; }; - DC0B379420514E4700189B9A /* ServerListBookmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */; }; - DC0CE19228C7DBE3009ABDFB /* OpenInWebAppAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */; }; DC0CE19D28C89CD9009ABDFB /* CreateDocumentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */; }; DC18898E218A773700CFB3F9 /* ownCloudMocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; DC1B270C209CF34B004715E1 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */; }; - DC20DE5C21C01A3D0096000B /* ownCloudMocking.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; DC20DE6A21C01B210096000B /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; DC20DE6B21C01B210096000B /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; }; DC2218C62822C5B900808BCE /* OCVFSNode+FileProviderItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2218C52822C5B900808BCE /* OCVFSNode+FileProviderItem.m */; }; @@ -288,7 +234,6 @@ DC24B28725BA2A2E005783E2 /* Branding.h in Headers */ = {isa = PBXBuildFile; fileRef = DC24B27125B9DF31005783E2 /* Branding.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC24B29825BA2A34005783E2 /* Branding.m in Sources */ = {isa = PBXBuildFile; fileRef = DC24B27225B9DF31005783E2 /* Branding.m */; }; DC24B2AB25BA316D005783E2 /* Branding+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC24B2AA25BA316D005783E2 /* Branding+App.swift */; }; - DC24B31D25BB6FC4005783E2 /* IssuesCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC24B31C25BB6FC4005783E2 /* IssuesCardViewController.swift */; }; DC24E0EA28B36A81002E4F5B /* OCSearchSegment.h in Headers */ = {isa = PBXBuildFile; fileRef = DC24E0E828B36A81002E4F5B /* OCSearchSegment.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC24E0EB28B36A81002E4F5B /* OCSearchSegment.m in Sources */ = {isa = PBXBuildFile; fileRef = DC24E0E928B36A81002E4F5B /* OCSearchSegment.m */; }; DC24E0F828B41694002E4F5B /* OCQueryCondition+SearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC24E0F728B41693002E4F5B /* OCQueryCondition+SearchToken.swift */; }; @@ -305,7 +250,23 @@ DC27A1A520CBEF85008ACB6C /* OCBookmark+FileProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DC27A1A420CBEF85008ACB6C /* OCBookmark+FileProvider.m */; }; DC27A1A820CC095C008ACB6C /* OCCore+FileProviderTools.m in Sources */ = {isa = PBXBuildFile; fileRef = DC27A1A720CC095C008ACB6C /* OCCore+FileProviderTools.m */; }; DC27A1E920CC56B0008ACB6C /* FileProviderExtensionThumbnailRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = DC27A1E820CC56B0008ACB6C /* FileProviderExtensionThumbnailRequest.m */; }; - DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC29F09222976B9200F77349 /* LibrarySharesTableViewController.swift */; }; + DC28F826294B733700AC4013 /* OCItemPolicy+Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC28F825294B733700AC4013 /* OCItemPolicy+Interactions.swift */; }; + DC28F828294BB5ED00AC4013 /* SortedItemDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC28F827294BB5ED00AC4013 /* SortedItemDataSource.swift */; }; + DC298C872934A405009FA87F /* ClientDefaultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298C862934A405009FA87F /* ClientDefaultViewController.swift */; }; + DC298C922934CF56009FA87F /* AccountConnectionErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298C8C2934B3E7009FA87F /* AccountConnectionErrorHandler.swift */; }; + DC298C972934D354009FA87F /* IssuesCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC24B31C25BB6FC4005783E2 /* IssuesCardViewController.swift */; }; + DC298C982934D381009FA87F /* OCIssue+DisplayIssues.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4FEAE6209E3A7700D4476B /* OCIssue+DisplayIssues.swift */; }; + DC298C992934D3F8009FA87F /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC83303242CF3AC00153F8C /* AlertViewController.swift */; }; + DC298C9A2934D3F8009FA87F /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832F7242CC94D00153F8C /* AlertView.swift */; }; + DC298C9C2934D47D009FA87F /* ThemeCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */; }; + DC298C9E2934D6D9009FA87F /* AccountAuthenticationUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298C902934BDAD009FA87F /* AccountAuthenticationUpdater.swift */; }; + DC298C9F2934D6D9009FA87F /* AccountAuthenticationUpdaterPasswordPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6C4DD24559B1600C1EAE1 /* AccountAuthenticationUpdaterPasswordPromptViewController.swift */; }; + DC298CA129357809009FA87F /* AccountConnectionAuthErrorConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298CA029357809009FA87F /* AccountConnectionAuthErrorConsumer.swift */; }; + DC298CA72936250D009FA87F /* ClientSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1E8292B6E6B00D27573 /* ClientSidebarViewController.swift */; }; + DC298CA929362523009FA87F /* ClientLocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298CA829362523009FA87F /* ClientLocationPicker.swift */; }; + DC298CAC29362710009FA87F /* ClientLocationPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298CAB29362710009FA87F /* ClientLocationPickerViewController.swift */; }; + DC298CAE2936598B009FA87F /* DriveGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298CAD2936598B009FA87F /* DriveGridCell.swift */; }; + DC298CB029366B96009FA87F /* AccountControllerSpacesGridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC298CAF29366B96009FA87F /* AccountControllerSpacesGridViewController.swift */; }; DC2A128428D0722F0088A2B7 /* OCSavedSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2A127C28D06F060088A2B7 /* OCSavedSearch.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC2A128528D072350088A2B7 /* OCVault+SavedSearches.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2A128028D0718B0088A2B7 /* OCVault+SavedSearches.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC2A128628D0725D0088A2B7 /* OCVault+SavedSearches.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2A128128D0718B0088A2B7 /* OCVault+SavedSearches.m */; }; @@ -313,15 +274,12 @@ DC2FE2DA24C30586002AFDB3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; DC33939622E0747400DD3DA4 /* MakeAvailableOfflineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC33939522E0747400DD3DA4 /* MakeAvailableOfflineAction.swift */; }; DC33939D22E076E300DD3DA4 /* MakeUnavailableOfflineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC33939C22E076E300DD3DA4 /* MakeUnavailableOfflineAction.swift */; }; - DC3393A022E0A1C000DD3DA4 /* ItemPolicyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC33939F22E0A1C000DD3DA4 /* ItemPolicyTableViewController.swift */; }; - DC3393A222E0A71100DD3DA4 /* ItemPolicyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3393A122E0A71100DD3DA4 /* ItemPolicyCell.swift */; }; DC36885824DC98BF00333600 /* OCFileProviderServiceSession.h in Headers */ = {isa = PBXBuildFile; fileRef = DC36885624DC98BF00333600 /* OCFileProviderServiceSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC36885924DC98BF00333600 /* OCFileProviderServiceSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DC36885724DC98BF00333600 /* OCFileProviderServiceSession.m */; }; DC36885D24DD916800333600 /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; DC36885F24DD917900333600 /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; }; DC36886224DDA9AB00333600 /* ProgressIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC36886024DDA3C300333600 /* ProgressIndicatorViewController.swift */; }; DC3AB1982808C35300789435 /* ClientItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB1972808C35300789435 /* ClientItemViewController.swift */; }; - DC3AB23E280FFE3400789435 /* ItemListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB23D280FFE3400789435 /* ItemListCell.swift */; }; DC3AB240280FFF2700789435 /* NSMutableAttributedString+AppendStyled.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB23F280FFF2700789435 /* NSMutableAttributedString+AppendStyled.swift */; }; DC3AB2422810404000789435 /* DriveHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB2412810404000789435 /* DriveHeaderCell.swift */; }; DC3AB24428104AA500789435 /* UIFont+Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB24328104AA500789435 /* UIFont+Weight.swift */; }; @@ -329,12 +287,13 @@ DC3AB2482810A10300789435 /* ExpandableResourceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB2472810A10300789435 /* ExpandableResourceCell.swift */; }; DC3BE0D82077BC5D002A0AC0 /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; DC3BE0DA2077BC6B002A0AC0 /* ownCloudSDK.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DC3BE0DF2077CC14002A0AC0 /* ClientRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */; }; + DC3DDF06287E1C0800E5586D /* UIViewController+HostBundleID.m in Sources */ = {isa = PBXBuildFile; fileRef = DC3DDF03287E1AC200E5586D /* UIViewController+HostBundleID.m */; }; + DC3DDF07287E1C0E00E5586D /* UIViewController+HostBundleID.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3DDF02287E1AC200E5586D /* UIViewController+HostBundleID.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC3DEC7B22AFA1F000F3352D /* DownloadItemsHUDViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEC7A22AFA1F000F3352D /* DownloadItemsHUDViewController.swift */; }; + DC3F0C2529828AE300C832DB /* OCLocation+Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3F0C2429828AE300C832DB /* OCLocation+Breadcrumbs.swift */; }; DC3F4522271A23A000ED2383 /* AcknowledgementsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3F4521271A23A000ED2383 /* AcknowledgementsTableViewController.swift */; }; DC4332002472E1B4002DC0E5 /* OCLicenseEMMProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DC4331FE2472E1B4002DC0E5 /* OCLicenseEMMProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC4332012472E1B4002DC0E5 /* OCLicenseEMMProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DC4331FF2472E1B4002DC0E5 /* OCLicenseEMMProvider.m */; }; - DC44343E21ABFA5200376B16 /* StaticLoginProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC44343D21ABFA5200376B16 /* StaticLoginProfile.swift */; }; DC46F3C72844A75200038880 /* OCDataItem+InteractionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46F3C62844A75200038880 /* OCDataItem+InteractionProtocols.swift */; }; DC46F3CC2844A8EA00038880 /* ViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46F3CB2844A8EA00038880 /* ViewCell.swift */; }; DC46F3CE2844A92A00038880 /* ActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46F3CD2844A92A00038880 /* ActionCell.swift */; }; @@ -349,14 +308,24 @@ DC49B55A28365C5F00DAF13B /* OCVault+VFSManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DC49B55828365C5F00DAF13B /* OCVault+VFSManager.m */; }; DC49C22128524D6C00BAA910 /* ThemeableCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC49C22028524D6C00BAA910 /* ThemeableCollectionViewCell.swift */; }; DC4C575D233958B70098BAE9 /* FixedHeightImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C575C233958B70098BAE9 /* FixedHeightImageView.swift */; }; - DC4D5A0A247C1398008ADDB6 /* MessageGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D5A09247C1398008ADDB6 /* MessageGroup.swift */; }; - DC4FEAE7209E3A7700D4476B /* OCIssue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4FEAE6209E3A7700D4476B /* OCIssue+Extension.swift */; }; DC51FD922475715F0069AB79 /* CellularSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC51FD912475715F0069AB79 /* CellularSettingsViewController.swift */; }; DC576EC022647A070087316D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DC576EC222647A070087316D /* Localizable.strings */; }; + DC5C48A32918FB7400EBC053 /* CollectionSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5C48A22918FB7400EBC053 /* CollectionSidebarViewController.swift */; }; + DC60F2A629802ABE00905EC8 /* UINavigationItem+NavigationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC60F2A529802ABE00905EC8 /* UINavigationItem+NavigationContent.swift */; }; + DC60F2A829802B0900905EC8 /* NavigationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC60F2A729802B0900905EC8 /* NavigationContent.swift */; }; + DC60F2AA29802D5800905EC8 /* NavigationContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC60F2A929802D5800905EC8 /* NavigationContentItem.swift */; }; + DC6179E728E0578400C7C4E0 /* OCFileProviderSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = DC6179E528E0578400C7C4E0 /* OCFileProviderSettings.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC6179E828E0578400C7C4E0 /* OCFileProviderSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6179E628E0578400C7C4E0 /* OCFileProviderSettings.m */; }; DC625141225C904700736874 /* NSError+MessageResolution.m in Sources */ = {isa = PBXBuildFile; fileRef = DC625140225C904700736874 /* NSError+MessageResolution.m */; }; DC625148225CEB2C00736874 /* UploadFileAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC625147225CEB2C00736874 /* UploadFileAction.swift */; }; DC62514A225CEB4300736874 /* UploadMediaAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC625149225CEB4300736874 /* UploadMediaAction.swift */; }; DC62514C225D254500736874 /* UploadBaseAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC62514B225D254500736874 /* UploadBaseAction.swift */; }; + DC62F567292504060095BB5D /* AccountConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC62F566292504060095BB5D /* AccountConnection.swift */; }; + DC62F569292504510095BB5D /* AccountConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC62F568292504510095BB5D /* AccountConnectionPool.swift */; }; + DC62F56C29250DC80095BB5D /* AccountConnectionConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC62F56B29250DC80095BB5D /* AccountConnectionConsumer.swift */; }; + DC62F57429268D710095BB5D /* ClientWebAppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */; }; + DC62F57529268D710095BB5D /* OpenInWebAppAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */; }; + DC62F6F5292819C80095BB5D /* AccountConnectionRichStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC62F6F4292819C80095BB5D /* AccountConnectionRichStatus.swift */; }; DC63207E21FCA731007EC0A8 /* libzip.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = DCE93FF321FCA434000E14F2 /* libzip.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DC63208321FCAC1E007EC0A8 /* ClientActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC63208221FCAC1E007EC0A8 /* ClientActivityViewController.swift */; }; DC63208521FCEBE9007EC0A8 /* ClientActivityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC63208421FCEBE9007EC0A8 /* ClientActivityCell.swift */; }; @@ -375,12 +344,14 @@ DC66F3AC23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m in Sources */ = {isa = PBXBuildFile; fileRef = DC66F3AA23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m */; }; DC66F3AD2396630100CF4812 /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = DC66F3A823965BF400CF4812 /* AppleIncRootCertificate.cer */; }; DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC680575212DF548006C3B1F /* CertificateManagementViewController.swift */; }; - DC68057A212EAB5E006C3B1F /* ThemeCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */; }; DC6A0E5426EA9E740076B533 /* AppLockSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = DC6A0E5226EA9E740076B533 /* AppLockSettings.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC6A0E5526EA9E740076B533 /* AppLockSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6A0E5326EA9E740076B533 /* AppLockSettings.m */; }; + DC6C0A4929239E560045FF2A /* AppRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6C0A4829239E560045FF2A /* AppRootViewController.swift */; }; + DC6C0A542923FFF30045FF2A /* AccountControllerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6C0A532923FFF30045FF2A /* AccountControllerSection.swift */; }; DC6C68362574FD0400E46BD4 /* PLCrashReporter.LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = DC6C68352574FD0400E46BD4 /* PLCrashReporter.LICENSE */; }; DC6CC3152642C3560040ECAC /* ExternalBrowserBusyHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6CC3142642C3560040ECAC /* ExternalBrowserBusyHandler.swift */; }; DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6CF7FA219446050013B9F9 /* LogSettingsViewController.swift */; }; + DC6FDAF72953AD50004F0C7F /* ClientSharedWithMeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6FDAF62953AD50004F0C7F /* ClientSharedWithMeViewController.swift */; }; DC70398526128B89009F2DC1 /* NSString+ByteCountParser.h in Headers */ = {isa = PBXBuildFile; fileRef = DC70398326128B89009F2DC1 /* NSString+ByteCountParser.h */; }; DC70398626128B89009F2DC1 /* NSString+ByteCountParser.m in Sources */ = {isa = PBXBuildFile; fileRef = DC70398426128B89009F2DC1 /* NSString+ByteCountParser.m */; }; DC774E5F22F44E57000B11A1 /* ZIPArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = DC774E5D22F44E4A000B11A1 /* ZIPArchive.m */; }; @@ -392,17 +363,38 @@ DC7C101224B5FD6500227085 /* OCBookmark+AppExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = DC7C100F24B5F81E00227085 /* OCBookmark+AppExtensions.m */; }; DC7DBA37207F84BF00E7337D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7DBA36207F84BF00E7337D /* main.swift */; }; DC82663C28168D2800F91F7D /* ClientContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC82663B28168D2800F91F7D /* ClientContext.swift */; }; - DC82665028183CFD00F91F7D /* OCResourceText+ViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB24A2810A69600789435 /* OCResourceText+ViewProvider.swift */; }; DC82D6FA23171339001551C5 /* ScanAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC82D6F923171339001551C5 /* ScanAction.swift */; }; DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC854935218331CF00782BA8 /* UserInterfaceSettingsSection.swift */; }; - DC85572C20513B8C00189B9A /* ServerListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC85572A20513B8C00189B9A /* ServerListTableViewController.swift */; }; - DC85572D20513B8C00189B9A /* ServerListTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC85572B20513B8C00189B9A /* ServerListTableViewController.xib */; }; - DC869A592153B1F60088977E /* OCMockingManager+SwiftTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC869A582153B1F60088977E /* OCMockingManager+SwiftTools.swift */; }; + DC89EA572992FDCD00BFF393 /* AppStateAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89EA562992FDCD00BFF393 /* AppStateAction.swift */; }; + DC89EA5C2993A0E000BFF393 /* AppStateActionConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89EA5B2993A0E000BFF393 /* AppStateActionConnect.swift */; }; + DC89EA602993AC4F00BFF393 /* NSUserActivity+SaveRestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89EA5F2993AC4F00BFF393 /* NSUserActivity+SaveRestore.swift */; }; + DC89EA672995456A00BFF393 /* BrowserNavigationBookmark+AccountController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89EA662995456A00BFF393 /* BrowserNavigationBookmark+AccountController.swift */; }; + DC89EA6929958DF500BFF393 /* UIViewController+BrowserNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89EA6829958DF500BFF393 /* UIViewController+BrowserNavigation.swift */; }; + DC89EA6B29959BD200BFF393 /* AppStateActionRestoreNavigationBookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89EA6A29959BD200BFF393 /* AppStateActionRestoreNavigationBookmark.swift */; }; + DC8E99DC297E79E900594697 /* BrowserNavigationHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E99DB297E79E900594697 /* BrowserNavigationHistory.swift */; }; + DC8E99E2297E906200594697 /* OCLicenseQAProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DC8E99DE297E8D3800594697 /* OCLicenseQAProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC8E99E3297E906700594697 /* OCLicenseQAProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DC8E99DF297E8D3800594697 /* OCLicenseQAProvider.m */; }; + DC8E99E5297EEB2800594697 /* ClientLocationBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E99E4297EEB2800594697 /* ClientLocationBarController.swift */; }; + DC8E99E8297F3BA700594697 /* ActionTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E99E7297F3BA700594697 /* ActionTapGestureRecognizer.swift */; }; + DC8E99E9297FF5C300594697 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */; }; DC8EB271239308E5009148F9 /* LicenseOffersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8EB270239308E5009148F9 /* LicenseOffersViewController.swift */; }; + DC9219FC2966179600F538EE /* OCShare+Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9219FB2966179600F538EE /* OCShare+Interactions.swift */; }; + DC9219FD2966229100F538EE /* UniversalItemListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9219F9296615B400F538EE /* UniversalItemListCell.swift */; }; + DC921A022966D5F800F538EE /* OCShare+UniversalItemListCellContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC921A012966D5F800F538EE /* OCShare+UniversalItemListCellContentProvider.swift */; }; + DC921A042966DFDC00F538EE /* OCItem+UniversalItemListCellContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC921A032966DFDC00F538EE /* OCItem+UniversalItemListCellContentProvider.swift */; }; + DC921A0B2968BA4D00F538EE /* ClientSharedByMeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC921A0A2968BA4D00F538EE /* ClientSharedByMeViewController.swift */; }; DC973BBE24A28ED0001DEEC4 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCEC3DE3242F665D0076B43C /* CoreServices.framework */; }; DC98BBCB20FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.m in Sources */ = {isa = PBXBuildFile; fileRef = DC98BBCA20FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.m */; }; DC98BBD420FF824600F4ED3E /* FileProviderEnumeratorObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DC98BBD320FF824600F4ED3E /* FileProviderEnumeratorObserver.m */; }; - DC9A116B27D0338400D90BA4 /* ClientSpacesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9A116A27D0338400D90BA4 /* ClientSpacesTableViewController.swift */; }; + DC99154C28E636A500DA0AB8 /* SegmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC99154B28E636A500DA0AB8 /* SegmentView.swift */; }; + DC99154E28E6371500DA0AB8 /* SegmentViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC99154D28E6371500DA0AB8 /* SegmentViewItem.swift */; }; + DC99155028E63D8300DA0AB8 /* SegmentViewItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC99154F28E63D8300DA0AB8 /* SegmentViewItemView.swift */; }; + DC99155228E63ECD00DA0AB8 /* UIView+EmbedAndLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC99155128E63ECD00DA0AB8 /* UIView+EmbedAndLayout.swift */; }; + DC9B4FC3293F453C0037F8F8 /* EmbeddingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9B4FC2293F453C0037F8F8 /* EmbeddingViewController.swift */; }; + DC9B4FC52940F8D60037F8F8 /* ShareExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9B4FC42940F8D60037F8F8 /* ShareExtensionViewController.swift */; }; + DC9B4FC629413AE20037F8F8 /* OCResourceText+ViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3AB24A2810A69600789435 /* OCResourceText+ViewProvider.swift */; }; + DC9B4FCA29413B480037F8F8 /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = DC9B4FC929413B480037F8F8 /* Down */; }; + DC9B4FCE2941DA6E0037F8F8 /* UINavigationItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9B4FCD2941DA6E0037F8F8 /* UINavigationItem+Extension.swift */; }; DC9BFBB320A19AF4007064B5 /* doc in Resources */ = {isa = PBXBuildFile; fileRef = DC9BFBB220A19AF3007064B5 /* doc */; }; DC9BFBBD20A1C37B007064B5 /* PasswordManagerAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9BFBBC20A1C37B007064B5 /* PasswordManagerAccess.swift */; }; DC9C1AEC247C76470067895A /* MessageGroupCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9C1AEB247C76470067895A /* MessageGroupCell.swift */; }; @@ -411,8 +403,6 @@ DCA35D3F24CEDA5200DBE2B0 /* DiagnosticViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA35D3E24CEDA5200DBE2B0 /* DiagnosticViewController.swift */; }; DCA35D8124D1707100DBE2B0 /* OCSyncRecordActivity+DiagnosticGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA35D8024D1707100DBE2B0 /* OCSyncRecordActivity+DiagnosticGenerator.swift */; }; DCA35DA724D309B600DBE2B0 /* OCFileProviderServiceSession+UploadByFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA35DA624D309B600DBE2B0 /* OCFileProviderServiceSession+UploadByFileProvider.swift */; }; - DCAEB05F21F9FB370067E147 /* OCBookmarkManager+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB05E21F9FB370067E147 /* OCBookmarkManager+Tools.swift */; }; - DCAEB06121F9FC510067E147 /* EarlGrey+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB06021F9FC510067E147 /* EarlGrey+Tools.swift */; }; DCB2C05F250C1F9E001083CA /* BrandingClassSettingsSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB2C05D250C1F9E001083CA /* BrandingClassSettingsSource.h */; }; DCB2C061250C253C001083CA /* BrandingClassSettingsSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB2C05E250C1F9E001083CA /* BrandingClassSettingsSource.m */; }; DCB458ED2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB458EB2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -421,8 +411,31 @@ DCB5D56B2861BEBE004AF425 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB5D56A2861BEBE004AF425 /* SearchViewController.swift */; }; DCB5D5A728632C17004AF425 /* SearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB5D5A628632C17004AF425 /* SearchScope.swift */; }; DCB5D60B25FC14B6004C52D9 /* OCIssue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB5D60A25FC14B6004C52D9 /* OCIssue+Extension.swift */; }; - DCB6C4D72453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6C4D62453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift */; }; - DCB6C4DE24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6C4DD24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift */; }; + DCB6B1E6292B6BE500D27573 /* OCLocation+Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1E5292B6BE500D27573 /* OCLocation+Interactions.swift */; }; + DCB6B1EB292B7C2400D27573 /* AppRootViewController+ItemActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1EA292B7C2400D27573 /* AppRootViewController+ItemActions.swift */; }; + DCB6B1ED292B963300D27573 /* AccountConnection+ItemActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1EC292B963300D27573 /* AccountConnection+ItemActions.swift */; }; + DCB6B1EF292B979600D27573 /* MessageGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4D5A09247C1398008ADDB6 /* MessageGroup.swift */; }; + DCB6B1F0292B979A00D27573 /* MessageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832E1242C0EAC00153F8C /* MessageSelector.swift */; }; + DCB6B1F2292B9A7A00D27573 /* OCMessage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE20271249AB50E0015A22A /* OCMessage+Extension.swift */; }; + DCB6B1F5292CC46B00D27573 /* AccountController+ItemActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1F4292CC46B00D27573 /* AccountController+ItemActions.swift */; }; + DCB6B1F7292CC8E200D27573 /* OCBookmarkManager+Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1F6292CC8E200D27573 /* OCBookmarkManager+Locking.swift */; }; + DCB6B1F9292CCAF200D27573 /* OCBookmarkManager+Management.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1F8292CCAF200D27573 /* OCBookmarkManager+Management.swift */; }; + DCB6B1FB292CD76200D27573 /* OCBookmarkManager+Management.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1FA292CD76200D27573 /* OCBookmarkManager+Management.swift */; }; + DCB6B1FD292CF2DB00D27573 /* NavigationRevocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1FC292CF2DB00D27573 /* NavigationRevocationManager.swift */; }; + DCB6B200292CF2FE00D27573 /* NavigationRevocationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B1FF292CF2FE00D27573 /* NavigationRevocationAction.swift */; }; + DCB6B203292D45AD00D27573 /* NavigationRevocationTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B202292D45AD00D27573 /* NavigationRevocationTrigger.swift */; }; + DCB6B205292D859B00D27573 /* UIViewController+NavigationRevocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B204292D859B00D27573 /* UIViewController+NavigationRevocation.swift */; }; + DCB6B206292E1D8400D27573 /* RoundedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C92289500E00B254A9 /* RoundedLabel.swift */; }; + DCB6B20A292E296800D27573 /* CollectionSidebarAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B209292E296800D27573 /* CollectionSidebarAction.swift */; }; + DCB6B20C292E428000D27573 /* AccountController+ExtraItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B20B292E428000D27573 /* AccountController+ExtraItems.swift */; }; + DCB6B20F292F843800D27573 /* CollectionViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B20E292F843800D27573 /* CollectionViewAction.swift */; }; + DCBAEADB29A3674700BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEAD429A361C100BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift */; }; + DCBAEADE29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEADD29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift */; }; + DCBAEAE029A554CC00BFF393 /* CollectionViewSupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEADF29A554CC00BFF393 /* CollectionViewSupplementaryItem.swift */; }; + DCBAEAE229A55D5A00BFF393 /* ViewSupplementaryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEAE129A55D5A00BFF393 /* ViewSupplementaryCell.swift */; }; + DCBAEAE429A5603600BFF393 /* CollectionViewSupplementaryCellProvider+StandardImplementations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEAE329A5603600BFF393 /* CollectionViewSupplementaryCellProvider+StandardImplementations.swift */; }; + DCBAEAE829A568D500BFF393 /* TitleSupplementaryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEAE729A568D500BFF393 /* TitleSupplementaryCell.swift */; }; + DCBAEAED29A627D900BFF393 /* DataSourceCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEAEC29A627D900BFF393 /* DataSourceCondition.swift */; }; DCBD8EA824B3751900D92E1F /* OCItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754E123279EED00119FCB /* OCItem+Extension.swift */; }; DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC085502293ED52008CC05C /* DisplaySettingsSection.swift */; }; DCC085652293F1FD008CC05C /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; @@ -438,16 +451,13 @@ DCC5E446232654DE002E5B84 /* NSObject+AnnotatedProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC5E445232654DE002E5B84 /* NSObject+AnnotatedProperties.m */; }; DCC5E4472326564F002E5B84 /* NSObject+AnnotatedProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC5E444232654DE002E5B84 /* NSObject+AnnotatedProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC6564A20C9B7E400110A97 /* FileProviderExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC6564920C9B7E400110A97 /* FileProviderExtension.m */; }; - DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DCC6564620C9B7E300110A97 /* ownCloud File Provider.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DCC6564620C9B7E300110A97 /* ownCloud File Provider.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DCC832DE242C0C3700153F8C /* DisplaySleepPreventer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832DD242C0C3700153F8C /* DisplaySleepPreventer.swift */; }; - DCC832E2242C0EAC00153F8C /* MessageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832E1242C0EAC00153F8C /* MessageSelector.swift */; }; DCC832F1242CC27B00153F8C /* NotificationMessagePresenter.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC832E6242CB18700153F8C /* NotificationMessagePresenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC832F2242CC28400153F8C /* NotificationMessagePresenter.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC832E7242CB18700153F8C /* NotificationMessagePresenter.m */; }; DCC832F3242CC28900153F8C /* NotificationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC832E9242CB4D600153F8C /* NotificationManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC832F4242CC28F00153F8C /* NotificationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC832EA242CB4D600153F8C /* NotificationManager.m */; }; DCC832F6242CC5F700153F8C /* CardIssueMessagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832F5242CC5F700153F8C /* CardIssueMessagePresenter.swift */; }; - DCC832F8242CC94D00153F8C /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832F7242CC94D00153F8C /* AlertView.swift */; }; - DCC83304242CF3AD00153F8C /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC83303242CF3AC00153F8C /* AlertViewController.swift */; }; DCC8535823CE1236007BA3EB /* LicenseInAppProductListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC8535723CE1236007BA3EB /* LicenseInAppProductListViewController.swift */; }; DCC8536023CE1AF8007BA3EB /* PurchasesSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC8535F23CE1AF8007BA3EB /* PurchasesSettingsSection.swift */; }; DCCD77792604C91600098573 /* NSDate+ComputedTimes.h in Headers */ = {isa = PBXBuildFile; fileRef = DCCD776A2604C81B00098573 /* NSDate+ComputedTimes.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -456,13 +466,14 @@ DCD1300A23A191C000255779 /* LicenseOfferButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD1300923A191C000255779 /* LicenseOfferButton.swift */; }; DCD1301123A23F4E00255779 /* OCLicenseManager+AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD1301023A23F4E00255779 /* OCLicenseManager+AppStore.swift */; }; DCD2D40622F06ECA0071FB8F /* DataSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD2D40522F06ECA0071FB8F /* DataSettingsSection.swift */; }; + DCD68A2D291D979400993FF5 /* AccountController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD68A2C291D979400993FF5 /* AccountController.swift */; }; + DCD68A2F291D9BE400993FF5 /* AccountControllerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD68A2E291D9BE400993FF5 /* AccountControllerCell.swift */; }; DCD71E7F27427463001592C6 /* BuildOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = DCD71E7C2742745D001592C6 /* BuildOptions.h */; }; DCD71E8027427463001592C6 /* BuildOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = DCD71E7D2742745D001592C6 /* BuildOptions.m */; }; DCD8109A23984AF2003B0053 /* OCLicenseDuration.h in Headers */ = {isa = PBXBuildFile; fileRef = DCD810922398492C003B0053 /* OCLicenseDuration.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCD8109B23984AF6003B0053 /* OCLicenseDuration.m in Sources */ = {isa = PBXBuildFile; fileRef = DCD810932398492C003B0053 /* OCLicenseDuration.m */; }; DCD863FB28115C8700CA6631 /* Down.LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = DCD863FA28115C8700CA6631 /* Down.LICENSE */; }; DCD8640628115FD600CA6631 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = DCD8640528115FD600CA6631 /* OpenSSL */; }; - DCD864102811821200CA6631 /* ClientRootViewController+ItemActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD8640F2811821200CA6631 /* ClientRootViewController+ItemActions.swift */; }; DCD864122811FC5700CA6631 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD864112811FC5700CA6631 /* GradientView.swift */; }; DCD954DF247D62FA00E184E6 /* MessageTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD954DE247D62FA00E184E6 /* MessageTableViewController.swift */; }; DCD9B87B2379612B00691929 /* OCLicenseManager+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DCD9B873237960E600691929 /* OCLicenseManager+Internal.h */; }; @@ -479,11 +490,9 @@ DCDC20AC2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCDC20AA2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m */; }; DCDF58B323CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDF58B223CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift */; }; DCE0275E21F1DF7E00F2544E /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; settings = {ATTRIBUTES = (Required, ); }; }; - DCE20272249AB50E0015A22A /* OCMessage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE20271249AB50E0015A22A /* OCMessage+Extension.swift */; }; DCE28F602433683700879DEC /* ClientSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE28F5F2433683700879DEC /* ClientSessionManager.swift */; }; DCE2F03E27FADF2600E9E136 /* UICollectionViewDiffableDataSource+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE2F03D27FADF2600E9E136 /* UICollectionViewDiffableDataSource+Tools.swift */; }; DCE442CE2387452000940A6D /* LicensingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC0856B2293F1FD008CC05C /* LicensingTests.m */; }; - DCE4E43124C197450051722F /* OpenItemUserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */; }; DCE4E43524C1999A0051722F /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E37F48A2188B27D00CF16CA /* Action.swift */; }; DCE4E43724C19A910051722F /* LicenseRequirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDC209B2399A4CF003CFF5B /* LicenseRequirements.swift */; }; DCE4E43924C19AB20051722F /* MoreStaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232B01F52126B10900366FA0 /* MoreStaticTableViewController.swift */; }; @@ -493,23 +502,11 @@ DCE4E43E24C19C3E0051722F /* Action+UserInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE4E43D24C19C3E0051722F /* Action+UserInterface.swift */; }; DCE4E43F24C19D370051722F /* UIAlertController+OCIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */; }; DCE4E44124C1A07E0051722F /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; - DCE4E44424C1A3E30051722F /* FileListTableViewController+OpenItemTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE4E44324C1A3E30051722F /* FileListTableViewController+OpenItemTableViewController.swift */; }; - DCE4E44524C1A4260051722F /* FileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */; }; DCE4E44724C1AC4F0051722F /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B289A7226F1EE000BE0E11 /* MessageView.swift */; }; - DCE4E44B24C1D3780051722F /* QueryFileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC29F08F22974AEA00F77349 /* QueryFileListTableViewController.swift */; }; - DCE4E44D24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE4E44C24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift */; }; - DCE4E44F24C1DF130051722F /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */; }; DCE4E45024C1E0400051722F /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39104E0A223991C8002FC02F /* UIButton+Extension.swift */; }; - DCE4E45124C1E4430051722F /* UIBarButtonItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */; }; - DCE4E45424C1EC040051722F /* BreadCrumbTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */; }; - DCE4E45624C1ED8D0051722F /* ClientQueryViewController+InlineMessageSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE4E45524C1ED8D0051722F /* ClientQueryViewController+InlineMessageSupport.swift */; }; - DCE4E45824C1F0F40051722F /* ClientDirectoryPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2308F93C21467F6200CF0B91 /* ClientDirectoryPickerViewController.swift */; }; - DCE4E45924C1F0F70051722F /* ClientQueryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */; }; - DCE4E46B24C1F5610051722F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE4E46A24C1F5610051722F /* ShareViewController.swift */; }; - DCE4E47224C1F5610051722F /* ownCloud Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DCE4E46824C1F5610051722F /* ownCloud Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DCE4E47224C1F5610051722F /* ownCloud Share Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DCE4E46824C1F5610051722F /* ownCloud Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DCE4E47B24C1F5870051722F /* ownCloudAppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; }; DCE4E48024C1F58D0051722F /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; - DCE4E48624C1F6B50051722F /* ShareNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE4E48524C1F6B50051722F /* ShareNavigationController.swift */; }; DCE4E48724C1F9F50051722F /* CreateFolderAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */; }; DCE4E48824C1FA430051722F /* NamingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D77FC6212BFBD100DE76F1 /* NamingViewController.swift */; }; DCE4E48924C1FB750051722F /* application-pdf.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DCE5E89A2080D780005F60CE /* application-pdf.tvg */; }; @@ -567,16 +564,14 @@ DCF575EC2796CBDF003BEBBA /* OCImage+ViewProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF575EA2796CBDF003BEBBA /* OCImage+ViewProvider.m */; }; DCF575EF2796CE38003BEBBA /* OCViewHost.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF575ED2796CE38003BEBBA /* OCViewHost.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCF575F02796CE38003BEBBA /* OCViewHost.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF575EE2796CE38003BEBBA /* OCViewHost.m */; }; - DCFB74BB21AD5C46005796AF /* StaticLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC44344321AC031600376B16 /* StaticLoginViewController.swift */; }; - DCFB74C121AD5C88005796AF /* StaticLoginStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4434C321AC894700376B16 /* StaticLoginStepViewController.swift */; }; - DCFB74C221AD5D10005796AF /* StaticLoginSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4434C521AC898700376B16 /* StaticLoginSetupViewController.swift */; }; - DCFB74C421AD7E18005796AF /* StaticLoginServerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFB74C321AD7E18005796AF /* StaticLoginServerListViewController.swift */; }; + DCFA56432975734A0092C89F /* BrowserNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFA56422975734A0092C89F /* BrowserNavigationViewController.swift */; }; + DCFA5645297574A60092C89F /* BrowserNavigationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFA5644297574A60092C89F /* BrowserNavigationItem.swift */; }; + DCFA9CAF2987E14B00004F24 /* BrowserNavigationBookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFA9CAB2987E14B00004F24 /* BrowserNavigationBookmark.swift */; }; DCFBAD0C21BE67A100943F76 /* ownCloudUI.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DCFC9ECC28002303005D9144 /* CollectionViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ECB28002303005D9144 /* CollectionViewSection.swift */; }; DCFC9ED128002335005D9144 /* CollectionViewCellProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ED028002335005D9144 /* CollectionViewCellProvider.swift */; }; DCFC9ED3280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ED2280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift */; }; DCFC9ED528002F33005D9144 /* CollectionViewCellConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ED428002F33005D9144 /* CollectionViewCellConfiguration.swift */; }; - DCFE682728D869A400091D2A /* ClientWebAppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */; }; DCFE682E28D9CEDD00091D2A /* ComposedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFE682D28D9CEDD00091D2A /* ComposedMessageView.swift */; }; DCFEF90926EFA45A001DC7A4 /* VendorServices+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFEF90526EFA45A001DC7A4 /* VendorServices+App.swift */; }; DCFEFE2A236876BD009A142F /* OCLicenseManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE28236876BD009A142F /* OCLicenseManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -596,9 +591,6 @@ DCFEFE982368D099009A142F /* OCLicenseEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE962368D099009A142F /* OCLicenseEnvironment.m */; }; DCFEFE9C2368D7FA009A142F /* OCLicenseObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE9A2368D7FA009A142F /* OCLicenseObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCFEFE9D2368D7FA009A142F /* OCLicenseObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE9B2368D7FA009A142F /* OCLicenseObserver.m */; }; - EA1D571C6B1E95925C459228 /* EarlGrey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; }; - EA88A55521BFD5BF0055A58F /* DeleteBookmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA88A55421BFD5BF0055A58F /* DeleteBookmarkTests.swift */; }; - EA9337E32226DB070054971F /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9337E22226DB070054971F /* SettingsTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -693,13 +685,6 @@ remoteGlobalIDString = 394A0AF822EEFC2C00603813; remoteInfo = ownCloudAppShared; }; - 59056CAF22414F3C00A18A22 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 233BDE94204FEFE500C06732 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 233BDE9B204FEFE500C06732; - remoteInfo = ownCloud; - }; DC0196A520F754CA00C41B78 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 233BDEBF204FEFF300C06732 /* ownCloudSDK.xcodeproj */; @@ -881,30 +866,6 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 59799F7C224157E0007E8008 /* EarlGrey Copy Files */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(TEST_HOST)/.."; - dstSubfolderSpec = 0; - files = ( - 59799F7E22415819007E8008 /* ownCloudMocking.framework in EarlGrey Copy Files */, - 59799F7D22415804007E8008 /* EarlGrey.framework in EarlGrey Copy Files */, - ); - name = "EarlGrey Copy Files"; - runOnlyForDeploymentPostprocessing = 0; - }; - D9876310DCEA650662AA6AF7 /* EarlGrey Copy Files */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(TEST_HOST)/.."; - dstSubfolderSpec = 0; - files = ( - DC20DE5C21C01A3D0096000B /* ownCloudMocking.framework in EarlGrey Copy Files */, - A45A8D98137C902524B84E6D /* EarlGrey.framework in EarlGrey Copy Files */, - ); - name = "EarlGrey Copy Files"; - runOnlyForDeploymentPostprocessing = 0; - }; DC7DBA32207F84BF00E7337D /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -929,24 +890,23 @@ name = "Copy Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - DCC6567020C9B7E400110A97 /* Embed App Extensions */ = { + DCC6567020C9B7E400110A97 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed App Extensions */, - DCE4E47224C1F5610051722F /* ownCloud Share Extension.appex in Embed App Extensions */, - DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed App Extensions */, - 39DC7CD725C2E1570001E08C /* ownCloud File Provider UI.appex in Embed App Extensions */, + 39A7138722E79C6700089423 /* ownCloud Intents.appex in Embed Foundation Extensions */, + DCE4E47224C1F5610051722F /* ownCloud Share Extension.appex in Embed Foundation Extensions */, + DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed Foundation Extensions */, + 39DC7CD725C2E1570001E08C /* ownCloud File Provider UI.appex in Embed Foundation Extensions */, ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 02072E5F23E46022006548A7 /* UIWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extension.swift"; sourceTree = ""; }; 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */ = {isa = PBXFileReference; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = UploadCameraMediaAction.swift; sourceTree = ""; }; 0234EF072515138A00AE921A /* PasscodeSetupCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeSetupCoordinator.swift; sourceTree = ""; }; 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayExifMetadataAction.swift; sourceTree = ""; }; @@ -958,16 +918,9 @@ 025FC741247D5004009307A7 /* MediaUploadOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadOperation.swift; sourceTree = ""; }; 025FC744247EF0F1009307A7 /* BackgroundUploadsSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundUploadsSettingsSection.swift; sourceTree = ""; }; 02633EFE2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNUserNotificationCenter+Extensions.swift"; sourceTree = ""; }; - 0269F588244DED02002E9D99 /* UIAlertController+UniversalLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+UniversalLinks.swift"; sourceTree = ""; }; 0287DD7C249131E000C912CA /* AppStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStatistics.swift; sourceTree = ""; }; 02D4C829255208E60000E299 /* PDFSearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSearchResultsView.swift; sourceTree = ""; }; 02DC7C8F24CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProPhotoUploadSettingsSection.swift; sourceTree = ""; }; - 02F2890F24BFAF0100E3D35C /* LegacyCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCredentials.swift; sourceTree = ""; }; - 02F2891024BFAF0100E3D35C /* MigrationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationViewController.swift; sourceTree = ""; }; - 02F2891124BFAF0100E3D35C /* Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; - 02F2891224BFAF0100E3D35C /* MigrationActivityCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationActivityCell.swift; sourceTree = ""; }; - 03AE98EEF23B4F4F2C0FDD0F /* Pods-ownCloudTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloudTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloudTests/Pods-ownCloudTests.debug.xcconfig"; sourceTree = ""; }; - 2308F93C21467F6200CF0B91 /* ClientDirectoryPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientDirectoryPickerViewController.swift; sourceTree = ""; }; 232B01F32126B0CE00366FA0 /* MoreViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreViewHeader.swift; sourceTree = ""; }; 232B01F52126B10900366FA0 /* MoreStaticTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreStaticTableViewController.swift; sourceTree = ""; }; 232F7CAE2097260400EE22E4 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; @@ -976,7 +929,6 @@ 233BDEA6204FEFE500C06732 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 233BDEAB204FEFE500C06732 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 233BDEB0204FEFE500C06732 /* ownCloudTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ownCloudTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 233BDEB4204FEFE500C06732 /* OwnCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnCloudTests.swift; sourceTree = ""; }; 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; }; @@ -1000,20 +952,16 @@ 3912208123436EB80026C290 /* SortMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 391220922344C30F0026C290 /* ImportPasteboardAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportPasteboardAction.swift; sourceTree = ""; }; 391220932344C30F0026C290 /* CutAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CutAction.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 = ""; }; 392CFEB62705831700631D2B /* LAContext+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LAContext+Extension.swift"; sourceTree = ""; }; 392DDB1324CF024C009E5406 /* ImportFilesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportFilesController.swift; sourceTree = ""; }; 3931206A2326451900E8DFBA /* Branding.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Branding.plist; path = ownCloud/Resources/Theming/Branding.plist; sourceTree = SOURCE_ROOT; }; 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; }; 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 = ""; }; 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 = ""; }; 39534BC824EA903200AD7907 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 39534BCA24EA903800AD7907 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 39534BCB24EA903B00AD7907 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1072,17 +1020,14 @@ 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 = ""; }; 399EA6EE25E6544000B6FF11 /* GroupSharingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupSharingTableViewController.swift; sourceTree = ""; }; - 399EA6EF25E6544000B6FF11 /* ShareClientItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareClientItemCell.swift; sourceTree = ""; }; 399EA6F025E6544000B6FF11 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; 399EA6F125E6544000B6FF11 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; 399EA6F225E6544000B6FF11 /* SharingTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTableViewController.swift; sourceTree = ""; }; 399EA6F325E6544000B6FF11 /* GroupSharingEditTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupSharingEditTableViewController.swift; sourceTree = ""; }; - 399EA70625E654B400B6FF11 /* PendingSharesTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingSharesTableViewController.swift; sourceTree = ""; }; 399EA71A25E6561D00B6FF11 /* OCShare+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCShare+Extension.swift"; sourceTree = ""; }; 399EA72525E6565900B6FF11 /* OCCore+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCCore+Extension.swift"; sourceTree = ""; }; 399EA73925E656A900B6FF11 /* UITableView+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 399EA74525E6575A00B6FF11 /* NotificationHUDViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationHUDViewController.swift; sourceTree = ""; }; - 399EA75925E66DB000B6FF11 /* ClientItemResolvingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientItemResolvingCell.swift; sourceTree = ""; }; 39A243C324BDD9E100F4441F /* StaticLoginBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticLoginBundle.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; }; @@ -1136,7 +1081,6 @@ 39DF77BA24EA7F1D0066E8F0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 39DF77BB24EA7F1E0066E8F0 /* th-TH */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "th-TH"; path = "th-TH.lproj/Localizable.strings"; sourceTree = ""; }; 39DF77BF24EA842C0066E8F0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; 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 = ""; }; 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCItemTracker.swift; sourceTree = ""; }; @@ -1146,13 +1090,11 @@ 39F48A6724D848550000E3F9 /* branding-splashscreen-background.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "branding-splashscreen-background.png"; path = "ownCloud/Resources/Theming/branding-splashscreen-background.png"; sourceTree = SOURCE_ROOT; }; 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; }; 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 = ""; }; @@ -1166,7 +1108,6 @@ 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 = ""; }; @@ -1182,24 +1123,12 @@ 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkInfoViewController.swift; sourceTree = ""; }; 4CC4A21122FA20AD00AE7E2C /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadQueue.swift; sourceTree = ""; }; - 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Extension.swift"; sourceTree = ""; }; 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloudScreenshotsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 59056CAA22414F3C00A18A22 /* ownCloudScreenshotsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ownCloudScreenshotsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 59056CAC22414F3C00A18A22 /* ownCloudScreenshotsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ownCloudScreenshotsTests.swift; sourceTree = ""; }; - 59056CAE22414F3C00A18A22 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5917244D20D3DC2100809B38 /* BiometricalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricalTests.swift; sourceTree = ""; }; - 59371D7B224103D300C6BC5B /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = ownCloudScreenshotsTests/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; }; 593A821220C7D4C5000E2A90 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 593A821820C7D4DC000E2A90 /* es */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; lineEnding = 0; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 593BAB44209AE1BC00023634 /* PasscodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeViewController.swift; sourceTree = ""; }; 593BAB45209AE1BC00023634 /* PasscodeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PasscodeViewController.xib; sourceTree = ""; }; 593BAB96209F8A0500023634 /* AppLockManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockManager.swift; sourceTree = ""; }; - 59538A0221E4A9C2005E543B /* CreateFolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFolderTests.swift; sourceTree = ""; }; - 59538A0A21E4C300005E543B /* MockOCQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOCQuery.swift; sourceTree = ""; }; - 59538A1B21E77FB6005E543B /* MockOCCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOCCore.swift; sourceTree = ""; }; - 5958C9BD20C000A700E0E567 /* PasscodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeTests.swift; sourceTree = ""; }; - 595E2C9E21EE4BF300F0E95D /* PropfindResponseNewFolder.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PropfindResponseNewFolder.xml; sourceTree = ""; }; - 5971CF3922046F530052FE9A /* MockClientRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientRootViewController.swift; sourceTree = ""; }; 597A404820AD59EF00B028B2 /* AppLockWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockWindow.swift; sourceTree = ""; }; 59AAD95921F76B6800D15F07 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; 59AAD95A21F76B6800D15F07 /* cs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; @@ -1227,15 +1156,8 @@ 59AAD98D21F786CC00D15F07 /* pt-PT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; 59AAD98E21F7870B00D15F07 /* th-TH */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "th-TH"; path = "th-TH.lproj/InfoPlist.strings"; sourceTree = ""; }; 59AAD98F21F7870B00D15F07 /* th-TH */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; lineEnding = 0; name = "th-TH"; path = "th-TH.lproj/Localizable.strings"; sourceTree = ""; }; - 59B09E5C21AD61DD007827B8 /* PropfindResponse.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PropfindResponse.xml; sourceTree = ""; }; - 59B09E5D21AD61DD007827B8 /* test_certificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_certificate.cer; sourceTree = ""; }; - 59B09E6821AD61F4007827B8 /* FileListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileListTests.swift; sourceTree = ""; }; - 59B09E6A21AD61F4007827B8 /* EditBookmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditBookmarkTests.swift; sourceTree = ""; }; - 59B09E6B21AD61F4007827B8 /* CreateBookmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateBookmarkTests.swift; sourceTree = ""; usesTabs = 1; }; - 59B09E6C21AD61F4007827B8 /* UtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsTests.swift; sourceTree = ""; }; 59D4895320C83F2E00369C2E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 59EACA8020CAA37F00F082EE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; - 5AA495E19A4CA58CF2678112 /* Pods-ownCloudScreenshotsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloudScreenshotsTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloudScreenshotsTests/Pods-ownCloudScreenshotsTests.release.xcconfig"; sourceTree = ""; }; 6E37F48A2188B27D00CF16CA /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; }; 6E3A103D219D5BBA00F90C96 /* RenameAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameAction.swift; sourceTree = ""; }; 6E3A104C219D6F0100F90C96 /* DuplicateAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuplicateAction.swift; sourceTree = ""; }; @@ -1249,10 +1171,7 @@ 6EB8EDBE2114358300C2BF44 /* folder-create.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "folder-create.tvg"; path = "img/filetypes-tvg/folder-create.tvg"; sourceTree = SOURCE_ROOT; }; 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFolderAction.swift; sourceTree = ""; }; A56EA84D8AD331FFA604138B /* Pods_ownCloudTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloudTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A80C5F83C59F9D52F8F64890 /* Pods-ownCloudScreenshotsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloudScreenshotsTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloudScreenshotsTests/Pods-ownCloudScreenshotsTests.debug.xcconfig"; sourceTree = ""; }; 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 = ""; }; DC018F8B20A1060A00135198 /* ProgressHUDViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressHUDViewController.swift; sourceTree = ""; }; @@ -1267,6 +1186,8 @@ DC080CE7238BD71F0044C5D2 /* OCLicenseAppStoreItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseAppStoreItem.h; sourceTree = ""; }; DC080CE8238BD71F0044C5D2 /* OCLicenseAppStoreItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseAppStoreItem.m; sourceTree = ""; }; DC080CF0238C8D850044C5D2 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + DC081C88299B8E5800BFF393 /* AppStateActionRevealItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateActionRevealItem.swift; sourceTree = ""; }; + DC081C8A299B9B9000BFF393 /* AppStateActionGoToPersonalFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateActionGoToPersonalFolder.swift; sourceTree = ""; }; DC0A35A024C1091400FB58FC /* UserInterfaceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceContext.swift; sourceTree = ""; }; DC0A5C422550C70800E6674B /* class-settings-sdk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "class-settings-sdk"; path = "ios-sdk/doc/class-settings-sdk"; sourceTree = SOURCE_ROOT; }; DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListBookmarkCell.swift; sourceTree = ""; }; @@ -1309,11 +1230,16 @@ DC27A1A720CC095C008ACB6C /* OCCore+FileProviderTools.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCCore+FileProviderTools.m"; sourceTree = ""; }; DC27A1E720CC56B0008ACB6C /* FileProviderExtensionThumbnailRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileProviderExtensionThumbnailRequest.h; sourceTree = ""; }; DC27A1E820CC56B0008ACB6C /* FileProviderExtensionThumbnailRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileProviderExtensionThumbnailRequest.m; sourceTree = ""; }; - DC297964226E4D1100E01BC7 /* PushTransitionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTransitionDelegate.swift; sourceTree = ""; }; - DC297966226E4D3100E01BC7 /* PushPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushPresentationController.swift; sourceTree = ""; }; - DC297968226E52E600E01BC7 /* PushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTransition.swift; sourceTree = ""; }; - DC29F08F22974AEA00F77349 /* QueryFileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryFileListTableViewController.swift; sourceTree = ""; }; - DC29F09222976B9200F77349 /* LibrarySharesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LibrarySharesTableViewController.swift; path = ownCloud/Client/Library/LibrarySharesTableViewController.swift; sourceTree = SOURCE_ROOT; }; + DC28F825294B733700AC4013 /* OCItemPolicy+Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCItemPolicy+Interactions.swift"; sourceTree = ""; }; + DC28F827294BB5ED00AC4013 /* SortedItemDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortedItemDataSource.swift; sourceTree = ""; }; + DC298C862934A405009FA87F /* ClientDefaultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientDefaultViewController.swift; sourceTree = ""; }; + DC298C8C2934B3E7009FA87F /* AccountConnectionErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConnectionErrorHandler.swift; sourceTree = ""; }; + DC298C902934BDAD009FA87F /* AccountAuthenticationUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAuthenticationUpdater.swift; sourceTree = ""; }; + DC298CA029357809009FA87F /* AccountConnectionAuthErrorConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConnectionAuthErrorConsumer.swift; sourceTree = ""; }; + DC298CA829362523009FA87F /* ClientLocationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientLocationPicker.swift; sourceTree = ""; }; + DC298CAB29362710009FA87F /* ClientLocationPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientLocationPickerViewController.swift; sourceTree = ""; }; + DC298CAD2936598B009FA87F /* DriveGridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DriveGridCell.swift; sourceTree = ""; }; + DC298CAF29366B96009FA87F /* AccountControllerSpacesGridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountControllerSpacesGridViewController.swift; sourceTree = ""; }; DC2A127C28D06F060088A2B7 /* OCSavedSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCSavedSearch.h; sourceTree = ""; }; DC2A127D28D06F060088A2B7 /* OCSavedSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSavedSearch.m; sourceTree = ""; }; DC2A128028D0718B0088A2B7 /* OCVault+SavedSearches.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCVault+SavedSearches.h"; sourceTree = ""; }; @@ -1321,14 +1247,11 @@ DC321260207EB01B00DB171D /* ThemeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeImage.swift; sourceTree = ""; }; DC33939522E0747400DD3DA4 /* MakeAvailableOfflineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeAvailableOfflineAction.swift; sourceTree = ""; }; DC33939C22E076E300DD3DA4 /* MakeUnavailableOfflineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeUnavailableOfflineAction.swift; sourceTree = ""; }; - DC33939F22E0A1C000DD3DA4 /* ItemPolicyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPolicyTableViewController.swift; sourceTree = ""; }; - DC3393A122E0A71100DD3DA4 /* ItemPolicyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPolicyCell.swift; sourceTree = ""; }; DC3393A722E0C4ED00DD3DA4 /* icon-available-offline.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "icon-available-offline.tvg"; path = "../img/filetypes-tvg/icon-available-offline.tvg"; sourceTree = ""; }; DC36885624DC98BF00333600 /* OCFileProviderServiceSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCFileProviderServiceSession.h; sourceTree = ""; }; DC36885724DC98BF00333600 /* OCFileProviderServiceSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCFileProviderServiceSession.m; sourceTree = ""; }; DC36886024DDA3C300333600 /* ProgressIndicatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicatorViewController.swift; sourceTree = ""; }; DC3AB1972808C35300789435 /* ClientItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientItemViewController.swift; sourceTree = ""; }; - DC3AB23D280FFE3400789435 /* ItemListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListCell.swift; sourceTree = ""; }; DC3AB23F280FFF2700789435 /* NSMutableAttributedString+AppendStyled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+AppendStyled.swift"; sourceTree = ""; }; DC3AB2412810404000789435 /* DriveHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DriveHeaderCell.swift; sourceTree = ""; }; DC3AB24328104AA500789435 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = ""; }; @@ -1336,12 +1259,13 @@ DC3AB2472810A10300789435 /* ExpandableResourceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableResourceCell.swift; sourceTree = ""; }; DC3AB24A2810A69600789435 /* OCResourceText+ViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCResourceText+ViewProvider.swift"; sourceTree = ""; }; DC3BAC63282472A80057FCD4 /* KNOWN_ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = KNOWN_ISSUES.md; sourceTree = ""; }; - DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientQueryViewController.swift; sourceTree = ""; }; - DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientRootViewController.swift; sourceTree = ""; }; DC3BE0E02077CD4B002A0AC0 /* Synchronized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Synchronized.swift; sourceTree = ""; }; + DC3DDF02287E1AC200E5586D /* UIViewController+HostBundleID.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+HostBundleID.h"; sourceTree = ""; }; + DC3DDF03287E1AC200E5586D /* UIViewController+HostBundleID.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+HostBundleID.m"; sourceTree = ""; }; DC3DEC7A22AFA1F000F3352D /* DownloadItemsHUDViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemsHUDViewController.swift; sourceTree = ""; }; DC3DEC7C22AFFE8E00F3352D /* KVOWaiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KVOWaiter.swift; sourceTree = ""; }; DC3DEC7F22B03AE700F3352D /* CardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewController.swift; sourceTree = ""; }; + DC3F0C2429828AE300C832DB /* OCLocation+Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCLocation+Breadcrumbs.swift"; sourceTree = ""; }; DC3F4521271A23A000ED2383 /* AcknowledgementsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsTableViewController.swift; sourceTree = ""; }; DC422449207CAFAA0006A2A6 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; DC42244B207CAFBB0006A2A6 /* ThemeCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCollection.swift; sourceTree = ""; }; @@ -1368,16 +1292,28 @@ DC49C22028524D6C00BAA910 /* ThemeableCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableCollectionViewCell.swift; sourceTree = ""; }; DC4C575C233958B70098BAE9 /* FixedHeightImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedHeightImageView.swift; sourceTree = ""; }; DC4D5A09247C1398008ADDB6 /* MessageGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGroup.swift; sourceTree = ""; }; - DC4FEAE6209E3A7700D4476B /* OCIssue+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCIssue+Extension.swift"; sourceTree = ""; }; + DC4FEAE6209E3A7700D4476B /* OCIssue+DisplayIssues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCIssue+DisplayIssues.swift"; sourceTree = ""; }; DC4FEAE9209E48E800D4476B /* DispatchQueueTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueTools.swift; sourceTree = ""; }; DC51FD912475715F0069AB79 /* CellularSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellularSettingsViewController.swift; sourceTree = ""; }; DC576EC122647A070087316D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + DC5C48A22918FB7400EBC053 /* CollectionSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionSidebarViewController.swift; sourceTree = ""; }; DC5D9E742496512400BFFE8E /* MessageQueueExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageQueueExample.swift; sourceTree = ""; }; + DC60F2A529802ABE00905EC8 /* UINavigationItem+NavigationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationItem+NavigationContent.swift"; sourceTree = ""; }; + DC60F2A729802B0900905EC8 /* NavigationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationContent.swift; sourceTree = ""; }; + DC60F2A929802D5800905EC8 /* NavigationContentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationContentItem.swift; sourceTree = ""; }; + DC60F2AB29812A9B00905EC8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DC6179E528E0578400C7C4E0 /* OCFileProviderSettings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCFileProviderSettings.h; sourceTree = ""; }; + DC6179E628E0578400C7C4E0 /* OCFileProviderSettings.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCFileProviderSettings.m; sourceTree = ""; }; DC62513F225C904700736874 /* NSError+MessageResolution.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSError+MessageResolution.h"; sourceTree = ""; }; DC625140225C904700736874 /* NSError+MessageResolution.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+MessageResolution.m"; sourceTree = ""; }; DC625147225CEB2C00736874 /* UploadFileAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadFileAction.swift; sourceTree = ""; }; DC625149225CEB4300736874 /* UploadMediaAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadMediaAction.swift; sourceTree = ""; }; DC62514B225D254500736874 /* UploadBaseAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadBaseAction.swift; sourceTree = ""; }; + DC62F566292504060095BB5D /* AccountConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConnection.swift; sourceTree = ""; }; + DC62F568292504510095BB5D /* AccountConnectionPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConnectionPool.swift; sourceTree = ""; }; + DC62F56B29250DC80095BB5D /* AccountConnectionConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConnectionConsumer.swift; sourceTree = ""; }; + DC62F56D2925112D0095BB5D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DC62F6F4292819C80095BB5D /* AccountConnectionRichStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConnectionRichStatus.swift; sourceTree = ""; }; DC63208221FCAC1E007EC0A8 /* ClientActivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientActivityViewController.swift; sourceTree = ""; }; DC63208421FCEBE9007EC0A8 /* ClientActivityCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientActivityCell.swift; sourceTree = ""; }; DC63208621FCEE5D007EC0A8 /* ProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; @@ -1398,9 +1334,12 @@ DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCertificateViewController.swift; sourceTree = ""; }; DC6A0E5226EA9E740076B533 /* AppLockSettings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppLockSettings.h; sourceTree = ""; }; DC6A0E5326EA9E740076B533 /* AppLockSettings.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppLockSettings.m; sourceTree = ""; }; + DC6C0A4829239E560045FF2A /* AppRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootViewController.swift; sourceTree = ""; }; + DC6C0A532923FFF30045FF2A /* AccountControllerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountControllerSection.swift; sourceTree = ""; }; DC6C68352574FD0400E46BD4 /* PLCrashReporter.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = PLCrashReporter.LICENSE; sourceTree = ""; }; DC6CC3142642C3560040ECAC /* ExternalBrowserBusyHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalBrowserBusyHandler.swift; sourceTree = ""; }; DC6CF7FA219446050013B9F9 /* LogSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogSettingsViewController.swift; sourceTree = ""; }; + DC6FDAF62953AD50004F0C7F /* ClientSharedWithMeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSharedWithMeViewController.swift; sourceTree = ""; }; DC70398326128B89009F2DC1 /* NSString+ByteCountParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+ByteCountParser.h"; sourceTree = ""; }; DC70398426128B89009F2DC1 /* NSString+ByteCountParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+ByteCountParser.m"; sourceTree = ""; }; DC774E5C22F44E4A000B11A1 /* ZIPArchive.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ZIPArchive.h; sourceTree = ""; }; @@ -1421,16 +1360,36 @@ DC85493321831B0B00782BA8 /* GitCommit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitCommit.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 = ""; }; - DC85572A20513B8C00189B9A /* ServerListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableViewController.swift; sourceTree = ""; }; - DC85572B20513B8C00189B9A /* ServerListTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ServerListTableViewController.xib; sourceTree = ""; }; - DC869A582153B1F60088977E /* OCMockingManager+SwiftTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCMockingManager+SwiftTools.swift"; sourceTree = ""; }; DC89C45C20860B5D0044BCAE /* ProgressSummarizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSummarizer.swift; sourceTree = ""; }; + DC89EA562992FDCD00BFF393 /* AppStateAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateAction.swift; sourceTree = ""; }; + DC89EA5B2993A0E000BFF393 /* AppStateActionConnect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateActionConnect.swift; sourceTree = ""; }; + DC89EA5E2993A0FE00BFF393 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DC89EA5F2993AC4F00BFF393 /* NSUserActivity+SaveRestore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+SaveRestore.swift"; sourceTree = ""; }; + DC89EA662995456A00BFF393 /* BrowserNavigationBookmark+AccountController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserNavigationBookmark+AccountController.swift"; sourceTree = ""; }; + DC89EA6829958DF500BFF393 /* UIViewController+BrowserNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+BrowserNavigation.swift"; sourceTree = ""; }; + DC89EA6A29959BD200BFF393 /* AppStateActionRestoreNavigationBookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateActionRestoreNavigationBookmark.swift; sourceTree = ""; }; + DC8E99DB297E79E900594697 /* BrowserNavigationHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationHistory.swift; sourceTree = ""; }; + DC8E99DE297E8D3800594697 /* OCLicenseQAProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseQAProvider.h; sourceTree = ""; }; + DC8E99DF297E8D3800594697 /* OCLicenseQAProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseQAProvider.m; sourceTree = ""; }; + DC8E99E4297EEB2800594697 /* ClientLocationBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientLocationBarController.swift; sourceTree = ""; }; + DC8E99E7297F3BA700594697 /* ActionTapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionTapGestureRecognizer.swift; sourceTree = ""; }; DC8EB270239308E5009148F9 /* LicenseOffersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseOffersViewController.swift; sourceTree = ""; }; + DC9219F9296615B400F538EE /* UniversalItemListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalItemListCell.swift; sourceTree = ""; }; + DC9219FB2966179600F538EE /* OCShare+Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCShare+Interactions.swift"; sourceTree = ""; }; + DC921A012966D5F800F538EE /* OCShare+UniversalItemListCellContentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCShare+UniversalItemListCellContentProvider.swift"; sourceTree = ""; }; + DC921A032966DFDC00F538EE /* OCItem+UniversalItemListCellContentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCItem+UniversalItemListCellContentProvider.swift"; sourceTree = ""; }; + DC921A0A2968BA4D00F538EE /* ClientSharedByMeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSharedByMeViewController.swift; sourceTree = ""; }; DC98BBC920FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSNumber+OCSyncAnchorData.h"; sourceTree = ""; }; DC98BBCA20FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSNumber+OCSyncAnchorData.m"; sourceTree = ""; }; DC98BBD220FF824600F4ED3E /* FileProviderEnumeratorObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileProviderEnumeratorObserver.h; sourceTree = ""; }; DC98BBD320FF824600F4ED3E /* FileProviderEnumeratorObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileProviderEnumeratorObserver.m; sourceTree = ""; }; - DC9A116A27D0338400D90BA4 /* ClientSpacesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSpacesTableViewController.swift; sourceTree = ""; }; + DC99154B28E636A500DA0AB8 /* SegmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentView.swift; sourceTree = ""; }; + DC99154D28E6371500DA0AB8 /* SegmentViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentViewItem.swift; sourceTree = ""; }; + DC99154F28E63D8300DA0AB8 /* SegmentViewItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentViewItemView.swift; sourceTree = ""; }; + DC99155128E63ECD00DA0AB8 /* UIView+EmbedAndLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+EmbedAndLayout.swift"; sourceTree = ""; }; + DC9B4FC2293F453C0037F8F8 /* EmbeddingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddingViewController.swift; sourceTree = ""; }; + DC9B4FC42940F8D60037F8F8 /* ShareExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionViewController.swift; sourceTree = ""; }; + DC9B4FCD2941DA6E0037F8F8 /* UINavigationItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationItem+Extension.swift"; sourceTree = ""; }; DC9BFBB220A19AF3007064B5 /* doc */ = {isa = PBXFileReference; lastKnownFileType = folder; path = doc; sourceTree = ""; }; DC9BFBB820A1AF2B007064B5 /* icon-locked.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "icon-locked.tvg"; path = "img/filetypes-tvg/icon-locked.tvg"; sourceTree = SOURCE_ROOT; }; DC9BFBBA20A1B3CA007064B5 /* icon-password-manager.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "icon-password-manager.tvg"; path = "img/filetypes-tvg/icon-password-manager.tvg"; sourceTree = SOURCE_ROOT; }; @@ -1442,8 +1401,6 @@ DCA35D8024D1707100DBE2B0 /* OCSyncRecordActivity+DiagnosticGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCSyncRecordActivity+DiagnosticGenerator.swift"; sourceTree = ""; }; DCA35DA624D309B600DBE2B0 /* OCFileProviderServiceSession+UploadByFileProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCFileProviderServiceSession+UploadByFileProvider.swift"; sourceTree = ""; }; DCAB9CC823417243009091B6 /* ThemedAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedAlertController.swift; sourceTree = ""; }; - DCAEB05E21F9FB370067E147 /* OCBookmarkManager+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Tools.swift"; sourceTree = ""; }; - DCAEB06021F9FC510067E147 /* EarlGrey+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EarlGrey+Tools.swift"; sourceTree = ""; }; DCB2C05D250C1F9E001083CA /* BrandingClassSettingsSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BrandingClassSettingsSource.h; sourceTree = ""; }; DCB2C05E250C1F9E001083CA /* BrandingClassSettingsSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BrandingClassSettingsSource.m; sourceTree = ""; }; DCB3256A2559989200058EEB /* generate_docs.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = generate_docs.sh; sourceTree = ""; }; @@ -1459,8 +1416,30 @@ DCB5D56A2861BEBE004AF425 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; DCB5D5A628632C17004AF425 /* SearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScope.swift; sourceTree = ""; }; DCB5D60A25FC14B6004C52D9 /* OCIssue+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCIssue+Extension.swift"; sourceTree = ""; }; - DCB6C4D62453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientAuthenticationUpdater.swift; sourceTree = ""; }; - DCB6C4DD24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientAuthenticationUpdaterViewController.swift; sourceTree = ""; }; + DCB6B1E5292B6BE500D27573 /* OCLocation+Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCLocation+Interactions.swift"; sourceTree = ""; }; + DCB6B1E8292B6E6B00D27573 /* ClientSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSidebarViewController.swift; sourceTree = ""; }; + DCB6B1EA292B7C2400D27573 /* AppRootViewController+ItemActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppRootViewController+ItemActions.swift"; sourceTree = ""; }; + DCB6B1EC292B963300D27573 /* AccountConnection+ItemActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountConnection+ItemActions.swift"; sourceTree = ""; }; + DCB6B1F4292CC46B00D27573 /* AccountController+ItemActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountController+ItemActions.swift"; sourceTree = ""; }; + DCB6B1F6292CC8E200D27573 /* OCBookmarkManager+Locking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Locking.swift"; sourceTree = ""; }; + DCB6B1F8292CCAF200D27573 /* OCBookmarkManager+Management.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Management.swift"; sourceTree = ""; }; + DCB6B1FA292CD76200D27573 /* OCBookmarkManager+Management.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Management.swift"; sourceTree = ""; }; + DCB6B1FC292CF2DB00D27573 /* NavigationRevocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRevocationManager.swift; sourceTree = ""; }; + DCB6B1FF292CF2FE00D27573 /* NavigationRevocationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRevocationAction.swift; sourceTree = ""; }; + DCB6B201292D3D2D00D27573 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DCB6B202292D45AD00D27573 /* NavigationRevocationTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRevocationTrigger.swift; sourceTree = ""; }; + DCB6B204292D859B00D27573 /* UIViewController+NavigationRevocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+NavigationRevocation.swift"; sourceTree = ""; }; + DCB6B209292E296800D27573 /* CollectionSidebarAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionSidebarAction.swift; sourceTree = ""; }; + DCB6B20B292E428000D27573 /* AccountController+ExtraItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountController+ExtraItems.swift"; sourceTree = ""; }; + DCB6B20E292F843800D27573 /* CollectionViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewAction.swift; sourceTree = ""; }; + DCB6C4DD24559B1600C1EAE1 /* AccountAuthenticationUpdaterPasswordPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAuthenticationUpdaterPasswordPromptViewController.swift; sourceTree = ""; }; + DCBAEAD429A361C100BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCItemPolicy+UniversalItemListCellContentProvider.swift"; sourceTree = ""; }; + DCBAEADD29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSupplementaryCellProvider.swift; sourceTree = ""; }; + DCBAEADF29A554CC00BFF393 /* CollectionViewSupplementaryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSupplementaryItem.swift; sourceTree = ""; }; + DCBAEAE129A55D5A00BFF393 /* ViewSupplementaryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewSupplementaryCell.swift; sourceTree = ""; }; + DCBAEAE329A5603600BFF393 /* CollectionViewSupplementaryCellProvider+StandardImplementations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewSupplementaryCellProvider+StandardImplementations.swift"; sourceTree = ""; }; + DCBAEAE729A568D500BFF393 /* TitleSupplementaryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleSupplementaryCell.swift; sourceTree = ""; }; + DCBAEAEC29A627D900BFF393 /* DataSourceCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceCondition.swift; sourceTree = ""; }; DCBD8EA924B3755B00D92E1F /* OCItem+AppExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCItem+AppExtension.swift"; sourceTree = ""; }; DCC085502293ED52008CC05C /* DisplaySettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettingsSection.swift; sourceTree = ""; }; DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ownCloudApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1499,12 +1478,13 @@ DCD1300923A191C000255779 /* LicenseOfferButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseOfferButton.swift; sourceTree = ""; }; DCD1301023A23F4E00255779 /* OCLicenseManager+AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCLicenseManager+AppStore.swift"; sourceTree = ""; }; DCD2D40522F06ECA0071FB8F /* DataSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSettingsSection.swift; sourceTree = ""; }; + DCD68A2C291D979400993FF5 /* AccountController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountController.swift; sourceTree = ""; }; + DCD68A2E291D9BE400993FF5 /* AccountControllerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountControllerCell.swift; sourceTree = ""; }; DCD71E7C2742745D001592C6 /* BuildOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BuildOptions.h; sourceTree = ""; }; DCD71E7D2742745D001592C6 /* BuildOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BuildOptions.m; sourceTree = ""; }; DCD810922398492C003B0053 /* OCLicenseDuration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseDuration.h; sourceTree = ""; }; DCD810932398492C003B0053 /* OCLicenseDuration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseDuration.m; sourceTree = ""; }; DCD863FA28115C8700CA6631 /* Down.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Down.LICENSE; sourceTree = ""; }; - DCD8640F2811821200CA6631 /* ClientRootViewController+ItemActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ClientRootViewController+ItemActions.swift"; sourceTree = ""; }; DCD864112811FC5700CA6631 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; DCD954DE247D62FA00E184E6 /* MessageTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewController.swift; sourceTree = ""; }; DCD9B873237960E600691929 /* OCLicenseManager+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCLicenseManager+Internal.h"; sourceTree = ""; }; @@ -1526,14 +1506,9 @@ DCE28F5F2433683700879DEC /* ClientSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSessionManager.swift; sourceTree = ""; }; DCE2F03D27FADF2600E9E136 /* UICollectionViewDiffableDataSource+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewDiffableDataSource+Tools.swift"; sourceTree = ""; }; DCE4E43D24C19C3E0051722F /* Action+UserInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+UserInterface.swift"; sourceTree = ""; }; - DCE4E44324C1A3E30051722F /* FileListTableViewController+OpenItemTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileListTableViewController+OpenItemTableViewController.swift"; sourceTree = ""; }; - DCE4E44C24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryFileListTableViewController+Multiselect.swift"; sourceTree = ""; }; - DCE4E45524C1ED8D0051722F /* ClientQueryViewController+InlineMessageSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ClientQueryViewController+InlineMessageSupport.swift"; sourceTree = ""; }; DCE4E46824C1F5610051722F /* ownCloud Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "ownCloud Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - DCE4E46A24C1F5610051722F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; DCE4E46F24C1F5610051722F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DCE4E48224C1F5B70051722F /* ownCloud Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ownCloud Share Extension.entitlements"; sourceTree = ""; }; - DCE4E48524C1F6B50051722F /* ShareNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareNavigationController.swift; sourceTree = ""; }; DCE4E4C624C255E00051722F /* AppExtensionNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionNavigationController.swift; sourceTree = ""; }; DCE5E88C2080D77E005F60CE /* video.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = video.tvg; path = "img/filetypes-tvg/video.tvg"; sourceTree = SOURCE_ROOT; }; DCE5E88D2080D77E005F60CE /* folder-external.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "folder-external.tvg"; path = "img/filetypes-tvg/folder-external.tvg"; sourceTree = SOURCE_ROOT; }; @@ -1556,6 +1531,7 @@ DCE5E89E2080D780005F60CE /* text-vcard.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "text-vcard.tvg"; path = "img/filetypes-tvg/text-vcard.tvg"; sourceTree = SOURCE_ROOT; }; DCE5E89F2080D780005F60CE /* folder-shared.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "folder-shared.tvg"; path = "img/filetypes-tvg/folder-shared.tvg"; sourceTree = SOURCE_ROOT; }; DCE5E8A02080D781005F60CE /* x-office-presentation.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "x-office-presentation.tvg"; path = "img/filetypes-tvg/x-office-presentation.tvg"; sourceTree = SOURCE_ROOT; }; + DCE741DE29B8A85D00BFF393 /* BookmarkManagementContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkManagementContext.swift; sourceTree = ""; }; DCE93FEE21FCA434000E14F2 /* libzip.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = libzip.xcodeproj; path = external/libzip/libzip.xcodeproj; sourceTree = SOURCE_ROOT; }; DCE974B1207E3AF80069FC2B /* ThemeNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeNavigationController.swift; sourceTree = ""; }; DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; @@ -1589,6 +1565,9 @@ DCF575EA2796CBDF003BEBBA /* OCImage+ViewProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCImage+ViewProvider.m"; sourceTree = ""; }; DCF575ED2796CE38003BEBBA /* OCViewHost.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCViewHost.h; sourceTree = ""; }; DCF575EE2796CE38003BEBBA /* OCViewHost.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCViewHost.m; sourceTree = ""; }; + DCFA56422975734A0092C89F /* BrowserNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationViewController.swift; sourceTree = ""; }; + DCFA5644297574A60092C89F /* BrowserNavigationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationItem.swift; sourceTree = ""; }; + DCFA9CAB2987E14B00004F24 /* BrowserNavigationBookmark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserNavigationBookmark.swift; sourceTree = ""; }; DCFB74C321AD7E18005796AF /* StaticLoginServerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLoginServerListViewController.swift; sourceTree = ""; }; DCFC9ECB28002303005D9144 /* CollectionViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSection.swift; sourceTree = ""; }; DCFC9ED028002335005D9144 /* CollectionViewCellProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewCellProvider.swift; sourceTree = ""; }; @@ -1596,7 +1575,6 @@ DCFC9ED428002F33005D9144 /* CollectionViewCellConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewCellConfiguration.swift; sourceTree = ""; }; DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientWebAppViewController.swift; sourceTree = ""; }; DCFE682D28D9CEDD00091D2A /* ComposedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposedMessageView.swift; sourceTree = ""; }; - DCFED971208095E200A2D984 /* ClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientItemCell.swift; sourceTree = ""; }; DCFED9B920809B8900A2D984 /* ThemeTVGResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTVGResource.swift; sourceTree = ""; }; DCFEF90526EFA45A001DC7A4 /* VendorServices+App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VendorServices+App.swift"; sourceTree = ""; }; DCFEFE28236876BD009A142F /* OCLicenseManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseManager.h; sourceTree = ""; }; @@ -1617,9 +1595,6 @@ DCFEFE962368D099009A142F /* OCLicenseEnvironment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseEnvironment.m; sourceTree = ""; }; DCFEFE9A2368D7FA009A142F /* OCLicenseObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseObserver.h; sourceTree = ""; }; DCFEFE9B2368D7FA009A142F /* OCLicenseObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseObserver.m; sourceTree = ""; }; - DF01EC88E3D07CDA6F366E32 /* Pods-ownCloud Screenshots Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.release.xcconfig"; sourceTree = ""; }; - EA88A55421BFD5BF0055A58F /* DeleteBookmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteBookmarkTests.swift; sourceTree = ""; }; - EA9337E22226DB070054971F /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1645,8 +1620,6 @@ DC20DE6A21C01B210096000B /* ownCloudSDK.framework in Frameworks */, DC20DE6B21C01B210096000B /* ownCloudUI.framework in Frameworks */, DC18898E218A773700CFB3F9 /* ownCloudMocking.framework in Frameworks */, - EA1D571C6B1E95925C459228 /* EarlGrey.framework in Frameworks */, - 75AC0B4AD332C8CC785FE349 /* Pods_ownCloudTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1654,6 +1627,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DC9B4FCA29413B480037F8F8 /* Down in Frameworks */, DC04920B258CB06A00DEDC27 /* PocketSVG in Frameworks */, DCDC0ACF23CD186400DFE36D /* ownCloudApp.framework in Frameworks */, 394A0B0A22EEFCF500603813 /* ownCloudSDK.framework in Frameworks */, @@ -1681,18 +1655,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 59056CA722414F3C00A18A22 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 59799F7F22415878007E8008 /* EarlGrey.framework in Frameworks */, - 59799F7222415758007E8008 /* ownCloudMocking.framework in Frameworks */, - 59799F7322415758007E8008 /* ownCloudSDK.framework in Frameworks */, - 59799F7422415758007E8008 /* ownCloudUI.framework in Frameworks */, - 46B9D336BF7FE50321823888 /* Pods_ownCloudScreenshotsTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DC7DBA31207F84BF00E7337D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1753,17 +1715,6 @@ path = "PhotoKit Extensions"; sourceTree = ""; }; - 02F2890C24BFAE8400E3D35C /* Migration */ = { - isa = PBXGroup; - children = ( - 02F2890F24BFAF0100E3D35C /* LegacyCredentials.swift */, - 02F2891124BFAF0100E3D35C /* Migration.swift */, - 02F2891224BFAF0100E3D35C /* MigrationActivityCell.swift */, - 02F2891024BFAF0100E3D35C /* MigrationViewController.swift */, - ); - path = Migration; - sourceTree = ""; - }; 233BDE93204FEFE500C06732 = { isa = PBXGroup; children = ( @@ -1780,11 +1731,9 @@ 39A7138122E79C6700089423 /* ownCloud Intents */, 394A0AFA22EEFC2C00603813 /* ownCloudAppShared */, DCE4E46924C1F5610051722F /* ownCloud Share Extension */, - 59056CAB22414F3C00A18A22 /* ownCloudScreenshotsTests */, 39DC7CCE25C2E1570001E08C /* ownCloud File Provider UI */, 233BDE9D204FEFE500C06732 /* Products */, DC85573220513CC700189B9A /* Frameworks */, - 5EE06126BB49344475598790 /* Pods */, ); sourceTree = ""; }; @@ -1797,7 +1746,6 @@ DCC6564620C9B7E300110A97 /* ownCloud File Provider.appex */, DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */, DCC085642293F1FD008CC05C /* ownCloudAppTests.xctest */, - 59056CAA22414F3C00A18A22 /* ownCloudScreenshotsTests.xctest */, 39A7138022E79C6700089423 /* ownCloud Intents.appex */, 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */, DCE4E46824C1F5610051722F /* ownCloud Share Extension.appex */, @@ -1813,17 +1761,15 @@ 3968C878239C54AC00AC28AC /* Release Notes */, 233BDE9F204FEFE500C06732 /* AppDelegate.swift */, 3961281522F8730A0087BD3A /* SceneDelegate.swift */, - DCD502B325BC3432007F9087 /* Issues */, + DCB6B1E7292B6E3400D27573 /* App Controllers */, DCF4F1612051925A00189B9A /* Bookmarks */, DC8EB26F239308C3009148F9 /* Licensing */, 4C51727422DE04BD001BC97F /* Tasks */, - 02F2890C24BFAE8400E3D35C /* Migration */, DCC832D1242BB3E900153F8C /* Messages */, DC3BE0DB2077CC13002A0AC0 /* Client */, DCA35D6724CF7B6E00DBE2B0 /* Diagnostic */, DC44343721ABF9A200376B16 /* Static Login */, DC7DF17C205140F400189B9A /* Server List */, - DC3AB2492810A67F00789435 /* View Providers */, DCF4F1802051A91500189B9A /* Settings */, 39E42D152315286300B82AC3 /* Key Commands */, DC422448207CAED60006A2A6 /* Theming */, @@ -1843,14 +1789,7 @@ isa = PBXGroup; children = ( DC26ADBD2550C02F0059680D /* Metadata */, - EA9337DC2226DAE00054971F /* Settings */, - 59B09E5B21AD61DD007827B8 /* Resources */, - D7F3B3E74D4B04F9CAF95C09 /* EarlGrey.swift */, 233BDEB6204FEFE500C06732 /* Info.plist */, - DCAEB05821F9FB0F0067E147 /* Tools */, - 59B09E6721AD61F4007827B8 /* File List */, - 59B09E6921AD61F4007827B8 /* Login */, - 5917244820D3DB1F00809B38 /* Security */, ); path = ownCloudTests; sourceTree = ""; @@ -1896,8 +1835,6 @@ 4CB8ADDF22DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift */, 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */, 39CC8AE5228C12100020253B /* Array+Extension.swift */, - 02072E5F23E46022006548A7 /* UIWindow+Extension.swift */, - 0269F588244DED02002E9D99 /* UIAlertController+UniversalLinks.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -1922,29 +1859,22 @@ 3912208023436E9B0026C290 /* Client */ = { isa = PBXGroup; children = ( + DCD68A2B291D973E00993FF5 /* Account */, DC46F3CF284546DA00038880 /* Data Item Interactions */, + DCBAEAEB29A627C100BFF393 /* Data Source Conditions */, DC82664028168DAA00F91F7D /* Context */, + DCB6B1FE292CF2E500D27573 /* Navigation Revocation */, DCEAF0842808250E00980B6D /* Collection Views */, + DC3AB1932808C2DE00789435 /* View Controllers */, DCB5D5692861BE9A004AF425 /* Search */, DCA2EDDB279B0E5D001F04E6 /* Resource Sources */, DCE4E43424C199860051722F /* Actions */, - DCE4E42D24C1961D0051722F /* File Lists */, 399EA6ED25E6544000B6FF11 /* Sharing */, DCE4E42F24C1963F0051722F /* User Interface */, ); path = Client; sourceTree = ""; }; - 3918FE742287DB9B00BACE03 /* Library */ = { - isa = PBXGroup; - children = ( - 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */, - DC29F09222976B9200F77349 /* LibrarySharesTableViewController.swift */, - DC33939E22E0A1A300DD3DA4 /* Item Policies */, - ); - path = Library; - sourceTree = ""; - }; 392DDB1224CF024C009E5406 /* Import */ = { isa = PBXGroup; children = ( @@ -2000,11 +1930,15 @@ 399EA72525E6565900B6FF11 /* OCCore+Extension.swift */, 399EA71A25E6561D00B6FF11 /* OCShare+Extension.swift */, DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */, + DCB6B1F6292CC8E200D27573 /* OCBookmarkManager+Locking.swift */, + DCB6B1F8292CCAF200D27573 /* OCBookmarkManager+Management.swift */, DCA35DA624D309B600DBE2B0 /* OCFileProviderServiceSession+UploadByFileProvider.swift */, DCBD8EA924B3755B00D92E1F /* OCItem+AppExtension.swift */, 397754E123279EED00119FCB /* OCItem+Extension.swift */, 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */, + DC4FEAE6209E3A7700D4476B /* OCIssue+DisplayIssues.swift */, DCB5D60A25FC14B6004C52D9 /* OCIssue+Extension.swift */, + DCE20271249AB50E0015A22A /* OCMessage+Extension.swift */, ); path = "SDK Extensions"; sourceTree = ""; @@ -2012,26 +1946,25 @@ 399725DF233DF37300FC3B94 /* UIKit Extension */ = { isa = PBXGroup; children = ( - DCE2F03D27FADF2600E9E136 /* UICollectionViewDiffableDataSource+Tools.swift */, 392CFEB62705831700631D2B /* LAContext+Extension.swift */, - 399EA73925E656A900B6FF11 /* UITableView+Extension.swift */, DC248C66213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift */, + DC3AB23F280FFF2700789435 /* NSMutableAttributedString+AppendStyled.swift */, DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */, - 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */, 39104E0A223991C8002FC02F /* UIButton+Extension.swift */, + DCE2F03D27FADF2600E9E136 /* UICollectionViewDiffableDataSource+Tools.swift */, 239F1318205A693A0029F186 /* UIColor+Extension.swift */, 23E22BB220C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift */, + DC3AB24328104AA500789435 /* UIFont+Weight.swift */, DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */, - 4C63F0E4231A91230088E8CA /* UIImageView+Thumbnails.swift */, - 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */, - 398FD4502334CF66004B68A1 /* UIView+Extension.swift */, - 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */, DC66A9F7279F467200792AC8 /* UIKeyCommand+Extension.swift */, - DC3AB23F280FFF2700789435 /* NSMutableAttributedString+AppendStyled.swift */, - DC3AB24328104AA500789435 /* UIFont+Weight.swift */, DC3AB2452810602500789435 /* UILabel+Extension.swift */, - DC46F3D62845FCFA00038880 /* UIView+OCDataItem.swift */, + 399EA73925E656A900B6FF11 /* UITableView+Extension.swift */, + 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */, DC65590E28A2633C0003D130 /* UITextField+Extension.swift */, + 398FD4502334CF66004B68A1 /* UIView+Extension.swift */, + DC46F3D62845FCFA00038880 /* UIView+OCDataItem.swift */, + 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */, + DC9B4FCD2941DA6E0037F8F8 /* UINavigationItem+Extension.swift */, ); path = "UIKit Extension"; sourceTree = ""; @@ -2053,7 +1986,6 @@ isa = PBXGroup; children = ( 399EA6EE25E6544000B6FF11 /* GroupSharingTableViewController.swift */, - 399EA6EF25E6544000B6FF11 /* ShareClientItemCell.swift */, 399EA6F025E6544000B6FF11 /* PublicLinkEditTableViewController.swift */, 399EA6F125E6544000B6FF11 /* PublicLinkTableViewController.swift */, 399EA6F225E6544000B6FF11 /* SharingTableViewController.swift */, @@ -2062,14 +1994,6 @@ path = Sharing; sourceTree = ""; }; - 399EA70525E654B400B6FF11 /* Sharing */ = { - isa = PBXGroup; - children = ( - 399EA70625E654B400B6FF11 /* PendingSharesTableViewController.swift */, - ); - path = Sharing; - sourceTree = ""; - }; 399EA74425E6575A00B6FF11 /* Notification */ = { isa = PBXGroup; children = ( @@ -2177,79 +2101,6 @@ path = "CoreImage Extensions"; sourceTree = ""; }; - 59056CAB22414F3C00A18A22 /* ownCloudScreenshotsTests */ = { - isa = PBXGroup; - children = ( - 59371D7B224103D300C6BC5B /* SnapshotHelper.swift */, - 59056CAC22414F3C00A18A22 /* ownCloudScreenshotsTests.swift */, - 59056CAE22414F3C00A18A22 /* Info.plist */, - ); - path = ownCloudScreenshotsTests; - sourceTree = ""; - }; - 5917244820D3DB1F00809B38 /* Security */ = { - isa = PBXGroup; - children = ( - 5958C9BD20C000A700E0E567 /* PasscodeTests.swift */, - 5917244D20D3DC2100809B38 /* BiometricalTests.swift */, - ); - path = Security; - sourceTree = ""; - }; - 59538A0921E4C2D2005E543B /* Subclasses */ = { - isa = PBXGroup; - children = ( - 59538A0A21E4C300005E543B /* MockOCQuery.swift */, - 59538A1B21E77FB6005E543B /* MockOCCore.swift */, - 5971CF3922046F530052FE9A /* MockClientRootViewController.swift */, - ); - path = Subclasses; - sourceTree = ""; - }; - 59B09E5B21AD61DD007827B8 /* Resources */ = { - isa = PBXGroup; - children = ( - 59B09E5C21AD61DD007827B8 /* PropfindResponse.xml */, - 595E2C9E21EE4BF300F0E95D /* PropfindResponseNewFolder.xml */, - 59B09E5D21AD61DD007827B8 /* test_certificate.cer */, - ); - path = Resources; - sourceTree = ""; - }; - 59B09E6721AD61F4007827B8 /* File List */ = { - isa = PBXGroup; - children = ( - 59538A0921E4C2D2005E543B /* Subclasses */, - 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */, - 59B09E6821AD61F4007827B8 /* FileListTests.swift */, - 59538A0221E4A9C2005E543B /* CreateFolderTests.swift */, - ); - path = "File List"; - sourceTree = ""; - }; - 59B09E6921AD61F4007827B8 /* Login */ = { - isa = PBXGroup; - children = ( - 59B09E6A21AD61F4007827B8 /* EditBookmarkTests.swift */, - 59B09E6B21AD61F4007827B8 /* CreateBookmarkTests.swift */, - EA88A55421BFD5BF0055A58F /* DeleteBookmarkTests.swift */, - ); - path = Login; - sourceTree = ""; - }; - 5EE06126BB49344475598790 /* Pods */ = { - isa = PBXGroup; - children = ( - 03AE98EEF23B4F4F2C0FDD0F /* Pods-ownCloudTests.debug.xcconfig */, - D6033BC23D1129172E6D6383 /* Pods-ownCloudTests.release.xcconfig */, - 3D753147564B1E4F47826109 /* Pods-ownCloud Screenshots Tests.debug.xcconfig */, - DF01EC88E3D07CDA6F366E32 /* Pods-ownCloud Screenshots Tests.release.xcconfig */, - A80C5F83C59F9D52F8F64890 /* Pods-ownCloudScreenshotsTests.debug.xcconfig */, - 5AA495E19A4CA58CF2678112 /* Pods-ownCloudScreenshotsTests.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; 6E586CF52199A70100F680C4 /* Actions+Extensions */ = { isa = PBXGroup; children = ( @@ -2278,8 +2129,6 @@ 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */, 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */, 39EF06AF25D6C3FC001E1E19 /* PresentationModeAction.swift */, - DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */, - DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */, DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */, ); path = "Actions+Extensions"; @@ -2322,12 +2171,18 @@ DCE4E43624C19A3B0051722F /* More */, DC0A359024C0E46800FB58FC /* Cursor Support */, DC0A355124C0E1EF00FB58FC /* Theme */, + DC298C9B2934D3FA009FA87F /* Alert View */, + DC60F2A429802A7500905EC8 /* Navigation Content */, + DCFA5641297573080092C89F /* Browser Navigation Controller */, + DCFA9CA72987E14B00004F24 /* State Restoration */, 2366821521144DCD0045EF72 /* Card Presentation Controller */, 399EA74425E6575A00B6FF11 /* Notification */, DC0A354D24C0E15100FB58FC /* Progress */, - DC297959226E4C9200E01BC7 /* Push Presentation Controller */, + DC9B4FC1293F451F0037F8F8 /* EmbeddingViewController */, DC0A354E24C0E15900FB58FC /* StaticTableView */, - DCE4E43224C197480051722F /* User Activities */, + DC99154A28E6364700DA0AB8 /* SegmentView */, + DC3AB2492810A67F00789435 /* View Providers */, + DC8E99E6297F3B8400594697 /* Gesture Recognizer */, ); path = "User Interface"; sourceTree = ""; @@ -2473,24 +2328,32 @@ path = "FileProvider Integration"; sourceTree = ""; }; - DC297959226E4C9200E01BC7 /* Push Presentation Controller */ = { + DC298C9B2934D3FA009FA87F /* Alert View */ = { isa = PBXGroup; children = ( - DC297966226E4D3100E01BC7 /* PushPresentationController.swift */, - DC297964226E4D1100E01BC7 /* PushTransitionDelegate.swift */, - DC297968226E52E600E01BC7 /* PushTransition.swift */, + DCC832F7242CC94D00153F8C /* AlertView.swift */, + DCC83303242CF3AC00153F8C /* AlertViewController.swift */, ); - path = "Push Presentation Controller"; + path = "Alert View"; sourceTree = ""; }; - DC29F09122974F8000F77349 /* FileList Extensions */ = { + DC298CA22935854F009FA87F /* Authentication Error Handling */ = { isa = PBXGroup; children = ( - DCE4E44324C1A3E30051722F /* FileListTableViewController+OpenItemTableViewController.swift */, - DCE4E44C24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift */, - DCE4E45524C1ED8D0051722F /* ClientQueryViewController+InlineMessageSupport.swift */, + DC298CA029357809009FA87F /* AccountConnectionAuthErrorConsumer.swift */, + DC298C902934BDAD009FA87F /* AccountAuthenticationUpdater.swift */, + DCB6C4DD24559B1600C1EAE1 /* AccountAuthenticationUpdaterPasswordPromptViewController.swift */, ); - path = "FileList Extensions"; + path = "Authentication Error Handling"; + sourceTree = ""; + }; + DC298CAA29362534009FA87F /* Location Picker */ = { + isa = PBXGroup; + children = ( + DC298CA829362523009FA87F /* ClientLocationPicker.swift */, + DC298CAB29362710009FA87F /* ClientLocationPickerViewController.swift */, + ); + path = "Location Picker"; sourceTree = ""; }; DC2A127B28D06EED0088A2B7 /* Saved Searches */ = { @@ -2516,19 +2379,16 @@ path = Search; sourceTree = ""; }; - DC33939E22E0A1A300DD3DA4 /* Item Policies */ = { - isa = PBXGroup; - children = ( - DC33939F22E0A1C000DD3DA4 /* ItemPolicyTableViewController.swift */, - DC3393A122E0A71100DD3DA4 /* ItemPolicyCell.swift */, - ); - path = "Item Policies"; - sourceTree = ""; - }; DC3AB1932808C2DE00789435 /* View Controllers */ = { isa = PBXGroup; children = ( DC3AB1972808C35300789435 /* ClientItemViewController.swift */, + DCB6B1E8292B6E6B00D27573 /* ClientSidebarViewController.swift */, + DC6FDAF62953AD50004F0C7F /* ClientSharedWithMeViewController.swift */, + DC921A0A2968BA4D00F538EE /* ClientSharedByMeViewController.swift */, + DC298C862934A405009FA87F /* ClientDefaultViewController.swift */, + DC3F0C2329828AB500C832DB /* Location Breadcrumbs */, + DC298CAA29362534009FA87F /* Location Picker */, ); path = "View Controllers"; sourceTree = ""; @@ -2544,18 +2404,11 @@ DC3BE0DB2077CC13002A0AC0 /* Client */ = { isa = PBXGroup; children = ( - DC29F09122974F8000F77349 /* FileList Extensions */, - 3918FE742287DB9B00BACE03 /* Library */, - 399EA70525E654B400B6FF11 /* Sharing */, 23EC774D2137F3CD0032D4E6 /* Viewer */, 236735A421217C2300E5834A /* Actions */, DC63208421FCEBE9007EC0A8 /* ClientActivityCell.swift */, DC63208221FCAC1E007EC0A8 /* ClientActivityViewController.swift */, - DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */, - DCD8640F2811821200CA6631 /* ClientRootViewController+ItemActions.swift */, DCE28F5F2433683700879DEC /* ClientSessionManager.swift */, - DCB6C4D62453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift */, - DCB6C4DD24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift */, 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */, 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */, 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */, @@ -2565,11 +2418,28 @@ path = Client; sourceTree = ""; }; + DC3DDEFE287E1AA500E5586D /* UIKit Extensions */ = { + isa = PBXGroup; + children = ( + DC3DDF03287E1AC200E5586D /* UIViewController+HostBundleID.m */, + DC3DDF02287E1AC200E5586D /* UIViewController+HostBundleID.h */, + ); + path = "UIKit Extensions"; + sourceTree = ""; + }; + DC3F0C2329828AB500C832DB /* Location Breadcrumbs */ = { + isa = PBXGroup; + children = ( + DC8E99E4297EEB2800594697 /* ClientLocationBarController.swift */, + DC3F0C2429828AE300C832DB /* OCLocation+Breadcrumbs.swift */, + ); + path = "Location Breadcrumbs"; + sourceTree = ""; + }; DC422448207CAED60006A2A6 /* Theming */ = { isa = PBXGroup; children = ( 397E276B23D05A5400117B07 /* ServerListToolCell.swift */, - DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */, ); path = Theming; sourceTree = ""; @@ -2612,11 +2482,60 @@ DC46F3D4284563BD00038880 /* OCAction+Interactions.swift */, DC46F3D22845583D00038880 /* OCDrive+Interactions.swift */, DC46F3D0284546F000038880 /* OCItem+Interactions.swift */, + DC28F825294B733700AC4013 /* OCItemPolicy+Interactions.swift */, + DCB6B1E5292B6BE500D27573 /* OCLocation+Interactions.swift */, DC46F69128DCB9C6008280CA /* OCSavedSearch+Interactions.swift */, + DC9219FB2966179600F538EE /* OCShare+Interactions.swift */, ); path = "Data Item Interactions"; sourceTree = ""; }; + DC60F2A429802A7500905EC8 /* Navigation Content */ = { + isa = PBXGroup; + children = ( + DC60F2AB29812A9B00905EC8 /* README.md */, + DC60F2A529802ABE00905EC8 /* UINavigationItem+NavigationContent.swift */, + DC60F2A729802B0900905EC8 /* NavigationContent.swift */, + DC60F2A929802D5800905EC8 /* NavigationContentItem.swift */, + ); + path = "Navigation Content"; + sourceTree = ""; + }; + DC62F56A2925059E0095BB5D /* Connection */ = { + isa = PBXGroup; + children = ( + DC62F568292504510095BB5D /* AccountConnectionPool.swift */, + DC62F566292504060095BB5D /* AccountConnection.swift */, + DCB6B1EC292B963300D27573 /* AccountConnection+ItemActions.swift */, + DC62F56B29250DC80095BB5D /* AccountConnectionConsumer.swift */, + DC62F6F4292819C80095BB5D /* AccountConnectionRichStatus.swift */, + DC298CA22935854F009FA87F /* Authentication Error Handling */, + ); + path = Connection; + sourceTree = ""; + }; + DC62F56E292514EF0095BB5D /* Controller */ = { + isa = PBXGroup; + children = ( + DCD68A2C291D979400993FF5 /* AccountController.swift */, + DC6C0A532923FFF30045FF2A /* AccountControllerSection.swift */, + DC298C8C2934B3E7009FA87F /* AccountConnectionErrorHandler.swift */, + DC298CAF29366B96009FA87F /* AccountControllerSpacesGridViewController.swift */, + DC89EA662995456A00BFF393 /* BrowserNavigationBookmark+AccountController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + DC62F57629268D740095BB5D /* Implementations */ = { + isa = PBXGroup; + children = ( + 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */, + DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */, + DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */, + ); + path = Implementations; + sourceTree = ""; + }; DC65592C28A644B60003D130 /* Tokenizer */ = { isa = PBXGroup; children = ( @@ -2727,6 +2646,7 @@ children = ( 39B39446238532DC00892E8D /* ThemeTableViewCell.swift */, DCE974B1207E3AF80069FC2B /* ThemeNavigationController.swift */, + DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */, DC0B37962051681600189B9A /* ThemeButton.swift */, 39B394482385334D00892E8D /* ThemeView.swift */, 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */, @@ -2761,9 +2681,6 @@ isa = PBXGroup; children = ( DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */, - DC85572A20513B8C00189B9A /* ServerListTableViewController.swift */, - DC85572B20513B8C00189B9A /* ServerListTableViewController.xib */, - 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */, ); path = "Server List"; sourceTree = ""; @@ -2772,6 +2689,7 @@ isa = PBXGroup; children = ( DC82663B28168D2800F91F7D /* ClientContext.swift */, + DC28F827294BB5ED00AC4013 /* SortedItemDataSource.swift */, ); path = Context; sourceTree = ""; @@ -2805,6 +2723,34 @@ name = Frameworks; sourceTree = ""; }; + DC89EA5A29939F9900BFF393 /* Actions */ = { + isa = PBXGroup; + children = ( + DC89EA5B2993A0E000BFF393 /* AppStateActionConnect.swift */, + DC89EA6A29959BD200BFF393 /* AppStateActionRestoreNavigationBookmark.swift */, + DC081C88299B8E5800BFF393 /* AppStateActionRevealItem.swift */, + DC081C8A299B9B9000BFF393 /* AppStateActionGoToPersonalFolder.swift */, + ); + path = Actions; + sourceTree = ""; + }; + DC8E99DD297E8D1800594697 /* QA */ = { + isa = PBXGroup; + children = ( + DC8E99DF297E8D3800594697 /* OCLicenseQAProvider.m */, + DC8E99DE297E8D3800594697 /* OCLicenseQAProvider.h */, + ); + path = QA; + sourceTree = ""; + }; + DC8E99E6297F3B8400594697 /* Gesture Recognizer */ = { + isa = PBXGroup; + children = ( + DC8E99E7297F3BA700594697 /* ActionTapGestureRecognizer.swift */, + ); + path = "Gesture Recognizer"; + sourceTree = ""; + }; DC8EB26F239308C3009148F9 /* Licensing */ = { isa = PBXGroup; children = ( @@ -2816,6 +2762,35 @@ path = Licensing; sourceTree = ""; }; + DC9219FE2966D5B800F538EE /* UniversalItemListCell Content Providers */ = { + isa = PBXGroup; + children = ( + DC921A012966D5F800F538EE /* OCShare+UniversalItemListCellContentProvider.swift */, + DC921A032966DFDC00F538EE /* OCItem+UniversalItemListCellContentProvider.swift */, + DCBAEAD429A361C100BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift */, + ); + path = "UniversalItemListCell Content Providers"; + sourceTree = ""; + }; + DC99154A28E6364700DA0AB8 /* SegmentView */ = { + isa = PBXGroup; + children = ( + DC99154B28E636A500DA0AB8 /* SegmentView.swift */, + DC99154D28E6371500DA0AB8 /* SegmentViewItem.swift */, + DC99154F28E63D8300DA0AB8 /* SegmentViewItemView.swift */, + DC99155128E63ECD00DA0AB8 /* UIView+EmbedAndLayout.swift */, + ); + path = SegmentView; + sourceTree = ""; + }; + DC9B4FC1293F451F0037F8F8 /* EmbeddingViewController */ = { + isa = PBXGroup; + children = ( + DC9B4FC2293F453C0037F8F8 /* EmbeddingViewController.swift */, + ); + path = EmbeddingViewController; + sourceTree = ""; + }; DCA2EDDB279B0E5D001F04E6 /* Resource Sources */ = { isa = PBXGroup; children = ( @@ -2836,18 +2811,6 @@ path = Diagnostic; sourceTree = ""; }; - DCAEB05821F9FB0F0067E147 /* Tools */ = { - isa = PBXGroup; - children = ( - DCAEB05E21F9FB370067E147 /* OCBookmarkManager+Tools.swift */, - DC869A582153B1F60088977E /* OCMockingManager+SwiftTools.swift */, - 233BDEB4204FEFE500C06732 /* OwnCloudTests.swift */, - 59B09E6C21AD61F4007827B8 /* UtilsTests.swift */, - DCAEB06021F9FC510067E147 /* EarlGrey+Tools.swift */, - ); - path = Tools; - sourceTree = ""; - }; DCB2C05C250C1ECD001083CA /* Branding */ = { isa = PBXGroup; children = ( @@ -2895,6 +2858,55 @@ path = Scopes; sourceTree = ""; }; + DCB6B1E7292B6E3400D27573 /* App Controllers */ = { + isa = PBXGroup; + children = ( + DC6C0A4829239E560045FF2A /* AppRootViewController.swift */, + DCB6B1EA292B7C2400D27573 /* AppRootViewController+ItemActions.swift */, + DCB6B1F4292CC46B00D27573 /* AccountController+ItemActions.swift */, + DCB6B20B292E428000D27573 /* AccountController+ExtraItems.swift */, + ); + path = "App Controllers"; + sourceTree = ""; + }; + DCB6B1EE292B96F900D27573 /* Messages */ = { + isa = PBXGroup; + children = ( + DC4D5A09247C1398008ADDB6 /* MessageGroup.swift */, + DCC832E1242C0EAC00153F8C /* MessageSelector.swift */, + ); + path = Messages; + sourceTree = ""; + }; + DCB6B1FE292CF2E500D27573 /* Navigation Revocation */ = { + isa = PBXGroup; + children = ( + DCB6B201292D3D2D00D27573 /* README.md */, + DCB6B1FC292CF2DB00D27573 /* NavigationRevocationManager.swift */, + DCB6B1FF292CF2FE00D27573 /* NavigationRevocationAction.swift */, + DCB6B202292D45AD00D27573 /* NavigationRevocationTrigger.swift */, + DCB6B204292D859B00D27573 /* UIViewController+NavigationRevocation.swift */, + ); + path = "Navigation Revocation"; + sourceTree = ""; + }; + DCBAEADC29A552D100BFF393 /* Supplementary Cells */ = { + isa = PBXGroup; + children = ( + DCBAEAE729A568D500BFF393 /* TitleSupplementaryCell.swift */, + DCBAEAE129A55D5A00BFF393 /* ViewSupplementaryCell.swift */, + ); + path = "Supplementary Cells"; + sourceTree = ""; + }; + DCBAEAEB29A627C100BFF393 /* Data Source Conditions */ = { + isa = PBXGroup; + children = ( + DCBAEAEC29A627D900BFF393 /* DataSourceCondition.swift */, + ); + path = "Data Source Conditions"; + sourceTree = ""; + }; DCBF0A5E2280393A00465530 /* PDF */ = { isa = PBXGroup; children = ( @@ -2946,6 +2958,7 @@ DC2A128828D076750088A2B7 /* Search */, DC0030BE2350B1CE00BB8570 /* Tools */, DCEA7F38282D3ACA0050A3C0 /* VFS */, + DC3DDEFE287E1AA500E5586D /* UIKit Extensions */, DC774E5B22F44E4A000B11A1 /* ZIP Archive */, DC774E6522F44EA7000B11A1 /* Resources */, ); @@ -3012,12 +3025,8 @@ DCC832D1242BB3E900153F8C /* Messages */ = { isa = PBXGroup; children = ( - DCC832F7242CC94D00153F8C /* AlertView.swift */, - DCC83303242CF3AC00153F8C /* AlertViewController.swift */, DCC832F5242CC5F700153F8C /* CardIssueMessagePresenter.swift */, DC9C1AEB247C76470067895A /* MessageGroupCell.swift */, - DC4D5A09247C1398008ADDB6 /* MessageGroup.swift */, - DCC832E1242C0EAC00153F8C /* MessageSelector.swift */, DCD954DE247D62FA00E184E6 /* MessageTableViewController.swift */, DC5D9E742496512400BFFE8E /* MessageQueueExample.swift */, ); @@ -3096,12 +3105,15 @@ path = Tools; sourceTree = ""; }; - DCD502B325BC3432007F9087 /* Issues */ = { + DCD68A2B291D973E00993FF5 /* Account */ = { isa = PBXGroup; children = ( - DC24B31C25BB6FC4005783E2 /* IssuesCardViewController.swift */, + DC62F56D2925112D0095BB5D /* README.md */, + DC62F56A2925059E0095BB5D /* Connection */, + DC62F56E292514EF0095BB5D /* Controller */, + DCB6B1EE292B96F900D27573 /* Messages */, ); - path = Issues; + path = Account; sourceTree = ""; }; DCD71E7827427446001592C6 /* Building */ = { @@ -3140,49 +3152,28 @@ path = Enterprise; sourceTree = ""; }; - DCE4E42D24C1961D0051722F /* File Lists */ = { - isa = PBXGroup; - children = ( - 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */, - DC29F08F22974AEA00F77349 /* QueryFileListTableViewController.swift */, - DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */, - 2308F93C21467F6200CF0B91 /* ClientDirectoryPickerViewController.swift */, - DC9A116A27D0338400D90BA4 /* ClientSpacesTableViewController.swift */, - ); - path = "File Lists"; - sourceTree = ""; - }; DCE4E42F24C1963F0051722F /* User Interface */ = { isa = PBXGroup; children = ( - 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */, - DCFED971208095E200A2D984 /* ClientItemCell.swift */, + DC24B31C25BB6FC4005783E2 /* IssuesCardViewController.swift */, DCFE682D28D9CEDD00091D2A /* ComposedMessageView.swift */, DCD864112811FC5700CA6631 /* GradientView.swift */, 39B289A7226F1EE000BE0E11 /* MessageView.swift */, 23D77FC6212BFBD100DE76F1 /* NamingViewController.swift */, DC24E10E28B7D2B9002E4F5B /* PopupButtonController.swift */, DC46F68D28DAFCDD008280CA /* RoundCornerBackgroundView.swift */, + 396BE4C92289500E00B254A9 /* RoundedLabel.swift */, 23FA23E520BFD3D8009A6D73 /* SortBar.swift */, 3912208123436EB80026C290 /* SortMethod.swift */, ); path = "User Interface"; sourceTree = ""; }; - DCE4E43224C197480051722F /* User Activities */ = { - isa = PBXGroup; - children = ( - 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */, - ); - path = "User Activities"; - sourceTree = ""; - }; DCE4E43424C199860051722F /* Actions */ = { isa = PBXGroup; children = ( - 399EA75925E66DB000B6FF11 /* ClientItemResolvingCell.swift */, 6E37F48A2188B27D00CF16CA /* Action.swift */, - 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */, + DC62F57629268D740095BB5D /* Implementations */, ); path = Actions; sourceTree = ""; @@ -3210,8 +3201,7 @@ children = ( 39534BC924EA903200AD7907 /* InfoPlist.strings */, DCE4E48224C1F5B70051722F /* ownCloud Share Extension.entitlements */, - DCE4E46A24C1F5610051722F /* ShareViewController.swift */, - DCE4E48524C1F6B50051722F /* ShareNavigationController.swift */, + DC9B4FC42940F8D60037F8F8 /* ShareExtensionViewController.swift */, DCE4E46F24C1F5610051722F /* Info.plist */, ); path = "ownCloud Share Extension"; @@ -3228,10 +3218,9 @@ DCE5E8B62080D8B8005F60CE /* SDK Extensions */ = { isa = PBXGroup; children = ( - 23EC77572137F3DD0032D4E6 /* OCExtensionType+Extension.swift */, - DC4FEAE6209E3A7700D4476B /* OCIssue+Extension.swift */, + DCB6B1FA292CD76200D27573 /* OCBookmarkManager+Management.swift */, 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */, - DCE20271249AB50E0015A22A /* OCMessage+Extension.swift */, + 23EC77572137F3DD0032D4E6 /* OCExtensionType+Extension.swift */, ); path = "SDK Extensions"; sourceTree = ""; @@ -3259,12 +3248,18 @@ isa = PBXGroup; children = ( DCEAF0882808252300980B6D /* Cells */, + DCBAEADC29A552D100BFF393 /* Supplementary Cells */, DCFC9ED428002F33005D9144 /* CollectionViewCellConfiguration.swift */, DCFC9ED028002335005D9144 /* CollectionViewCellProvider.swift */, DCFC9ED2280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift */, DCFC9ECB28002303005D9144 /* CollectionViewSection.swift */, + DCBAEADD29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift */, + DCBAEAE329A5603600BFF393 /* CollectionViewSupplementaryCellProvider+StandardImplementations.swift */, + DCBAEADF29A554CC00BFF393 /* CollectionViewSupplementaryItem.swift */, DC04FFC727F5B79000F22569 /* CollectionViewController.swift */, - DC3AB1932808C2DE00789435 /* View Controllers */, + DCB6B20E292F843800D27573 /* CollectionViewAction.swift */, + DC5C48A22918FB7400EBC053 /* CollectionSidebarViewController.swift */, + DCB6B209292E296800D27573 /* CollectionSidebarAction.swift */, ); path = "Collection Views"; sourceTree = ""; @@ -3272,14 +3267,17 @@ DCEAF0882808252300980B6D /* Cells */ = { isa = PBXGroup; children = ( + DC49C22028524D6C00BAA910 /* ThemeableCollectionViewCell.swift */, + DC01AF2028411D8400903101 /* ThemeableCollectionViewListCell.swift */, + DCD68A2E291D9BE400993FF5 /* AccountControllerCell.swift */, DC46F3CD2844A92A00038880 /* ActionCell.swift */, + DC298CAD2936598B009FA87F /* DriveGridCell.swift */, DC3AB2412810404000789435 /* DriveHeaderCell.swift */, DCEAF0892808254800980B6D /* DriveListCell.swift */, DC3AB2472810A10300789435 /* ExpandableResourceCell.swift */, - DC3AB23D280FFE3400789435 /* ItemListCell.swift */, DC46F68F28DCA1B8008280CA /* SavedSearchCell.swift */, - DC49C22028524D6C00BAA910 /* ThemeableCollectionViewCell.swift */, - DC01AF2028411D8400903101 /* ThemeableCollectionViewListCell.swift */, + DC9219FE2966D5B800F538EE /* UniversalItemListCell Content Providers */, + DC9219F9296615B400F538EE /* UniversalItemListCell.swift */, DC46F3CB2844A8EA00038880 /* ViewCell.swift */, ); path = Cells; @@ -3330,6 +3328,8 @@ DCF2DA7524C82C9F0026D790 /* OCFileProviderService.h */, DC049155258C00C400DEDC27 /* OCFileProviderServiceStandby.m */, DC049154258C00C400DEDC27 /* OCFileProviderServiceStandby.h */, + DC6179E628E0578400C7C4E0 /* OCFileProviderSettings.m */, + DC6179E528E0578400C7C4E0 /* OCFileProviderSettings.h */, ); path = "File Provider Services"; sourceTree = ""; @@ -3339,6 +3339,7 @@ children = ( DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */, 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */, + DCE741DE29B8A85D00BFF393 /* BookmarkManagementContext.swift */, ); path = Bookmarks; sourceTree = ""; @@ -3351,7 +3352,6 @@ 6EA78B8E2179B55400A5216A /* ImageScrollView.swift */, 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */, 3998F5D422411EDF00B66713 /* BorderedLabel.swift */, - 396BE4C92289500E00B254A9 /* RoundedLabel.swift */, ); path = "UI Elements"; sourceTree = ""; @@ -3408,6 +3408,29 @@ path = "View Providers"; sourceTree = ""; }; + DCFA5641297573080092C89F /* Browser Navigation Controller */ = { + isa = PBXGroup; + children = ( + DCFA5644297574A60092C89F /* BrowserNavigationItem.swift */, + DC8E99DB297E79E900594697 /* BrowserNavigationHistory.swift */, + DCFA56422975734A0092C89F /* BrowserNavigationViewController.swift */, + DCFA9CAB2987E14B00004F24 /* BrowserNavigationBookmark.swift */, + DC89EA6829958DF500BFF393 /* UIViewController+BrowserNavigation.swift */, + ); + path = "Browser Navigation Controller"; + sourceTree = ""; + }; + DCFA9CA72987E14B00004F24 /* State Restoration */ = { + isa = PBXGroup; + children = ( + DC89EA5E2993A0FE00BFF393 /* README.md */, + DC89EA562992FDCD00BFF393 /* AppStateAction.swift */, + DC89EA5F2993AC4F00BFF393 /* NSUserActivity+SaveRestore.swift */, + DC89EA5A29939F9900BFF393 /* Actions */, + ); + path = "State Restoration"; + sourceTree = ""; + }; DCFEFE2223687637009A142F /* Licensing */ = { isa = PBXGroup; children = ( @@ -3434,6 +3457,7 @@ DC080CDC238AE3D00044C5D2 /* App Store */, DC4331F82472E17F002DC0E5 /* EMM */, DCDC20A42399A898003CFF5B /* Enterprise */, + DC8E99DD297E8D1800594697 /* QA */, ); path = Providers; sourceTree = ""; @@ -3497,14 +3521,6 @@ path = Manager; sourceTree = ""; }; - EA9337DC2226DAE00054971F /* Settings */ = { - isa = PBXGroup; - children = ( - EA9337E22226DB070054971F /* SettingsTests.swift */, - ); - name = Settings; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3530,6 +3546,7 @@ DC4332002472E1B4002DC0E5 /* OCLicenseEMMProvider.h in Headers */, DCF072DC279857A300E0B01D /* OCCircularContentView.h in Headers */, DC2A128428D0722F0088A2B7 /* OCSavedSearch.h in Headers */, + DC3DDF07287E1C0E00E5586D /* UIViewController+HostBundleID.h in Headers */, DCFEFE39236877A7009A142F /* OCLicenseFeature.h in Headers */, DC23D1DA238F391200423F62 /* OCLicenseAppStoreReceipt.h in Headers */, DC24E0EA28B36A81002E4F5B /* OCSearchSegment.h in Headers */, @@ -3552,6 +3569,7 @@ DCC832F1242CC27B00153F8C /* NotificationMessagePresenter.h in Headers */, DC080CF2238C8DF70044C5D2 /* OCLicenseAppStoreItem.h in Headers */, DCD9B87B2379612B00691929 /* OCLicenseManager+Internal.h in Headers */, + DC6179E728E0578400C7C4E0 /* OCFileProviderSettings.h in Headers */, DC774E6322F44E6D000B11A1 /* OCCore+BundleImport.h in Headers */, DCDBB60A2525305600FAD707 /* NotificationAuthErrorForwarder.h in Headers */, DC080CE6238AE3F40044C5D2 /* OCLicenseAppStoreProvider.h in Headers */, @@ -3573,6 +3591,7 @@ DCFEFE972368D099009A142F /* OCLicenseEnvironment.h in Headers */, DC6A0E5426EA9E740076B533 /* AppLockSettings.h in Headers */, DC2A128528D072350088A2B7 /* OCVault+SavedSearches.h in Headers */, + DC8E99E2297E906200594697 /* OCLicenseQAProvider.h in Headers */, DC49B55928365C5F00DAF13B /* OCVault+VFSManager.h in Headers */, DC36885824DC98BF00333600 /* OCFileProviderServiceSession.h in Headers */, ); @@ -3592,12 +3611,12 @@ DC85573420513CCC00189B9A /* Copy Frameworks */, DC63207821FCA6A4007EC0A8 /* Copy libzip license */, 23DFDCF120AEEC77003BD16B /* Update LastGitCommit key in Info.plist */, - DCC6567020C9B7E400110A97 /* Embed App Extensions */, + DCC6567020C9B7E400110A97 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( - DC82664F2818056C00F91F7D /* PBXTargetDependency */, + DC9B4FCC29413B4D0037F8F8 /* PBXTargetDependency */, DC63207D21FCA71B007EC0A8 /* PBXTargetDependency */, DC3BE0CF2077BC52002A0AC0 /* PBXTargetDependency */, DC3BE0D12077BC52002A0AC0 /* PBXTargetDependency */, @@ -3623,10 +3642,7 @@ isa = PBXNativeTarget; buildConfigurationList = 233BDEBC204FEFE600C06732 /* Build configuration list for PBXNativeTarget "ownCloudTests" */; buildPhases = ( - CCD5F274B7E08D493D596856 /* [CP] Check Pods Manifest.lock */, 233BDEAC204FEFE500C06732 /* Sources */, - FF929A2C3849C01CA20C4475 /* [CP] Embed Pods Frameworks */, - D9876310DCEA650662AA6AF7 /* EarlGrey Copy Files */, 233BDEAD204FEFE500C06732 /* Frameworks */, 233BDEAE204FEFE500C06732 /* Resources */, ); @@ -3656,6 +3672,7 @@ buildRules = ( ); dependencies = ( + DC9B4FC829413B400037F8F8 /* PBXTargetDependency */, DC0491D8258CB00000DEDC27 /* PBXTargetDependency */, DCDC0ACE23CD185F00DFE36D /* PBXTargetDependency */, 394A0B0C22EEFD2800603813 /* PBXTargetDependency */, @@ -3663,6 +3680,7 @@ name = ownCloudAppShared; packageProductDependencies = ( DC04920A258CB06A00DEDC27 /* PocketSVG */, + DC9B4FC929413B480037F8F8 /* Down */, ); productName = ownCloudAppShared; productReference = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; @@ -3709,27 +3727,6 @@ productReference = 39DC7CCD25C2E1570001E08C /* ownCloud File Provider UI.appex */; productType = "com.apple.product-type.app-extension"; }; - 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 59056CB122414F3C00A18A22 /* Build configuration list for PBXNativeTarget "ownCloudScreenshotsTests" */; - buildPhases = ( - 0C2F07233D9E9217FF85FB98 /* [CP] Check Pods Manifest.lock */, - 59056CA622414F3C00A18A22 /* Sources */, - 59056CA722414F3C00A18A22 /* Frameworks */, - 59056CA822414F3C00A18A22 /* Resources */, - CB0A5CD67C8C00CD02627343 /* [CP] Embed Pods Frameworks */, - 59799F7C224157E0007E8008 /* EarlGrey Copy Files */, - ); - buildRules = ( - ); - dependencies = ( - 59056CB022414F3C00A18A22 /* PBXTargetDependency */, - ); - name = ownCloudScreenshotsTests; - productName = ownCloudScreenshotsTests; - productReference = 59056CAA22414F3C00A18A22 /* ownCloudScreenshotsTests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; DC7DBA33207F84BF00E7337D /* MakeTVG */ = { isa = PBXNativeTarget; buildConfigurationList = DC7DBA38207F84BF00E7337D /* Build configuration list for PBXNativeTarget "MakeTVG" */; @@ -3837,7 +3834,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1330; + LastUpgradeCheck = 1420; ORGANIZATIONNAME = "ownCloud GmbH"; TargetAttributes = { 233BDE9B204FEFE500C06732 = { @@ -3867,7 +3864,7 @@ }; 394A0AF822EEFC2C00603813 = { CreatedOnToolsVersion = 11.0; - LastSwiftMigration = 1340; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; }; 39A7137F22E79C6700089423 = { @@ -3877,11 +3874,6 @@ 39DC7CCC25C2E1570001E08C = { CreatedOnToolsVersion = 12.2; }; - 59056CA922414F3C00A18A22 = { - CreatedOnToolsVersion = 10.1; - ProvisioningStyle = Automatic; - TestTargetID = 233BDE9B204FEFE500C06732; - }; DC7DBA33207F84BF00E7337D = { CreatedOnToolsVersion = 9.3; ProvisioningStyle = Automatic; @@ -3967,7 +3959,6 @@ DCC0855B2293F1FD008CC05C /* ownCloudApp */, 394A0AF822EEFC2C00603813 /* ownCloudAppShared */, DCC085632293F1FD008CC05C /* ownCloudAppTests */, - 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */, ); }; /* End PBXProject section */ @@ -4034,7 +4025,6 @@ 398393BE246D63B0001A212B /* branding-login-background.png in Resources */, 3968C883239C54AD00AC28AC /* ReleaseNotes.plist in Resources */, 398393BF246D63B0001A212B /* branding-login-logo.png in Resources */, - DC85572D20513B8C00189B9A /* ServerListTableViewController.xib in Resources */, 59D4895220C83F2E00369C2E /* InfoPlist.strings in Resources */, 392378FF24EBD1A1006E86DE /* branding-splashscreen.png in Resources */, 593A821120C7D4C5000E2A90 /* Localizable.strings in Resources */, @@ -4043,9 +4033,6 @@ 233BDEA7204FEFE500C06732 /* Assets.xcassets in Resources */, DC6C68362574FD0400E46BD4 /* PLCrashReporter.LICENSE in Resources */, 39F48A6A24D89D7E0000E3F9 /* branding-bookmark-icon.png in Resources */, - 595E2CA821EE501400F0E95D /* PropfindResponseNewFolder.xml in Resources */, - 59B09E6521AD61DD007827B8 /* test_certificate.cer in Resources */, - 595E2CA721EE4FC400F0E95D /* PropfindResponse.xml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4053,10 +4040,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 595E2C9F21EE4BF300F0E95D /* PropfindResponseNewFolder.xml in Resources */, DC0A5C432550C70800E6674B /* class-settings-sdk in Resources */, - 59B09E6621AD61DD007827B8 /* test_certificate.cer in Resources */, - 59B09E6421AD61DD007827B8 /* PropfindResponse.xml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4113,13 +4097,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 59056CA822414F3C00A18A22 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DCC0855A2293F1FD008CC05C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4151,30 +4128,9 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0C2F07233D9E9217FF85FB98 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ownCloudScreenshotsTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 23813A32205286E100DB9488 /* Run SwiftLint */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -4189,6 +4145,7 @@ }; 23DFDCF120AEEC77003BD16B /* Update LastGitCommit key in Info.plist */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -4202,44 +4159,9 @@ shellPath = /bin/sh; shellScript = "LASTGITCOMMIT=$(git rev-parse --short HEAD)\ndefaults write \"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH%.*}\" \"LastGitCommit\" \"${LASTGITCOMMIT}\"\n"; }; - CB0A5CD67C8C00CD02627343 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ownCloudScreenshotsTests/Pods-ownCloudScreenshotsTests-frameworks.sh", - "${PODS_ROOT}/EarlGrey/EarlGrey/EarlGrey.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EarlGrey.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ownCloudScreenshotsTests/Pods-ownCloudScreenshotsTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - CCD5F274B7E08D493D596856 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ownCloudTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; DC049259258CB33600DEDC27 /* Copy PocketSVG license (inactive) */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -4276,24 +4198,6 @@ shellPath = /bin/sh; shellScript = "echo \"${PROJECT_DIR}/external/libzip/LICENSE\" \"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/libzip.LICENSE\"\ncp \"${PROJECT_DIR}/external/libzip/LICENSE\" \"${TARGET_BUILD_DIR}/${WRAPPER_NAME}/libzip.LICENSE\"\n"; }; - FF929A2C3849C01CA20C4475 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ownCloudTests/Pods-ownCloudTests-frameworks.sh", - "${PODS_ROOT}/EarlGrey/EarlGrey/EarlGrey.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EarlGrey.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ownCloudTests/Pods-ownCloudTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -4301,16 +4205,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */, DC3DEC7B22AFA1F000F3352D /* DownloadItemsHUDViewController.swift in Sources */, 39EF06B325D6C3FC001E1E19 /* PresentationModeAction.swift in Sources */, - DCFB74BB21AD5C46005796AF /* StaticLoginViewController.swift in Sources */, DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */, 4CB8ADE322DF6BA700F1FEBC /* PHAsset+Upload.swift in Sources */, 4C7295D8228C384E00FA4E68 /* LogFilesViewController.swift in Sources */, DC01CDCC212EDDF600FC8E38 /* TextViewController.swift in Sources */, 4CB8ADE622DF6C2B00F1FEBC /* CIImage+Extensions.swift in Sources */, - 391C79AE24E187C400CB6333 /* LegacyCredentials.swift in Sources */, DC33939D22E076E300DD3DA4 /* MakeUnavailableOfflineAction.swift in Sources */, 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */, 6E4F1734217749910049A71B /* ImageDisplayViewController.swift in Sources */, @@ -4329,29 +4230,23 @@ 025FC745247EF0F1009307A7 /* BackgroundUploadsSettingsSection.swift in Sources */, DC63208321FCAC1E007EC0A8 /* ClientActivityViewController.swift in Sources */, DC9C1AEC247C76470067895A /* MessageGroupCell.swift in Sources */, - DC82665028183CFD00F91F7D /* OCResourceText+ViewProvider.swift in Sources */, 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */, 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */, - DCC832E2242C0EAC00153F8C /* MessageSelector.swift in Sources */, DCDC208F23994DFB003CFF5B /* LicenseTransactionsViewController.swift in Sources */, 4CC4A21222FA20AD00AE7E2C /* URL+Extensions.swift in Sources */, 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */, 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */, 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */, DCA35D8124D1707100DBE2B0 /* OCSyncRecordActivity+DiagnosticGenerator.swift in Sources */, - DC3BE0DF2077CC14002A0AC0 /* ClientRootViewController.swift in Sources */, 4CB8ADE022DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift in Sources */, - 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */, 394E1FFF233E43F5009D2897 /* LinksAction.swift in Sources */, 392DDB1424CF024D009E5406 /* ImportFilesController.swift in Sources */, - DC0CE19228C7DBE3009ABDFB /* OpenInWebAppAction.swift in Sources */, 396C82FB2319AFDD00938262 /* CollaborateAction.swift in Sources */, 0233F45E246E9D960095A799 /* UploadCameraMediaAction.swift in Sources */, DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */, DC0030CB2350B75000BB8570 /* ScanViewController.swift in Sources */, 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */, DCD1300A23A191C000255779 /* LicenseOfferButton.swift in Sources */, - 0269F589244DED02002E9D99 /* UIAlertController+UniversalLinks.swift in Sources */, DCFEF90926EFA45A001DC7A4 /* VendorServices+App.swift in Sources */, 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, 399698ED260A3CEE00E5AEBA /* ImportPasteboardAction.swift in Sources */, @@ -4371,47 +4266,34 @@ DC62514A225CEB4300736874 /* UploadMediaAction.swift in Sources */, 025FC72924781659009307A7 /* AutoUploadSettingsSection.swift in Sources */, DC9BFBBD20A1C37B007064B5 /* PasswordManagerAccess.swift in Sources */, - 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */, 23D5241521491C670002C566 /* DisplayViewController.swift in Sources */, - DCFB74C221AD5D10005796AF /* StaticLoginSetupViewController.swift in Sources */, - DCD864102811821200CA6631 /* ClientRootViewController+ItemActions.swift in Sources */, - DC0B379420514E4700189B9A /* ServerListBookmarkCell.swift in Sources */, - 397E276A23D04D7100117B07 /* StaticLoginSingleAccountServerListViewController.swift in Sources */, - DCE20272249AB50E0015A22A /* OCMessage+Extension.swift in Sources */, + DCB6B1EB292B7C2400D27573 /* AppRootViewController+ItemActions.swift in Sources */, + DCB6B20C292E428000D27573 /* AccountController+ExtraItems.swift in Sources */, 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */, 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */, - 02072E6023E46022006548A7 /* UIWindow+Extension.swift in Sources */, - DC44343E21ABFA5200376B16 /* StaticLoginProfile.swift in Sources */, 4C82D07022C9387300835F0B /* MediaDisplayViewController.swift in Sources */, DC6CC3152642C3560040ECAC /* ExternalBrowserBusyHandler.swift in Sources */, - DCFB74C421AD7E18005796AF /* StaticLoginServerListViewController.swift in Sources */, 232F7CAF2097260400EE22E4 /* SettingsViewController.swift in Sources */, DCA35D3F24CEDA5200DBE2B0 /* DiagnosticViewController.swift in Sources */, 4C464BF32187AF1500D30602 /* PDFOutlineViewController.swift in Sources */, - DC85572C20513B8C00189B9A /* ServerListTableViewController.swift in Sources */, 233BDEA0204FEFE500C06732 /* AppDelegate.swift in Sources */, 4C51727E22DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift in Sources */, 4C05D8A5238708D40073EF50 /* MediaUploadStorage.swift in Sources */, + DCB6B1FB292CD76200D27573 /* OCBookmarkManager+Management.swift in Sources */, 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */, - DCC832F8242CC94D00153F8C /* AlertView.swift in Sources */, 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */, - 02F2891524BFAF0100E3D35C /* Migration.swift in Sources */, 4C96463C238489E4003278B7 /* MediaUploadActivity.swift in Sources */, 02633EFF2483D2EB00B5F58F /* UNUserNotificationCenter+Extensions.swift in Sources */, 6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */, 4CC4A21922FB4F4C00AE7E2C /* MediaUploadQueue.swift in Sources */, 4CC46D212284C677009E938F /* BookmarkInfoViewController.swift in Sources */, - DC3393A022E0A1C000DD3DA4 /* ItemPolicyTableViewController.swift in Sources */, - 02F2891624BFAF0100E3D35C /* MigrationActivityCell.swift in Sources */, + DCB6B1F5292CC46B00D27573 /* AccountController+ItemActions.swift in Sources */, 6E5FC172221590B000F60846 /* DisplayHostViewController.swift in Sources */, 4C51727F22DE04BD001BC97F /* ScheduledTaskManager.swift in Sources */, 39BC9C3023DB831F0097C52D /* DocumentEditingAction.swift in Sources */, - DCFE682728D869A400091D2A /* ClientWebAppViewController.swift in Sources */, - DCB6C4D72453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift in Sources */, 399697F5260255B100E5AEBA /* PDFGotoPageAction.swift in Sources */, 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */, DCC3701624D4D365008B0DEB /* OCScanJobActivity+DiagnosticGenerator.swift in Sources */, - 39A243C424BDD9E100F4441F /* StaticLoginBundle.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, DC0CE19D28C89CD9009ABDFB /* CreateDocumentAction.swift in Sources */, 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, @@ -4429,36 +4311,25 @@ 6EA78B8F2179B55400A5216A /* ImageScrollView.swift in Sources */, DCD954DF247D62FA00E184E6 /* MessageTableViewController.swift in Sources */, DCC5E446232654DE002E5B84 /* NSObject+AnnotatedProperties.m in Sources */, - DC68057A212EAB5E006C3B1F /* ThemeCertificateViewController.swift in Sources */, 025FC73A247BF8BE009307A7 /* PHAsset+InstantUploads.swift in Sources */, 399DD7C722A691BC00B45EB2 /* UnshareAction.swift in Sources */, 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */, - DCE4E45624C1ED8D0051722F /* ClientQueryViewController+InlineMessageSupport.swift in Sources */, 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */, 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */, DCEE1C9C23A0EADD00FE8D98 /* LicenseOfferView.swift in Sources */, - DCB6C4DE24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift in Sources */, 6E3A104D219D6F0100F90C96 /* DuplicateAction.swift in Sources */, - DCFB74C121AD5C88005796AF /* StaticLoginStepViewController.swift in Sources */, - 02F2891424BFAF0100E3D35C /* MigrationViewController.swift in Sources */, - DC4D5A0A247C1398008ADDB6 /* MessageGroup.swift in Sources */, - DCE4E44424C1A3E30051722F /* FileListTableViewController+OpenItemTableViewController.swift in Sources */, 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */, DC51FD922475715F0069AB79 /* CellularSettingsViewController.swift in Sources */, - DCE4E44D24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift in Sources */, DCC8536023CE1AF8007BA3EB /* PurchasesSettingsSection.swift in Sources */, DCD2D40622F06ECA0071FB8F /* DataSettingsSection.swift in Sources */, 39969904260A3CF500E5AEBA /* CutAction.swift in Sources */, 4C464BF22187AF1500D30602 /* PDFSearchViewController.swift in Sources */, - DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */, - 399EA70725E654B400B6FF11 /* PendingSharesTableViewController.swift in Sources */, + DC6C0A4929239E560045FF2A /* AppRootViewController.swift in Sources */, 02DC7C9024CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift in Sources */, DCC832F6242CC5F700153F8C /* CardIssueMessagePresenter.swift in Sources */, DCD1301123A23F4E00255779 /* OCLicenseManager+AppStore.swift in Sources */, - DC24B31D25BB6FC4005783E2 /* IssuesCardViewController.swift in Sources */, DC62514C225D254500736874 /* UploadBaseAction.swift in Sources */, 4C6B78102226B83300C5F3DB /* PhotoAlbumTableViewController.swift in Sources */, - DC3393A222E0A71100DD3DA4 /* ItemPolicyCell.swift in Sources */, 23EC77592137F3DD0032D4E6 /* DisplayExtension.swift in Sources */, DCDF58B323CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift in Sources */, DC33939622E0747400DD3DA4 /* MakeAvailableOfflineAction.swift in Sources */, @@ -4466,10 +4337,7 @@ 39057AA3233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, 6E586CFE2199A75900F680C4 /* MoveAction.swift in Sources */, DC625148225CEB2C00736874 /* UploadFileAction.swift in Sources */, - DCC83304242CF3AD00153F8C /* AlertViewController.swift in Sources */, 394E1FDC233E3750009D2897 /* UnfavoriteAction.swift in Sources */, - DC4FEAE7209E3A7700D4476B /* OCIssue+Extension.swift in Sources */, - 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */, 6E586D002199A78E00F680C4 /* DeleteAction.swift in Sources */, DC0196AB20F7690C00C41B78 /* OCBookmark+FileProvider.m in Sources */, 4C6B78122226B86300C5F3DB /* PhotoAlbumTableViewCell.swift in Sources */, @@ -4481,26 +4349,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 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 */, - EA88A55521BFD5BF0055A58F /* DeleteBookmarkTests.swift in Sources */, - 6D107AA0B21417432C72755A /* EarlGrey.swift in Sources */, - 59B09E6D21AD61F4007827B8 /* FileListTests.swift in Sources */, - 4C16CBA7226F0F1A00D67BB6 /* FileTests.swift in Sources */, - 5958C9BE20C000A700E0E567 /* PasscodeTests.swift in Sources */, - 5917244E20D3DC2100809B38 /* BiometricalTests.swift in Sources */, - 59538A0B21E4C301005E543B /* MockOCQuery.swift in Sources */, DC26ADDE2550C0B20059680D /* MetadataDocumentationTests.swift in Sources */, - 5971CF3A22046F530052FE9A /* MockClientRootViewController.swift in Sources */, - 59B09E7021AD61F4007827B8 /* UtilsTests.swift in Sources */, - 59538A1C21E77FB7005E543B /* MockOCCore.swift in Sources */, - DCAEB06121F9FC510067E147 /* EarlGrey+Tools.swift in Sources */, - DCAEB05F21F9FB370067E147 /* OCBookmarkManager+Tools.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4508,103 +4358,150 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 399EA6F625E6544100B6FF11 /* ShareClientItemCell.swift in Sources */, DC0A357E24C0E43C00FB58FC /* ThemeStyle+Extensions.swift in Sources */, + DC298C872934A405009FA87F /* ClientDefaultViewController.swift in Sources */, DC04FFC827F5B79000F22569 /* CollectionViewController.swift in Sources */, + DC298CA72936250D009FA87F /* ClientSidebarViewController.swift in Sources */, + DC921A042966DFDC00F538EE /* OCItem+UniversalItemListCellContentProvider.swift in Sources */, + DCB6B1E6292B6BE500D27573 /* OCLocation+Interactions.swift in Sources */, DC3AB2422810404000789435 /* DriveHeaderCell.swift in Sources */, - DCE4E44524C1A4260051722F /* FileListTableViewController.swift in Sources */, DC65590F28A2633C0003D130 /* UITextField+Extension.swift in Sources */, + DCBAEAE429A5603600BFF393 /* CollectionViewSupplementaryCellProvider+StandardImplementations.swift in Sources */, + DC298C9A2934D3F8009FA87F /* AlertView.swift in Sources */, DC24E10728B7BFD6002E4F5B /* ItemSearchScope.swift in Sources */, + DC8E99E9297FF5C300594697 /* UIViewController+Extension.swift in Sources */, DC46F3D72845FCFA00038880 /* UIView+OCDataItem.swift in Sources */, - 399EA75A25E66DB000B6FF11 /* ClientItemResolvingCell.swift in Sources */, DCE4E43524C1999A0051722F /* Action.swift in Sources */, + DC298CAE2936598B009FA87F /* DriveGridCell.swift in Sources */, DC46F3D5284563BD00038880 /* OCAction+Interactions.swift in Sources */, + DC8E99E5297EEB2800594697 /* ClientLocationBarController.swift in Sources */, DCFC9ECC28002303005D9144 /* CollectionViewSection.swift in Sources */, + DC89EA602993AC4F00BFF393 /* NSUserActivity+SaveRestore.swift in Sources */, + DC62F567292504060095BB5D /* AccountConnection.swift in Sources */, + DC9219FD2966229100F538EE /* UniversalItemListCell.swift in Sources */, + DC28F826294B733700AC4013 /* OCItemPolicy+Interactions.swift in Sources */, + DC89EA5C2993A0E000BFF393 /* AppStateActionConnect.swift in Sources */, + DCB6B206292E1D8400D27573 /* RoundedLabel.swift in Sources */, + DC99155028E63D8300DA0AB8 /* SegmentViewItemView.swift in Sources */, DC3AB2462810602500789435 /* UILabel+Extension.swift in Sources */, DC0A356C24C0E42200FB58FC /* AppLockWindow.swift in Sources */, DCFC9ED128002335005D9144 /* CollectionViewCellProvider.swift in Sources */, DC46F3D32845583D00038880 /* OCDrive+Interactions.swift in Sources */, + DC081C8B299B9B9000BFF393 /* AppStateActionGoToPersonalFolder.swift in Sources */, DCE4E43B24C19B4F0051722F /* NSLayoutConstraint+Extension.swift in Sources */, DCE2F03E27FADF2600E9E136 /* UICollectionViewDiffableDataSource+Tools.swift in Sources */, DC0A355724C0E35B00FB58FC /* UIDevice+UIUserInterfaceIdiom.swift in Sources */, DC0A359224C0E55800FB58FC /* UIColor+Extension.swift in Sources */, 399EA6F825E6544100B6FF11 /* PublicLinkTableViewController.swift in Sources */, DCE4E45024C1E0400051722F /* UIButton+Extension.swift in Sources */, + DC89EA672995456A00BFF393 /* BrowserNavigationBookmark+AccountController.swift in Sources */, DC0A357024C0E42700FB58FC /* StaticTableViewSection.swift in Sources */, DC0A357624C0E43200FB58FC /* ProgressHUDViewController.swift in Sources */, 399EA6F925E6544100B6FF11 /* SharingTableViewController.swift in Sources */, DC46F69228DCB9C6008280CA /* OCSavedSearch+Interactions.swift in Sources */, - DCE4E45824C1F0F40051722F /* ClientDirectoryPickerViewController.swift in Sources */, DCDC0AD123CD18D200DFE36D /* OCLicenseManager+Setup.swift in Sources */, DCE4E44724C1AC4F0051722F /* MessageView.swift in Sources */, DCFE682E28D9CEDD00091D2A /* ComposedMessageView.swift in Sources */, + DCD68A2D291D979400993FF5 /* AccountController.swift in Sources */, + DC9B4FCE2941DA6E0037F8F8 /* UINavigationItem+Extension.swift in Sources */, + DC89EA572992FDCD00BFF393 /* AppStateAction.swift in Sources */, + DC298CB029366B96009FA87F /* AccountControllerSpacesGridViewController.swift in Sources */, DCE4E48824C1FA430051722F /* NamingViewController.swift in Sources */, DC01AF2128411D8400903101 /* ThemeableCollectionViewListCell.swift in Sources */, + DC60F2A629802ABE00905EC8 /* UINavigationItem+NavigationContent.swift in Sources */, 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */, + DC298C9E2934D6D9009FA87F /* AccountAuthenticationUpdater.swift in Sources */, DCE4E44124C1A07E0051722F /* UITableViewController+Extension.swift in Sources */, DC0A358E24C0E44B00FB58FC /* ThemeableColoredView.swift in Sources */, DC0A356D24C0E42200FB58FC /* PasscodeViewController.swift in Sources */, DC24B2AB25BA316D005783E2 /* Branding+App.swift in Sources */, 0234EF0E2515138B00AE921A /* PasscodeSetupCoordinator.swift in Sources */, DCA35DA724D309B600DBE2B0 /* OCFileProviderServiceSession+UploadByFileProvider.swift in Sources */, + DC298C922934CF56009FA87F /* AccountConnectionErrorHandler.swift in Sources */, 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */, DC0A357A24C0E43700FB58FC /* CardViewController.swift in Sources */, DC46F3D1284546F000038880 /* OCItem+Interactions.swift in Sources */, + DCBAEAED29A627D900BFF393 /* DataSourceCondition.swift in Sources */, DC0A356B24C0E42200FB58FC /* AppLockManager.swift in Sources */, + DCFA9CAF2987E14B00004F24 /* BrowserNavigationBookmark.swift in Sources */, + DC298C9F2934D6D9009FA87F /* AccountAuthenticationUpdaterPasswordPromptViewController.swift in Sources */, DCE4E4C724C255E00051722F /* AppExtensionNavigationController.swift in Sources */, - DC9A116B27D0338400D90BA4 /* ClientSpacesTableViewController.swift in Sources */, + DC3F0C2529828AE300C832DB /* OCLocation+Breadcrumbs.swift in Sources */, DC0A358024C0E43C00FB58FC /* NSObject+ThemeApplication.swift in Sources */, DC0A358224C0E44200FB58FC /* TVGImage.swift in Sources */, - DCE4E44F24C1DF130051722F /* UIViewController+Extension.swift in Sources */, DC0A357F24C0E43C00FB58FC /* ThemeStyle+DefaultStyles.swift in Sources */, DCB5D60B25FC14B6004C52D9 /* OCIssue+Extension.swift in Sources */, DC46F69028DCA1B8008280CA /* SavedSearchCell.swift in Sources */, DC66A9F4279EEBF900792AC8 /* ThemeView.swift in Sources */, + DC60F2AA29802D5800905EC8 /* NavigationContentItem.swift in Sources */, DC0A356F24C0E42700FB58FC /* StaticTableViewController.swift in Sources */, - DC0A357224C0E42D00FB58FC /* PushPresentationController.swift in Sources */, + DC62F569292504510095BB5D /* AccountConnectionPool.swift in Sources */, DC0A358F24C0E46000FB58FC /* PointerEffect.swift in Sources */, + DC921A022966D5F800F538EE /* OCShare+UniversalItemListCellContentProvider.swift in Sources */, DC0A358924C0E44B00FB58FC /* ThemeNavigationController.swift in Sources */, DCBD8EA824B3751900D92E1F /* OCItem+Extension.swift in Sources */, 391C79A824E186DC00CB6333 /* OCBookmark+Extension.swift in Sources */, DC0A358A24C0E44B00FB58FC /* ThemeButton.swift in Sources */, + DCB6B1F7292CC8E200D27573 /* OCBookmarkManager+Locking.swift in Sources */, 399EA71B25E6561E00B6FF11 /* OCShare+Extension.swift in Sources */, + DC62F6F5292819C80095BB5D /* AccountConnectionRichStatus.swift in Sources */, + DCB6B1ED292B963300D27573 /* AccountConnection+ItemActions.swift in Sources */, + DCB6B200292CF2FE00D27573 /* NavigationRevocationAction.swift in Sources */, DC0A355424C0E2C200FB58FC /* SortBar.swift in Sources */, - DCE4E45924C1F0F70051722F /* ClientQueryViewController.swift in Sources */, - DCE4E45124C1E4430051722F /* UIBarButtonItem+Extension.swift in Sources */, + DC9B4FC3293F453C0037F8F8 /* EmbeddingViewController.swift in Sources */, + DC298C972934D354009FA87F /* IssuesCardViewController.swift in Sources */, DCA2EDE2279B16F1001F04E6 /* ResourceSourceItemIcons.swift in Sources */, + DCBAEADE29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift in Sources */, DCEAF08A2808254800980B6D /* DriveListCell.swift in Sources */, DCB5D5A728632C17004AF425 /* SearchScope.swift in Sources */, + DC6FDAF72953AD50004F0C7F /* ClientSharedWithMeViewController.swift in Sources */, + DC9219FC2966179600F538EE /* OCShare+Interactions.swift in Sources */, + DC9B4FC629413AE20037F8F8 /* OCResourceText+ViewProvider.swift in Sources */, + DCB6B1FD292CF2DB00D27573 /* NavigationRevocationManager.swift in Sources */, DC0A357724C0E43200FB58FC /* ProgressSummarizer.swift in Sources */, + DCB6B205292D859B00D27573 /* UIViewController+NavigationRevocation.swift in Sources */, 392CFEB72705831700631D2B /* LAContext+Extension.swift in Sources */, DCE4E48724C1F9F50051722F /* CreateFolderAction.swift in Sources */, 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */, DC0A358624C0E44600FB58FC /* ThemeTVGResource.swift in Sources */, + DC62F57529268D710095BB5D /* OpenInWebAppAction.swift in Sources */, + DC99155228E63ECD00DA0AB8 /* UIView+EmbedAndLayout.swift in Sources */, DC46F68E28DAFCDD008280CA /* RoundCornerBackgroundView.swift in Sources */, + DCB6B1F0292B979A00D27573 /* MessageSelector.swift in Sources */, + DC298CA129357809009FA87F /* AccountConnectionAuthErrorConsumer.swift in Sources */, DC3AB24428104AA500789435 /* UIFont+Weight.swift in Sources */, + DCB6B1F9292CCAF200D27573 /* OCBookmarkManager+Management.swift in Sources */, DC0A358524C0E44600FB58FC /* ThemeResource.swift in Sources */, + DC99154C28E636A500DA0AB8 /* SegmentView.swift in Sources */, DCD864122811FC5700CA6631 /* GradientView.swift in Sources */, + DC99154E28E6371500DA0AB8 /* SegmentViewItem.swift in Sources */, DCFC9ED528002F33005D9144 /* CollectionViewCellConfiguration.swift in Sources */, + DCBAEADB29A3674700BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift in Sources */, DC0A355624C0E33A00FB58FC /* Log.swift in Sources */, DC0A359624C0E61500FB58FC /* UIView+Extension.swift in Sources */, + DCB6B1F2292B9A7A00D27573 /* OCMessage+Extension.swift in Sources */, DCE4E43924C19AB20051722F /* MoreStaticTableViewController.swift in Sources */, DC82663C28168D2800F91F7D /* ClientContext.swift in Sources */, DC0A358C24C0E44B00FB58FC /* ThemeWindow.swift in Sources */, DC0A357124C0E42700FB58FC /* StaticTableViewRow.swift in Sources */, DC3AB1982808C35300789435 /* ClientItemViewController.swift in Sources */, - DCE4E43124C197450051722F /* OpenItemUserActivity.swift in Sources */, + DC298CAC29362710009FA87F /* ClientLocationPickerViewController.swift in Sources */, + DC081C89299B8E5800BFF393 /* AppStateActionRevealItem.swift in Sources */, + DCB6B1EF292B979600D27573 /* MessageGroup.swift in Sources */, + DC5C48A32918FB7400EBC053 /* CollectionSidebarViewController.swift in Sources */, + DC6C0A542923FFF30045FF2A /* AccountControllerSection.swift in Sources */, + DCFA56432975734A0092C89F /* BrowserNavigationViewController.swift in Sources */, DCFC9ED3280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift in Sources */, + DCB6B20A292E296800D27573 /* CollectionSidebarAction.swift in Sources */, DC0A358D24C0E44B00FB58FC /* ThemedAlertController.swift in Sources */, DC24E10F28B7D2B9002E4F5B /* PopupButtonController.swift in Sources */, DCE4E43C24C19B660051722F /* FrameViewController.swift in Sources */, DC0A35A124C1091400FB58FC /* UserInterfaceContext.swift in Sources */, - DC0A357424C0E42D00FB58FC /* PushTransition.swift in Sources */, DC0A357824C0E43700FB58FC /* CardPresentationController.swift in Sources */, 393D2B3F23FEB6DC00ED4F8C /* DispatchQueueTools.swift in Sources */, DC66A9F6279EEC3100792AC8 /* ResourceViewHost.swift in Sources */, DC65592E28A644E10003D130 /* SearchTokenizer.swift in Sources */, DC24E0F828B41694002E4F5B /* OCQueryCondition+SearchToken.swift in Sources */, - DC0A357324C0E42D00FB58FC /* PushTransitionDelegate.swift in Sources */, - DCE4E45424C1EC040051722F /* BreadCrumbTableViewController.swift in Sources */, 399EA73A25E656A900B6FF11 /* UITableView+Extension.swift in Sources */, 399EA6F725E6544100B6FF11 /* PublicLinkEditTableViewController.swift in Sources */, DC46F3C72844A75200038880 /* OCDataItem+InteractionProtocols.swift in Sources */, @@ -4614,40 +4511,57 @@ DC36886224DDA9AB00333600 /* ProgressIndicatorViewController.swift in Sources */, 399EA72625E6565900B6FF11 /* OCCore+Extension.swift in Sources */, DC0A359424C0E5C800FB58FC /* GitCommit.swift in Sources */, - DCE4E44B24C1D3780051722F /* QueryFileListTableViewController.swift in Sources */, - DC3AB23E280FFE3400789435 /* ItemListCell.swift in Sources */, DC3AB2482810A10300789435 /* ExpandableResourceCell.swift in Sources */, DC3AB240280FFF2700789435 /* NSMutableAttributedString+AppendStyled.swift in Sources */, DC0A357C24C0E43C00FB58FC /* ThemeCollection.swift in Sources */, + DC28F828294BB5ED00AC4013 /* SortedItemDataSource.swift in Sources */, + DC298C992934D3F8009FA87F /* AlertViewController.swift in Sources */, + DC8E99DC297E79E900594697 /* BrowserNavigationHistory.swift in Sources */, + DCBAEAE829A568D500BFF393 /* TitleSupplementaryCell.swift in Sources */, 399EA6F525E6544100B6FF11 /* GroupSharingTableViewController.swift in Sources */, DC0A357924C0E43700FB58FC /* CardTransitionDelegate.swift in Sources */, DC0A358724C0E44600FB58FC /* ThemeImage.swift in Sources */, DC0A355824C0E35B00FB58FC /* Synchronized.swift in Sources */, DCA2EDE4279B1789001F04E6 /* ResourceItemIcon.swift in Sources */, + DC8E99E8297F3BA700594697 /* ActionTapGestureRecognizer.swift in Sources */, DCB5D56B2861BEBE004AF425 /* SearchViewController.swift in Sources */, 0287DD7D249131E000C912CA /* AppStatistics.swift in Sources */, + DC60F2A829802B0900905EC8 /* NavigationContent.swift in Sources */, DC49C22128524D6C00BAA910 /* ThemeableCollectionViewCell.swift in Sources */, + DC62F56C29250DC80095BB5D /* AccountConnectionConsumer.swift in Sources */, + DCBAEAE029A554CC00BFF393 /* CollectionViewSupplementaryItem.swift in Sources */, DC0A359924C0E6FE00FB58FC /* VendorServices.swift in Sources */, DC0A358324C0E44200FB58FC /* VectorImage.swift in Sources */, DC0A359824C0E68700FB58FC /* OCItem+AppExtension.swift in Sources */, DC0A358424C0E44200FB58FC /* VectorImageView.swift in Sources */, + DCFA5645297574A60092C89F /* BrowserNavigationItem.swift in Sources */, DC24E10D28B7C19F002E4F5B /* AccountSearchScope.swift in Sources */, DC65593228A648680003D130 /* SearchElement.swift in Sources */, - DC0A355324C0E2C200FB58FC /* ClientItemCell.swift in Sources */, + DCBAEAE229A55D5A00BFF393 /* ViewSupplementaryCell.swift in Sources */, + DCB6B203292D45AD00D27573 /* NavigationRevocationTrigger.swift in Sources */, + DC921A0B2968BA4D00F538EE /* ClientSharedByMeViewController.swift in Sources */, DC0A357D24C0E43C00FB58FC /* ThemeStyle.swift in Sources */, DC46F3CC2844A8EA00038880 /* ViewCell.swift in Sources */, + DC298C982934D381009FA87F /* OCIssue+DisplayIssues.swift in Sources */, + DCB6B20F292F843800D27573 /* CollectionViewAction.swift in Sources */, DC0A357524C0E43200FB58FC /* ProgressView.swift in Sources */, + DC62F57429268D710095BB5D /* ClientWebAppViewController.swift in Sources */, DC24E10B28B7C185002E4F5B /* SingleFolderSearchScope.swift in Sources */, 3912208223436EB80026C290 /* SortMethod.swift in Sources */, DCE4E43F24C19D370051722F /* UIAlertController+OCIssue.swift in Sources */, + DC298CA929362523009FA87F /* ClientLocationPicker.swift in Sources */, DC0A359524C0E5F900FB58FC /* UIImage+Extension.swift in Sources */, + DC89EA6B29959BD200BFF393 /* AppStateActionRestoreNavigationBookmark.swift in Sources */, DC66A9F8279F467200792AC8 /* UIKeyCommand+Extension.swift in Sources */, DC46F3CE2844A92A00038880 /* ActionCell.swift in Sources */, DCE4E43724C19A910051722F /* LicenseRequirements.swift in Sources */, DCE4E43A24C19ADC0051722F /* MoreViewHeader.swift in Sources */, + DC298C9C2934D47D009FA87F /* ThemeCertificateViewController.swift in Sources */, 399EA74625E6575B00B6FF11 /* NotificationHUDViewController.swift in Sources */, DC0A357B24C0E43C00FB58FC /* Theme.swift in Sources */, 399EA6FA25E6544100B6FF11 /* GroupSharingEditTableViewController.swift in Sources */, + DCD68A2F291D9BE400993FF5 /* AccountControllerCell.swift in Sources */, + DC89EA6929958DF500BFF393 /* UIViewController+BrowserNavigation.swift in Sources */, DC0A358B24C0E44B00FB58FC /* ThemeRoundedButton.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4681,19 +4595,6 @@ ); 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 */, - 59799F8122415956007E8008 /* EarlGrey+Tools.swift in Sources */, - 59799F80224158FD007E8008 /* EarlGrey.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DC7DBA30207F84BF00E7337D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4720,6 +4621,8 @@ DC24B29825BA2A34005783E2 /* Branding.m in Sources */, DCFEFE3E236877B7009A142F /* OCLicenseProduct.m in Sources */, DC774E6422F44E6D000B11A1 /* OCCore+BundleImport.m in Sources */, + DC6179E828E0578400C7C4E0 /* OCFileProviderSettings.m in Sources */, + DC8E99E3297E906700594697 /* OCLicenseQAProvider.m in Sources */, DCF2DA7E24C835BF0026D790 /* OCVault+FPServices.m in Sources */, DC2A128728D0725D0088A2B7 /* OCSavedSearch.m in Sources */, DCDC208D239912DC003CFF5B /* OCLicenseTransaction.m in Sources */, @@ -4737,6 +4640,7 @@ DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */, DCF072D72798558B00E0B01D /* OCCircularImageView.m in Sources */, DC774E5F22F44E57000B11A1 /* ZIPArchive.m in Sources */, + DC3DDF06287E1C0800E5586D /* UIViewController+HostBundleID.m in Sources */, DCDBB60B2525306000FAD707 /* NotificationAuthErrorForwarder.m in Sources */, DCD71E8027427463001592C6 /* BuildOptions.m in Sources */, DC080CE5238AE3F40044C5D2 /* OCLicenseAppStoreProvider.m in Sources */, @@ -4791,8 +4695,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DCE4E46B24C1F5610051722F /* ShareViewController.swift in Sources */, - DCE4E48624C1F6B50051722F /* ShareNavigationController.swift in Sources */, + DC9B4FC52940F8D60037F8F8 /* ShareExtensionViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4844,11 +4747,6 @@ target = 394A0AF822EEFC2C00603813 /* ownCloudAppShared */; targetProxy = 39DC7CF625C305E80001E08C /* PBXContainerItemProxy */; }; - 59056CB022414F3C00A18A22 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 233BDE9B204FEFE500C06732 /* ownCloud */; - targetProxy = 59056CAF22414F3C00A18A22 /* PBXContainerItemProxy */; - }; DC0491AA258CAF9800DEDC27 /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = DC0491A9258CAF9800DEDC27 /* PocketSVG */; @@ -4903,9 +4801,13 @@ name = libzip; targetProxy = DC774E6622F44F65000B11A1 /* PBXContainerItemProxy */; }; - DC82664F2818056C00F91F7D /* PBXTargetDependency */ = { + DC9B4FC829413B400037F8F8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = DC9B4FC729413B400037F8F8 /* Down */; + }; + DC9B4FCC29413B4D0037F8F8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = DC82664E2818056C00F91F7D /* Down */; + productRef = DC9B4FCB29413B4D0037F8F8 /* Down */; }; DCB2C059250C1C3F001083CA /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -5124,7 +5026,7 @@ APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; APP_SHORT_VERSION = 12.0; - APP_VERSION = 228; + APP_VERSION = 252; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -5194,7 +5096,7 @@ APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; APP_SHORT_VERSION = 12.0; - APP_VERSION = 228; + APP_VERSION = 252; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -5316,7 +5218,6 @@ }; 233BDEBD204FEFE600C06732 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 03AE98EEF23B4F4F2C0FDD0F /* Pods-ownCloudTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5337,7 +5238,6 @@ }; 233BDEBE204FEFE600C06732 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D6033BC23D1129172E6D6383 /* Pods-ownCloudTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5537,65 +5437,6 @@ }; name = Release; }; - 59056CB222414F3C00A18A22 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A80C5F83C59F9D52F8F64890 /* Pods-ownCloudScreenshotsTests.debug.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 4AP2STM4H5; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Pods/EarlGrey/EarlGrey", - ); - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = ownCloudScreenshotsTests/Info.plist; - 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.ownCloudScreenshotsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = ownCloud; - }; - name = Debug; - }; - 59056CB322414F3C00A18A22 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 5AA495E19A4CA58CF2678112 /* Pods-ownCloudScreenshotsTests.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 4AP2STM4H5; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Pods/EarlGrey/EarlGrey", - ); - INFOPLIST_FILE = ownCloudScreenshotsTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.owncloud.ownCloudScreenshotsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = ownCloud; - }; - name = Release; - }; DC7DBA39207F84BF00E7337D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5904,15 +5745,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 59056CB122414F3C00A18A22 /* Build configuration list for PBXNativeTarget "ownCloudScreenshotsTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 59056CB222414F3C00A18A22 /* Debug */, - 59056CB322414F3C00A18A22 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; DC7DBA38207F84BF00E7337D /* Build configuration list for PBXNativeTarget "MakeTVG" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -6021,7 +5853,17 @@ package = DC049197258CAF8200DEDC27 /* XCRemoteSwiftPackageReference "PocketSVG" */; productName = PocketSVG; }; - DC82664E2818056C00F91F7D /* Down */ = { + DC9B4FC729413B400037F8F8 /* Down */ = { + isa = XCSwiftPackageProductDependency; + package = DCEAF08B28084B3800980B6D /* XCRemoteSwiftPackageReference "Down" */; + productName = Down; + }; + DC9B4FC929413B480037F8F8 /* Down */ = { + isa = XCSwiftPackageProductDependency; + package = DCEAF08B28084B3800980B6D /* XCRemoteSwiftPackageReference "Down" */; + productName = Down; + }; + DC9B4FCB29413B4D0037F8F8 /* Down */ = { isa = XCSwiftPackageProductDependency; package = DCEAF08B28084B3800980B6D /* XCRemoteSwiftPackageReference "Down" */; productName = Down; diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme index 619f2d312..6aa2916ba 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme @@ -1,6 +1,6 @@ diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme index 144dd6682..d198c6ad5 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme @@ -1,6 +1,6 @@ diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Intents.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Intents.xcscheme index bc5b777df..9c6d01fd3 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Intents.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Intents.xcscheme @@ -1,6 +1,6 @@ diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Share Extension.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Share Extension.xcscheme index a1c5be808..6d6589dfa 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Share Extension.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud Share Extension.xcscheme @@ -1,6 +1,6 @@ diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index 958a371be..d7fc0b3a6 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + - - - - diff --git a/ownCloud.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ownCloud.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index e2b9b6417..000000000 --- a/ownCloud.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,41 +0,0 @@ -{ - "pins" : [ - { - "identity" : "down", - "kind" : "remoteSourceControl", - "location" : "https://github.com/johnxnguyen/Down", - "state" : { - "branch" : "master", - "revision" : "e754ab1c80920dd51a8e08290c912ac1c2ac8b58" - } - }, - { - "identity" : "openssl", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/OpenSSL.git", - "state" : { - "revision" : "8697a05bcddbbeb5ac2d29cd60442cf93ee0d3ae", - "version" : "1.1.1400" - } - }, - { - "identity" : "plcrashreporter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/microsoft/plcrashreporter.git", - "state" : { - "revision" : "81cdec2b3827feb03286cb297f4c501a8eb98df1", - "version" : "1.10.2" - } - }, - { - "identity" : "pocketsvg", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pocketsvg/PocketSVG.git", - "state" : { - "revision" : "51d4fd9ae48e79207034afdd53f365fc2b9d6068", - "version" : "2.7.0" - } - } - ], - "version" : 2 -} diff --git a/ownCloud/App Controllers/AccountController+ExtraItems.swift b/ownCloud/App Controllers/AccountController+ExtraItems.swift new file mode 100644 index 000000000..378de5bf5 --- /dev/null +++ b/ownCloud/App Controllers/AccountController+ExtraItems.swift @@ -0,0 +1,73 @@ +// +// AccountController+ExtraItems.swift +// ownCloud +// +// Created by Felix Schwarz on 23.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ownCloudAppShared + +extension AccountController: AccountControllerExtraItems { + var activitySideBarItem: CollectionSidebarAction? { + var sideBarItem: CollectionSidebarAction? = specialItems[.activity] as? CollectionSidebarAction + + if sideBarItem == nil { + sideBarItem = CollectionSidebarAction(with: "Status".localized, icon: OCSymbol.icon(forSymbolName: "bolt"), viewControllerProvider: { (context, action) in + let activityViewController = ClientActivityViewController(connection: context?.accountConnection) + activityViewController.revoke(in: context, when: [ .connectionClosed ]) + activityViewController.navigationBookmark = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: context?.accountConnection?.bookmark.uuid, specialItem: .activity) + return activityViewController + }) + sideBarItem?.identifier = specialItemsDataReferences[.activity] as? String + + let messageCountObservation = connection?.observe(\.messageCount, options: .initial, changeHandler: { [weak sideBarItem] connection, change in + let messageCount = connection.messageCount + + OnMainThread(inline: true) { [weak self, weak sideBarItem] in + sideBarItem?.badgeCount = (messageCount == 0) ? nil : messageCount + if let sideBarItemReference = sideBarItem?.dataItemReference { + self?.extraItemsDataSource.signalUpdates(forItemReferences: Set([sideBarItemReference])) + } + } + }) + + sideBarItem?.properties[OCActionPropertyKey(rawValue: "messageCountObservation")] = messageCountObservation + + specialItems[.activity] = sideBarItem + } + + return sideBarItem + } + + public func updateExtraItems(dataSource: OCDataSourceArray) { + if let activitySideBarItem = activitySideBarItem, configuration.showActivity { + dataSource.setVersionedItems([ activitySideBarItem ]) + } + } + + public func provideExtraItemViewController(for specialItem: SpecialItem, in context: ClientContext) -> UIViewController? { + switch specialItem { + case .activity: + let activityViewController = ClientActivityViewController(connection: context.accountConnection) + activityViewController.revoke(in: context, when: [ .connectionClosed ]) + activityViewController.navigationBookmark = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: context.accountConnection?.bookmark.uuid, specialItem: .activity) + return activityViewController + + default: + return nil + } + } +} diff --git a/ownCloud/App Controllers/AccountController+ItemActions.swift b/ownCloud/App Controllers/AccountController+ItemActions.swift new file mode 100644 index 000000000..cef90cb0b --- /dev/null +++ b/ownCloud/App Controllers/AccountController+ItemActions.swift @@ -0,0 +1,172 @@ +// +// AccountController+ItemActions.swift +// ownCloud +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ownCloudApp +import ownCloudAppShared + +extension AccountController { + public var localizedDeleteTitle: String { + return VendorServices.shared.isBranded ? "Log out".localized : "Delete".localized + } + + public func editBookmark(on hostViewController: UIViewController, completion completionHandler: (() -> Void)? = nil) { + if let bookmark = connection?.bookmark { + self.disconnect { _ in + BookmarkViewController.showBookmarkUI(on: hostViewController, edit: bookmark, removeAuthDataFromCopy: false) + completionHandler?() + } + } else { + completionHandler?() + } + } + + public func deleteBookmark(withAlertOn hostViewController: UIViewController, completion completionHandler: (() -> Void)? = nil) { + if let bookmark = connection?.bookmark { + self.disconnect { _ in + OCBookmarkManager.shared.delete(withAlertOn: hostViewController, bookmark: bookmark, completion: { + completionHandler?() + }) + } + } else { + completionHandler?() + } + } + + public func manageBookmark(on hostViewController: UIViewController, completion completionHandler: (() -> Void)? = nil) { + if let bookmark = connection?.bookmark { + self.disconnect { _ in + OCBookmarkManager.shared.manage(bookmark: bookmark, presentOn: hostViewController, completion: completionHandler) + } + } else { + completionHandler?() + } + } +} + +extension AccountController: DataItemSwipeInteraction { + public func provideTrailingSwipeActions(with context: ClientContext?) -> UISwipeActionsConfiguration? { + if let hostViewController = context?.originatingViewController ?? context?.rootViewController { + let deleteRowAction = UIContextualAction(style: .destructive, title: localizedDeleteTitle, handler: { [weak self, weak hostViewController] (_, _, completionHandler) in + guard let hostViewController = hostViewController else { + completionHandler(false) + return + } + + self?.deleteBookmark(withAlertOn: hostViewController, completion: { + completionHandler(true) + }) + }) + + return UISwipeActionsConfiguration(actions: [deleteRowAction]) + } + + return nil + } +} + +extension AccountController { + func openInNewWindowUserActivity(with context: ClientContext) -> NSUserActivity? { + if let bookmark { + let activity = AppStateAction(with: [ + .connection(with: bookmark, children: [ + .goToPersonalFolder() + ]) + ]).userActivity(with: clientContext) + + return activity + } + + return nil + } +} + +extension AccountController: DataItemContextMenuInteraction { + public func composeContextMenuItems(in viewController: UIViewController?, location: OCExtensionLocationIdentifier, with context: ClientContext?) -> [UIMenuElement]? { + if let hostViewController = context?.originatingViewController ?? context?.rootViewController { + var menuItems: [UIMenuElement] = [] + + // Open in a new window + if UIDevice.current.isIpad { + let openWindow = UIAction(title: "Open in a new Window".localized, image: UIImage(systemName: "uiwindow.split.2x1")) { [weak self] _ in + if let clientContext = self?.clientContext, let userActivity = self?.openInNewWindowUserActivity(with: clientContext) { + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: nil) + } + } + + menuItems.append(openWindow) + } + + // Edit + if VendorServices.shared.canEditAccount { + let editAction = UIAction(handler: { [weak self, weak hostViewController] action in + guard let hostViewController = hostViewController else { return } + + self?.editBookmark(on: hostViewController) + }) + editAction.title = "Edit".localized + editAction.image = OCSymbol.icon(forSymbolName: "pencil") + + menuItems.append(editAction) + } + + // Manage + let manageAction = UIAction(handler: { [weak self, weak hostViewController] action in + guard let hostViewController = hostViewController else { return } + + self?.manageBookmark(on: hostViewController) + }) + manageAction.title = "Manage".localized + manageAction.image = OCSymbol.icon(forSymbolName: "gearshape") + + menuItems.append(manageAction) + + // Delete + let deleteAction = UIAction(handler: { [weak self, weak hostViewController] action in + guard let hostViewController = hostViewController else { return } + + self?.deleteBookmark(withAlertOn: hostViewController) + }) + deleteAction.title = localizedDeleteTitle + deleteAction.image = OCSymbol.icon(forSymbolName: "trash") + deleteAction.attributes = .destructive + + menuItems.append(deleteAction) + + return menuItems + } + + return (nil) + } +} + +extension AccountController: DataItemDragInteraction { + public func provideDragItems(with context: ClientContext?) -> [UIDragItem]? { + if let bookmark, let userActivity = openInNewWindowUserActivity(with: clientContext) { + let itemProvider = NSItemProvider(item: bookmark, typeIdentifier: "com.owncloud.ios-app.ocbookmark") + itemProvider.registerObject(userActivity, visibility: .all) + + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = bookmark + + return [dragItem] + } + + return nil + } +} diff --git a/ownCloud/App Controllers/AppRootViewController+ItemActions.swift b/ownCloud/App Controllers/AppRootViewController+ItemActions.swift new file mode 100644 index 000000000..b1c2597bf --- /dev/null +++ b/ownCloud/App Controllers/AppRootViewController+ItemActions.swift @@ -0,0 +1,54 @@ +// +// AppRootViewController+ItemActions.swift +// ownCloud +// +// Created by Felix Schwarz on 21.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ownCloudAppShared + +extension AppRootViewController: ViewItemAction { + public func provideViewer(for item: OCDataItem, context: ClientContext) -> UIViewController? { + let queryDatasource = context.queryDatasource ?? context.query?.queryResultsDataSource + + guard let item = item as? OCItem, context.core != nil else { + return nil + } + + let itemViewController = DisplayHostViewController(clientContext: context, selectedItem: item, queryDataSource: queryDatasource) + itemViewController.hidesBottomBarWhenPushed = true + itemViewController.progressSummarizer = context.progressSummarizer + + return itemViewController + } +} + +extension AppRootViewController: MoreItemAction { + public func moreOptions(for item: OCDataItem, at locationIdentifier: OCExtensionLocationIdentifier, context: ClientContext, sender: AnyObject?) -> Bool { + guard let sender = sender, let core = context.core, let item = item as? OCItem else { + return false + } + let originatingViewController : UIViewController = context.originatingViewController ?? self + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: locationIdentifier) + let actionContext = ActionContext(viewController: originatingViewController, clientContext: context, core: core, query: context.query, items: [item], location: actionsLocation, sender: sender) + + if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: context.actionProgressHandlerProvider?.makeActionProgressHandler(), completionHandler: nil) { + originatingViewController.present(asCard: moreViewController, animated: true) + } + + return true + } +} diff --git a/ownCloud/App Controllers/AppRootViewController.swift b/ownCloud/App Controllers/AppRootViewController.swift new file mode 100644 index 000000000..797dd3320 --- /dev/null +++ b/ownCloud/App Controllers/AppRootViewController.swift @@ -0,0 +1,426 @@ +// +// AppRootViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 15.11.22. +// Copyright © 2022 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 ownCloudApp +import ownCloudAppShared + +open class AppRootViewController: EmbeddingViewController, BrowserNavigationViewControllerDelegate, BrowserNavigationBookmarkRestore { + var clientContext: ClientContext + var controllerConfiguration: AccountController.Configuration + + var focusedBookmarkObservation: NSKeyValueObservation? + + init(with context: ClientContext, controllerConfiguration: AccountController.Configuration = .defaultConfiguration) { + clientContext = context + self.controllerConfiguration = controllerConfiguration + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Controllers + var rootContext: ClientContext? + + public var leftNavigationController: ThemeNavigationController? + public var sidebarViewController: ClientSidebarViewController? + public var contentBrowserController: BrowserNavigationViewController = BrowserNavigationViewController() + + private var contentBrowserControllerObserver: NSKeyValueObservation? + + // MARK: - Message presentation + var alertQueue : OCAsyncSequentialQueue = OCAsyncSequentialQueue() + + var notificationPresenter: NotificationMessagePresenter? + var cardMessagePresenter: CardIssueMessagePresenter? + + @objc dynamic var focusedBookmark: OCBookmark? { + willSet { + // Remove message presenters + if let notificationPresenter { + OCMessageQueue.global.remove(presenter: notificationPresenter) + } + + if let cardMessagePresenter { + OCMessageQueue.global.remove(presenter: cardMessagePresenter) + } + } + + didSet { + if let focusedBookmark { + // Create message presenters + notificationPresenter = NotificationMessagePresenter(forBookmarkUUID: focusedBookmark.uuid) + cardMessagePresenter = CardIssueMessagePresenter(with: focusedBookmark.uuid as OCBookmarkUUID, limitToSingleCard: true, presenter: { [weak self] (viewController) in + self?.presentAlertAsCard(viewController: viewController, withHandle: false, dismissable: true) + // Log.debug("Present \(viewController.debugDescription)") + }) + + // Add message presenters + if let notificationPresenter { + OCMessageQueue.global.add(presenter: notificationPresenter) + } + + if let cardMessagePresenter { + OCMessageQueue.global.add(presenter: cardMessagePresenter) + } + } + } + } + + // MARK: - View Controller Events + override open func viewDidLoad() { + super.viewDidLoad() + + // Add icons + AppRootViewController.addIcons() + + // Create client context, using contentBrowserController to manage content + sidebar + rootContext = ClientContext(with: clientContext, rootViewController: self, alertQueue: alertQueue, modifier: { context in + context.viewItemHandler = self + context.moreItemHandler = self + context.bookmarkEditingHandler = self + context.browserController = self.contentBrowserController + }) + + // Build sidebar + sidebarViewController = ClientSidebarViewController(context: rootContext!, controllerConfiguration: controllerConfiguration) + sidebarViewController?.addToolbarItems() + + leftNavigationController = ThemeNavigationController(rootViewController: sidebarViewController!) + leftNavigationController?.setToolbarHidden(false, animated: false) + + focusedBookmarkObservation = sidebarViewController?.observe(\.focusedBookmark, changeHandler: { [weak self] sidebarViewController, change in + self?.focusedBookmark = self?.sidebarViewController?.focusedBookmark + }) + + // Build split view controller + contentBrowserController.sidebarViewController = leftNavigationController + + // Make browser navigation view controller the content + contentViewController = contentBrowserController + + // Observe browserController contentViewController and update sidebar selection accordingly + contentBrowserController.delegate = self + + // Setup app icon badge message count + setupAppIconBadgeMessageCount() + } + + var shownFirstTime = true + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + ClientSessionManager.shared.add(delegate: self) + + if AppLockManager.shared.passcode == nil && AppLockSettings.shared.isPasscodeEnforced { + PasscodeSetupCoordinator(parentViewController: self, action: .setup).start() + } else if let passcode = AppLockManager.shared.passcode, passcode.count < AppLockSettings.shared.requiredPasscodeDigits { + PasscodeSetupCoordinator(parentViewController: self, action: .upgrade).start() + } + + // Release Notes, Beta warning, Review prompts… + considerLaunchPopups() + + shownFirstTime = false + } + + open override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + ClientSessionManager.shared.remove(delegate: self) + } + + // MARK: - BrowserNavigationViewControllerDelegate + public func browserNavigation(viewController: ownCloudAppShared.BrowserNavigationViewController, contentViewControllerDidChange toViewController: UIViewController?) { + sidebarViewController?.updateSelection(for: toViewController?.navigationBookmark) + } + + // MARK: - BrowserNavigationBookmarkRestore + public func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context: ClientContext?, completion: @escaping ((Error?, UIViewController?) -> Void)) { + if let bookmarkUUID = navigationBookmark.bookmarkUUID, let accountController = sidebarViewController?.accountController(for: bookmarkUUID) { + if let specialItem = navigationBookmark.specialItem, + let viewController = accountController.provideViewController(for: specialItem, in: context) { + completion(nil, viewController) + return + } + } + + completion(NSError(ocError: .insufficientParameters), nil) + } + + // MARK: - App Badge: Message Counts + var messageCountSelector: MessageSelector? + + func setupAppIconBadgeMessageCount() { + messageCountSelector = MessageSelector(filter: nil, handler: { (messages, _, _) in + var unresolvedMessagesCount = 0 + + if let messages = messages { + for message in messages { + if !message.resolved { + unresolvedMessagesCount += 1 + } + } + } + + OnMainThread { + if !ProcessInfo.processInfo.arguments.contains("UI-Testing") { + NotificationManager.shared.requestAuthorization(options: .badge) { (granted, _) in + if granted { + OnMainThread { + UIApplication.shared.applicationIconBadgeNumber = unresolvedMessagesCount + } + } + } + } + } + }) + } + + // MARK: - Launch popups + func considerLaunchPopups() { + var shownPopup = false + + if VendorServices.shared.showBetaWarning, shownFirstTime, !shownPopup { + shownPopup = considerLaunchPopupBetaWarning() + } + + if shownFirstTime, !shownPopup { + shownPopup = considerLaunchPopupReleaseNotes() + } + + if !shownFirstTime { + VendorServices.shared.considerReviewPrompt() + } + } + + // MARK: - Beta warning + func considerLaunchPopupBetaWarning() -> Bool { + let lastBetaWarningCommit = OCAppIdentity.shared.userDefaults?.string(forKey: "LastBetaWarningCommit") + + Log.log("Show beta warning: \(String(describing: VendorServices.classSetting(forOCClassSettingsKey: .showBetaWarning) as? Bool))") + + if VendorServices.classSetting(forOCClassSettingsKey: .showBetaWarning) as? Bool == true, + let lastGitCommit = LastGitCommit(), + (lastBetaWarningCommit == nil) || (lastBetaWarningCommit != lastGitCommit) { + // Beta warning has never been shown before - or has last been shown for a different release + let betaAlert = ThemedAlertController(with: "Beta Warning".localized, message: "\nThis is a BETA release that may - and likely will - still contain bugs.\n\nYOU SHOULD NOT USE THIS BETA VERSION WITH PRODUCTION SYSTEMS, PRODUCTION DATA OR DATA OF VALUE. YOU'RE USING THIS BETA AT YOUR OWN RISK.\n\nPlease let us know about any issues that come up via the \"Send Feedback\" option in the settings.".localized, okLabel: "Agree".localized) { + OCAppIdentity.shared.userDefaults?.set(lastGitCommit, forKey: "LastBetaWarningCommit") + OCAppIdentity.shared.userDefaults?.set(NSDate(), forKey: "LastBetaWarningAcceptDate") + } + + self.present(betaAlert, animated: true, completion: nil) + + return true + } + + return false + } + + // MARK: - Release notes + func considerLaunchPopupReleaseNotes() -> Bool { + defer { + ReleaseNotesDatasource.updateLastSeenAppVersion() + } + + if ReleaseNotesDatasource.shouldShowReleaseNotes { + let releaseNotesHostController = ReleaseNotesHostViewController() + releaseNotesHostController.modalPresentationStyle = .formSheet + self.present(releaseNotesHostController, animated: true, completion: nil) + + return true + } + + return false + } +} + +// MARK: - Authentication: bookmark editing +extension AppRootViewController: AccountAuthenticationHandlerBookmarkEditingHandler { + public func handleAuthError(for viewController: UIViewController, error: NSError, editBookmark: OCBookmark?, preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?) { + BookmarkViewController.showBookmarkUI(on: viewController, edit: editBookmark, performContinue: true, attemptLoginOnSuccess: true, removeAuthDataFromCopy: true) + } +} + +// MARK: - Message presentation +extension AppRootViewController : ClientSessionManagerDelegate { + var selectedAccountConnection: AccountController? { + if let accountControllerSection = self.sidebarViewController?.sectionOfCurrentSelection as? AccountControllerSection { + return accountControllerSection.accountController + } + + return nil + } + + func canPresent(bookmark: OCBookmark, message: OCMessage?) -> OCMessagePresentationPriority { + if let themeWindow = self.viewIfLoaded?.window as? ThemeWindow, themeWindow.themeWindowInForeground { + if !OCBookmarkManager.isLocked(bookmark: bookmark) { + if let selectedAccountConnection { + if selectedAccountConnection.connection?.bookmark.uuid == bookmark.uuid { + return .high + } else { + return .default + } + } else if presentedViewController == nil { + return .high + } + } + + return .low + } + + return .wontPresent + } + + func present(bookmark: OCBookmark, message: OCMessage?) { + OnMainThread { + /* + if self.presentedViewController == nil { + self.connect(to: bookmark, lastVisibleItemId: nil, animated: true, present: message) + } else { + */ + + if let message = message { + self.presentInClient(message: message) + } + } + } + + func presentInClient(message: OCMessage) { + if let cardMessagePresenter { + OnMainThread { // Wait for next runloop cycle + OCMessageQueue.global.present(message, with: cardMessagePresenter) + } + } + } + + func presentAlertAsCard(viewController: UIViewController, withHandle: Bool = false, dismissable: Bool = true) { + alertQueue.async { [weak self] (queueCompletionHandler) in + if let startViewController = self { + var hostViewController : UIViewController = startViewController + + while hostViewController.presentedViewController != nil, + hostViewController.presentedViewController?.isBeingDismissed == false { + hostViewController = hostViewController.presentedViewController! + } + + hostViewController.present(asCard: viewController, animated: true, withHandle: withHandle, dismissable: dismissable, completion: { + queueCompletionHandler() + }) + } else { + queueCompletionHandler() + } + } + } +} + +// MARK: - Sidebar toolbar +extension ClientSidebarViewController { + // MARK: - Add toolbar items + func addToolbarItems(addAccount: Bool = true, settings addSettings: Bool = true) { + var toolbarItems: [UIBarButtonItem] = [] + + if addAccount { + let addAccountBarButtonItem = UIBarButtonItem(systemItem: .add, primaryAction: UIAction(handler: { [weak self] action in + self?.addBookmark() + })) + + toolbarItems.append(addAccountBarButtonItem) + } + + if addSettings { + let settingsBarButtonItem = UIBarButtonItem(title: "Settings".localized, style: UIBarButtonItem.Style.plain, target: self, action: #selector(settings)) + settingsBarButtonItem.accessibilityIdentifier = "settingsBarButtonItem" + + toolbarItems.append(contentsOf: [ + UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil), + settingsBarButtonItem + ]) + } + + self.toolbarItems = toolbarItems + } + + // MARK: - Open settings + @IBAction func settings() { + let viewController : SettingsViewController = SettingsViewController(style: .grouped) + self.present(ThemeNavigationController(rootViewController: viewController), animated: true) + } + + // MARK: - Add account + func addBookmark() { + BookmarkViewController.showBookmarkUI(on: self, attemptLoginOnSuccess: true) + } + + // MARK: - Update selection + public func section(for bookmarkUUID: UUID) -> AccountControllerSection? { + for section in allSections { + if let accountControllerSection = section as? AccountControllerSection, + let sectionBookmark = accountControllerSection.accountController.bookmark, + sectionBookmark.uuid == bookmarkUUID { + return accountControllerSection + } + } + + return nil + } + + public func accountController(for bookmarkUUID: UUID) -> AccountController? { + return section(for: bookmarkUUID)?.accountController + } + + public func itemReferences(for itemReferences: [OCDataItemReference], inSectionFor bookmarkUUID: UUID?) -> [ItemRef]? { + if let bookmarkUUID, let section = section(for: bookmarkUUID) { + return section.collectionViewController?.wrap(references: itemReferences, forSection: section.identifier) + } + + return nil + } + + func updateSelection(for navigationBookmark: BrowserNavigationBookmark?) { + if let sideBarItemRefs = navigationBookmark?.representationSideBarItemRefs, + let bookmarkUUID = navigationBookmark?.bookmarkUUID, + let selectionItemRefs = itemReferences(for: sideBarItemRefs, inSectionFor: bookmarkUUID), + let highlightAction = CollectionViewAction(kind: .highlight(animated: false, scrollPosition: []), itemReferences: selectionItemRefs) { + // Highlight all + addActions([ + highlightAction + ]) + } else { + // Unhighlight all + addActions([ + CollectionViewAction(kind: .unhighlightAll(animated: false)) + ]) + } + } +} + +// MARK: - Branding +public extension AppRootViewController { + static func addIcons() { + Theme.shared.add(tvgResourceFor: "icon-available-offline") + Theme.shared.add(tvgResourceFor: "status-flash") + Theme.shared.add(tvgResourceFor: "owncloud-logo") + + OCItem.registerIcons() + } +} diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 01131ac6e..94c1eec77 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -27,14 +27,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private let delayForLinkResolution = 0.2 - var window: ThemeWindow? - var serverListTableViewController: ServerListTableViewController? - var staticLoginViewController : StaticLoginViewController? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - var navigationController: UINavigationController? - var rootViewController : UIViewController? - // Set up logging (incl. stderr redirection) and log launch time, app version, build number and commit Log.log("ownCloud \(VendorServices.shared.appVersion) (\(VendorServices.shared.appBuildNumber)) #\(LastGitCommit() ?? "unknown") finished launching with log settings: \(Log.logOptionStatus)") @@ -48,35 +41,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCHTTPPipelineManager.setupPersistentPipelines() // Set up app - window = ThemeWindow(frame: UIScreen.main.bounds) - ThemeStyle.registerDefaultStyles() CollectionViewCellProvider.registerStandardImplementations() - - if VendorServices.shared.isBranded { - staticLoginViewController = StaticLoginViewController(with: StaticLoginBundle.defaultBundle) - navigationController = ThemeNavigationController(rootViewController: staticLoginViewController!) - navigationController?.setNavigationBarHidden(true, animated: false) - rootViewController = navigationController - } else { - if OCBookmarkManager.shared.bookmarks.count == 1 { - serverListTableViewController = StaticLoginSingleAccountServerListViewController(style: .insetGrouped) - } else { - serverListTableViewController = ServerListTableViewController(style: .plain) - } - - navigationController = ThemeNavigationController(rootViewController: serverListTableViewController!) - rootViewController = navigationController - } - - // Only set up window on non-iPad devices and not on macOS 11 (Apple Silicon) which is >= iOS 14 - if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac { - // do not set the rootViewController for iOS app on Mac - } else { - window?.rootViewController = rootViewController! - window?.makeKeyAndVisible() - } + CollectionViewSupplementaryCellProvider.registerStandardImplementations() ImportFilesController.removeImportDirectory() @@ -175,7 +143,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { url.matchesAppScheme { // + URL matches app scheme guard let window = UserInterfaceContext.shared.currentWindow else { return false } - openPrivateLink(url: url, in: window) + openAppSchemeLink(url: url, in: window) } else if url.isFileURL { var copyBeforeUsing = true if let shouldOpenInPlace = options[UIApplication.OpenURLOptionsKey.openInPlace] as? Bool { @@ -207,47 +175,143 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let url = userActivity.webpageURL else { - return false - } - - guard let window = UserInterfaceContext.shared.currentWindow else { return false } - - openPrivateLink(url: url, in: window) - - return true + // Not applicable here at the app delegate level. + return false } +// guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, +// let url = userActivity.webpageURL else { +// return false +// } +// +// guard let window = UserInterfaceContext.shared.currentWindow else { return false } +// +// openPrivateLink(url: url, in: window) +// +// return true +// } // MARK: UISceneSession Lifecycle - @available(iOS 13.0, *) - func application(_ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) -> UISceneConfiguration { + 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) { } - private func openPrivateLink(url:URL, in window:UIWindow) { - if UIApplication.shared.applicationState == .background { - // If the app is already running, just start link resolution - url.resolveAndPresent(in: window) - } else { + // MARK: - App Scheme URL handling + open func openAppSchemeLink(url: URL, in inWindow: UIWindow? = nil, clientContext: ClientContext? = nil, autoDelay: Bool = true) { + guard let window = inWindow ?? (clientContext?.scene as? UIWindowScene)?.windows.first else { return } + + if UIApplication.shared.applicationState != .background, autoDelay { // Delay a resolution of private link on cold launch, since it could be that we would otherwise interfer // with activities of the just instantiated ServerListTableViewController - OnMainThread(after:delayForLinkResolution) { - url.resolveAndPresent(in: window) + OnMainThread(after: delayForLinkResolution) { + self.openAppSchemeLink(url: url, in: window, clientContext: clientContext, autoDelay: false) + } + + return + } + + // App is already running, just start link resolution + if openPrivateLink(url: url, clientContext: clientContext) { return } + if openPostBuild(url: url, in: window) { return } + } + + // MARK: Post Build + private func openPostBuild(url: URL, in window: UIWindow) -> Bool { + // owncloud://pb/[set|clear]/[all|flatID]/?[int|string|sarray]=[value] + /* + Examples: + owncloud://pb/set/branding.app-name?string=ocisCloud + owncloud://pb/clear/branding.app-name + owncloud://pb/clear/all + */ + if url.host == "pb" { + let components = url.pathComponents + + if components.count >= 3 { + let command = components[1] + let targetID = components[2] + var relaunchReason: String? + + switch command { + case "set": + if targetID == "all" { break } + + let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) + if let queryItems = urlComponents?.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + switch queryItem.name { + case "int": + if let intVal = Int(value) as? NSNumber { + let error = OCClassSettingsFlatSourcePostBuild.sharedPostBuildSettings.setValue(intVal, forFlatIdentifier: targetID) + if error == nil { + relaunchReason = "Changed \(targetID) to int(\(intVal)).".localized + } + } + + case "string": + let error = OCClassSettingsFlatSourcePostBuild.sharedPostBuildSettings.setValue(value, forFlatIdentifier: targetID) + if error == nil { + relaunchReason = "Changed \(targetID) to string(\(value)).".localized + } + + case "sarray": + let strings = (value as NSString).components(separatedBy: ".") + let error = OCClassSettingsFlatSourcePostBuild.sharedPostBuildSettings.setValue(strings, forFlatIdentifier: targetID) + if error == nil { + relaunchReason = "Changed \(targetID) to stringArray(\(strings.joined(separator: ", "))).".localized + } + + default: break + } + } + } + } + + case "clear": + if targetID == "all" { + // Clear all post build settings + OCClassSettingsFlatSourcePostBuild.sharedPostBuildSettings.clear() + relaunchReason = "Cleared all.".localized + break + } + + // Clear specific setting + let error = OCClassSettingsFlatSourcePostBuild.sharedPostBuildSettings.setValue(nil, forFlatIdentifier: targetID) + if error == nil { + relaunchReason = "Cleared \(targetID).".localized + } + + default: break + } + + if let relaunchReason { + OnMainThread { + self.offerRelaunchForReason(relaunchReason) + } + } } + return true } + return false + } + + // MARK: Private Link + private func openPrivateLink(url: URL, clientContext: ClientContext?) -> Bool { + if let clientContext, url.privateLinkItemID != nil { + url.resolveAndPresentPrivateLink(with: clientContext) + return true + } + + return false } } extension UserInterfaceContext : UserInterfaceContextProvider { public func provideRootView() -> UIView? { - return (UIApplication.shared.delegate as? AppDelegate)?.window + return provideCurrentWindow() } public func provideCurrentWindow() -> UIWindow? { @@ -281,11 +345,15 @@ extension AppDelegate : NotificationResponseHandler { } @objc func offerRelaunchAfterMDMPush() { + offerRelaunchForReason("New settings received from MDM".localized) + } + + func offerRelaunchForReason(_ reason: String) { NotificationManager.shared.requestAuthorization(options: [.alert, .sound], completionHandler: { (granted, _) in if granted { let content = UNMutableNotificationContent() - content.title = "New settings received from MDM".localized + content.title = reason content.body = "Tap to quit the app.".localized let request = UNNotificationRequest(identifier: NotificationManagerComposeIdentifier(AppDelegate.self, "terminate-app"), content: content, trigger: nil) diff --git a/ownCloud/Bookmarks/BookmarkInfoViewController.swift b/ownCloud/Bookmarks/BookmarkInfoViewController.swift index 1965aa27b..eaf097ff3 100644 --- a/ownCloud/Bookmarks/BookmarkInfoViewController.swift +++ b/ownCloud/Bookmarks/BookmarkInfoViewController.swift @@ -26,6 +26,7 @@ class BookmarkInfoViewController: StaticTableViewController { var deviceAvailableStorageInfoRow: StaticTableViewRow? var bookmark : OCBookmark? + var completionHandler: (() -> Void)? lazy var byteCounterFormatter: ByteCountFormatter = { let formatter = ByteCountFormatter() @@ -169,7 +170,11 @@ class BookmarkInfoViewController: StaticTableViewController { // MARK: - User actions @objc func userActionDone() { - self.presentingViewController?.dismiss(animated: true, completion: nil) + let completionHandler = completionHandler + + self.presentingViewController?.dismiss(animated: true, completion: { + completionHandler?() + }) } // MARK: - Helper methods diff --git a/ownCloud/Bookmarks/BookmarkViewController.swift b/ownCloud/Bookmarks/BookmarkViewController.swift index fecc5cab9..c53984017 100644 --- a/ownCloud/Bookmarks/BookmarkViewController.swift +++ b/ownCloud/Bookmarks/BookmarkViewController.swift @@ -68,6 +68,8 @@ class BookmarkViewController: StaticTableViewController { var bookmark : OCBookmark? var originalBookmark : OCBookmark? + var generationOptions: [OCAuthenticationMethodKey : Any]? + enum BookmarkViewControllerMode { case create case edit @@ -171,9 +173,8 @@ class BookmarkViewController: StaticTableViewController { changedBookmark = true } - if self?.bookmark?.certificate != nil { - self?.bookmark?.certificate = nil - self?.bookmark?.certificateModificationDate = nil + if let certificateCount = self?.bookmark?.certificateStore?.allRecords.count, certificateCount > 0 { + self?.bookmark?.certificateStore?.removeAllCertificates() changedBookmark = true } @@ -188,7 +189,7 @@ class BookmarkViewController: StaticTableViewController { }, placeholder: "https://", keyboardType: .URL, autocorrectionType: .no, identifier: "row-url-url", accessibilityLabel: "Server URL".localized) certificateRow = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in - if let certificate = self?.bookmark?.certificate { + if let certificate = self?.bookmark?.primaryCertificate { let certificateViewController : ThemeCertificateViewController = ThemeCertificateViewController(certificate: certificate, compare: nil) let navigationController = ThemeNavigationController(rootViewController: certificateViewController) @@ -256,6 +257,15 @@ class BookmarkViewController: StaticTableViewController { self.navigationItem.title = "Add account".localized self.navigationItem.rightBarButtonItem = continueBarButtonItem + // Support for bookmark default name + if let defaultNameString = self.classSetting(forOCClassSettingsKey: .bookmarkDefaultName) as? String { + self.bookmark?.name = defaultNameString + + if bookmark != nil { + updateUI(from: bookmark!) { (_) -> Bool in return(true) } + } + } + // Support for bookmark default URL if let defaultURLString = self.classSetting(forOCClassSettingsKey: .bookmarkDefaultURL) as? String { self.bookmark?.url = URL(string: defaultURLString) @@ -277,8 +287,8 @@ class BookmarkViewController: StaticTableViewController { } self.usernameRow?.enabled = - (bookmark?.authenticationMethodIdentifier == nil) || // Enable if no authentication method was set (to keep it available) - ((bookmark?.authenticationMethodIdentifier != nil) && (bookmark?.isPassphraseBased == true) && (((self.usernameRow?.value as? String) ?? "").count == 0)) // Enable if authentication method was set, is not tokenbased, but username is not available (i.e. when keychain was deleted/not migrated) + (bookmark?.authenticationMethodIdentifier == nil) || // Enable if no authentication method was set (to keep it available) + ((bookmark?.authenticationMethodIdentifier != nil) && (bookmark?.isPassphraseBased == true) && (((self.usernameRow?.value as? String) ?? "").count == 0)) // Enable if authentication method was set, is not tokenbased, but username is not available (i.e. when keychain was deleted/not migrated) self.navigationItem.title = "Edit account".localized self.navigationItem.rightBarButtonItem = saveBarButtonItem @@ -343,9 +353,9 @@ class BookmarkViewController: StaticTableViewController { //if bookmark?.isTokenBased == true, removeAuthDataFromCopy { if mode == .edit, nameChanged, !urlChanged, let bookmark = bookmark, bookmark.authenticationData != nil { - updateBookmark(bookmark: bookmark) - completeAndDismiss(with: hudCompletion) - return + updateBookmark(bookmark: bookmark) + completeAndDismiss(with: hudCompletion) + return } if (bookmark?.url == nil) || (bookmark?.authenticationMethodIdentifier == nil) { @@ -408,11 +418,11 @@ class BookmarkViewController: StaticTableViewController { if let connectionBookmark = bookmark { let connection = instantiateConnection(for: connectionBookmark) - let previousCertificate = bookmark?.certificate + let previousCertificate = bookmark?.primaryCertificate hud?.present(on: self, label: "Contacting server…".localized) - connection.prepareForSetup(options: nil) { (issue, _, _, preferredAuthenticationMethods) in + connection.prepareForSetup(options: nil) { (issue, _, _, preferredAuthenticationMethods, generationOptions) in hudCompletion({ // Update URL self.urlRow?.textField?.text = serverURL.absoluteString @@ -423,7 +433,7 @@ class BookmarkViewController: StaticTableViewController { self?.updateInputFocus() } - if self?.bookmark?.certificate == previousCertificate, + if self?.bookmark?.primaryCertificate == previousCertificate, let authMethodIdentifier = self?.bookmark?.authenticationMethodIdentifier, OCAuthenticationMethod.isAuthenticationMethodTokenBased(authMethodIdentifier as OCAuthenticationMethodIdentifier) == true { @@ -431,6 +441,8 @@ class BookmarkViewController: StaticTableViewController { } } + self.generationOptions = generationOptions + if issue != nil { // Parse issue for display if let issue = issue { @@ -470,7 +482,7 @@ class BookmarkViewController: StaticTableViewController { func handleContinueAuthentication(hud: ProgressHUDViewController?, hudCompletion: @escaping (((() -> Void)?) -> Void)) { if let connectionBookmark = bookmark { - var options : [OCAuthenticationMethodKey : Any] = [:] + var options : [OCAuthenticationMethodKey : Any] = generationOptions ?? [:] let connection = instantiateConnection(for: connectionBookmark) @@ -489,14 +501,23 @@ class BookmarkViewController: StaticTableViewController { hud?.present(on: self, label: "Authenticating…".localized) connection.generateAuthenticationData(withMethod: bookmarkAuthenticationMethodIdentifier, options: options) { (error, authMethodIdentifier, authMethodData) in - if error == nil { + if error == nil, let authMethodIdentifier, let authMethodData { self.bookmark?.authenticationMethodIdentifier = authMethodIdentifier self.bookmark?.authenticationData = authMethodData self.bookmark?.scanForAuthenticationMethodsRequired = false OnMainThread { hud?.updateLabel(with: "Fetching user information…".localized) } - self.save(hudCompletion: hudCompletion) + + // Retrieve available instances for this account to chose from + connection.retrieveAvailableInstances(options: options, authenticationMethodIdentifier: authMethodIdentifier, authenticationData: authMethodData, completionHandler: { error, instances in + // No account chooser implemented at this time. If an account is returned, use the URL of the first one. + if error == nil, let instance = instances?.first { + self.bookmark?.apply(instance) + } + + self.save(hudCompletion: hudCompletion) + }) } else { hudCompletion({ var issue : OCIssue? @@ -767,11 +788,11 @@ class BookmarkViewController: StaticTableViewController { } // URL section: certificate details - show if there's one - if bookmark?.certificate != nil { + if bookmark?.primaryCertificate != nil { if certificateRow != nil, certificateRow?.attached == false { urlSection?.add(row: certificateRow!, animated: animated) showedOAuthInfoHeader = true - bookmark?.certificate?.validationResult(completionHandler: { (_, shortDescription, longDescription, color, _) in + bookmark?.primaryCertificate?.validationResult(completionHandler: { (_, shortDescription, longDescription, color, _) in OnMainThread { guard let accessoryView = self.certificateRow?.additionalAccessoryView as? BorderedLabel else { return } accessoryView.update(text: shortDescription, color: color) @@ -993,7 +1014,7 @@ class BookmarkViewController: StaticTableViewController { var password : String? if let authMethodIdentifier = bookmark.authenticationMethodIdentifier, - OCAuthenticationMethod.isAuthenticationMethodPassphraseBased(authMethodIdentifier as OCAuthenticationMethodIdentifier), + OCAuthenticationMethod.isAuthenticationMethodPassphraseBased(authMethodIdentifier as OCAuthenticationMethodIdentifier), let authData = bookmark.authenticationData, let authenticationMethodClass = OCAuthenticationMethod.registeredAuthenticationMethod(forIdentifier: authMethodIdentifier) { userName = authenticationMethodClass.userName(fromAuthenticationData: authData) @@ -1030,12 +1051,52 @@ class BookmarkViewController: StaticTableViewController { } } +// MARK: - Convenience for presentation +extension BookmarkViewController { + static func showBookmarkUI(on hostViewController: UIViewController, edit bookmark: OCBookmark? = nil, performContinue: Bool = false, attemptLoginOnSuccess: Bool = false, autosolveErrorOnSuccess: NSError? = nil, removeAuthDataFromCopy: Bool = true) { + var editBookmark = bookmark + + if let bookmark { + // Retrieve latest version of bookmark from OCBookmarkManager + if let latestStoredBookmarkVersion = OCBookmarkManager.shared.bookmark(forUUIDString: bookmark.uuid.uuidString) { + editBookmark = latestStoredBookmarkVersion + } + } + + let bookmarkViewController : BookmarkViewController = BookmarkViewController(editBookmark, removeAuthDataFromCopy: removeAuthDataFromCopy) + bookmarkViewController.userActionCompletionHandler = { (bookmark, success) in + if success, let bookmark = bookmark { + if let error = autosolveErrorOnSuccess as Error? { + OCMessageQueue.global.resolveIssues(forError: error, forBookmarkUUID: bookmark.uuid) + } + + if attemptLoginOnSuccess { + AccountConnectionPool.shared.connection(for: bookmark)?.connect() + } + } + } + + let navigationController : ThemeNavigationController = ThemeNavigationController(rootViewController: bookmarkViewController) + navigationController.isModalInPresentation = true + + hostViewController.present(navigationController, animated: true, completion: { + OnMainThread { + if performContinue { + bookmarkViewController.showedOAuthInfoHeader = true // needed for HTTP+OAuth2 connections to really continue on .handleContinue() call + bookmarkViewController.handleContinue() + } + } + }) + } +} + // MARK: - OCClassSettings support extension OCClassSettingsIdentifier { static let bookmark = OCClassSettingsIdentifier("bookmark") } extension OCClassSettingsKey { + static let bookmarkDefaultName = OCClassSettingsKey("default-name") static let bookmarkDefaultURL = OCClassSettingsKey("default-url") static let bookmarkURLEditable = OCClassSettingsKey("url-editable") static let prepopulation = OCClassSettingsKey("prepopulation") @@ -1062,40 +1123,47 @@ extension BookmarkViewController : OCClassSettingsSupport { static func classSettingsMetadata() -> [OCClassSettingsKey : [OCClassSettingsMetadataKey : Any]]? { return [ - .bookmarkDefaultURL : [ + .bookmarkDefaultName : [ .type : OCClassSettingsMetadataType.string, - .description : "The default URL for the creation of new bookmarks.", - .category : "Bookmarks", - .status : OCClassSettingsKeyStatus.supported - ], - - .bookmarkURLEditable : [ - .type : OCClassSettingsMetadataType.boolean, - .description : "Controls whether the server URL in the text field during the creation of new bookmarks can be changed.", + .description : "The default name for the creation of new bookmarks.", .category : "Bookmarks", .status : OCClassSettingsKeyStatus.supported ], - .prepopulation : [ - .type : OCClassSettingsMetadataType.string, - .description : "Controls prepopulation of the local database with the full item set during account setup.", - .category : "Bookmarks", - .status : OCClassSettingsKeyStatus.supported, - .possibleValues : [ - [ - OCClassSettingsMetadataKey.description : "No prepopulation. Request the contents of every folder individually.", - OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.doNot.rawValue - ], - [ - OCClassSettingsMetadataKey.description : "Parse the prepopulation metadata while receiving it.", - OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.streaming.rawValue - ], - [ - OCClassSettingsMetadataKey.description : "Parse the prepopulation metadata after receiving it as a whole.", - OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.split.rawValue + .bookmarkDefaultURL : [ + .type : OCClassSettingsMetadataType.string, + .description : "The default URL for the creation of new bookmarks.", + .category : "Bookmarks", + .status : OCClassSettingsKeyStatus.supported + ], + + .bookmarkURLEditable : [ + .type : OCClassSettingsMetadataType.boolean, + .description : "Controls whether the server URL in the text field during the creation of new bookmarks can be changed.", + .category : "Bookmarks", + .status : OCClassSettingsKeyStatus.supported + ], + + .prepopulation : [ + .type : OCClassSettingsMetadataType.string, + .description : "Controls prepopulation of the local database with the full item set during account setup.", + .category : "Bookmarks", + .status : OCClassSettingsKeyStatus.supported, + .possibleValues : [ + [ + OCClassSettingsMetadataKey.description : "No prepopulation. Request the contents of every folder individually.", + OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.doNot.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Parse the prepopulation metadata while receiving it.", + OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.streaming.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Parse the prepopulation metadata after receiving it as a whole.", + OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.split.rawValue + ] ] ] - ] ] } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift index 179735c0d..de76197f8 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift @@ -108,34 +108,46 @@ class CopyAction : Action { } func showDirectoryPicker() { - guard context.items.count > 0, let viewController = context.viewController, let core = self.core else { - completed(with: NSError(ocError: .insufficientParameters)) + guard context.items.count > 0, let clientContext = context.clientContext, let bookmark = context.core?.bookmark else { + self.completed(with: NSError(ocError: .insufficientParameters)) return } let items = context.items + let startLocation: OCLocation = .account(bookmark) - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, location: .legacyRoot, selectButtonTitle: "Copy here".localized, avoidConflictsWith: items, choiceHandler: { (selectedDirectory, _) in - if let targetDirectory = selectedDirectory { - items.forEach({ (item) in + var titleText: String - if let progress = self.core?.copy(item, to: targetDirectory, withName: item.name!, options: nil, resultHandler: { (error, _, _, _) in - if error != nil { - self.completed(with: error) - } else { - self.completed() - } + if items.count > 1 { + titleText = "Copy {{itemCount}} items".localized(["itemCount" : "\(items.count)"]) + } else { + titleText = "Copy \"{{itemName}}\"".localized(["itemName" : items.first?.name ?? "?"]) + } - }) { - self.publish(progress: progress) - } - }) + let locationPicker = ClientLocationPicker(location: startLocation, selectButtonTitle: "Copy here".localized, headerTitle: titleText, headerSubTitle: "Select target.".localized, avoidConflictsWith: items, choiceHandler: { (selectedDirectoryItem, location, _, cancelled) in + guard !cancelled, let selectedDirectoryItem else { + self.completed(with: NSError(ocError: OCError.cancelled)) + return } + items.forEach({ (item) in + guard let itemName = item.name else { + return + } + + if let progress = self.core?.copy(item, to: selectedDirectoryItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in + if error != nil { + Log.error("Error \(String(describing: error)) copying \(String(describing: itemName)) to \(String(describing: location))") + } + }) { + self.publish(progress: progress) + } + }) + + self.completed() }) - let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) - viewController.present(pickerNavigationController, animated: true) + locationPicker.present(in: clientContext) } func copyToPasteboard() { diff --git a/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift index 4bc1094fe..b76431d0c 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift @@ -103,7 +103,7 @@ class CreateDocumentAction: Action { return } - core.suggestUnusedNameBased(on: "New document".localized.appending((fileType.extension != nil) ? ".\(fileType.extension!)" : ""), at: itemLocation, isDirectory: true, using: .numbered, filteredBy: nil, resultHandler: { (suggestedName, _) in + core.suggestUnusedNameBased(on: "New document".localized.appending((fileType.extension != nil) ? ".\(fileType.extension!)" : ""), at: itemLocation, isDirectory: false, using: .numbered, filteredBy: nil, resultHandler: { (suggestedName, _) in guard let suggestedName = suggestedName else { return } OnMainThread { @@ -127,6 +127,22 @@ class CreateDocumentAction: Action { } if let progress = core.connection.createAppFile(of: fileType, in: parentItem, withName: newFileName, completionHandler: { (error, fileID, item) in + if let error = error { + OnMainThread { + let alertController = ThemedAlertController( + with: "Error creating {{itemName}}".localized(["itemName" : newFileName]), + message: error.localizedDescription, + okLabel: "OK".localized, + action: nil) + + viewController.present(alertController, animated: true) + + self.completed(with: error) + } + + return + } + if error == nil, let query = self.context.clientContext?.query { self.core?.reload(query) } diff --git a/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift b/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift index ca9c7e421..08ffd3fad 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift @@ -194,18 +194,19 @@ class ImportPasteboardAction : Action { useUTI = UTType.data.identifier } + let finalUTI = useUTI ?? UTType.data.identifier var fileName: String? - item.loadFileRepresentation(forTypeIdentifier: useUTI!) { (url, _ error) in + item.loadFileRepresentation(forTypeIdentifier: finalUTI) { (url, _ error) in guard let url = url else { return } let fileNameMaxLength = 16 - if useUTI == UTType.utf8PlainText.identifier { + if finalUTI == UTType.utf8PlainText.identifier { fileName = try? String(String(contentsOf: url, encoding: .utf8).prefix(fileNameMaxLength) + ".txt") } - if useUTI == UTType.rtf.identifier { + if finalUTI == UTType.rtf.identifier { let options = [NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.rtf] fileName = try? String(NSAttributedString(url: url, options: options, documentAttributes: nil).string.prefix(fileNameMaxLength) + ".rtf") } diff --git a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift index 8770399d2..059e04911 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift @@ -42,15 +42,35 @@ class MoveAction : Action { // MARK: - Action implementation override func run() { - guard context.items.count > 0, let viewController = context.viewController, let core = self.core else { + guard context.items.count > 0, let clientContext = context.clientContext, let bookmark = context.core?.bookmark else { self.completed(with: NSError(ocError: .insufficientParameters)) return } let items = context.items + let driveID = items.first?.driveID + var startLocation: OCLocation + var baseContext: ClientContext? - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, location: OCLocation.legacyRoot, selectButtonTitle: "Move here".localized, avoidConflictsWith: items, choiceHandler: { (selectedDirectory, _) in - guard let selectedDirectory = selectedDirectory else { + if let driveID { + // Limit to same drive + startLocation = .drive(driveID, bookmark: bookmark) + baseContext = clientContext + } else { + // Limit to account + startLocation = .account(bookmark) + } + + var titleText: String + + if items.count > 1 { + titleText = "Move {{itemCount}} items".localized(["itemCount" : "\(items.count)"]) + } else { + titleText = "Move \"{{itemName}}\"".localized(["itemName" : items.first?.name ?? "?"]) + } + + let locationPicker = ClientLocationPicker(location: startLocation, selectButtonTitle: "Move here".localized, headerTitle: titleText, headerSubTitle: "Select target.".localized, avoidConflictsWith: items, choiceHandler: { (selectedDirectoryItem, location, _, cancelled) in + guard !cancelled, let selectedDirectoryItem else { self.completed(with: NSError(ocError: OCError.cancelled)) return } @@ -60,9 +80,9 @@ class MoveAction : Action { return } - if let progress = self.core?.move(item, to: selectedDirectory, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in + if let progress = self.core?.move(item, to: selectedDirectoryItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in if error != nil { - Log.error("Error \(String(describing: error)) moving \(String(describing: itemName))") + Log.error("Error \(String(describing: error)) moving \(String(describing: itemName)) to \(String(describing: location))") } }) { self.publish(progress: progress) @@ -72,8 +92,7 @@ class MoveAction : Action { self.completed() }) - let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) - viewController.present(pickerNavigationController, animated: true) + locationPicker.present(in: clientContext, baseContext: baseContext) } override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift index 671916c03..1230511f0 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift @@ -23,7 +23,7 @@ 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, .moreDetailItem, .multiSelection, .dropAction, .keyboardShortcut, .contextMenuItem] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreDetailItem, .multiSelection, .dropAction, .keyboardShortcut, .contextMenuItem, .unviewableFileType] } override class var keyCommand : String? { return "O" } override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } @@ -93,8 +93,11 @@ class OpenInAction: Action { // Store reference to temporary export root URL for later deletion self.temporaryExportURL = temporaryExportFolderURL + // Obey to excluded activity types + let excludedActivityTypes : [UIActivity.ActivityType]? = Action.classSetting(forOCClassSettingsKey: .excludedSystemActivities) as? [UIActivity.ActivityType] + // UIDocumentInteractionController can only be used with a single file - if exportURLs.count == 1 { + if exportURLs.count == 1, excludedActivityTypes == nil || excludedActivityTypes?.count == 0 { if let fileURL = exportURLs.first { // Make sure self is around until interactionControllerDispatchGroup.leave() is called by the documentInteractionControllerDidDismissOptionsMenu delegate method implementation self.interactionControllerDispatchGroup = DispatchGroup() @@ -126,11 +129,11 @@ class OpenInAction: Action { 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 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) } @@ -138,6 +141,12 @@ class OpenInAction: Action { } else { // Handle multiple files with a fallback solution let activityController = UIActivityViewController(activityItems: exportURLs, applicationActivities: nil) + + if let excludedActivityTypes = excludedActivityTypes { + // Apply excluded activity types + activityController.excludedActivityTypes = excludedActivityTypes + } + activityController.completionWithItemsHandler = { (_, _, _, _) in // Remove temporary export root URL with contents try? FileManager.default.removeItem(at: temporaryExportFolderURL) diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift index 5daa8caf2..4c14874b0 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift @@ -20,7 +20,6 @@ import UIKit import ownCloudSDK import ownCloudAppShared -@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 } @@ -43,17 +42,26 @@ class OpenSceneAction: Action { // 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) + if context.items.count == 1, let item = context.items.first { + if let bookmark = context.core?.bookmark, + let clientContext = context.clientContext, + let destinationLocationBookmark = BrowserNavigationBookmark.from(dataItem: item, clientContext: clientContext, restoreAction: .open) { + let activity = AppStateAction(with: [ + .connection(with: bookmark, children: [ + .navigate(to: destinationLocationBookmark) + ]) + ]).userActivity(with: clientContext) + + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil) + + completed(with: nil) + return + } } } + + completed(with: NSError(ocError: .insufficientParameters)) } override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { diff --git a/ownCloud/Client/Actions/Actions+Extensions/PresentationModeAction.swift b/ownCloud/Client/Actions/Actions+Extensions/PresentationModeAction.swift index 0ea946aef..d850ca6ec 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/PresentationModeAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/PresentationModeAction.swift @@ -34,8 +34,8 @@ class PresentationModeAction: Action { override class func applicablePosition(forContext context: ActionContext) -> ActionPosition { if context.items.first?.cloudStatus == .cloudOnly { return .none - } else if let hostViewController = context.viewController, type(of: hostViewController) === ClientQueryViewController.self { - return .none +// } else if let hostViewController = context.viewController, type(of: hostViewController) === ClientQueryViewController.self { +// return .none } else if let hostViewController = context.viewController, (hostViewController.navigationController?.isNavigationBarHidden ?? false) { return .none } @@ -51,7 +51,7 @@ class PresentationModeAction: Action { } if !DisplaySleepPreventer.shared.isPreventing(for: PresentationModeAction.reason) { - let alertController = UIAlertController(title: "Presentation Mode".localized, message: "Enabling presentation mode will prevent the display from sleep mode until the view is closed.".localized, preferredStyle: .alert) + let alertController = ThemedAlertController(title: "Presentation Mode".localized, message: "Enabling presentation mode will prevent the display from sleep mode until the view is closed.".localized, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) alertController.addAction(UIAlertAction(title: "Enable".localized, style: .default, handler: { (_) in DisplaySleepPreventer.shared.startPreventingDisplaySleep(for: PresentationModeAction.reason) diff --git a/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift index 61c5fbe0b..bb29865d3 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift @@ -71,21 +71,25 @@ class UnshareAction : Action { let unshareItemAndPublishProgress = { (items: [OCItem]) in for item in items { - if let owner = item.owner { - if !owner.isRemote { - _ = self.core?.sharesSharedWithMe(for: item, initialPopulationHandler: { (shares) in - let userGroupShares = shares.filter { (share) -> Bool in - return share.type != .link - } - if let share = userGroupShares.first, let progress = self.core?.makeDecision(on: share, accept: false, completionHandler: { (error) in - if error != nil { - Log.log("Error \(String(describing: error)) unshare \(String(describing: item.path))") - } - }) { - self.publish(progress: progress) + let unshareItem = { + _ = self.core?.sharesSharedWithMe(for: item, initialPopulationHandler: { (shares) in + let userGroupShares = shares.filter { (share) -> Bool in + return share.type != .link + } + if let share = userGroupShares.first, let progress = self.core?.makeDecision(on: share, accept: false, completionHandler: { (error) in + if error != nil { + Log.log("Error \(String(describing: error)) unshare \(String(describing: item.path))") } + }) { + self.publish(progress: progress) + } - }, keepRunning: false) + }, keepRunning: false) + } + + if let owner = item.owner { + if !owner.isRemote { + unshareItem() } else { _ = self.core?.acceptedCloudShares(for: item, initialPopulationHandler: { (shares) in let userGroupShares = shares.filter { (share) -> Bool in @@ -101,6 +105,8 @@ class UnshareAction : Action { }, keepRunning: false) } + } else if item.isSharedWithUser { + unshareItem() } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift index 359b9691b..9211bf656 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift @@ -155,7 +155,7 @@ class UploadMediaAction: UploadBaseAction { if granted { self.presentImageGalleryPicker() } else { - let alert = UIAlertController.alertControllerForPhotoLibraryAuthorizationInSettings() + let alert = ThemedAlertController.alertControllerForPhotoLibraryAuthorizationInSettings() viewController.present(alert, animated: true) self.completed() } diff --git a/ownCloud/Client/Actions/EditDocumentViewController.swift b/ownCloud/Client/Actions/EditDocumentViewController.swift index 5cc346fad..a23769707 100644 --- a/ownCloud/Client/Actions/EditDocumentViewController.swift +++ b/ownCloud/Client/Actions/EditDocumentViewController.swift @@ -125,15 +125,28 @@ class EditDocumentViewController: QLPreviewController, Themeable { @objc func enableEditingMode() { // Activate editing mode by performing the action on pencil icon. Unfortunately that's the only way to do it apparently - if self.navigationItem.rightBarButtonItems?.count ?? 0 > 2 { - guard let markupButton = self.navigationItem.rightBarButtonItems?[1] else { return } - _ = markupButton.target?.perform(markupButton.action, with: markupButton) - } else if UIDevice.current.isIpad, self.navigationItem.rightBarButtonItems?.count ?? 0 == 2 { - guard let markupButton = self.navigationItem.rightBarButtonItems?[1] else { return } - _ = markupButton.target?.perform(markupButton.action, with: markupButton) - } else { - guard let markupButton = self.navigationItem.rightBarButtonItems?.first else { return } - _ = markupButton.target?.perform(markupButton.action, with: markupButton) + if #available(iOS 16.0, *) { + if self.navigationItem.rightBarButtonItems?.count ?? 0 > 3 { + guard let markupButton = self.navigationItem.rightBarButtonItems?[0] else { return } + _ = markupButton.target?.perform(markupButton.action, with: markupButton) + } else if self.toolbarItems?.count ?? 0 > 4 { + guard let markupButton = self.toolbarItems?[4] else { return } + _ = markupButton.target?.perform(markupButton.action, with: markupButton) + } else if self.toolbarItems?.count ?? 0 < 4 { + guard let markupButton = self.toolbarItems?[2] else { return } + _ = markupButton.target?.perform(markupButton.action, with: markupButton) + } + } else if #available(iOS 15.0, *) { + if self.navigationItem.rightBarButtonItems?.count ?? 0 > 2 { + guard let markupButton = self.navigationItem.rightBarButtonItems?[1] else { return } + _ = markupButton.target?.perform(markupButton.action, with: markupButton) + } else if UIDevice.current.isIpad, self.navigationItem.rightBarButtonItems?.count ?? 0 == 2 { + guard let markupButton = self.navigationItem.rightBarButtonItems?[1] else { return } + _ = markupButton.target?.perform(markupButton.action, with: markupButton) + } else { + guard let markupButton = self.navigationItem.rightBarButtonItems?.first else { return } + _ = markupButton.target?.perform(markupButton.action, with: markupButton) + } } } diff --git a/ownCloud/Client/ClientActivityViewController.swift b/ownCloud/Client/ClientActivityViewController.swift index ea699b4b9..5c4f20059 100644 --- a/ownCloud/Client/ClientActivityViewController.swift +++ b/ownCloud/Client/ClientActivityViewController.swift @@ -20,8 +20,7 @@ import UIKit import ownCloudSDK import ownCloudAppShared -class ClientActivityViewController: UITableViewController, Themeable, MessageGroupCellDelegate, ClientActivityCellDelegate { - +class ClientActivityViewController: UITableViewController, Themeable, MessageGroupCellDelegate, ClientActivityCellDelegate, AccountConnectionMessageUpdates, AccountConnectionStatusObserver { enum ActivitySection : Int, CaseIterable { case messageGroups case activities @@ -75,12 +74,59 @@ class ClientActivityViewController: UITableViewController, Themeable, MessageGro } } - deinit { + var consumer: AccountConnectionConsumer? + weak var connection: AccountConnection? { + willSet { + if let consumer { + connection?.remove(consumer: consumer) + } + } + + didSet { + core = connection?.core + messageSelector = connection?.messageSelector + if let consumer { + connection?.add(consumer: consumer) + } + } + } + + private func setConnection(_ connection: AccountConnection?) { + // Work around willSet/didSet not being called when set directly in the initializer + self.connection = connection + } + + init(connection: AccountConnection? = nil) { + super.init(style: .plain) + + if let connection { + consumer = AccountConnectionConsumer(owner: self, statusObserver: self, messageUpdateHandler: self) + setConnection(connection) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func winddown() { Theme.shared.unregister(client: self) self.shouldPauseDisplaySleep = false + self.connection = nil self.core = nil } + deinit { + winddown() + } + + func account(connection: AccountConnection, changedStatusTo status: AccountConnection.Status, initial: Bool) { + if core == nil { + core = connection.core + messageSelector = connection.messageSelector + } + } + @objc func handleActivityNotification(_ notification: Notification) { if let activitiyUpdates = notification.userInfo?[OCActivityManagerNotificationUserInfoUpdatesKey] as? [ [ String : Any ] ] { for activityUpdate in activitiyUpdates { diff --git a/ownCloud/Client/ClientRootViewController+ItemActions.swift b/ownCloud/Client/ClientRootViewController+ItemActions.swift deleted file mode 100644 index 96fde2e40..000000000 --- a/ownCloud/Client/ClientRootViewController+ItemActions.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// ClientRootViewController+ItemActions.swift -// ownCloud -// -// Created by Felix Schwarz on 21.04.22. -// Copyright © 2022 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2022, 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 ownCloudAppShared -import ownCloudApp - -extension ClientRootViewController : ActionProgressHandlerProvider { - func makeActionProgressHandler() -> ActionProgressHandler { - return { [weak self] (progress, publish) in - if publish { - self?.rootContext?.progressSummarizer?.startTracking(progress: progress) - } else { - self?.rootContext?.progressSummarizer?.stopTracking(progress: progress) - } - } - } -} - -extension ClientRootViewController : MoreItemAction { - func moreOptions(for item: OCDataItem, at locationIdentifier: OCExtensionLocationIdentifier, context: ClientContext, sender: AnyObject?) -> Bool { - guard let sender = sender, let core = context.core, let item = item as? OCItem else { - return false - } - let originatingViewController : UIViewController = context.originatingViewController ?? self - let actionsLocation = OCExtensionLocation(ofType: .action, identifier: locationIdentifier) - let actionContext = ActionContext(viewController: originatingViewController, clientContext: context, core: core, query: context.query, items: [item], location: actionsLocation, sender: sender) - - if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: makeActionProgressHandler(), completionHandler: nil) { - originatingViewController.present(asCard: moreViewController, animated: true) - } - - return true - } -} - -extension ClientRootViewController : ViewItemAction { - func provideViewer(for item: OCDataItem, context: ClientContext) -> UIViewController? { - guard let item = item as? OCItem, let query = context.query, let core = context.core else { - return nil - } - - let itemViewController = DisplayHostViewController(clientContext: context, core: core, selectedItem: item, query: query) - itemViewController.hidesBottomBarWhenPushed = true - itemViewController.progressSummarizer = context.progressSummarizer - - return itemViewController - } -} - -extension ClientRootViewController : InlineMessageCenter { - public func hasInlineMessage(for item: OCItem) -> Bool { - guard let activeSyncRecordIDs = item.activeSyncRecordIDs, let syncRecordIDsWithMessages = self.syncRecordIDsWithMessages else { - return false - } - - return syncRecordIDsWithMessages.contains { (syncRecordID) -> Bool in - return activeSyncRecordIDs.contains(syncRecordID) - } - } - - public func showInlineMessageFor(item: OCItem) { - if let messages = self.messageSelector?.selection, - let firstMatchingMessage = messages.first(where: { (message) -> Bool in - guard let syncRecordID = message.syncIssue?.syncRecordID, let containsSyncRecordID = item.activeSyncRecordIDs?.contains(syncRecordID) else { - return false - } - - return containsSyncRecordID - }) { - firstMatchingMessage.showInApp() - } - } -} diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift deleted file mode 100644 index dac3d23cc..000000000 --- a/ownCloud/Client/ClientRootViewController.swift +++ /dev/null @@ -1,869 +0,0 @@ -// -// ClientViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 05.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 -import ownCloudApp -import ownCloudAppShared - -protocol ClientRootViewControllerAuthenticationDelegate : AnyObject { - func handleAuthError(for clientViewController: ClientRootViewController, error: NSError, editBookmark: OCBookmark?, preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?) -} - -class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTabBarToggling, UINavigationControllerDelegate { - - // MARK: - Constants - let folderButtonsSize: CGSize = CGSize(width: 25.0, height: 25.0) - - // MARK: - Instance variables. - let bookmark : OCBookmark - weak var core : OCCore? - private var coreRequested : Bool = false - private var userAvatarUpdated : Bool = false - var filesNavigationController : ThemeNavigationController? - let emptyViewController = UIViewController() - var activityNavigationController : ThemeNavigationController? - var activityViewController : ClientActivityViewController? - var libraryNavigationController : ThemeNavigationController? - var libraryViewController : LibraryTableViewController? - var progressBar : CollapsibleProgressBar? - var progressBarBottomConstraint: NSLayoutConstraint? - var progressSummarizer : ProgressSummarizer? - var toolbar : UIToolbar? - - var notificationPresenter : NotificationMessagePresenter? - var cardMessagePresenter : CardIssueMessagePresenter? - - weak var authDelegate : ClientRootViewControllerAuthenticationDelegate? - - var skipAuthorizationFailure : Bool = false - - var connectionStatusObservation : NSKeyValueObservation? - var connectionStatusSummary : ProgressSummary? { - willSet { - if newValue != nil { - progressSummarizer?.pushPrioritySummary(summary: newValue!) - } - } - - didSet { - if oldValue != nil { - progressSummarizer?.popPrioritySummary(summary: oldValue!) - } - } - } - - var messageSelector : MessageSelector? - - var fpServiceStandby : OCFileProviderServiceStandby? - - var alertQueue : OCAsyncSequentialQueue = OCAsyncSequentialQueue() - - var appProviderObservation: NSKeyValueObservation? - - init(bookmark inBookmark: OCBookmark) { - bookmark = inBookmark - - super.init(nibName: nil, bundle: nil) - - notificationPresenter = NotificationMessagePresenter(forBookmarkUUID: bookmark.uuid) - cardMessagePresenter = CardIssueMessagePresenter(with: bookmark.uuid as OCBookmarkUUID, limitToSingleCard: true, presenter: { [weak self] (viewController) in - self?.presentAlertAsCard(viewController: viewController, withHandle: false, dismissable: true) - }) - - progressSummarizer = ProgressSummarizer.shared(forBookmark: inBookmark) - if progressSummarizer != nil { - progressSummarizer?.addObserver(self) { [weak self] (summarizer, summary) in - var useSummary : ProgressSummary = summary - let prioritySummary : ProgressSummary? = summarizer.prioritySummary - - if (summary.progress == 1) && (summarizer.fallbackSummary != nil) { - useSummary = summarizer.fallbackSummary ?? summary - } - - if prioritySummary != nil { - useSummary = prioritySummary! - } - - self?.progressBar?.update(with: useSummary.message, progress: Float(useSummary.progress)) - - self?.progressBar?.autoCollapse = (((summarizer.fallbackSummary == nil) || (useSummary.progressCount == 0)) && (prioritySummary == nil)) || (self?.allowProgressBarAutoCollapse ?? false) - } - } - - self.delegate = self - } - - public var allowProgressBarAutoCollapse : Bool = false { - didSet { - progressSummarizer?.setNeedsUpdate() - } - } - - func updateConnectionStatusSummary() { - var summary : ProgressSummary? = ProgressSummary(indeterminate: true, progress: 1.0, message: nil, progressCount: 1) - - if let connectionStatus = core?.connectionStatus { - var connectionShortDescription = core?.connectionStatusShortDescription - - connectionShortDescription = connectionShortDescription != nil ? (connectionShortDescription!.hasSuffix(".") ? connectionShortDescription! + " " : connectionShortDescription! + ". ") : "" - - switch connectionStatus { - case .online: - summary = nil - - case .connecting: - summary?.message = "Connecting…".localized - - case .offline, .unavailable: - summary?.message = String(format: "%@%@", connectionShortDescription!, "Contents from cache.".localized) - } - - if connectionStatus == .online, !userAvatarUpdated, let user = core?.connection.loggedInUser { - // Update avatar on every connect - userAvatarUpdated = true - - let avatarRequest = OCResourceRequestAvatar(for: user, maximumSize: OCAvatar.defaultSize, scale: 0, waitForConnectivity: true, changeHandler: { [weak self] request, error, ongoing, previousResource, newResource in - if !ongoing, - let bookmarkUUID = self?.bookmark.uuid, - let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID), - let newResource = newResource as? OCViewProvider { - bookmark.avatar = newResource - OCBookmarkManager.shared.updateBookmark(bookmark) - } - }) - avatarRequest.lifetime = .singleRun - - core?.vault.resourceManager?.start(avatarRequest) - } - } - - self.connectionStatusSummary = summary - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - connectionStatusObservation = nil - - if let statusSummary = connectionStatusSummary { - ProgressSummarizer.shared(forBookmark: bookmark).popPrioritySummary(summary: statusSummary) - } - ProgressSummarizer.shared(forBookmark: bookmark).removeObserver(self) - ProgressSummarizer.shared(forBookmark: bookmark).reset() - - if core?.delegate === self { - core?.delegate = nil - } - - Theme.shared.unregister(client: self) - - // Remove message presenters - if let notificationPresenter = self.notificationPresenter { - core?.messageQueue.remove(presenter: notificationPresenter) - } - - if let cardMessagePresenter = self.cardMessagePresenter { - core?.messageQueue.remove(presenter: cardMessagePresenter) - } - - appProviderActionExtensions = nil - - if self.coreRequested { - self.fpServiceStandby?.stop() - OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) - } - } - - // MARK: - Startup - func afterCoreStart(_ lastVisibleItemId: String?, busyHandler: OCCoreBusyStatusHandler? = nil, completionHandler: @escaping ((_ error: Error?) -> Void)) { - OCCoreManager.shared.requestCore(for: bookmark, setup: { (core, _) in - self.coreRequested = true - self.core = core - core?.delegate = self - - if let busyHandler = busyHandler { - // Wrap busyHandler to ensure that a ObjC block is generated and not a Swift block is passed - core?.busyStatusHandler = { (progress) in - busyHandler(progress) - } - } - - // Add message presenters - if let notificationPresenter = self.notificationPresenter { - core?.messageQueue.add(presenter: notificationPresenter) - } - - if let cardMessagePresenter = self.cardMessagePresenter { - core?.messageQueue.add(presenter: cardMessagePresenter) - } - - // Observe .appProvider property - self.appProviderObservation = core?.observe(\OCCore.appProvider, options: .initial, changeHandler: { [weak self] (core, change) in - self?.appProviderChanged(to: core.appProvider) - }) - - // Remove skip available offline when user opens the bookmark - core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) - }, completionHandler: { (core, error) in - if error == nil { - // Start FP standby in 5 seconds regardless of connnection status - // (or below: after it's clear that authentication worked) - OnBackgroundQueue(async: true, after: 5.0) { [weak self] in - self?.startFPServiceStandbyIfNotRunning() - } - - // Core is ready - self.coreReady(lastVisibleItemId) - - // Start showing connection status - OnMainThread { [weak self] () in - self?.connectionStatusObservation = core?.observe(\OCCore.connectionStatus, options: [.initial], changeHandler: { [weak self] (_, _) in - self?.updateConnectionStatusSummary() - - if let connectionStatus = self?.core?.connectionStatus, - connectionStatus == .online { - // Start FP service standby after it's clear that authentication worked - // (or above: after 5 seconds regardless of connnection status) - self?.startFPServiceStandbyIfNotRunning() - } - }) - } - } else { - self.core = nil - self.coreRequested = false - - Log.error("Error requesting/starting core: \(String(describing: error))") - } - - OnMainThread { - completionHandler(error) - } - }) - } - - func startFPServiceStandbyIfNotRunning() { - // Set up FP standby - OCSynchronized(self) { - if let core = core, - core.state == .starting || core.state == .running, - self.fpServiceStandby == nil { - self.fpServiceStandby = OCFileProviderServiceStandby(core: core) - self.fpServiceStandby?.start() - } - } - } - - var pushTransition : PushTransitionDelegate? - - override func viewDidLoad() { - super.viewDidLoad() - - self.view.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor - self.navigationController?.setNavigationBarHidden(true, animated: true) - - 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?.navigationBar.isTranslucent = false - filesNavigationController?.tabBarItem.title = "Files".localized - filesNavigationController?.tabBarItem.image = Theme.shared.image(for: "folder", size: folderButtonsSize) - filesNavigationController?.delegate = self - - activityViewController = ClientActivityViewController() - activityNavigationController = ThemeNavigationController(rootViewController: activityViewController!) - activityNavigationController?.tabBarItem.title = "Status".localized - activityNavigationController?.tabBarItem.image = Theme.shared.image(for: "status-flash", size: CGSize(width: 25, height: 25)) - - libraryViewController = LibraryTableViewController(style: .grouped) - libraryNavigationController = ThemeNavigationController(rootViewController: libraryViewController!) - libraryNavigationController?.tabBarItem.title = "Quick Access".localized - libraryNavigationController?.tabBarItem.image = Branding.shared.brandedImageNamed(.bookmarkIcon)?.scaledImageFitting(in: CGSize(width: 25.0, height: 25.0)) - - progressBar = CollapsibleProgressBar(frame: CGRect.zero) - progressBar?.translatesAutoresizingMaskIntoConstraints = false - - self.view.addSubview(progressBar!) - - progressBar?.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true - progressBar?.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true - progressBarBottomConstraint = progressBar?.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -1 * self.tabBar.bounds.height) - progressBarBottomConstraint?.isActive = true - - toolbar = UIToolbar(frame: .zero) - toolbar?.translatesAutoresizingMaskIntoConstraints = false - toolbar?.insetsLayoutMarginsFromSafeArea = true - toolbar?.isTranslucent = false - - self.view.addSubview(toolbar!) - - toolbar?.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true - toolbar?.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true - toolbar?.topAnchor.constraint(equalTo: self.tabBar.topAnchor).isActive = true - toolbar?.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true - - toolbar?.isHidden = true - - Theme.shared.register(client: self, applyImmediately: true) - - if let filesNavigationController = filesNavigationController, - let activityNavigationController = activityNavigationController, let libraryNavigationController = libraryNavigationController { - self.viewControllers = [ filesNavigationController, libraryNavigationController, activityNavigationController ] - } - } - - var closeClientCompletionHandler : (() -> Void)? - - func closeClient(completion: (() -> Void)? = nil) { - OCBookmarkManager.lastBookmarkSelectedForConnection = nil - - self.dismiss(animated: true, completion: { - if completion != nil { - OnMainThread { // Work-around to make sure the self.presentingViewController is ready to present something new. Immediately after .dismiss returns, it isn't, so we wait one runloop-cycle for it to complete - completion?() - } - } - }) - } - - func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - if viewController == emptyViewController { - closeClient() - - // Prevent re-opening of items on next launch in case user has returned to the bookmark list - view.window?.windowScene?.userActivity = nil - } else { - updateProgressBarFor(viewController: viewController, animate: animated) - } - } - - func updateProgressBarFor(viewController: UIViewController, animate: Bool) { - let hideProgressBar = viewController.isKind(of: DisplayHostViewController.self) - - if animate { - self.progressBar?.superview?.layoutIfNeeded() - } - - self.allowProgressBarAutoCollapse = hideProgressBar - - if hideProgressBar { - self.progressBarBottomConstraint?.constant = 0 - } else { - self.progressBarBottomConstraint?.constant = -1 * self.tabBar.bounds.height - } - - if animate { - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: { - self.progressBar?.superview?.layoutIfNeeded() - }) - } - } - - var rootContext : ClientContext? - - func coreReady(_ lastVisibleItemId: String?) { - OnMainThread { - if let core = self.core { - self.rootContext = ClientContext(core: core, rootViewController: self, progressSummarizer: self.progressSummarizer, modifier: { context in - context.inlineMessageCenter = self - context.moreItemHandler = self - context.viewItemHandler = self - context.actionProgressHandlerProvider = self - }) - - core.vault.resourceManager?.add(ResourceSourceItemIcons(core: core)) - - if let localItemId = lastVisibleItemId { - self.createFileListStack(for: localItemId) - } else { - let topLevelViewController : UIViewController? - - if core.useDrives { - topLevelViewController = CollectionViewController(context: ClientContext(with: self.rootContext, navigationController: self.filesNavigationController), sections: [ - CollectionViewSection(identifier: "top", dataSource: core.hierarchicDrivesDataSource, cellLayout: .list(appearance: .insetGrouped)), - CollectionViewSection(identifier: "projects", dataSource: core.projectDrivesDataSource, cellLayout: .list(appearance: .insetGrouped)) - ]) - } else { - let query = OCQuery(for: .legacyRoot) - topLevelViewController = ClientQueryViewController(core: core, drive: nil, query: query, rootViewController: self) - } - - // 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, topLevelViewController!], animated: false) - } - - let emptyViewController = self.emptyViewController - - if VendorServices.shared.isBranded, !VendorServices.shared.canAddAccount { - emptyViewController.navigationItem.title = "Manage".localized - } else { - emptyViewController.navigationItem.title = "Accounts".localized - } - - self.filesNavigationController?.popLastHandler = { [weak self] (viewController) in - if viewController == emptyViewController { - OnMainThread { - self?.closeClient() - - // Prevent re-opening of items on next launch in case user has returned to the bookmark list - self?.view.window?.windowScene?.userActivity = nil - } - } - - return (viewController != emptyViewController) - } - - let bookmarkUUID = core.bookmark.uuid - - self.activityViewController?.core = core - self.libraryViewController?.core = core - - self.messageSelector = MessageSelector(from: core.messageQueue, filter: { (message) in - return (message.bookmarkUUID == bookmarkUUID) && !message.resolved - }, provideGroupedSelection: true, provideSyncRecordIDs: true, handler: { [weak self] (messages, groups, syncRecordIDs) in - self?.updateMessageSelectionWith(messages: messages, groups: groups, syncRecordIDs: syncRecordIDs) - }) - - self.activityViewController?.messageSelector = self.messageSelector - - self.connectionInitializedObservation = core.observe(\OCCore.connection.connectionInitializationPhaseCompleted, options: [.initial], changeHandler: { [weak self] (core, _) in - if core.connection.connectionInitializationPhaseCompleted { - self?.connectionInitialized() - } - }) - } - } - } - - private var connectionInitializedObservation : NSKeyValueObservation? - - func connectionInitialized() { - OCSynchronized(self) { - if connectionInitializedObservation == nil { - return - } - - connectionInitializedObservation = nil - } - - OnMainThread { - self.libraryViewController?.setupQueries() - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - updateProgressBarFor(viewController: topMostViewController, animate: false) - } - - func updateMessageSelectionWith(messages: [OCMessage]?, groups : [MessageGroup]?, syncRecordIDs : Set?) { - OnMainThread { - self.activityViewController?.handleMessagesUpdates(messages: messages, groups: groups) - - if syncRecordIDs != self.syncRecordIDsWithMessages { - self.syncRecordIDsWithMessages = syncRecordIDs - } - } - } - - var syncRecordIDsWithMessages : Set? { - didSet { - NotificationCenter.default.post(name: .ClientSyncRecordIDsWithMessagesChanged, object: self.core) - } - } - - func createFileListStack(for itemLocalID: String) { - if let core = core { - // retrieve the item for the item id - core.retrieveItemFromDatabase(forLocalID: itemLocalID, completionHandler: { (error, _, item) in - OnMainThread { - let query = OCQuery(for: .legacyRoot) - let queryViewController = ClientQueryViewController(core: core, drive: nil, query: query, rootViewController: self) - - if error == nil, let item = item, item.isRoot == false { - // 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) - - 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, pushViewController: true) - } else { - // Fallback, if item no longer exists show root folder - self.filesNavigationController?.setViewControllers([self.emptyViewController, queryViewController], animated: false) - } - } - }) - } - } - - func open(item: OCItem, in controller: ClientQueryViewController) -> ClientQueryViewController? { - if let subController = controller.open(item: item, animated: false, pushViewController: false) as? ClientQueryViewController { - return subController - } - - return nil - } - - var appProviderActionExtensions : [OCExtension]? { - willSet { - if let extensions = appProviderActionExtensions { - for ext in extensions { - OCExtensionManager.shared.removeExtension(ext) - } - } - } - - didSet { - if let extensions = appProviderActionExtensions { - for ext in extensions { - OCExtensionManager.shared.addExtension(ext) - } - } - } - } - - func appProviderChanged(to appProvider: OCAppProvider?) { - var actionExtensions : [OCExtension] = [] - - if let core = core { - if let apps = core.appProvider?.apps { - for app in apps { - // Pre-load app icon - if let appIconRequest = app.iconResourceRequest { - core.vault.resourceManager?.start(appIconRequest) - } - - // Create app-specific open-in-web-app action - let openInWebAction = OpenInWebAppAction.createActionExtension(for: app, core: core) - actionExtensions.append(openInWebAction) - } - } - - if let types = core.appProvider?.types { - let creationTypes = types.filter({ type in - return type.allowCreation - }) - - if creationTypes.count > 0 { - // Pre-load document icons - for type in creationTypes { - if let typeIconRequest = type.iconResourceRequest { - core.vault.resourceManager?.start(typeIconRequest) - } - } - - // Log.debug("Creation Types: \(String(describing: creationTypes))") - } - } - } - - appProviderActionExtensions = actionExtensions - } -} - -extension ClientRootViewController : Themeable { - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.tabBar.applyThemeCollection(collection) - - self.toolbar?.applyThemeCollection(Theme.shared.activeCollection) - - self.view.backgroundColor = collection.tableBackgroundColor - } -} - -extension ClientRootViewController : OCCoreDelegate { - func core(_ core: OCCore, handleError error: Error?, issue inIssue: OCIssue?) { - var issue = inIssue - var isAuthFailure : Bool = false - var authFailureMessage : String? - var authFailureTitle : String = "Authorization failed".localized - var authFailureHasEditOption : Bool = true - var authFailureIgnoreLabel = "Continue offline".localized - var authFailureIgnoreStyle = UIAlertAction.Style.destructive - let editBookmark = self.bookmark - var nsError = error as NSError? - - Log.debug("Received error \(nsError?.description ?? "nil")), issue \(issue?.description ?? "nil")") - - if let authError = issue?.authenticationError { - // Turn issues that are just converted authorization errors back into errors and discard the issue - nsError = authError - issue = nil - } - - Log.debug("Received error \(nsError?.description ?? "nil")), issue \(issue?.description ?? "nil")") - - if let nsError = nsError { - if nsError.isOCError(withCode: .authorizationFailed) { - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError, underlyingError.isDAVException, underlyingError.davExceptionMessage == "User disabled" { - authFailureHasEditOption = false - authFailureIgnoreStyle = .cancel - authFailureIgnoreLabel = "Continue offline".localized - authFailureMessage = "The account has been disabled." - } else { - if bookmark.isTokenBased == true { - authFailureTitle = "Access denied".localized - authFailureMessage = "The connection's access token has expired or become invalid. Sign in again to re-gain access.".localized - - if let localizedDescription = nsError.userInfo[NSLocalizedDescriptionKey] { - authFailureMessage = "\(authFailureMessage!)\n\n(\(localizedDescription))" - } - } else { - authFailureMessage = "The server declined access with the credentials stored for this connection.".localized - } - } - - isAuthFailure = true - } - - if nsError.isOCError(withCode: .authorizationNoMethodData) || nsError.isOCError(withCode: .authorizationMissingData) { - authFailureMessage = "No authentication data has been found for this connection.".localized - - isAuthFailure = true - } - - if nsError.isOCError(withCode: .authorizationMethodNotAllowed) { - authFailureMessage = NSString(format: "Authentication with %@ is no longer allowed. Re-authentication needed.".localized as NSString, core.connection.authenticationMethod?.name ?? "??") as String - - isAuthFailure = true - } - - if isAuthFailure { - // Make sure only the first auth failure will actually lead to an alert - // (otherwise alerts could keep getting enqueued while the first alert is being shown, - // and then be presented even though they're no longer relevant). It's ok to only show - // an alert for the first auth failure, because the options are "Continue offline" (=> no longer show them) - // and "Edit" (=> log out, go to bookmark editing) - var doSkip = false - - OCSynchronized(self) { - doSkip = skipAuthorizationFailure // Keep in mind OCSynchronized() contents is running as a block, so "return" in here wouldn't have the desired effect - skipAuthorizationFailure = true - } - - if doSkip { - Log.debug("Skip authorization failure") - return - } - } - } - - let presentAlert : (_ authFailureHasEditOption: Bool, _ authFailureIgnoreStyle: UIAlertAction.Style, _ authFailureIgnoreLabel: String, _ authFailureMessage: String?, _ preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?) -> Void = { (authFailureHasEditOption, authFailureIgnoreStyle, authFailureIgnoreLabel, authFailureMessage, preferredAuthenticationMethods) in - self.alertQueue.async { [weak self] (queueCompletionHandler) in - var presentIssue : OCIssue? = issue - var queueCompletionHandlerScheduled : Bool = false - - if isAuthFailure { - self?.presentAuthAlert(for: editBookmark, error: nsError, title: authFailureTitle, message: authFailureMessage, ignoreLabel: authFailureIgnoreLabel, ignoreStyle: authFailureIgnoreStyle, hasEditOption: authFailureHasEditOption, preferredAuthenticationMethods: preferredAuthenticationMethods, completionHandler: queueCompletionHandler) - - queueCompletionHandlerScheduled = true - - return - } - - if issue == nil, let error = error { - presentIssue = OCIssue(forError: error, level: .error, issueHandler: nil) - } - - if presentIssue != nil { - var presentViewController : UIViewController? - var onViewController : UIViewController? - - if let startViewController = self { - var hostViewController : UIViewController = startViewController - - while hostViewController.presentedViewController != nil, - hostViewController.presentedViewController?.isBeingDismissed == false { - hostViewController = hostViewController.presentedViewController! - } - - onViewController = hostViewController - } - - if let presentIssue = presentIssue, presentIssue.type == .multipleChoice { - presentViewController = ThemedAlertController(with: presentIssue, completion: queueCompletionHandler) - } else if let onViewController = onViewController, let presentIssue = presentIssue { - IssuesCardViewController.present(on: onViewController, issue: presentIssue, bookmark: self?.bookmark, completion: { [weak presentIssue] (response) in - switch response { - case .cancel: - presentIssue?.reject() - - case .approve: - presentIssue?.approve() - - case .dismiss: break - } - queueCompletionHandler() - }) - - queueCompletionHandlerScheduled = true - } - - if let presentViewController = presentViewController, let onViewController = onViewController { - queueCompletionHandlerScheduled = true - onViewController.present(presentViewController, animated: true, completion: nil) - } - } - - if !queueCompletionHandlerScheduled { - queueCompletionHandler() - } - } - } - - Log.debug("Handling error \(String(describing: error)) / \(String(describing: issue)) with isAuthFailure=\(isAuthFailure), bookmarkURL= \(String(describing: self.bookmark.url)), authFailureHasEditOption=\(authFailureHasEditOption), authFailureIgnoreStyle=\(authFailureIgnoreStyle), authFailureIgnoreLabel=\(authFailureIgnoreLabel), authFailureMessage=\(String(describing: authFailureMessage))") - - if isAuthFailure { - if let bookmarkURL = self.bookmark.url { - // Clone bookmark - let clonedBookmark = OCBookmark(for: bookmarkURL) - - // Carry over permission for plain HTTP connections - clonedBookmark.userInfo[OCBookmarkUserInfoKey.allowHTTPConnection] = self.bookmark.userInfo[OCBookmarkUserInfoKey.allowHTTPConnection] - - // Create connection - let connection = OCConnection(bookmark: clonedBookmark) - - if let cookieSupportEnabled = OCCore.classSetting(forOCClassSettingsKey: .coreCookieSupportEnabled) as? Bool, cookieSupportEnabled == true { - connection.cookieStorage = OCHTTPCookieStorage() - Log.debug("Created cookie storage \(String(describing: connection.cookieStorage)) for client root view auth method detection") - } - - connection.prepareForSetup(options: nil, completionHandler: { (issue, suggestedURL, supportedMethods, preferredMethods) in - Log.debug("Preparing for handling authentication error: issue=\(issue?.description ?? "nil"), suggestedURL=\(suggestedURL?.absoluteString ?? "nil"), supportedMethods: \(supportedMethods?.description ?? "nil"), preferredMethods: \(preferredMethods?.description ?? "nil"), existingAuthMethod: \(self.bookmark.authenticationMethodIdentifier?.rawValue ?? "nil"))") - - if let preferredMethods = preferredMethods, preferredMethods.count > 0 { - if let existingAuthMethod = self.bookmark.authenticationMethodIdentifier, !preferredMethods.contains(existingAuthMethod) { - // Authentication method no longer supported - self.bookmark.scanForAuthenticationMethodsRequired = true // Mark bookmark as requiring a scan for available authentication methods before editing - OCBookmarkManager.shared.updateBookmark(self.bookmark) - } - } else { - // Supported authentication methods unclear -> rescan - self.bookmark.scanForAuthenticationMethodsRequired = true // Mark bookmark as requiring a scan for available authentication methods before editing - OCBookmarkManager.shared.updateBookmark(self.bookmark) - } - - presentAlert(authFailureHasEditOption, authFailureIgnoreStyle, authFailureIgnoreLabel, authFailureMessage, preferredMethods) - }) - } - } else { - presentAlert(authFailureHasEditOption, authFailureIgnoreStyle, authFailureIgnoreLabel, authFailureMessage, nil) - } - } - - func presentAuthAlert(for editBookmark: OCBookmark, error nsError: NSError?, title authFailureTitle: String, message authFailureMessage: String?, ignoreLabel authFailureIgnoreLabel: String, ignoreStyle authFailureIgnoreStyle: UIAlertAction.Style, hasEditOption authFailureHasEditOption: Bool, preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?, completionHandler: @escaping () -> Void) { - let alertController = ThemedAlertController(title: authFailureTitle, - message: authFailureMessage, - preferredStyle: .alert) - - alertController.addAction(UIAlertAction(title: authFailureIgnoreLabel, style: authFailureIgnoreStyle, handler: { (_) in - completionHandler() - })) - - if authFailureHasEditOption { - alertController.addAction(UIAlertAction(title: "Sign in".localized, style: .default, handler: { [weak self] (_) in - completionHandler() - - var notifyAuthDelegate = true - - if let bookmark = self?.bookmark { - let updater = ClientAuthenticationUpdater(with: bookmark, preferredAuthenticationMethods: preferredAuthenticationMethods) - - if updater.canUpdateInline, let self = self { - notifyAuthDelegate = false - - updater.updateAuthenticationData(on: self, completion: { (error) in - if error == nil { - OCSynchronized(self) { - self.skipAuthorizationFailure = false // Auth failure fixed -> allow new failures to prompt for sign in again - } - } else if let nsError = error as NSError?, !nsError.isOCError(withCode: .authorizationCancelled) { - // Error updating authentication -> inform the user and provide option to retry - self.alertQueue.async { [weak self] (queueCompletionHandler) in - self?.presentAuthAlert(for: editBookmark, error: error as NSError?, title: "Error".localized, message: error?.localizedDescription, ignoreLabel: authFailureIgnoreLabel, ignoreStyle: authFailureIgnoreStyle, hasEditOption: authFailureHasEditOption, preferredAuthenticationMethods: preferredAuthenticationMethods, completionHandler: queueCompletionHandler) - } - } - }) - } - } - - if notifyAuthDelegate, let authDelegate = self?.authDelegate, let self = self, let nsError = nsError { - authDelegate.handleAuthError(for: self, error: nsError, editBookmark: editBookmark, preferredAuthenticationMethods: preferredAuthenticationMethods) - } - })) - } - - self.present(alertController, animated: true, completion: nil) - } - - func presentAlertAsCard(viewController: UIViewController, withHandle: Bool = false, dismissable: Bool = true) { - alertQueue.async { [weak self] (queueCompletionHandler) in - if let startViewController = self { - var hostViewController : UIViewController = startViewController - - while hostViewController.presentedViewController != nil, - hostViewController.presentedViewController?.isBeingDismissed == false { - hostViewController = hostViewController.presentedViewController! - } - - hostViewController.present(asCard: viewController, animated: true, withHandle: withHandle, dismissable: dismissable, completion: { - queueCompletionHandler() - }) - } else { - queueCompletionHandler() - } - } - } -} - -extension ClientRootViewController: UITabBarControllerDelegate { - func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { - if tabBarController.selectedViewController == viewController { - if let navigationController = viewController as? ThemeNavigationController { - let navigationStack = navigationController.viewControllers - - if navigationStack.count > 1 { - navigationController.popToViewController(navigationStack[1], animated: true) - return false - } - } - } - - return true - } -} diff --git a/ownCloud/Client/ClientSessionManager.swift b/ownCloud/Client/ClientSessionManager.swift index 68c17649d..ba42b4752 100644 --- a/ownCloud/Client/ClientSessionManager.swift +++ b/ownCloud/Client/ClientSessionManager.swift @@ -28,13 +28,10 @@ protocol ClientSessionManagerDelegate : AnyObject { class ClientSessionManager: NSObject { public static let shared : ClientSessionManager = { return ClientSessionManager() }() - - var clientViewControllersByBookmarkUUID : [UUID : NSHashTable] = [ : ] - var delegates : NSHashTable override init() { - delegates = NSHashTable() + delegates = NSHashTable.weakObjects() super.init() @@ -45,30 +42,6 @@ class ClientSessionManager: NSObject { NotificationCenter.default.removeObserver(self, name: .NotificationMessagePresenterShowMessage, object: nil) } - func startSession(for bookmark: OCBookmark) -> ClientRootViewController? { - let clientViewController = ClientRootViewController(bookmark: bookmark) - - if clientViewControllersByBookmarkUUID[bookmark.uuid] == nil { - OCSynchronized(self) { - self.clientViewControllersByBookmarkUUID[bookmark.uuid] = NSHashTable.weakObjects() - } - } - - guard let existingViewControllers = clientViewControllersByBookmarkUUID[bookmark.uuid] else { - return nil - } - - OCSynchronized(self) { - existingViewControllers.add(clientViewController) - } - - return clientViewController - } - - func sessions(for bookmark: OCBookmark) -> NSHashTable? { - return self.clientViewControllersByBookmarkUUID[bookmark.uuid] - } - @objc func showMessage(notification: Notification) { if let message = notification.object as? OCMessage { // Ask delegates if they can open a session to present the issue diff --git a/ownCloud/Client/FileList Extensions/ClientQueryViewController+InlineMessageSupport.swift b/ownCloud/Client/FileList Extensions/ClientQueryViewController+InlineMessageSupport.swift deleted file mode 100644 index 2cdf23104..000000000 --- a/ownCloud/Client/FileList Extensions/ClientQueryViewController+InlineMessageSupport.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ClientQueryViewController+InlineMessageSupport.swift -// ownCloud -// -// Created by Felix Schwarz on 17.07.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudSDK -import ownCloudAppShared - -extension ClientQueryViewController : InlineMessageCenter { - public func hasInlineMessage(for item: OCItem) -> Bool { - guard let activeSyncRecordIDs = item.activeSyncRecordIDs, let syncRecordIDsWithMessages = (clientRootViewController as? ClientRootViewController)?.syncRecordIDsWithMessages else { - return false - } - - return syncRecordIDsWithMessages.contains { (syncRecordID) -> Bool in - return activeSyncRecordIDs.contains(syncRecordID) - } - } - - public func showInlineMessageFor(item: OCItem) { - if let messages = (clientRootViewController as? ClientRootViewController)?.messageSelector?.selection, - let firstMatchingMessage = messages.first(where: { (message) -> Bool in - guard let syncRecordID = message.syncIssue?.syncRecordID, let containsSyncRecordID = item.activeSyncRecordIDs?.contains(syncRecordID) else { - return false - } - - return containsSyncRecordID - }) { - firstMatchingMessage.showInApp() - } - } -} diff --git a/ownCloud/Client/FileList Extensions/FileListTableViewController+OpenItemTableViewController.swift b/ownCloud/Client/FileList Extensions/FileListTableViewController+OpenItemTableViewController.swift deleted file mode 100644 index ea82b92fb..000000000 --- a/ownCloud/Client/FileList Extensions/FileListTableViewController+OpenItemTableViewController.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// FileListTableViewController+OpenItemTableViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 17.07.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudSDK -import ownCloudAppShared - -extension FileListTableViewController : OpenItemHandling { - @discardableResult public func open(item: OCItem, animated: Bool, pushViewController: Bool) -> UIViewController? { - if let core = self.core { - if let bookmarkContainer = self.tabBarController as? BookmarkContainer { - let activity = OpenItemUserActivity(detailItem: item, detailBookmark: bookmarkContainer.bookmark) - view.window?.windowScene?.userActivity = activity.openItemUserActivity - } - - switch item.type { - case .collection: - if let location = item.location { - let clientQueryViewController = ClientQueryViewController(core: core, query: OCQuery(for: location)) - 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 - } -} - -extension FileListTableViewController : MoreItemHandling { - public func moreOptions(for item: OCItem, at locationIdentifier: OCExtensionLocationIdentifier, core: OCCore, query: OCQuery?, sender: AnyObject?) -> Bool { - guard let sender = sender else { - return false - } - let actionsLocation = OCExtensionLocation(ofType: .action, identifier: locationIdentifier) - let actionContext = ActionContext(viewController: self, core: core, query: query, items: [item], location: actionsLocation, sender: sender) - - if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: makeActionProgressHandler(), completionHandler: nil) { - self.present(asCard: moreViewController, animated: true) - } - - return true - } -} diff --git a/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift b/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift deleted file mode 100644 index a6735ddf4..000000000 --- a/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// QueryFileListTableViewController+Multiselect.swift -// ownCloud -// -// Created by Felix Schwarz on 17.07.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudSDK -import ownCloudAppShared - -extension QueryFileListTableViewController : MultiSelectSupport { - - public func setupMultiselection() { - selectDeselectAllButtonItem = UIBarButtonItem(title: "Select All".localized, style: .done, target: self, action: #selector(selectAllItems)) - exitMultipleSelectionBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(exitMultiselection)) - - // Create bar button items for the toolbar - deleteMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named:"trash"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: DeleteAction.identifier!) - deleteMultipleBarButtonItem?.accessibilityLabel = "Delete".localized - deleteMultipleBarButtonItem?.isEnabled = false - - moveMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named:"folder"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: MoveAction.identifier!) - moveMultipleBarButtonItem?.accessibilityLabel = "Move".localized - moveMultipleBarButtonItem?.isEnabled = false - - duplicateMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named: "duplicate-file"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: DuplicateAction.identifier!) - duplicateMultipleBarButtonItem?.accessibilityLabel = "Duplicate".localized - duplicateMultipleBarButtonItem?.isEnabled = false - - copyMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named: "copy-file"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: CopyAction.identifier!) - copyMultipleBarButtonItem?.accessibilityLabel = "Copy".localized - copyMultipleBarButtonItem?.isEnabled = false - - cutMultipleBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "scissors"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: CutAction.identifier!) - cutMultipleBarButtonItem?.accessibilityLabel = "Cut".localized - cutMultipleBarButtonItem?.isEnabled = false - - openMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named: "open-in"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: OpenInAction.identifier!) - openMultipleBarButtonItem?.accessibilityLabel = "Open in".localized - openMultipleBarButtonItem?.isEnabled = false - } - - // MARK: - Toolbar actions handling multiple selected items - fileprivate func updateSelectDeselectAllButton() { - var selectedCount = 0 - if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { - selectedCount = selectedIndexPaths.count - } - - if selectedCount == self.items.count { - selectDeselectAllButtonItem?.title = "Deselect All".localized - selectDeselectAllButtonItem?.target = self - selectDeselectAllButtonItem?.action = #selector(deselectAllItems) - } else { - selectDeselectAllButtonItem?.title = "Select All".localized - selectDeselectAllButtonItem?.target = self - selectDeselectAllButtonItem?.action = #selector(selectAllItems) - } - self.navigationItem.titleView = nil - if selectedCount == 1 { - self.navigationItem.title = String(format: "%d Item".localized, selectedCount) - } else if selectedCount > 1 { - self.title = String(format: "%d Items".localized, selectedCount) - } else { - self.navigationItem.title = UIDevice.current.isIpad ? "Select Items".localized : "" - } - } - - fileprivate func updateActions() { - guard let tabBarController = self.tabBarController as? ClientRootViewController else { return } - - guard let toolbarItems = tabBarController.toolbar?.items else { return } - - if selectedItemIds.count > 0 { - if let context = self.actionContext { - - self.actions = Action.sortedApplicableActions(for: context) - - // Enable / disable tool-bar items depending on action availability - for item in toolbarItems { - if self.actions?.contains(where: {type(of:$0).identifier == item.actionIdentifier}) ?? false { - item.isEnabled = true - } else { - item.isEnabled = false - } - } - } - - } else { - self.actions = nil - for item in toolbarItems { - item.isEnabled = false - } - } - } - - @objc public func exitMultiselection() { - if self.tableView.isEditing { - self.tableView.setEditing(false, animated: true) - - selectedItemIds.removeAll() - removeToolbar() - sortBar?.showSelectButton = true - - self.tableView.overrideUserInterfaceStyle = .unspecified - - self.navigationItem.rightBarButtonItems = self.regularRightBarButtons - self.navigationItem.leftBarButtonItems = self.regularLeftBarButtons - - self.regularRightBarButtons = nil - self.regularLeftBarButtons = nil - - exitedMultiselection() - } - } - - @objc public func exitedMultiselection() { - // may be overriden in subclasses - } - - @objc public func updateMultiselection() { - updateSelectDeselectAllButton() - updateActions() - } - - open func populateToolbar() { - self.populateToolbar(with: [ - openMultipleBarButtonItem!, - flexibleSpaceBarButton, - moveMultipleBarButtonItem!, - flexibleSpaceBarButton, - copyMultipleBarButtonItem!, - flexibleSpaceBarButton, - cutMultipleBarButtonItem!, - flexibleSpaceBarButton, - duplicateMultipleBarButtonItem!, - flexibleSpaceBarButton, - deleteMultipleBarButtonItem!]) - } - - @objc func actOnMultipleItems(_ sender: UIButton) { - // 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 - OnMainThread { - self?.exitMultiselection() - } - } - - // Execute the action - action.perform() - } - } - - // MARK: Multiple Selection - - @objc public func enterMultiselection() { - - self.tableView.overrideUserInterfaceStyle = Theme.shared.activeCollection.interfaceStyle.userInterfaceStyle - - if !self.tableView.isEditing { - self.regularLeftBarButtons = self.navigationItem.leftBarButtonItems - self.regularRightBarButtons = self.navigationItem.rightBarButtonItems - } - - updateMultiselection() - - self.tableView.setEditing(true, animated: true) - sortBar?.showSelectButton = false - - populateToolbar() - - self.navigationItem.leftBarButtonItem = selectDeselectAllButtonItem! - self.navigationItem.rightBarButtonItems = [exitMultipleSelectionBarButtonItem!] - } - - @objc public func selectAllItems(_ sender: UIBarButtonItem) { - (0.. IndexPath in - return IndexPath(item: item, section: 0) - }.forEach { (indexPath) in - self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) - } - selectedItemIds = self.items.compactMap({$0.localID as OCLocalID?}) - self.actionContext?.replace(items: self.items) - - updateMultiselection() - } - - @objc public func deselectAllItems(_ sender: UIBarButtonItem) { - - self.tableView.indexPathsForSelectedRows?.forEach({ (indexPath) in - self.tableView.deselectRow(at: indexPath, animated: true) - }) - selectedItemIds.removeAll() - self.actionContext?.removeAllItems() - - updateMultiselection() - } -} diff --git a/ownCloud/Client/Library/Item Policies/ItemPolicyCell.swift b/ownCloud/Client/Library/Item Policies/ItemPolicyCell.swift deleted file mode 100644 index fe678ecff..000000000 --- a/ownCloud/Client/Library/Item Policies/ItemPolicyCell.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// ItemPolicyCell.swift -// ownCloud -// -// Created by Felix Schwarz on 18.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 ownCloudSDK -import ownCloudAppShared - -class ItemPolicyCell: ClientItemResolvingCell { - var iconSize : CGSize = CGSize(width: 40, height: 40) - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Item policy item - override func titleLabelString(for item: OCItem?) -> NSAttributedString { - if itemResolutionLocation?.isRoot == true { - return NSAttributedString(string: "Root folder".localized) - } - - if let item = item { - return super.titleLabelString(for: item) - } else { - return NSAttributedString(string: "\((itemResolutionLocation?.path as NSString?)?.lastPathComponent ?? "") \("(no match)".localized)") - } - } - - override func detailLabelString(for item: OCItem?) -> String { - if itemPolicy?.localID != nil, let itemPath = item?.path { - return "\("at".localized) \(itemPath)" - } else if let itemPolicyPath = itemPolicy?.location?.path as NSString?, itemPolicyPath.length > 0 { - return "\("at".localized) \(itemPolicyPath)" - } else { - return super.detailLabelString(for: item) - } - } - - var itemPolicy : OCItemPolicy? { - didSet { - if let itemPolicy = itemPolicy { - if let itemLocation = itemPolicy.location, let itemPath = itemLocation.path { - if itemPath.hasSuffix("/") { - self.iconView.activeViewProvider = ResourceItemIcon.folder - - self.itemResolutionLocation = itemLocation - } else { - self.iconView.activeViewProvider = ResourceItemIcon.file - - if let itemLocalID = itemPolicy.localID { - self.itemResolutionLocalID = itemLocalID - } else { - self.itemResolutionLocation = itemLocation - } - } - - self.iconView.alpha = 0.5 - self.isUserInteractionEnabled = false - } - } - - self.updateLabels(with: self.item) - } - } - - override func updateWith(_ item: OCItem) { - super.updateWith(item) - - self.iconView.alpha = 1.0 - self.isUserInteractionEnabled = true - } -} diff --git a/ownCloud/Client/Library/Item Policies/ItemPolicyTableViewController.swift b/ownCloud/Client/Library/Item Policies/ItemPolicyTableViewController.swift deleted file mode 100644 index 61df1d4a2..000000000 --- a/ownCloud/Client/Library/Item Policies/ItemPolicyTableViewController.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// ItemPolicyTableViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 18.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 ownCloudSDK -import ownCloudAppShared - -enum ItemPolicySectionIndex : Int { - case all - case policies -} - -class ItemPolicyTableViewController : FileListTableViewController { - - var policyKind: OCItemPolicyKind - weak var policyProcessor : OCItemPolicyProcessor? - - var messageView : MessageView? - - init(core: OCCore, policyKind: OCItemPolicyKind) { - self.policyKind = policyKind - - super.init(core: core, style: .grouped) - - policyProcessor = core.itemPolicyProcessor(forKind: policyKind) - - if let policyProcessor = policyProcessor { - self.navigationItem.title = policyProcessor.localizedName - } - - NotificationCenter.default.addObserver(self, selector: #selector(loadItemPolicies), name: .OCCoreItemPolicyProcessorUpdated, object: policyProcessor) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - NotificationCenter.default.removeObserver(self, name: .OCCoreItemPolicyProcessorUpdated, object: nil) - } - - override func viewDidLoad() { - super.viewDidLoad() - - messageView = MessageView(add: self.view) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.loadItemPolicies() - } - - @objc func loadItemPolicies() { - self.core?.retrievePolicies(ofKind: policyKind, affectingItem: nil, includeInternal: false, completionHandler: { (_, policies) in - self.itemPolicies = policies?.sorted(by: { (policy1, policy2) -> Bool in - if let path1 = policy1.location?.path, let path2 = policy2.location?.path { - return path1.compare(path2) == .orderedAscending - } - - return false - }) ?? [] - }) - } - - var itemPolicies : [OCItemPolicy] = [] { - didSet { - OnMainThread { - if self.itemPolicies.count == 0 { - self.messageView?.message(show: true, imageName: "icon-available-offline", title: "Available Offline".localized, message: "No items have been selected for offline availability.".localized) - } else { - self.messageView?.message(show: false) - } - - self.reloadTableData() - } - } - } - - override func registerCellClasses() { - self.tableView.register(ItemPolicyCell.self, forCellReuseIdentifier: "itemCell") - self.tableView.register(ThemeTableViewCell.self, forCellReuseIdentifier: "metaCell") - } - - // MARK: - Table view data source - func itemPolicyAt(_ indexPath : IndexPath) -> OCItemPolicy { - return itemPolicies[indexPath.row] - } - - override func itemAt(indexPath: IndexPath) -> OCItem? { - if let section = ItemPolicySectionIndex(rawValue: indexPath.section) { - switch section { - case .all: return nil - case .policies: return super.itemAt(indexPath: indexPath) - } - } - - return nil - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return (itemPolicies.count > 0) ? 2 : 0 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let section = ItemPolicySectionIndex(rawValue: section) { - switch section { - case .all: return 1 - case .policies: return itemPolicies.count - } - } else { - return 0 - } - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - if let section = ItemPolicySectionIndex(rawValue: section) { - switch section { - case .all: return "Overview".localized - case .policies: return "Locations".localized - } - } else { - return nil - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if let section = ItemPolicySectionIndex(rawValue: indexPath.section) { - switch section { - case .all: - let cell = tableView.dequeueReusableCell(withIdentifier: "metaCell", for: indexPath) as? ThemeTableViewCell - - cell?.textLabel?.text = "All Files".localized - cell?.imageView?.image = UIImage(named: "cloud-available-offline")?.tinted(with: Theme.shared.activeCollection.tableRowColors.labelColor)?.paddedTo(width: 60) - cell?.accessoryType = .disclosureIndicator - - return cell! - - case .policies: - let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as? ItemPolicyCell - let itemPolicy = itemPolicyAt(indexPath) - - cell?.accessibilityIdentifier = itemPolicy.location?.path ?? itemPolicy.localID - cell?.core = self.core - cell?.itemPolicy = itemPolicy - cell?.isMoreButtonPermanentlyHidden = true - - if cell?.delegate == nil { - cell?.delegate = self - } - - return cell! - } - } - - return UITableViewCell() - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let section = ItemPolicySectionIndex(rawValue: indexPath.section) { - var query : OCQuery? - var title : String? - - switch section { - case .all: - query = OCQuery(condition: .require([ - .where(.downloadTrigger, isEqualTo: OCItemDownloadTriggerID.availableOffline) - ]), inputFilter:nil) - case .policies: - if let item = self.itemAt(indexPath: indexPath) { - if item.type == .collection { - query = self.query(forItem: item) - title = item.name - } else { - super.tableView(tableView, didSelectRowAt: indexPath) - } - } - } - - if let core = core, let query = query { - let customFileListController = QueryFileListTableViewController(core: core, query: query) - customFileListController.title = title - customFileListController.pullToRefreshAction = nil - self.navigationController?.pushViewController(customFileListController, animated: true) - } - } - } - - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - if let section = ItemPolicySectionIndex(rawValue: indexPath.section), section == .policies { - return UISwipeActionsConfiguration(actions: [UIContextualAction(style: .destructive, title: "Make unavailable offline".localized, handler: { [weak self] (_, _, completionHandler) in - if let core = self?.core, let itemPolicy = self?.itemPolicyAt(indexPath) { - core.removeAvailableOfflinePolicy(itemPolicy, completionHandler: nil) - } - completionHandler(true) - })]) - } else { - return nil - } - } - - // MARK: - Theming - override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - super.applyThemeCollection(theme: theme, collection: collection, event: event) - - self.view.backgroundColor = theme.activeCollection.tableGroupBackgroundColor - } - -} diff --git a/ownCloud/Client/Library/LibrarySharesTableViewController.swift b/ownCloud/Client/Library/LibrarySharesTableViewController.swift deleted file mode 100644 index 74c43102e..000000000 --- a/ownCloud/Client/Library/LibrarySharesTableViewController.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// LibrarySharesTableViewController.swift -// ownCloud -// -// Created by Matthias Hühne on 13.05.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 ownCloudAppShared - -class LibrarySharesTableViewController: FileListTableViewController { - - var shareView : LibraryShareView? - - var shares : [OCShare] = [] { - didSet { - OnMainThread { - self.reloadTableData() - } - } - } - - override func registerCellClasses() { - self.tableView.register(ShareClientItemCell.self, forCellReuseIdentifier: "itemCell") - } - - // MARK: - Table view data source - func shareAtIndexPath(_ indexPath : IndexPath) -> OCShare { - return shares[indexPath.row] - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.shares.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as? ShareClientItemCell - let newItem = shareAtIndexPath(indexPath) - - cell?.accessibilityIdentifier = newItem.name - cell?.core = self.core - cell?.share = newItem - - if cell?.delegate == nil { - cell?.delegate = self - } - - return cell! - } -} - -extension LibrarySharesTableViewController : LibraryShareList { - func updateWith(shares: [OCShare]) { - self.shares = shares - } -} diff --git a/ownCloud/Client/Library/LibraryTableViewController.swift b/ownCloud/Client/Library/LibraryTableViewController.swift deleted file mode 100644 index a1ac0c41a..000000000 --- a/ownCloud/Client/Library/LibraryTableViewController.swift +++ /dev/null @@ -1,471 +0,0 @@ -// -// LibraryTableViewController.swift -// ownCloud -// -// Created by Matthias Hühne on 12.05.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 ownCloudAppShared - -protocol LibraryShareList: UIViewController { - func updateWith(shares: [OCShare]) -} - -struct QuickAccessQuery { - var name : String - var mimeType : [String] - var imageName : String -} - -class LibraryShareView { - enum Identifier : String { - case sharedWithYou - case sharedWithOthers - case publicLinks - case pending - } - - var identifier : Identifier - - var title : String - var image : UIImage - - var showBadge : Bool { - return identifier == .pending - } - - var viewController : LibraryShareList? - - var row : StaticTableViewRow? - - var shares : [OCShare]? - - init(identifier: LibraryShareView.Identifier, title: String, image: UIImage) { - self.identifier = identifier - - self.title = title - self.image = image - } -} - -class LibraryTableViewController: StaticTableViewController { - - weak var core : OCCore? - - deinit { - for applierToken in applierTokens { - Theme.shared.remove(applierForToken: applierToken) - } - - self.stopQueries() - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.title = "Quick Access".localized - self.navigationController?.navigationBar.prefersLargeTitles = true - self.tableView.contentInset.bottom = self.tabBarController?.tabBar.frame.height ?? 0 - - Theme.shared.add(tvgResourceFor: "icon-available-offline") - - shareSection = StaticTableViewSection(headerTitle: "Shares".localized, footerTitle: nil, identifier: "share-section") - self.addThemableBackgroundView() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.navigationBar.prefersLargeTitles = true - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - self.navigationController?.navigationBar.prefersLargeTitles = false - } - - // MARK: - Share setup - var startedQueries : [OCCoreQuery] = [] - - var shareQueryWithUser : OCShareQuery? - var shareQueryByUser : OCShareQuery? - var shareQueryAcceptedCloudShares : OCShareQuery? - var shareQueryPendingCloudShares : OCShareQuery? - private var applierTokens : [ThemeApplierToken] = [] - - private func start(query: OCCoreQuery) { - core?.start(query) - startedQueries.append(query) - } - - func reloadQueries() { - for query in startedQueries { - core?.reload(query) - } - } - - private func stopQueries() { - for query in startedQueries { - core?.stop(query) - } - startedQueries.removeAll() - } - - func setupQueries() { - // Shared with user - shareQueryWithUser = OCShareQuery(scope: .sharedWithUser, item: nil) - - if let shareQueryWithUser = shareQueryWithUser { - shareQueryWithUser.refreshInterval = 60 - - shareQueryWithUser.initialPopulationHandler = { [weak self] (_) in - self?.updateSharedWithYouResult() - self?.updatePendingSharesResult() - } - shareQueryWithUser.changesAvailableNotificationHandler = shareQueryWithUser.initialPopulationHandler - - start(query: shareQueryWithUser) - } - - // Accepted cloud shares - shareQueryAcceptedCloudShares = OCShareQuery(scope: .acceptedCloudShares, item: nil) - - if let shareQueryAcceptedCloudShares = shareQueryAcceptedCloudShares { - shareQueryAcceptedCloudShares.refreshInterval = 60 - - shareQueryAcceptedCloudShares.initialPopulationHandler = { [weak self] (_) in - self?.updateSharedWithYouResult() - self?.updatePendingSharesResult() - } - shareQueryAcceptedCloudShares.changesAvailableNotificationHandler = shareQueryAcceptedCloudShares.initialPopulationHandler - - start(query: shareQueryAcceptedCloudShares) - } - - // Pending cloud shares - shareQueryPendingCloudShares = OCShareQuery(scope: .pendingCloudShares, item: nil) - - if let shareQueryPendingCloudShares = shareQueryPendingCloudShares { - shareQueryPendingCloudShares.refreshInterval = 60 - - shareQueryPendingCloudShares.initialPopulationHandler = { [weak self] (query) in - if let library = self { - library.pendingCloudSharesCounter = query.queryResults.count - self?.updatePendingSharesResult() - } - } - shareQueryPendingCloudShares.changesAvailableNotificationHandler = shareQueryPendingCloudShares.initialPopulationHandler - - start(query: shareQueryPendingCloudShares) - } - - // Shared by user - shareQueryByUser = OCShareQuery(scope: .sharedByUser, item: nil) - - if let shareQueryByUser = shareQueryByUser { - shareQueryByUser.refreshInterval = 60 - - shareQueryByUser.initialPopulationHandler = { [weak self] (_) in - self?.updateSharedByUserResults() - } - shareQueryByUser.changesAvailableNotificationHandler = shareQueryByUser.initialPopulationHandler - - start(query: shareQueryByUser) - } - - setupViews() - setupCollectionSection() - } - - // MARK: - Share views - var viewsByIdentifier : [LibraryShareView.Identifier : LibraryShareView] = [ : ] - - func add(view: LibraryShareView) { - viewsByIdentifier[view.identifier] = view - } - - func setupViews() { - self.add(view: LibraryShareView(identifier: .sharedWithOthers, title: "Shared with others".localized, image: UIImage(named: "group")!)) - self.add(view: LibraryShareView(identifier: .sharedWithYou, title: "Shared with you".localized, image: UIImage(named: "group")!)) - self.add(view: LibraryShareView(identifier: .publicLinks, title: "Public Links".localized, image: UIImage(named: "link")!)) - self.add(view: LibraryShareView(identifier: .pending, title: "Pending Invites".localized, image: UIImage(named: "group")!)) - } - - func updateView(identifier: LibraryShareView.Identifier, with shares: [OCShare]?, badge: Int? = 0) { - if let view = viewsByIdentifier[identifier] { - let shares = shares ?? [] - - view.shares = shares - view.viewController?.updateWith(shares: shares) - - if shares.count > 0 { - if view.row == nil, let core = core { - var badgeLabel : RoundedLabel? - - if view.showBadge, let badge = badge { - badgeLabel = RoundedLabel(text: "\(badge)", style: .token) - badgeLabel?.isHidden = (badge == 0) - } - - view.row = StaticTableViewRow(rowWithAction: { [weak self, weak view] (_, _) in - guard let view = view else { return } - - var viewController : LibraryShareList? = view.viewController - - if viewController == nil { - if view.identifier == .pending { - let pendingSharesController = PendingSharesTableViewController(style: .grouped) - - pendingSharesController.title = view.title - pendingSharesController.core = core - pendingSharesController.libraryViewController = self - - viewController = pendingSharesController - } else { - let sharesFileListController = LibrarySharesTableViewController(core: core) - - sharesFileListController.title = view.title - - viewController = sharesFileListController - } - - view.viewController = viewController - } - - if let viewController = viewController { - viewController.updateWith(shares: view.shares ?? []) - - self?.navigationController?.pushViewController(viewController, animated: true) - } - }, title: view.title, image: view.image, accessoryType: .disclosureIndicator, accessoryView: badgeLabel, identifier: identifier.rawValue) - - if let row = view.row { - shareSection?.add(row: row, animated: true) - } - } else if view.showBadge, let badge = badge { - guard let accessoryView = view.row?.additionalAccessoryView as? RoundedLabel else { return } - - accessoryView.labelText = "\(badge)" - accessoryView.isHidden = (badge == 0) - } - } else { - if let row = view.row { - shareSection?.remove(rows: [row], animated: true) - view.row = nil - } - } - - self.updateShareSectionVisibility() - } - } - - // MARK: - Handle sharing updates - var pendingSharesCounter : Int = 0 { - didSet { - OnMainThread { - if self.pendingSharesCounter > 0 { - self.navigationController?.tabBarItem.badgeValue = String(self.pendingSharesCounter) - } else { - self.navigationController?.tabBarItem.badgeValue = nil - } - } - } - } - var pendingLocalSharesCounter : Int = 0 { - didSet { - pendingSharesCounter = pendingCloudSharesCounter + pendingLocalSharesCounter - } - } - var pendingCloudSharesCounter : Int = 0 { - didSet { - pendingSharesCounter = pendingCloudSharesCounter + pendingLocalSharesCounter - } - } - - func updateSharedWithYouResult() { - var shareResults : [OCShare] = [] - - if let queryResults = shareQueryWithUser?.queryResults { - shareResults.append(contentsOf: queryResults) - } - - if let queryResults = shareQueryAcceptedCloudShares?.queryResults { - shareResults.append(contentsOf: queryResults) - } - - let uniqueShares = shareResults.unique { $0.itemLocation } - - let sharedWithUserAccepted = uniqueShares.filter({ (share) -> Bool in - return ((share.type == .remote) && (share.accepted == true)) || - ((share.type != .remote) && (share.state == .accepted)) - }) - - OnMainThread { - self.updateView(identifier: .sharedWithYou, with: sharedWithUserAccepted) - } - } - - func updatePendingSharesResult() { - var shareResults : [OCShare] = [] - - if let queryResults = shareQueryWithUser?.queryResults { - shareResults.append(contentsOf: queryResults) - } - if let queryResults = shareQueryPendingCloudShares?.queryResults { - shareResults.append(contentsOf: queryResults) - } - - let sharedWithUserPending = shareResults.filter({ (share) -> Bool in - return ((share.type == .remote) && (share.accepted == false)) || - ((share.type != .remote) && (share.state != .accepted)) - }) - pendingLocalSharesCounter = sharedWithUserPending.filter({ (share) -> Bool in - return (share.type != .remote) && (share.state == .pending) - }).count - - OnMainThread { - self.updateView(identifier: .pending, with: sharedWithUserPending, badge: self.pendingSharesCounter) - } - } - - func updateSharedByUserResults() { - guard let shares = shareQueryByUser?.queryResults else { return} - - let sharedByUserLinks = shares.filter({ (share) -> Bool in - return share.type == .link - }) - - let sharedByUser = shares.filter({ (share) -> Bool in - return share.type != .link - }) - - OnMainThread { - self.updateView(identifier: .sharedWithOthers, with: sharedByUser.unique { $0.itemLocation }) - self.updateView(identifier: .publicLinks, with: sharedByUserLinks.unique { $0.itemLocation }) - } - } - - // MARK: - Sharing Section Updates - var shareSection : StaticTableViewSection? - - func updateShareSectionVisibility() { - if let shareSection = shareSection { - if shareSection.rows.count > 0 { - if !shareSection.attached { - self.insertSection(shareSection, at: 0, animated: false) - } - } else { - if shareSection.attached { - self.removeSection(shareSection, animated: false) - } - } - } - } - - // MARK: - Collection Section - func setupCollectionSection() { - if self.sectionForIdentifier("collection-section") == nil { - let section = StaticTableViewSection(headerTitle: "Collection".localized, footerTitle: nil, identifier: "collection-section") - self.addSection(section) - - addCollectionRow(to: section, title: "Recents".localized, image: UIImage(named: "recents")!, queryCreator: { - let lastWeekDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date())! - - var recentsQuery = OCQuery(condition: .require([ - .where(.lastUsed, isGreaterThan: lastWeekDate), - .where(.name, isNotEqualTo: "/") - ]), inputFilter:nil) - - if let condition = OCQueryCondition.fromSearchTerm(":1w :file") { - recentsQuery = OCQuery(condition:condition, inputFilter: nil) - } - - return recentsQuery - }, actionHandler: nil) - - addCollectionRow(to: section, title: "Favorites".localized, image: UIImage(named: "star")!, queryCreator: { - return OCQuery(condition: .where(.isFavorite, isEqualTo: true), inputFilter:nil) - }, actionHandler: { [weak self] (completion) in - self?.core?.refreshFavorites(completionHandler: { (_, _) in - completion() - }) - }) - - addCollectionRow(to: section, title: "Available Offline".localized, image: UIImage(named: "cloud-available-offline")!, queryCreator: nil, actionHandler: { [weak self] (completion) in - if let core = self?.core { - let availableOfflineListController = ItemPolicyTableViewController(core: core, policyKind: .availableOffline) - - self?.navigationController?.pushViewController(availableOfflineListController, animated: true) - } - completion() - }) - - let queries = [ - QuickAccessQuery(name: "PDF Documents".localized, mimeType: ["pdf"], imageName: "application-pdf"), - QuickAccessQuery(name: "Documents".localized, mimeType: ["doc", "application/vnd", "application/msword", "application/ms-doc", "text/rtf", "application/rtf", "application/mspowerpoint", "application/powerpoint", "application/x-mspowerpoint", "application/excel", "application/x-excel", "application/x-msexcel"], imageName: "x-office-document"), - QuickAccessQuery(name: "Text".localized, mimeType: ["text/plain"], imageName: "text"), - QuickAccessQuery(name: "Images".localized, mimeType: ["image"], imageName: "image"), - QuickAccessQuery(name: "Videos".localized, mimeType: ["video"], imageName: "video"), - QuickAccessQuery(name: "Audio".localized, mimeType: ["audio"], imageName: "audio") - ] - - for query in queries { - addCollectionRow(to: section, title: query.name, image: Theme.shared.image(for: query.imageName, size: CGSize(width: 25, height: 25))!, queryCreator: { - let conditions = query.mimeType.map { (mimeType) -> OCQueryCondition in - return .where(.mimeType, contains: mimeType) - } - - return OCQuery(condition: .any(of: conditions), inputFilter:nil) - }, actionHandler: nil) - } - } - } - - func addCollectionRow(to section: StaticTableViewSection, title: String, image: UIImage? = nil, themeImageName: String? = nil, queryCreator: (() -> OCQuery?)?, actionHandler: ((_ completion: @escaping () -> Void) -> Void)?) { - let identifier = String(format:"%@-collection-row", title) - if section.row(withIdentifier: identifier) == nil, let core = core { - let row = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in - - if let query = queryCreator?() { - let customFileListController = QueryFileListTableViewController(core: core, query: query) - customFileListController.title = title - customFileListController.pullToRefreshAction = actionHandler - self?.navigationController?.pushViewController(customFileListController, animated: true) - } - - actionHandler?({}) - }, title: title, image: image, imageTintColorKey: "secondaryLabelColor", accessoryType: .disclosureIndicator, identifier: identifier) - - if themeImageName != nil { - let themeApplierToken = Theme.shared.add(applier: { [weak row] (theme, _, _) in - if let themeImageName = themeImageName { - row?.cell?.imageView?.image = theme.image(for: themeImageName, size: CGSize(width: 25, height: 25)) - } - }, applyImmediately: true) - - applierTokens.append(themeApplierToken) - } - - section.add(row: row) - } - } - - // MARK: - Theming - override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - super.applyThemeCollection(theme: theme, collection: collection, event: event) - - self.navigationController?.view.backgroundColor = theme.activeCollection.navigationBarColors.backgroundColor - } -} diff --git a/ownCloud/Client/Sharing/PendingSharesTableViewController.swift b/ownCloud/Client/Sharing/PendingSharesTableViewController.swift deleted file mode 100644 index 9de7fb02f..000000000 --- a/ownCloud/Client/Sharing/PendingSharesTableViewController.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// PendingSharesTableViewController.swift -// ownCloud -// -// Created by Matthias Hühne on 12.05.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 ownCloudAppShared - -class PendingSharesTableViewController: StaticTableViewController { - - var shares : [OCShare]? { - didSet { - OnMainThread { - self.handleSharesUpdate() - } - } - } - weak var core : OCCore? - weak var libraryViewController : LibraryTableViewController? - var messageView : MessageView? - private static let imageWidth : CGFloat = 50 - private static let imageHeight : CGFloat = 50 - - private var didLoad : Bool = false - - let dateFormatter = DateFormatter() - - override func viewDidLoad() { - super.viewDidLoad() - - self.navigationController?.navigationBar.prefersLargeTitles = false - self.tableView.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor - - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .short - - didLoad = true - handleSharesUpdate() - } - - func handleSharesUpdate() { - guard let shares = shares, didLoad else { return } - let pendingShares = shares.filter { (share) -> Bool in - return ((share.type == .remote) && (share.accepted == false)) || // Federated share (pending) - ((share.type != .remote) && (share.state == .pending)) // Local share (pending) - } - - let rejectedShares = shares.filter { (share) -> Bool in - return ((share.type != .remote) && (share.state == .rejected)) // Local share (rejected) - } - - updateSection(for: pendingShares, title: "Pending".localized, sectionID: "pending", placeAtTop: true) - updateSection(for: rejectedShares, title: "Declined".localized, sectionID: "declined", placeAtTop: false) - - if (pendingShares.count == 0) && (rejectedShares.count == 0) && self.presentedViewController == nil { - // Pop back to the Library when there are no longer any shares to present and no alert is active - self.navigationController?.popViewController(animated: true) - } - } - - func updateSection(for shares: [OCShare], title: String, sectionID: String, placeAtTop: Bool) { - var section : StaticTableViewSection? = sectionForIdentifier(sectionID) - - if shares.count == 0 { - if let section = section { - removeSection(section, animated: true) - } - return - } - - if section == nil { - section = StaticTableViewSection(headerTitle: title, footerTitle: nil, identifier: sectionID) - } - - if let section = section { - // Clear existing rows - section.remove(rows: section.rows) - - // Create new rows - for share in shares { - var ownerName : String? - if share.itemOwner?.displayName != nil { - ownerName = share.itemOwner?.displayName - } else if share.owner?.userName != nil { - ownerName = share.owner?.userName - } - - if let displayName = ownerName { - var itemImageType = "file" - if share.itemType == .collection { - itemImageType = "folder" - } - var footer = String(format: "Shared by %@".localized, displayName) - if let date = share.creationDate { - footer = footer.appendingFormat("\n%@", dateFormatter.string(from: date)) - } - - var itemName = share.name - if let itemPath = share.itemLocation.path, itemPath.count > 0 { - itemName = (itemPath as NSString).lastPathComponent - } - - let row = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in - guard let self = self else { return } - var presentationStyle: UIAlertController.Style = .actionSheet - if UIDevice.current.isIpad { - presentationStyle = .alert - } - - let alertController = ThemedAlertController(title: String(format: "Accept Invite %@".localized, itemName ?? ""), - message: nil, - preferredStyle: presentationStyle) - alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) - - alertController.addAction(UIAlertAction(title: "Accept".localized, style: .default, handler: { [weak self] (_) in - self?.handleDecision(on: share, accept: true) - })) - - if share.state != .rejected { - alertController.addAction(UIAlertAction(title: "Decline".localized, style: .destructive, handler: { [weak self] (_) in - self?.handleDecision(on: share, accept: false) - })) - } - - self.present(alertController, animated: true, completion: nil) - }, title: itemName ?? "Share".localized, subtitle: footer, image: Theme.shared.image(for: itemImageType, size: CGSize(width: PendingSharesTableViewController.imageWidth, height: PendingSharesTableViewController.imageHeight)), identifier: "row") - - row.representedObject = share - - section.add(row: row) - - if let itemPath = share.itemLocation.path, itemPath.count > 0 { - if (share.state == .accepted) || (share.accepted == true) { - // Item should exist -> track it - if let itemTracker = core?.trackItem(at: share.itemLocation, trackingHandler: { (error, item, isInitial) in - if error == nil, isInitial { - OnMainThread { - row.cell?.imageView?.image = item?.icon(fitInSize: CGSize(width: PendingSharesTableViewController.imageWidth, height: PendingSharesTableViewController.imageHeight)) - } - } - }) { - row.representedObject = itemTracker // End tracking when the row is deallocated - } - } else { - // Item doesn't exist in our scope -> use placeholder icons - var iconName : String? - - switch share.itemType { - case .collection: - iconName = "folder" - - case .file: - iconName = "file" - } - - if let iconName = iconName { - OnMainThread { - row.cell?.imageView?.image = Theme.shared.image(for: iconName, size: CGSize(width: PendingSharesTableViewController.imageWidth, height: PendingSharesTableViewController.imageHeight)) - } - } - } - } - } - } - } - - if let section = section, !section.attached { - if placeAtTop { - insertSection(section, at: 0) - } else { - addSection(section) - } - } - } - - // MARK: - TableView Delegate - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let row = self.staticRowForIndexPath(indexPath) - guard let share = row.representedObject as? OCShare else { return nil } - - let acceptAction = UIContextualAction(style: .normal, title: "Accept".localized, handler: { [weak self] (_, _, completionHandler) in - self?.handleDecision(on: share, accept: true) - completionHandler(true) - }) - let declineAction = UIContextualAction(style: .destructive, title: "Decline".localized, handler: { [weak self] (_, _, completionHandler) in - self?.handleDecision(on: share, accept: false) - completionHandler(true) - }) - - if share.state != .rejected { - return UISwipeActionsConfiguration(actions: [acceptAction, declineAction]) - } else { - return UISwipeActionsConfiguration(actions: [acceptAction]) - } - } - - // MARK: - Decision handling - func makeDecision(on share: OCShare, accept: Bool) { - if let core = core { - core.makeDecision(on: share, accept: accept, completionHandler: { [weak self] (error) in - guard let strongSelf = self else { return } - - OnMainThread { - if error != nil { - if let shareError = error { - let alertController = ThemedAlertController(with: (accept ? "Accept Share failed".localized : "Decline Share failed".localized), message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) - strongSelf.present(alertController, animated: true) - } - } else if let libraryViewController = strongSelf.libraryViewController { - libraryViewController.reloadQueries() - } - } - }) - } - } - - func handleDecision(on share: OCShare, accept: Bool) { - if accept { - makeDecision(on: share, accept: accept) - } else { - if share.type == .remote { - var itemName = share.name - if let itemPath = share.itemLocation.path, itemPath.count > 0 { - itemName = (itemPath as NSString).lastPathComponent - } - - let alertController = ThemedAlertController(title: String(format: "Decline Invite %@".localized, itemName ?? ""), message: "Decline cannot be undone.", preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) - alertController.addAction(UIAlertAction(title: "Decline".localized, style: .destructive, handler: { [weak self] (_) in - self?.makeDecision(on: share, accept: accept) - })) - self.present(alertController, animated: true, completion: nil) - } else { - makeDecision(on: share, accept: accept) - } - } - } -} - -extension PendingSharesTableViewController : LibraryShareList { - func updateWith(shares: [OCShare]) { - self.shares = shares - } -} diff --git a/ownCloud/Client/Viewer/DisplayExtension.swift b/ownCloud/Client/Viewer/DisplayExtension.swift index b5f4b9479..4a434b442 100644 --- a/ownCloud/Client/Viewer/DisplayExtension.swift +++ b/ownCloud/Client/Viewer/DisplayExtension.swift @@ -45,7 +45,9 @@ extension DisplayExtension { } let displayExtension = OCExtension(identifier: rawIdentifier, type: .viewer, locations: locationIdentifiers, features: features, objectProvider: { (_ rawExtension, _ context, _ error) -> Any? in - return Self() + let displayViewController = Self() + displayViewController.clientContext = (context as? DisplayExtensionContext)?.clientContext + return displayViewController }, customMatcher:customMatcher) return displayExtension diff --git a/ownCloud/Client/Viewer/DisplayHostViewController.swift b/ownCloud/Client/Viewer/DisplayHostViewController.swift index e83e38a6f..c958f382a 100644 --- a/ownCloud/Client/Viewer/DisplayHostViewController.swift +++ b/ownCloud/Client/Viewer/DisplayHostViewController.swift @@ -20,8 +20,11 @@ import UIKit import ownCloudSDK import ownCloudAppShared -class DisplayHostViewController: UIPageViewController { +class DisplayExtensionContext: OCExtensionContext { + public var clientContext: ClientContext? +} +class DisplayHostViewController: UIPageViewController { enum PagePosition { case before, after } @@ -30,7 +33,7 @@ class DisplayHostViewController: UIPageViewController { let mediaFilterRegexp: String = "\\A(((image|audio|video)/*))" // Filters all the mime types that are images (incluiding gif and svg) // MARK: - Instance Variables - weak var core: OCCore? + public var clientContext : ClientContext? private var initialItem: OCItem @@ -45,44 +48,57 @@ class DisplayHostViewController: UIPageViewController { private var playableItems: [OCItem]? - private var query: OCQuery - private var queryStarted : Bool = false - private var queryObservation : NSKeyValueObservation? + private var parentFolderQuery: OCQuery? - var progressSummarizer : ProgressSummarizer? + private var queryDatasource: OCDataSource? + private var queryDatasourceSubscription: OCDataSourceSubscription? - public var clientContext : ClientContext? + var progressSummarizer : ProgressSummarizer? // MARK: - Init & deinit - init(clientContext: ClientContext? = nil, core: OCCore, selectedItem: OCItem, query: OCQuery) { - self.core = core - self.initialItem = selectedItem - self.query = query + init(clientContext inClientContext: ClientContext? = nil, core: OCCore? = nil, selectedItem: OCItem, queryDataSource inQueryDataSource: OCDataSource? = nil) { + var clientContext = inClientContext - super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + initialItem = selectedItem + queryDatasource = inQueryDataSource ?? clientContext?.queryDatasource - self.clientContext = clientContext + if queryDatasource == nil, let parentLocation = selectedItem.location?.parent, let core = clientContext?.core { + // If no data source was given, create one for the parent location + let query = OCQuery(for: parentLocation) + core.start(query) - if query.state == .stopped { - self.core?.start(query) - queryStarted = true + parentFolderQuery = query + queryDatasource = parentFolderQuery?.queryResultsDataSource + + clientContext = ClientContext(with: inClientContext) + clientContext?.queryDatasource = queryDatasource } - queryObservation = query.observe(\OCQuery.hasChangesAvailable, options: [.initial, .new]) { [weak self] (query, _) in - //guard self?.items == nil else { return } + super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + + self.clientContext = ClientContext(with: clientContext, originatingViewController: self) - query.requestChangeSet(withFlags: .onlyResults) { ( _, changeSet) in - guard let changeSet = changeSet else { return } - if let queryResult = changeSet.queryResult, let newItems = self?.applyMediaFilesFilter(items: queryResult) { - let shallUpdateDatasource = self?.items?.count != newItems.count ? true : false + if let queryDatasource { + queryDatasourceSubscription = queryDatasource.subscribe(updateHandler: { [weak self] subscription in + guard let self = self, let queryDataSource = self.queryDatasource else { + return + } - self?.items = newItems + let snapshot = subscription.snapshotResettingChangeTracking(true) + var allItems : [OCItem] = [] - if shallUpdateDatasource { - self?.updateDatasource() + for itemRef in snapshot.items { + if let itemRecord = try? queryDataSource.record(forItemRef: itemRef) { + if let item = itemRecord.item as? OCItem { + allItems.append(item) + } } } - } + + self.items = self.applyMediaFilesFilter(items: allItems) + + self.updatePageViewControllerDatasource() + }, on: .main, trackDifferences: true, performInitialUpdate: true) } Theme.shared.register(client: self) @@ -92,22 +108,24 @@ class DisplayHostViewController: UIPageViewController { fatalError("init(coder:) has not been implemented") } - deinit { + private func windDown() { NotificationCenter.default.removeObserver(self, name: MediaDisplayViewController.MediaPlaybackFinishedNotification, object: nil) NotificationCenter.default.removeObserver(self, name: MediaDisplayViewController.MediaPlaybackNextTrackNotification, object: nil) NotificationCenter.default.removeObserver(self, name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) - queryObservation?.invalidate() - queryObservation = nil + queryDatasourceSubscription?.terminate() - if queryStarted { - core?.stop(query) - queryStarted = false + if let parentFolderQuery { + clientContext?.core?.stop(parentFolderQuery) } Theme.shared.unregister(client: self) } + deinit { + windDown() + } + // MARK: - ViewController lifecycle override func viewDidLoad() { super.viewDidLoad() @@ -180,7 +198,7 @@ class DisplayHostViewController: UIPageViewController { } // MARK: - Helper methods - private func updateDatasource() { + private func updatePageViewControllerDatasource() { OnMainThread { [weak self] in self?.dataSource = nil if let itemCount = self?.items?.count { @@ -252,7 +270,8 @@ class DisplayHostViewController: UIPageViewController { private func createDisplayViewController(for mimeType: String) -> (DisplayViewController) { let locationIdentifier = OCExtensionLocationIdentifier(rawValue: mimeType) let location: OCExtensionLocation = OCExtensionLocation(ofType: .viewer, identifier: locationIdentifier) - let context = OCExtensionContext(location: location, requirements: nil, preferences: nil) + let context = DisplayExtensionContext(location: location, requirements: nil, preferences: nil) + context.clientContext = clientContext var extensions: [OCExtensionMatch]? @@ -286,7 +305,7 @@ class DisplayHostViewController: UIPageViewController { let newViewController = createDisplayViewController(for: mimeType) newViewController.progressSummarizer = progressSummarizer - newViewController.core = core + newViewController.core = clientContext?.core newViewController.itemIndex = index newViewController.item = item diff --git a/ownCloud/Client/Viewer/DisplayViewController.swift b/ownCloud/Client/Viewer/DisplayViewController.swift index 810e2b53c..392843579 100644 --- a/ownCloud/Client/Viewer/DisplayViewController.swift +++ b/ownCloud/Client/Viewer/DisplayViewController.swift @@ -110,6 +110,8 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { private var query : OCQuery? private var itemClaimIdentifier : UUID? + var clientContext: ClientContext? + func generateClaim(for item: OCItem) -> OCClaim? { if let core = core { return core.generateTemporaryClaim(for: .view) @@ -182,6 +184,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { private var cancelButton = ThemeButton(type: .custom) private var metadataInfoLabel = UILabel() private var showPreviewButton = ThemeButton(type: .custom) + private var primaryUnviewableActionButton = ThemeButton(type: .custom) private var infoLabel = UILabel() private var connectionActivityView = UIActivityIndicatorView(style: .medium) @@ -221,6 +224,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { PointerEffect.install(on: cancelButton, effectStyle: .highlight) PointerEffect.install(on: showPreviewButton, effectStyle: .highlight) + PointerEffect.install(on: primaryUnviewableActionButton, effectStyle: .highlight) iconImageView.translatesAutoresizingMaskIntoConstraints = false @@ -240,15 +244,22 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { cancelButton.translatesAutoresizingMaskIntoConstraints = false cancelButton.setTitle("Cancel".localized, for: .normal) - cancelButton.addTarget(self, action: #selector(cancelDownload(sender:)), for: UIControl.Event.touchUpInside) + cancelButton.addTarget(self, action: #selector(cancelDownload(sender:)), for: .primaryActionTriggered) view.addSubview(cancelButton) showPreviewButton.translatesAutoresizingMaskIntoConstraints = false showPreviewButton.setTitle("Open file".localized, for: .normal) - showPreviewButton.addTarget(self, action: #selector(downloadItem), for: UIControl.Event.touchUpInside) + showPreviewButton.addTarget(self, action: #selector(downloadItem), for: .primaryActionTriggered) view.addSubview(showPreviewButton) + let title = primaryUnviewableAction?.actionExtension.name ?? "" + + primaryUnviewableActionButton.translatesAutoresizingMaskIntoConstraints = false + primaryUnviewableActionButton.setTitle(title, for: .normal) + primaryUnviewableActionButton.addTarget(self, action: #selector(primaryUnviewableActionPressed), for: .primaryActionTriggered) + view.addSubview(primaryUnviewableActionButton) + infoLabel.translatesAutoresizingMaskIntoConstraints = false infoLabel.adjustsFontForContentSizeCategory = true infoLabel.textAlignment = .center @@ -280,6 +291,9 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { showPreviewButton.centerXAnchor.constraint(equalTo: iconImageView.centerXAnchor), showPreviewButton.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: verticalSpacing), + primaryUnviewableActionButton.centerXAnchor.constraint(equalTo: iconImageView.centerXAnchor), + primaryUnviewableActionButton.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: verticalSpacing), + infoLabel.centerXAnchor.constraint(equalTo: metadataInfoLabel.centerXAnchor), infoLabel.topAnchor.constraint(equalTo: metadataInfoLabel.bottomAnchor, constant: verticalSpacing), infoLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: horizontalSpacing), @@ -348,19 +362,40 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { } } - @objc func optionsBarButtonPressed(_ sender: UIBarButtonItem) { - guard let core = core, let item = item else { + @objc func primaryUnviewableActionPressed(sender: Any? = nil) { + primaryUnviewableAction?.run() + } + + @objc func actionsBarButtonPressed(_ sender: UIBarButtonItem) { + guard let core = core ?? clientContext?.core, let item = item else { return } let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreDetailItem) - let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation, sender: sender) + let actionContext = ActionContext(viewController: self, clientContext: clientContext, 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) } } + var hasPrimaryUnviewableAction : Bool { + return primaryUnviewableAction != nil + } + + var primaryUnviewableAction : Action? { + if let item = item, let core = core { + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .unviewableFileType) + let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation, sender: nil) + + let actions = Action.sortedApplicableActions(for: actionContext) + + return actions.first + } + + return nil + } + // MARK: - Update control enum UpdateStrategy { case ask @@ -383,7 +418,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { switch updateStrategy { case .ask: OnMainThread { - let alert = UIAlertController(title: String(format: "%@ was updated".localized, item.name ?? "File".localized), message: "Would you like to view the updated version?".localized, preferredStyle: .alert) + let alert = ThemedAlertController(title: NSString(format: "%@ was updated".localized as NSString, item.name ?? "File".localized) as String, message: "Would you like to view the updated version?".localized, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Show new version".localized, style: .default, handler: { [weak self] (_) in self?.updateStrategy = .ask @@ -454,7 +489,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { var actionBarButtonItem : UIBarButtonItem { let itemName = item?.name ?? "" - let actionsBarButtonItem = UIBarButtonItem(image: UIImage(named: "more-dots"), style: .plain, target: self, action: #selector(optionsBarButtonPressed)) + let actionsBarButtonItem = UIBarButtonItem(image: UIImage(named: "more-dots"), style: .plain, target: self, action: #selector(actionsBarButtonPressed)) actionsBarButtonItem.tag = moreButtonTag actionsBarButtonItem.accessibilityLabel = itemName + " " + "Actions".localized @@ -489,16 +524,20 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { case .initial: hideProgressIndicators() showPreviewButton.isHidden = true + primaryUnviewableActionButton.isHidden = true case .online: connectionActivityView.stopAnimating() hideProgressIndicators() showPreviewButton.isHidden = true + primaryUnviewableActionButton.isHidden = true if let item = self.item, !canPreviewCurrentItem { if self.core?.localCopy(of:item) == nil { showPreviewButton.isHidden = false showPreviewButton.setTitle("Download".localized, for: .normal) + } else { + primaryUnviewableActionButton.isHidden = !hasPrimaryUnviewableAction } } @@ -512,6 +551,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { progressView.isHidden = true cancelButton.isHidden = true showPreviewButton.isHidden = true + primaryUnviewableActionButton.isHidden = true infoLabel.isHidden = false infoLabel.text = "Network unavailable".localized @@ -525,11 +565,13 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { cancelButton.isHidden = false infoLabel.isHidden = true showPreviewButton.isHidden = true + primaryUnviewableActionButton.isHidden = true case .downloadFinished: cancelButton.isHidden = true progressView.isHidden = true showPreviewButton.isHidden = true + primaryUnviewableActionButton.isHidden = true if canPreviewCurrentItem { iconImageView.isHidden = true @@ -537,6 +579,8 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { metadataInfoLabel.isHidden = true infoLabel.isHidden = true cancelButton.isHidden = true + } else { + primaryUnviewableActionButton.isHidden = !hasPrimaryUnviewableAction } case .previewFailed: @@ -693,6 +737,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { cancelButton.applyThemeCollection(collection) metadataInfoLabel.applyThemeCollection(collection) showPreviewButton.applyThemeCollection(collection) + primaryUnviewableActionButton.applyThemeCollection(collection) infoLabel.applyThemeCollection(collection) } diff --git a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift index 645bd9062..0d648e43d 100644 --- a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift +++ b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift @@ -24,6 +24,16 @@ import ownCloudAppShared import CoreServices import UniformTypeIdentifiers +extension AVPlayer { + var isAudioAvailable: Bool? { + return self.currentItem?.asset.tracks.filter({$0.mediaType == .audio}).count != 0 + } + + var isVideoAvailable: Bool? { + return self.currentItem?.asset.tracks.filter({$0.mediaType == .video}).count != 0 + } +} + class MediaDisplayViewController : DisplayViewController { static let MediaPlaybackFinishedNotification = NSNotification.Name("media_playback.finished") @@ -64,6 +74,23 @@ class MediaDisplayViewController : DisplayViewController { override func viewDidLoad() { super.viewDidLoad() + playerViewController = AVPlayerViewController() + + guard let playerViewController = playerViewController else { return } + + addChild(playerViewController) + self.view.addSubview(playerViewController.view) + playerViewController.didMove(toParent: self) + + playerViewController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + playerViewController.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), + playerViewController.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor), + playerViewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + playerViewController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) + ]) + NotificationCenter.default.addObserver(self, selector: #selector(handleDidEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleWillEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAVPlayerItem(notification:)), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil) @@ -122,32 +149,18 @@ class MediaDisplayViewController : DisplayViewController { if player == nil { player = AVPlayer(playerItem: playerItem) player?.allowsExternalPlayback = true - playerViewController = AVPlayerViewController() if let playerViewController = self.playerViewController { playerViewController.updatesNowPlayingInfoCenter = false if UIApplication.shared.applicationState == .active { playerViewController.player = player } - - addChild(playerViewController) - self.view.addSubview(playerViewController.view) - playerViewController.didMove(toParent: self) - - playerViewController.view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - playerViewController.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), - playerViewController.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor), - playerViewController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - playerViewController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) - ]) } // 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 !(player?.isVideoAvailable ?? false), let artworkMetadataItem = asset.commonMetadata.filter({$0.commonKey == AVMetadataKey.commonKeyArtwork}).first, + let imageData = artworkMetadataItem.dataValue, + let overlayView = playerViewController?.contentOverlayView { if let artworkImage = UIImage(data: imageData) { diff --git a/ownCloud/Import/ImportFilesController.swift b/ownCloud/Import/ImportFilesController.swift index 251310264..4151239ea 100644 --- a/ownCloud/Import/ImportFilesController.swift +++ b/ownCloud/Import/ImportFilesController.swift @@ -7,14 +7,14 @@ // /* -* 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 . -* -*/ + * 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 @@ -47,29 +47,6 @@ class ImportFilesController: NSObject { var isVisible: Bool = false var fileCoordinator : NSFileCoordinator? - var header : MoreViewHeader? - - // MARK: - Init & Deinit - - override init() { - //self.url = url - //importFiles?.append(url) - //fileIsLocalCopy = copyBeforeUsing - } - /* - init(url: URL, copyBeforeUsing: Bool) { - self.url = url - //importFiles?.append(url) - fileIsLocalCopy = copyBeforeUsing - }*/ - - deinit { - //removeLocalCopy() - } - -} - -extension ImportFilesController { public func importAllowed(alertUserOtherwise: Bool) -> Bool { let importAllowed = Branding.shared.isImportMethodAllowed(.openWith) @@ -104,53 +81,112 @@ extension ImportFilesController { } } - func makeLocalCopy(of itemURL: URL, completion: (_ error: Error?) -> Void) { - if let appGroupURL = OCAppIdentity.shared.appGroupContainerURL { - let fileManager = FileManager.default + // MARK: - User Interface + var locationPicker: ClientLocationPicker? - var inboxURL = appGroupURL.appendingPathComponent("File-Import") - if !fileManager.fileExists(atPath: inboxURL.path) { - do { - try fileManager.createDirectory(at: inboxURL, withIntermediateDirectories: false, attributes: [ .protectionKey : FileProtectionType.completeUntilFirstUserAuthentication]) - } catch let error as NSError { - Log.debug("Error creating directory \(inboxURL) \(error.localizedDescription)") + func showAccountUI() { + let fileNames = importFiles.map { (importFile) -> String in + return importFile.url.lastPathComponent + } - completion(error) - return - } - } + let headerTitle = importFiles.count == 1 ? + "Import \"{{itemName}}\"".localized(["itemName" : "\(importFiles.first!.url.lastPathComponent)"]) : + "Import {{itemCount}} files".localized(["itemCount" : "\(importFiles.count)"]) + let headerSubTitle = "Select target.".localized /* importFiles.count == 1 ? + "Select target.".localized : + fileNames.joined(separator: ", ") */ - let uuid = UUID().uuidString - inboxURL = inboxURL.appendingPathComponent(uuid) - if !fileManager.fileExists(atPath: inboxURL.path) { - do { - try fileManager.createDirectory(at: inboxURL, withIntermediateDirectories: false, attributes: [ .protectionKey : FileProtectionType.completeUntilFirstUserAuthentication]) - } catch let error as NSError { - Log.debug("Error creating directory \(inboxURL) \(error.localizedDescription)") + if !isVisible { + // Create new picker + isVisible = true - completion(error) - return + OnMainThread { + self.locationPicker = ClientLocationPicker(location: .accounts, selectButtonTitle: "Save here".localized, headerTitle: headerTitle, headerSubTitle: headerSubTitle, avoidConflictsWith: nil, choiceHandler: { [weak self] (chosenItem, location, _, cancelled) in + self?.handlePickerDecision(parentItem: chosenItem, location: location, cancelled: cancelled) + }) + + if let window = UserInterfaceContext.shared.currentWindow { + let viewController = window.rootViewController + var presentationViewController: UIViewController? = viewController + + if let navigationController = viewController as? UINavigationController, let viewController = navigationController.visibleViewController { + presentationViewController = viewController + } + + self.locationPicker?.present(in: ClientContext(originatingViewController: presentationViewController)) } } - self.localCopyContainerURL = inboxURL + } else { + // Update existing picker + locationPicker?.headerTitle = headerTitle + locationPicker?.headerSubTitle = headerSubTitle + } + } - inboxURL = inboxURL.appendingPathComponent(itemURL.lastPathComponent) - do { - try fileManager.copyItem(at: itemURL, to: inboxURL) - //self.url = inboxURL - self.localCopyURL = inboxURL - //self.fileIsLocalCopy = true - } catch let error as NSError { - Log.debug("Error copying file \(inboxURL) \(error.localizedDescription)") + func handlePickerDecision(parentItem: OCItem?, location: OCLocation?, cancelled: Bool) { + isVisible = false + locationPicker = nil - completion(error) - return + if cancelled { + // Remove all local copies of files queued for import + for importFile in importFiles { + removeLocalCopy(importFile: importFile) } + + importFiles = [] } - completion(nil) + if !cancelled, let targetDirectory = parentItem, let bookmarkUUID = location?.bookmarkUUID, let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { + // Schedule uploads + let waitGroup = DispatchGroup() + + OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { core, error in + OnMainThread { + for importFile in self.importFiles { + let name = importFile.url.lastPathComponent + + waitGroup.enter() + if core?.importItemNamed(name, + at: targetDirectory, + from: importFile.url, + isSecurityScoped: false, + options: [OCCoreOption.importByCopying : true, + OCCoreOption.automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue], + placeholderCompletionHandler: { (error, item) in + if error != nil { + Log.debug("Error uploading \(Log.mask(name)) to \(Log.mask(targetDirectory.path)), error: \(error?.localizedDescription ?? "" )") + } + + waitGroup.leave() + }, + resultHandler: { (error, _ core, _ item, _) in + if error != nil { + Log.debug("Error uploading \(Log.mask(name)) to \(Log.mask(targetDirectory.path)), error: \(error?.localizedDescription ?? "" )") + } else { + Log.debug("Success uploading \(Log.mask(name)) to \(Log.mask(targetDirectory.path))") + + self.removeLocalCopy(importFile: importFile) + } + } + ) == nil { + Log.debug("Error setting up upload of \(Log.mask(name)) to \(Log.mask(targetDirectory.path))") + } + } + } + + waitGroup.notify(queue: .main, execute: { + OnBackgroundQueue(after: 2) { + // Return OCCore after 2 seconds, giving the core a chance to schedule the uploads with a NSURLSession + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { + + }) + } + }) + }) + } } + // MARK: - File import func prepareInputFileForImport(file: ImportFile, completion: @escaping (_ error: Error?) -> Void) { let securityScopedURL = file.url var isAccessingSecurityScopedResource = false @@ -177,136 +213,51 @@ extension ImportFilesController { }) } - func showAccountUI() { - if !isVisible { - let bookmarks : [OCBookmark] = OCBookmarkManager.shared.bookmarks as [OCBookmark] - if bookmarks.count > 0, let url = importFiles.first?.url { - let moreViewController = self.cardViewController(for: self.localCopyURL ?? url) - if let window = UserInterfaceContext.shared.currentWindow, let moreViewController = moreViewController { - let viewController = window.rootViewController - if let navigationController = viewController as? UINavigationController, let viewController = navigationController.visibleViewController { - OnMainThread { - self.isVisible = true - viewController.present(asCard: moreViewController, animated: true) - } - } else { - OnMainThread { - self.isVisible = true - viewController?.present(asCard: moreViewController, animated: true) - } - } - } - } - } else { - - let fileNames = importFiles.map { (importFile) -> String in - return importFile.url.lastPathComponent - } - - header?.updateHeader(title: "\(importFiles.count) Files", subtitle: fileNames.joined(separator: ", ")) - } - } - - func importItemsWithDirectoryPicker(into bookmark: OCBookmark) { - OCCoreManager.shared.requestCore(for: bookmark, setup: { (_, _) in - }, completionHandler: { (core, error) in - if let core = core, error == nil { - OnMainThread { [weak core] in - let waitGroup = DispatchGroup() - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core!, location: OCLocation.legacyRoot, selectButtonTitle: "Save here".localized, avoidConflictsWith: [], choiceHandler: { (selectedDirectory, _) in - if let targetDirectory = selectedDirectory { - for importFile in self.importFiles { - let name = importFile.url.lastPathComponent - - waitGroup.enter() - if core?.importItemNamed(name, - at: targetDirectory, - from: importFile.url, - isSecurityScoped: false, - options: [OCCoreOption.importByCopying : true, - OCCoreOption.automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue], - placeholderCompletionHandler: { (error, item) in - if error != nil { - Log.debug("Error uploading \(Log.mask(name)) to \(Log.mask(targetDirectory.path)), error: \(error?.localizedDescription ?? "" )") - } - - waitGroup.leave() - }, - resultHandler: { (error, _ core, _ item, _) in - if error != nil { - Log.debug("Error uploading \(Log.mask(name)) to \(Log.mask(targetDirectory.path)), error: \(error?.localizedDescription ?? "" )") - } else { - Log.debug("Success uploading \(Log.mask(name)) to \(Log.mask(targetDirectory.path))") - - self.removeLocalCopy(importFile: importFile) - } - } - ) == nil { - Log.debug("Error setting up upload of \(Log.mask(name)) to \(Log.mask(targetDirectory.path))") - } - } - - waitGroup.notify(queue: .main, execute: { - OnBackgroundQueue(after: 2) { - // Return OCCore after 2 seconds, giving the core a chance to schedule the uploads with a NSURLSession - OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { - - }) - } - }) - self.isVisible = false - } - }) + func makeLocalCopy(of itemURL: URL, completion: (_ error: Error?) -> Void) { + if let appGroupURL = OCAppIdentity.shared.appGroupContainerURL { + let fileManager = FileManager.default - let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) - pickerNavigationController.modalPresentationStyle = .formSheet + var inboxURL = appGroupURL.appendingPathComponent("File-Import") + if !fileManager.fileExists(atPath: inboxURL.path) { + do { + try fileManager.createDirectory(at: inboxURL, withIntermediateDirectories: false, attributes: [ .protectionKey : FileProtectionType.completeUntilFirstUserAuthentication]) + } catch let error as NSError { + Log.debug("Error creating directory \(inboxURL) \(error.localizedDescription)") - if let window = UserInterfaceContext.shared.currentWindow { - let viewController = window.rootViewController - if let navCon = viewController as? UINavigationController, let viewController = navCon.visibleViewController { - viewController.present(pickerNavigationController, animated: true) - } else { - viewController?.present(pickerNavigationController, animated: true) - } - } + completion(error) + return } } - }) - } - - func cardViewController(for url: URL) -> FrameViewController? { - let tableViewController = MoreStaticTableViewController(style: .grouped) - header = MoreViewHeader(url: url) - guard let header = header else { return nil } - - let moreViewController = FrameViewController(header: header, viewController: tableViewController) - let title = NSAttributedString(string: "Save File".localized, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .heavy)]) + let uuid = UUID().uuidString + inboxURL = inboxURL.appendingPathComponent(uuid) + if !fileManager.fileExists(atPath: inboxURL.path) { + do { + try fileManager.createDirectory(at: inboxURL, withIntermediateDirectories: false, attributes: [ .protectionKey : FileProtectionType.completeUntilFirstUserAuthentication]) + } catch let error as NSError { + Log.debug("Error creating directory \(inboxURL) \(error.localizedDescription)") - var actionsRows: [StaticTableViewRow] = [] - let bookmarks : [OCBookmark] = OCBookmarkManager.shared.bookmarks as [OCBookmark] + completion(error) + return + } + } + self.localCopyContainerURL = inboxURL - let rowDescription = StaticTableViewRow(label: "Choose an account and folder to import the file into.\n\nOnly one file can be imported at once.".localized, alignment: .center) - actionsRows.append(rowDescription) + inboxURL = inboxURL.appendingPathComponent(itemURL.lastPathComponent) + do { + try fileManager.copyItem(at: itemURL, to: inboxURL) + //self.url = inboxURL + self.localCopyURL = inboxURL + //self.fileIsLocalCopy = true + } catch let error as NSError { + Log.debug("Error copying file \(inboxURL) \(error.localizedDescription)") - for (bookmark) in bookmarks { - let row = StaticTableViewRow(buttonWithAction: { (_ row, _ sender) in - moreViewController.dismiss(animated: true, completion: { - self.importItemsWithDirectoryPicker(into: bookmark) - }) - }, title: bookmark.shortName, style: .plain, image: Theme.shared.image(for: "owncloud-logo", size: CGSize(width: 25, height: 25)), imageWidth: 25, alignment: .left) - actionsRows.append(row) + completion(error) + return + } } - let row = StaticTableViewRow(buttonWithAction: { (_ row, _ sender) in - self.isVisible = false - moreViewController.dismiss(animated: true, completion: nil) - }, title: "Cancel".localized, style: .destructive, alignment: .center) - actionsRows.append(row) - - tableViewController.addSection(MoreStaticTableViewSection(headerAttributedTitle: title, identifier: "actions-section", rows: actionsRows)) - - return moreViewController + completion(nil) } func removeLocalCopy(importFile: ImportFile) { @@ -332,6 +283,7 @@ extension ImportFilesController { importFiles.remove(object: importFile) } + // MARK: - Cleanup on startup class func removeImportDirectory() { if let appGroupURL = OCAppIdentity.shared.appGroupContainerURL { let fileManager = FileManager.default diff --git a/ownCloud/Licensing/Offers/LicenseOfferView.swift b/ownCloud/Licensing/Offers/LicenseOfferView.swift index b5ad9de7c..7ee3ef03d 100644 --- a/ownCloud/Licensing/Offers/LicenseOfferView.swift +++ b/ownCloud/Licensing/Offers/LicenseOfferView.swift @@ -240,7 +240,7 @@ class LicenseOfferView: UIView, Themeable { OnMainThread { guard let self = self else { return } - let alertController = UIAlertController(title: "Purchase failed".localized, message: error.localizedDescription, preferredStyle: .alert) + let alertController = ThemedAlertController(title: "Purchase failed".localized, message: error.localizedDescription, preferredStyle: .alert) let nsError = error as NSError if nsError.domain == OCLicenseAppStoreProviderErrorDomain, nsError.code == OCLicenseAppStoreProviderError.purchasesNotAllowedForVPPCopies.rawValue { diff --git a/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift b/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift index 6bc5a8ad6..8a7ae1e39 100644 --- a/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift +++ b/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift @@ -21,12 +21,13 @@ import UIKit import ownCloudApp import ownCloudAppShared +import StoreKit class LicenseTransactionsViewController: StaticTableViewController { init() { super.init(style: .grouped) - self.navigationItem.title = "Purchases".localized + self.navigationItem.title = "Purchases & Subscriptions".localized self.toolbarItems = [ UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), @@ -40,7 +41,7 @@ class LicenseTransactionsViewController: StaticTableViewController { func fetchTransactions() { OCLicenseManager.shared.retrieveAllTransactions(completionHandler: { (error, transactionsByProvider) in if let error = error { - let alert = UIAlertController(title: "Error fetching transactions".localized, message: error.localizedDescription, preferredStyle: .alert) + let alert = ThemedAlertController(title: "Error fetching transactions".localized, message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) @@ -90,7 +91,23 @@ class LicenseTransactionsViewController: StaticTableViewController { if let links = transaction.links { for (title, url) in links { - section.add(row: StaticTableViewRow(rowWithAction: { (_, _) in + section.add(row: StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + #if !targetEnvironment(macCatalyst) + if url == OCLicenseAppStoreProvider.appStoreManagementURL, let windowScene = self?.view.window?.windowScene, !ProcessInfo.processInfo.isiOSAppOnMac { + Task { + do { + try await AppStore.showManageSubscriptions(in: windowScene) + } catch { + Log.error("Error \(error) showing subscription manager") + + // Fallback to URL + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + return + } + #endif + UIApplication.shared.open(url, options: [:], completionHandler: nil) }, title: title, alignment: .center)) } diff --git a/ownCloud/Messages/CardIssueMessagePresenter.swift b/ownCloud/Messages/CardIssueMessagePresenter.swift index 5a555de7f..d08ca893d 100644 --- a/ownCloud/Messages/CardIssueMessagePresenter.swift +++ b/ownCloud/Messages/CardIssueMessagePresenter.swift @@ -18,6 +18,7 @@ import UIKit import ownCloudSDK +import ownCloudAppShared class CardIssueMessagePresenter: OCMessagePresenter { diff --git a/ownCloud/Migration/LegacyCredentials.swift b/ownCloud/Migration/LegacyCredentials.swift deleted file mode 100644 index e38a6aa57..000000000 --- a/ownCloud/Migration/LegacyCredentials.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// LegacyCredentials.swift -// ownCloud -// -// Created by Michael Neuwert on 24.03.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* -* Copyright (C) 2020, ownCloud GmbH. -* -* This code is covered by the GNU Public License Version 3. -* -* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ -* You should have received a copy of this license along with this program. If not, see . -* -*/ - -import Foundation - -@objc(OCCredentialsDto) -class OCCredentialsDto : NSObject, NSCoding { - - enum AuthenticationMethod : Int, RawRepresentable { - case unknown = 0 - case none = 1 - case basicHttpAuth = 2 - case bearerToken = 3 - case samlWebSSO = 4 - } - - var userId: String? - var baseURL: String? - var userName: String? - var accessToken: String? - var authenticationMethod: AuthenticationMethod = .unknown - - //optionals credentials used with oauth2 - var refreshToken: String? - var expiresIn: String? - var tokenType: String? - - var userDisplayName: String? - - func encode(with coder: NSCoder) { - coder.encode(self.userId, forKey: "userId") - coder.encode(self.baseURL, forKey: "baseURL") - coder.encode(self.userName, forKey: "userName") - coder.encode(self.accessToken, forKey: "accessToken") - coder.encode(self.refreshToken, forKey: "refreshToken") - coder.encode(self.expiresIn, forKey: "expiresIn") - coder.encode(self.tokenType, forKey: "tokenType") - coder.encode(self.userDisplayName, forKey: "userDisplayName") - coder.encode(self.authenticationMethod, forKey: "authenticationMethod") - } - - required init?(coder: NSCoder) { - self.userId = coder.decodeObject(forKey: "userId") as? String - self.baseURL = coder.decodeObject(forKey: "baseURL") as? String - self.userName = coder.decodeObject(forKey: "userName") as? String - self.accessToken = coder.decodeObject(forKey: "accessToken") as? String - self.refreshToken = coder.decodeObject(forKey: "refreshToken") as? String - if let expiresIn = coder.decodeObject(forKey: "expiresIn") as? Int { - self.expiresIn = String(expiresIn) - } - self.tokenType = coder.decodeObject(forKey: "tokenType") as? String - self.userDisplayName = coder.decodeObject(forKey: "userDisplayName") as? String - - let authMethodValue = coder.decodeInteger(forKey: "authenticationMethod") - if let authMethod = AuthenticationMethod(rawValue: authMethodValue) { - self.authenticationMethod = authMethod - } - } - - func oauth2Data() -> Data? { - - var serializedData : Data? - - // Generate OCBookmark compatible authentication data blob, but since we can't reliably get information on when - // OAuth2 token was generated we force it's refresh by declaring access token as expired - // - // See oC SDK class OCAuthenticationMethodOAuth2 for further reference - // - if self.authenticationMethod == .bearerToken, - let accessToken = self.accessToken, - let refreshToken = self.refreshToken, - let tokenType = self.tokenType, - let user = self.userDisplayName ?? self.userName, - let expiresIn = self.expiresIn { - let tokenResponseDict : [String : Any] = [ - "access_token" : accessToken, - "refresh_token" : refreshToken, - "expires_in" : expiresIn, - "token_type" : tokenType, - "user_id" : user - ] - - let authenticationDataDict : [String : Any] = [ - "expirationDate" : Date.distantPast, - "bearerString" : "Bearer \(accessToken)", - "tokenResponse" : tokenResponseDict - ] - - serializedData = try? PropertyListSerialization.data(fromPropertyList: authenticationDataDict, format: .binary, options: 0) - } - - return serializedData - } -} diff --git a/ownCloud/Migration/Migration.swift b/ownCloud/Migration/Migration.swift deleted file mode 100644 index 355e1bce6..000000000 --- a/ownCloud/Migration/Migration.swift +++ /dev/null @@ -1,601 +0,0 @@ -// -// Migration.swift -// ownCloud -// -// Created by Michael Neuwert on 03.03.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* -* Copyright (C) 2020, ownCloud GmbH. -* -* This code is covered by the GNU Public License Version 3. -* -* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ -* You should have received a copy of this license along with this program. If not, see . -* -*/ - -import Foundation -import ownCloudSDK -import ownCloudApp -import ownCloudAppShared -import Photos - -class MigrationActivity { - - enum State { - case initiated, finished, failed - } - - enum ActivityType { - case account, settings, passcode - } - - var title: String? - var description: String? - var state: State = .initiated - var type: ActivityType = .account -} - -struct Entitlements : Codable { - - var appGroups : [String]? - var keychainAccessGroups : [String]? - - enum CodingKeys: String, CodingKey { - case appGroups = "com.apple.security.application-groups" - case keychainAccessGroups = "keychain-access-groups" - } -} - -class Migration { - - static let ActivityUpdateNotification = NSNotification.Name(rawValue: "MigrationActivityUpdateNotification") - static let FinishedNotification = NSNotification.Name(rawValue: "MigrationFinishedNotification") - - private static let legacyDbFilename = "DB.sqlite" - private static let legacyCacheFolder = "cache_folder" - private static let legacyInstantUploadFolder = "/InstantUpload" - - static let shared = Migration() - - private lazy var appGroupId : String? = { - // Try to read entitlements - if let entitlements = readAppEntitlements() { - return entitlements.appGroups?.first - } - - // Try to construct group id from bundle identifier - if let bundleId = Bundle.main.bundleIdentifier { - return "group." + bundleId - } - - return nil - }() - - private lazy var legacyDataDirectoryURL: URL? = { - if let groupId = self.appGroupId { - let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) - return containerURL?.appendingPathComponent(Migration.legacyCacheFolder) - } - return nil - }() - - // MARK: - Public API - - var legacyDataFound : Bool { - var isDirectory : ObjCBool = false - if let directoryPath = self.legacyDataDirectoryURL?.path { - let pathExists = FileManager.default.fileExists(atPath: directoryPath, isDirectory: &isDirectory) - return (pathExists && isDirectory.boolValue == true) - } - return false - } - - private let migrationQueue = DispatchQueue(label: "com.owncloud.migration-queue") - - func migrateAccountsAndSettings(_ parentViewController:UIViewController? = nil) { - - guard let legacyDbURL = self.legacyDataDirectoryURL?.appendingPathComponent(Migration.legacyDbFilename) else { return } - - if FileManager.default.fileExists(atPath: legacyDbURL.path) { - let db = OCSQLiteDB(url: legacyDbURL) - db.open(with: .readOnly) { (_, err) in - if err == nil { - Log.debug(tagged: ["MIGRATION"], "Legacy database successfully opened") - let queryResultHandler : OCSQLiteDBResultHandler = { (db, error, transaction, resultSet) in - - if error != nil { - Log.error(tagged: ["MIGRATION"], "Failed to fetch users table from legacy database with error: \(String(describing: error))") - } - - if let resultSet = resultSet { - var error : NSError? - - resultSet.iterate({ (_, _, rowDict, _) in - self.migrationQueue.async { - if let userId = rowDict["id"] as? Int, - let serverURL = rowDict["url"] as? String { - - Log.debug(tagged: ["MIGRATION"], "Migrating account data for user id \(userId)") - - let bookmark = OCBookmark() - bookmark.url = URL(string: serverURL) - let connection = OCConnection(bookmark: bookmark) - - if let cookieSupportEnabled = OCCore.classSetting(forOCClassSettingsKey: .coreCookieSupportEnabled) as? Bool, cookieSupportEnabled == true { - connection.cookieStorage = OCHTTPCookieStorage() - Log.debug("Created cookie storage \(String(describing: connection.cookieStorage)) for migration") - } - - if let userCredentials = self.getCredentialsDataItem(for: userId) { - let bookmarkActivity = "\(userCredentials.userDisplayName ?? userCredentials.userName ?? "")@\(bookmark.url?.absoluteString ?? "")" - - self.postAccountMigrationNotification(activity: bookmarkActivity, type: .account) - - if let authMethods = self.setup(connection: connection, parentViewController: parentViewController) { - - // Generate authorization data - self.authorize(bookmark: bookmark, - using: connection, - credentials: userCredentials, - supportedAuthMethods: authMethods, - parentViewController: parentViewController) - - // Delete old auth data from the keychain - self.removeCredentials(for: userId) - - // Save the bookmark - OCBookmarkManager.shared.addBookmark(bookmark) - - // For the active account, migrate instant upload settings - if let activeAccount = rowDict["activeaccount"] as? Int, activeAccount == 1 { - self.migrationQueue.async { - self.migrateInstantUploadSettings(for: bookmark, userId: userId, accountDictionary: rowDict) - } - } - - Log.debug(tagged: ["MIGRATION"], "Bookmark successfully added") - - self.postAccountMigrationNotification(activity: bookmarkActivity, state: .finished, type: .account) - - } else { - self.postAccountMigrationNotification(activity: bookmarkActivity, state: .failed, type: .account) - } - } else { - Log.debug(tagged: ["MIGRATION"], "No credentials found for user id \(userId)") - } - } - } - - }, error: &error) - } - } - - if let usersQuery = OCSQLiteQuery(selectingColumns: nil, fromTable: "users", where: nil, orderBy: nil, limit: nil, resultHandler: queryResultHandler) { - db.execute(usersQuery) - } - - self.migrationQueue.async { - // Check if the passcode is set - let passcodeQuery = OCSQLiteQuery(selectingColumns: ["passcode", "is_touch_id"], fromTable: "passcode", where: nil, orderBy: "id DESC", limit: "1") { (_, _, _, resultSet) in - if let dict = try? resultSet?.nextRowDictionary(), let passcode = dict["passcode"] as? String { - - let activityName = "App Passcode".localized - self.postAccountMigrationNotification(activity: activityName, state: .initiated, type: .passcode) - - Log.debug(tagged: ["MIGRATION"], "Migrating passcode lock") - - if passcode.count == 4 && passcode.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil { - AppLockManager.shared.passcode = passcode - AppLockSettings.shared.lockEnabled = true - - if let biometricalIdEnabled = dict["is_touch_id"] as? Bool { - AppLockSettings.shared.biometricalSecurityEnabled = biometricalIdEnabled - } - - self.postAccountMigrationNotification(activity: activityName, state: .finished, type: .passcode) - } else { - self.postAccountMigrationNotification(activity: activityName, state: .failed, type: .passcode) - Log.error(tagged: ["MIGRATION"], "Passcode is invalid") - } - } - } - if let query = passcodeQuery { - db.execute(query) - } - } - - self.migrationQueue.async { - DispatchQueue.main.async { - NotificationCenter.default.post(name: Migration.FinishedNotification, object: nil) - } - } - - } else { - Log.error(tagged: ["MIGRATION"], "Couldn't open legacy database, error \(String(describing: err))") - - DispatchQueue.main.async { - let alertController = ThemedAlertController(with: "Failed to access legacy user data".localized, message: err!.localizedDescription, action: {() in - NotificationCenter.default.post(name: Migration.FinishedNotification, object: nil) - }) - - parentViewController?.present(alertController, animated: true) - } - } - } - } - } - - // MARK: - Private helpers - - private func setup(connection:OCConnection, parentViewController:UIViewController? = nil) -> [OCAuthenticationMethodIdentifier]? { - let connectGroup = DispatchGroup() - var supportedAuthMethods: [OCAuthenticationMethodIdentifier]? - - func connectAndAuthorize() { - Log.debug(tagged: ["MIGRATION"], "Preparing for setup") - - connectGroup.enter() - connection.prepareForSetup(options: nil) { (issue, _, supportedAuthenticationMethods, _) in - supportedAuthMethods = supportedAuthenticationMethods - - Log.debug(tagged: ["MIGRATION"], "Prepared setting up \(connection) with issue \(String(describing: issue))") - - if supportedAuthMethods != nil { - connectGroup.leave() - return - } - - guard let issue = issue else { - connectGroup.leave() - return - } - - guard issue.level != .error else { - Log.error(tagged: ["MIGRATION"], "Issue raised during connection setup: \(issue)") - connectGroup.leave() - return - } - - let displayIssues = issue.prepareForDisplay() - - guard displayIssues.isAtLeast(level: .warning) else { - connectGroup.leave() - return - } - - if let parentViewController = parentViewController { - // Present issues if the level is >= warning - DispatchQueue.main.async { - IssuesCardViewController.present(on: parentViewController, issue: issue, displayIssues: displayIssues, completion: { (response) in - Log.debug(tagged: ["MIGRATION"], "User responded to the issue with \(response)") - - switch response { - case .cancel: - issue.reject() - case .approve: - issue.approve() - connectAndAuthorize() - case .dismiss: - break - } - - connectGroup.leave() - }) - - Log.debug(tagged: ["MIGRATION"], "Presenting the issue to the user") - } - } else { - Log.debug(tagged: ["MIGRATION"], "Can't present issue view controller, rejecting the issue") - issue.reject() - connectGroup.leave() - } - } - } - - connectAndAuthorize() - - connectGroup.wait() - - Log.debug(tagged: ["MIGRATION"], "Finished setting up connection \(connection)") - - return supportedAuthMethods - } - - private func authorize(bookmark:OCBookmark, - using connection:OCConnection, - credentials:OCCredentialsDto, - supportedAuthMethods:[OCAuthenticationMethodIdentifier], - parentViewController:UIViewController? = nil) { - - var authMethod = OCAuthenticationMethodIdentifier.basicAuth - var options : [OCAuthenticationMethodKey : Any] = [:] - - var unsupportedAuthMethod = false - - switch credentials.authenticationMethod { - case .basicHttpAuth: - // If server supports OAuth2, switch basic auth user to this more secure method - if supportedAuthMethods.contains(OCAuthenticationMethodIdentifier.oAuth2) { - Log.debug(tagged: ["MIGRATION"], "Converting basic auth to OAuth2") - authMethod = OCAuthenticationMethodIdentifier.oAuth2 - } - - case .bearerToken: - if supportedAuthMethods.contains(OCAuthenticationMethodIdentifier.oAuth2) { - authMethod = OCAuthenticationMethodIdentifier.oAuth2 - // Migrate OAuth2 data if possible. Note that the below method forces token expiration and subsequent refresh - if let authData = credentials.oauth2Data() { - Log.debug(tagged: ["MIGRATION"], "OAuth2 data found, adding it to the bookmark") - bookmark.authenticationData = authData - } - } else { - unsupportedAuthMethod = true - } - - case .samlWebSSO: - // If server supports OAuth2, switch basic auth user to this more secure method - if supportedAuthMethods.contains(OCAuthenticationMethodIdentifier.oAuth2) { - Log.debug(tagged: ["MIGRATION"], "Converting SAML SSO to OAuth2") - authMethod = OCAuthenticationMethodIdentifier.oAuth2 - } else { - unsupportedAuthMethod = true - } - default: - break - } - - bookmark.authenticationMethodIdentifier = authMethod - - if unsupportedAuthMethod == false { - // In case we use OAuth2 and we had already required auth data, finalize account migration - if bookmark.authenticationData == nil { - // For basic auth, set userName and password - if authMethod == OCAuthenticationMethodIdentifier.basicAuth { - options[.usernameKey] = credentials.userName - options[.passphraseKey] = credentials.accessToken - } - - if authMethod == OCAuthenticationMethodIdentifier.oAuth2 { - // Set parent view controller to display a webview with auth server UI - options[.presentingViewControllerKey] = parentViewController - } - - Log.debug(tagged: ["MIGRATION"], "Generating authentication data with options: \(options)") - let semaphore = DispatchSemaphore(value: 0) - connection.generateAuthenticationData(withMethod: authMethod, options: options) { (error, authMethodIdentifier, authMethodData) in - if error == nil { - Log.debug(tagged: ["MIGRATION"], "Auth data generated") - bookmark.authenticationMethodIdentifier = authMethodIdentifier - bookmark.authenticationData = authMethodData - } else { - Log.error(tagged: ["MIGRATION"], "Failed to generate authentication data") - } - semaphore.signal() - } - semaphore.wait() - } - } else { - Log.warning(tagged: ["MIGRATION"], "Can't convert auth data for the account since auth method not supported by the server") - } - } - - private func postAccountMigrationNotification(activity:String, state:MigrationActivity.State = .initiated, type:MigrationActivity.ActivityType = .account) { - DispatchQueue.main.async { - - let migrationActivity = MigrationActivity() - migrationActivity.title = activity - migrationActivity.state = state - migrationActivity.type = type - - switch state { - case .initiated: - migrationActivity.description = "Migrating".localized - case .finished: - migrationActivity.description = "Migrated".localized - case .failed: - migrationActivity.description = "Failed to migrate".localized - } - NotificationCenter.default.post(name: Migration.ActivityUpdateNotification, object: migrationActivity) - } - } - - private func migrateInstantUploadSettings(for bookmark:OCBookmark, userId:Int, accountDictionary: [String : Any]) { - // In the legacy app only active account could have been used for instant photo / video upload - guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } - - guard let legacyInstantPhotoUploadActive = accountDictionary["image_instant_upload"] as? Int else { return } - - guard let legacyInstantVideoUploadActive = accountDictionary["video_instant_upload"] as? Int else { return } - - guard PHPhotoLibrary.authorizationStatus() == .authorized else { return } - - // If one of the instant upload options is active, check and request if needed photo library permissions - if legacyInstantPhotoUploadActive > 0 || legacyInstantVideoUploadActive > 0 { - - Log.debug(tagged: ["MIGRATION"], "Migrating instant media upload settings") - - let activityName = "Instant Upload Settings".localized - - postAccountMigrationNotification(activity: activityName, type: .settings) - - func setupInstantUpload() { - Log.debug(tagged: ["MIGRATION"], "Setting up instant upload") - - userDefaults.instantPhotoUploadPath = Migration.legacyInstantUploadFolder - userDefaults.instantVideoUploadPath = Migration.legacyInstantUploadFolder - userDefaults.instantPhotoUploadBookmarkUUID = bookmark.uuid - userDefaults.instantVideoUploadBookmarkUUID = bookmark.uuid - - userDefaults.instantUploadPhotos = legacyInstantPhotoUploadActive > 0 ? true : false - userDefaults.instantUploadVideos = legacyInstantVideoUploadActive > 0 ? true : false - - if let legacyLastPhotoUploadTimeInterval = accountDictionary["timestamp_last_instant_upload_image"] as? TimeInterval, legacyLastPhotoUploadTimeInterval > 0 { - let timestamp = Date(timeIntervalSince1970: legacyLastPhotoUploadTimeInterval) - userDefaults.instantUploadPhotosAfter = timestamp - } - - if let legacyLastVideoUploadTimeInterval = accountDictionary["timestamp_last_instant_upload_video"] as? TimeInterval, legacyLastVideoUploadTimeInterval > 0 { - let timestamp = Date(timeIntervalSince1970: legacyLastVideoUploadTimeInterval) - userDefaults.instantUploadVideosAfter = timestamp - } - - self.postAccountMigrationNotification(activity: activityName, state: .finished, type: .settings) - } - - // In the legacy app the instant upload folder was hardcoded to \InstantUpload - // So, check if the directory is present and create it if it is absent - var uploadFolderAvailable = false - let trackItemGroup = DispatchGroup() - Log.debug(tagged: ["MIGRATION"], "Check if the root folder is accessible") - - // Track root folder - trackItemGroup.enter() - OCItemTracker(for: bookmark, at: .legacyRootPath("/")) { (error, core, rootItem) in - defer { - trackItemGroup.leave() - } - guard let rootItem = rootItem else { return } - - // Track InstantUpload subfolder - trackItemGroup.enter() - OCItemTracker(for: bookmark, at: .legacyRootPath(Migration.legacyInstantUploadFolder)) { (error, core, item) in - defer { - trackItemGroup.leave() - } - - guard error == nil else { return } - - // Check if the upload folder does exist eventually - guard item == nil else { - Log.debug(tagged: ["MIGRATION"], "Instant upload folder already exists") - uploadFolderAvailable = true - return - } - - Log.debug(tagged: ["MIGRATION"], "Creating folder for instant upload") - - // Create upload subfolder - trackItemGroup.enter() - core?.createFolder(Migration.legacyInstantUploadFolder, inside: rootItem, options: nil, placeholderCompletionHandler: { (error, _) in - uploadFolderAvailable = (error == nil) - trackItemGroup.leave() - }) - } - } - - trackItemGroup.wait() - - if uploadFolderAvailable == true { - setupInstantUpload() - } else { - Log.error(tagged: ["MIGRATION"], "Folder \(Migration.legacyInstantUploadFolder) was not found and couldn't be created") - self.postAccountMigrationNotification(activity: activityName, state: .failed, type: .settings) - } - } - - } - - func wipeLegacyData() { - var isDirectory: ObjCBool = false - if let legacyDataPath = self.legacyDataDirectoryURL?.path { - if FileManager.default.fileExists(atPath: legacyDataPath, isDirectory: &isDirectory) { - do { - try FileManager.default.removeItem(atPath: legacyDataPath) - Log.debug(tagged: ["MIGRATION"], "Removed legacy cache and database") - } catch { - Log.error(tagged: ["MIGRATION"], "Failed to remove legacy data (database, cached files)") - } - } - } - } - - private func getCredentialsDataItem(for userId:Int) -> OCCredentialsDto? { - - guard let groupId = self.appGroupId else { return nil } - guard let bundleSeed = self.bundleSeedID() else { return nil } - - let fullGroupId = "\(bundleSeed).\(groupId)" - - let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecAttrAccessGroup as String: fullGroupId, - kSecAttrAccount as String: "\(userId)", - kSecReturnData as String : true, - kSecReturnAttributes as String : true] - - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) - if status == errSecSuccess { - guard let existingItem = item as? [String : Any], - let credentialsData = existingItem[kSecValueData as String] as? Data - else { - Log.error(tagged: ["MIGRATION"], "Failed to fetch credentials for user id \(userId) from the keychain") - return nil - } - - let credentials = NSKeyedUnarchiver.unarchiveObject(with: credentialsData) as? OCCredentialsDto - - return credentials - } - - return nil - } - - private func removeCredentials(for userId:Int) { - guard let groupId = self.appGroupId else { return } - guard let bundleSeed = self.bundleSeedID() else { return } - - let fullGroupId = "\(bundleSeed).\(groupId)" - - let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecAttrAccessGroup as String: fullGroupId, - kSecAttrAccount as String: "\(userId)"] - let status = SecItemDelete(query as CFDictionary) - if status != errSecSuccess { - Log.error(tagged: ["MIGRATION"], "Failed to delete credentials for user id \(userId) from keychain") - } - } - - private func readAppEntitlements() -> Entitlements? { - guard let productName = Bundle.main.infoDictionary?[String(kCFBundleNameKey)] as? String else { return nil } - guard let entitlementsPath = Bundle.main.path(forResource: productName, ofType: "entitlements") else { return nil } - guard let plistData = FileManager.default.contents(atPath: entitlementsPath) else { return nil} - - var entitlements : Entitlements? - let decoder = PropertyListDecoder() - entitlements = try? decoder.decode(Entitlements.self, from: plistData) - - return entitlements - } - - private func bundleSeedID() -> String? { - let query: [String: AnyObject] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: "bundleSeedID" as AnyObject, - kSecAttrService as String: "" as AnyObject, - kSecReturnAttributes as String: kCFBooleanTrue - ] - - var result : AnyObject? - var status = withUnsafeMutablePointer(to: &result) { - SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) - } - - if status == errSecItemNotFound { - status = withUnsafeMutablePointer(to: &result) { - SecItemAdd(query as CFDictionary, UnsafeMutablePointer($0)) - } - } - - if status == noErr { - if let resultDict = result as? [String: Any], let accessGroup = resultDict[kSecAttrAccessGroup as String] as? String { - let components = accessGroup.components(separatedBy: ".") - return components.first - } - } - - return nil - } -} diff --git a/ownCloud/Migration/MigrationActivityCell.swift b/ownCloud/Migration/MigrationActivityCell.swift deleted file mode 100644 index e183cf751..000000000 --- a/ownCloud/Migration/MigrationActivityCell.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// MigrationActivityCell.swift -// ownCloud -// -// Created by Michael Neuwert on 31.03.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudAppShared - -class MigrationActivityCell: ThemeTableViewCell { - - static let verticalMargin: CGFloat = 20.0 - static let horizontalMargin: CGFloat = 10.0 - static let horizontalSpace: CGFloat = 15.0 - static let verticalSpace: CGFloat = 5.0 - - static let identifier = "migration-activity-cell" - - var activity : MigrationActivity? { - didSet { - if let activity = self.activity { - titleLabel.text = activity.title - descriptionLabel.text = activity.description - - switch activity.state { - case .initiated: - activityView.startAnimating() - case .finished: - self.statusImageView.isHidden = false - self.statusImageView.image = UIImage(named: "checkmark_circle")?.tinted(with: UIColor.systemGreen) - activityView.stopAnimating() - case .failed: - self.statusImageView.isHidden = false - self.statusImageView.image = UIImage(named: "multiply_circle")?.tinted(with: UIColor.systemRed) - activityView.stopAnimating() - } - - switch activity.type { - case .account: - self.activityTypeImageView.image = UIImage(named: "person_circle")?.withRenderingMode(.alwaysTemplate) - case .settings: - self.activityTypeImageView.image = UIImage(named: "gear")?.withRenderingMode(.alwaysTemplate) - case .passcode: - self.activityTypeImageView.image = UIImage(named: "lock_shield")?.withRenderingMode(.alwaysTemplate) - } - } - } - } - - var titleLabel = UILabel() - var descriptionLabel = UILabel() - var activityView = UIActivityIndicatorView(style: .medium) - var statusImageView = UIImageView() - var activityTypeImageView = UIImageView() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - prepareViewAndConstraints() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - func prepareViewAndConstraints() { - - self.accessoryType = .none - - activityView.hidesWhenStopped = true - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false - activityView.translatesAutoresizingMaskIntoConstraints = false - - statusImageView.translatesAutoresizingMaskIntoConstraints = false - statusImageView.isHidden = true - statusImageView.contentMode = .scaleAspectFit - - activityTypeImageView.translatesAutoresizingMaskIntoConstraints = false - activityTypeImageView.contentMode = .scaleAspectFit - - titleLabel.font = UIFont.preferredFont(forTextStyle: .callout) - titleLabel.adjustsFontForContentSizeCategory = true - titleLabel.lineBreakMode = .byTruncatingMiddle - - descriptionLabel.font = UIFont.preferredFont(forTextStyle: .footnote) - descriptionLabel.adjustsFontForContentSizeCategory = true - - self.contentView.addSubview(titleLabel) - self.contentView.addSubview(descriptionLabel) - self.contentView.addSubview(activityView) - self.contentView.addSubview(statusImageView) - self.contentView.addSubview(activityTypeImageView) - - NSLayoutConstraint.activate([ - - activityTypeImageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), - activityTypeImageView.widthAnchor.constraint(equalToConstant: 35.0), - activityTypeImageView.heightAnchor.constraint(equalTo: activityTypeImageView.widthAnchor), - activityTypeImageView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: MigrationActivityCell.horizontalMargin), - - titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: MigrationActivityCell.verticalMargin), - titleLabel.bottomAnchor.constraint(equalTo: descriptionLabel.topAnchor, constant: -MigrationActivityCell.verticalSpace), - descriptionLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -MigrationActivityCell.verticalMargin), - - titleLabel.leftAnchor.constraint(equalTo: self.activityTypeImageView.rightAnchor, constant: MigrationActivityCell.horizontalSpace), - descriptionLabel.leftAnchor.constraint(equalTo: self.activityTypeImageView.rightAnchor, constant: MigrationActivityCell.horizontalSpace), - - titleLabel.rightAnchor.constraint(equalTo: activityView.leftAnchor, constant: -MigrationActivityCell.horizontalSpace), - descriptionLabel.rightAnchor.constraint(equalTo: activityView.leftAnchor, constant: -MigrationActivityCell.horizontalSpace), - - activityView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), - activityView.widthAnchor.constraint(equalToConstant: 30.0), - activityView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -MigrationActivityCell.horizontalMargin), - - statusImageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), - statusImageView.widthAnchor.constraint(equalToConstant: 20.0), - statusImageView.heightAnchor.constraint(equalTo: statusImageView.widthAnchor), - statusImageView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -MigrationActivityCell.horizontalMargin) - ]) - } - - // MARK: - Themeing - - override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection) { - let itemState = ThemeItemState(selected: self.isSelected) - - self.titleLabel.applyThemeCollection(collection, itemStyle: .title, itemState: itemState) - self.descriptionLabel.applyThemeCollection(collection, itemStyle: .message, itemState: itemState) - activityTypeImageView.tintColor = collection.tableRowColors.symbolColor - } -} diff --git a/ownCloud/Migration/MigrationViewController.swift b/ownCloud/Migration/MigrationViewController.swift deleted file mode 100644 index 37e78e911..000000000 --- a/ownCloud/Migration/MigrationViewController.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// MigrationViewController.swift -// ownCloud -// -// Created by Michael Neuwert on 31.03.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2020, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudApp -import ownCloudAppShared - -class MigrationViewController: UITableViewController, Themeable { - - var activities = [MigrationActivity]() - - var migrationFinishedHandler: (() -> Void)? - - var doneBarButtonItem: UIBarButtonItem? - var headerLabel = UILabel() - - deinit { - NotificationCenter.default.removeObserver(self, name: Migration.ActivityUpdateNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: Migration.FinishedNotification, object: nil) - Theme.shared.unregister(client: self) - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.title = "Account Migration".localized - - self.tableView.register(MigrationActivityCell.self, forCellReuseIdentifier: MigrationActivityCell.identifier) - self.tableView.rowHeight = UITableView.automaticDimension - self.tableView.estimatedRowHeight = 80.0 - self.tableView.allowsSelection = false - - doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done, target: self, action: #selector(finishMigration)) - self.navigationItem.rightBarButtonItem = doneBarButtonItem - doneBarButtonItem?.isEnabled = false - - Theme.shared.register(client: self, applyImmediately: true) - - NotificationCenter.default.addObserver(self, selector: #selector(handleActivityNotification), name: Migration.ActivityUpdateNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleMigrationFinishedNotification), name: Migration.FinishedNotification, object: nil) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - Migration.shared.migrateAccountsAndSettings(self) - } - - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.tableView.applyThemeCollection(collection) - self.tableView.separatorColor = self.tableView.backgroundColor - headerLabel.applyThemeCollection(collection, itemStyle: .welcomeMessage) - } - - // MARK: - User Actions - - @IBAction func finishMigration() { - self.migrationFinishedHandler?() - self.dismiss(animated: true) - } - - // MARK: - Table view data source - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return activities.count - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let rootView = UIView() - - let backgroundImageView = UIImageView() - backgroundImageView.translatesAutoresizingMaskIntoConstraints = false - rootView.addSubview(backgroundImageView) - - let headerLogoView = UIImageView() - headerLogoView.translatesAutoresizingMaskIntoConstraints = false - rootView.addSubview(headerLogoView) - - headerLabel.translatesAutoresizingMaskIntoConstraints = false - rootView.addSubview(headerLabel) - - NSLayoutConstraint.activate([ - // Background image view - backgroundImageView.topAnchor.constraint(equalTo: rootView.topAnchor), - backgroundImageView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor), - backgroundImageView.leftAnchor.constraint(equalTo: rootView.leftAnchor), - backgroundImageView.rightAnchor.constraint(equalTo: rootView.rightAnchor), - - // Logo size - headerLogoView.leftAnchor.constraint(equalTo: rootView.leftAnchor), - headerLogoView.rightAnchor.constraint(equalTo: rootView.rightAnchor), - headerLogoView.heightAnchor.constraint(equalTo: rootView.heightAnchor, multiplier: 0.5, constant: 0), - headerLogoView.topAnchor.constraint(equalTo: rootView.topAnchor, constant: 15), - - // Header Label - headerLabel.leftAnchor.constraint(equalTo: rootView.leftAnchor, constant: 15), - headerLabel.rightAnchor.constraint(equalTo: rootView.rightAnchor, constant: -15), - headerLabel.topAnchor.constraint(equalTo: headerLogoView.bottomAnchor, constant: 10), - headerLabel.bottomAnchor.constraint(equalTo: rootView.bottomAnchor, constant: -15) - ]) - - if let organizationLogoImage = Branding.shared.brandedImageNamed(.splashscreenLogo) { - headerLogoView.image = organizationLogoImage - headerLogoView.contentMode = .scaleAspectFit - } - - if let organizationBackgroundImage = Branding.shared.brandedImageNamed(.splashscreenBackground) { - backgroundImageView.image = organizationBackgroundImage - } - - headerLabel.text = "The app was upgraded to a new version. Below there is an overview of all migrated accounts.".localized - headerLabel.textAlignment = .center - headerLabel.numberOfLines = 0 - - return rootView - } - - open override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return self.view.frame.height * 0.35 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: MigrationActivityCell.identifier, for: indexPath) as? MigrationActivityCell else { - return UITableViewCell() - } - - if indexPath.row < activities.count { - cell.activity = self.activities[indexPath.row] - } - - return cell - } - - @objc func handleActivityNotification(_ notification: Notification) { - if let updatedActivity = notification.object as? MigrationActivity { - - let index = self.activities.firstIndex { (activity) -> Bool in - if activity.title == updatedActivity.title { - return true - } - return false - } - - if index != nil { - self.activities[index!] = updatedActivity - self.tableView.reloadRows(at: [IndexPath(row: index!, section: 0)], with: .automatic) - } else { - self.activities.append(updatedActivity) - self.tableView.insertRows(at: [IndexPath(row: self.activities.count - 1, section: 0)], with: .automatic) - } - } - } - - @objc func handleMigrationFinishedNotification(_ notification: Notification) { - self.doneBarButtonItem?.isEnabled = true - } -} diff --git a/ownCloud/Release Notes/ReleaseNotes.plist b/ownCloud/Release Notes/ReleaseNotes.plist index 1b56de0f7..9f7637acf 100644 --- a/ownCloud/Release Notes/ReleaseNotes.plist +++ b/ownCloud/Release Notes/ReleaseNotes.plist @@ -1613,6 +1613,80 @@ Added an optional "Wait for completion" option to the "Save File& + + Version + 11.10.1 + ReleaseNotes + + + Title + Available Offline Folders + Subtitle + Shows the contents of the available folders when offline. + Type + Fix + ImageName + wrench + + + + + Version + 11.11.0 + ReleaseNotes + + + Title + New Dark Mode Themes + Subtitle + Two new dark mode themes are available. + Type + New + ImageName + paintbrush + + + Title + iOS 16: Markup Mode + Subtitle + Markup mode was not enabled automatically on iOS 16. + Type + Fix + ImageName + wrench + + + Title + iOS 16: Video Player + Subtitle + Video player controls were not showing on iOS 16. + Type + Fix + ImageName + wrench + + + Title + Video Player + Subtitle + Metadata image could overlay the video player canvas. + Type + Fix + ImageName + wrench + + + Title + Passcode Interval + Subtitle + The passcode lock interval was not taken into use in the share extension. + Type + Fix + ImageName + wrench + + + diff --git a/ownCloud/Release Notes/ReleaseNotesHostViewController.swift b/ownCloud/Release Notes/ReleaseNotesHostViewController.swift index 26b22bed9..e5bebf09d 100644 --- a/ownCloud/Release Notes/ReleaseNotesHostViewController.swift +++ b/ownCloud/Release Notes/ReleaseNotesHostViewController.swift @@ -166,7 +166,7 @@ extension ReleaseNotesHostViewController : Themeable { class ReleaseNotesDatasource : NSObject, OCClassSettingsUserPreferencesSupport { - var shouldShowReleaseNotes: Bool { + static var shouldShowReleaseNotes: Bool { if VendorServices.shared.isBranded { return false } else if let lastSeenReleaseNotesVersion = self.classSetting(forOCClassSettingsKey: .lastSeenReleaseNotesVersion) as? String { @@ -204,7 +204,7 @@ class ReleaseNotesDatasource : NSObject, OCClassSettingsUserPreferencesSupport { return false } - func releaseNotes(for version: String) -> [[String:Any]]? { + static func releaseNotes(for version: String) -> [[String:Any]]? { if let path = Bundle.main.path(forResource: "ReleaseNotes", ofType: "plist") { if let releaseNotesValues = NSDictionary(contentsOfFile: path), let versionsValues = releaseNotesValues["Versions"] as? NSArray { @@ -223,10 +223,14 @@ class ReleaseNotesDatasource : NSObject, OCClassSettingsUserPreferencesSupport { return nil } - func image(for key: String) -> UIImage? { + static func image(for key: String) -> UIImage? { let homeSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 32, weight: .thin) return UIImage(systemName: key, withConfiguration: homeSymbolConfiguration)?.withRenderingMode(.alwaysTemplate) } + + static func updateLastSeenAppVersion() { + ReleaseNotesDatasource.setUserPreferenceValue(NSString(utf8String: VendorServices.shared.appVersion), forClassSettingsKey: .lastSeenAppVersion) + } } public extension OCClassSettingsIdentifier { diff --git a/ownCloud/Release Notes/ReleaseNotesTableViewController.swift b/ownCloud/Release Notes/ReleaseNotesTableViewController.swift index 56ce0543d..9ee70362d 100644 --- a/ownCloud/Release Notes/ReleaseNotesTableViewController.swift +++ b/ownCloud/Release Notes/ReleaseNotesTableViewController.swift @@ -29,7 +29,7 @@ class ReleaseNotesTableViewController: StaticTableViewController { } func prepareReleaseNotes() { - if let relevantReleaseNotes = ReleaseNotesDatasource().releaseNotes(for: VendorServices.shared.appVersion), ReleaseNotesDatasource().releaseNotes(for: VendorServices.shared.appVersion) != nil { + if let relevantReleaseNotes = ReleaseNotesDatasource.releaseNotes(for: VendorServices.shared.appVersion), ReleaseNotesDatasource.releaseNotes(for: VendorServices.shared.appVersion) != nil { let section = StaticTableViewSection() for aDict in relevantReleaseNotes { @@ -46,7 +46,7 @@ class ReleaseNotesTableViewController: StaticTableViewController { } } - if let imageName = releaseNote["ImageName"], let image = ReleaseNotesDatasource().image(for: imageName) { + if let imageName = releaseNote["ImageName"], let image = ReleaseNotesDatasource.image(for: imageName) { let row = StaticTableViewRow(rowWithAction: { (_, _) in self.dismissAnimated() }, title: processedTitle, subtitle: processedSubtitle, image: image, imageWidth:50.0, alignment: .left, accessoryType: .none) diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index ea8d3ca81..6738603bf 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -75,11 +75,13 @@ LSSupportsOpeningDocumentsInPlace + #ifndef DISABLE_PLAIN_HTTP NSAppTransportSecurity NSAllowsArbitraryLoads + #endif NSAppleMusicUsageDescription This permission is needed for uploading media files to your server. NSCameraUsageDescription @@ -111,8 +113,7 @@ GetFileIntent PathExistsIntent SaveFileIntent - com.owncloud.ios-app.openAccount - com.owncloud.ios-app.openItem + com.owncloud.captureRestoreActivityType OCAppComponentIdentifier app diff --git a/ownCloud/Resources/ar.lproj/Localizable.strings b/ownCloud/Resources/ar.lproj/Localizable.strings index 0bf4e5781..8bc41a4f2 100644 Binary files a/ownCloud/Resources/ar.lproj/Localizable.strings and b/ownCloud/Resources/ar.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/cs.lproj/Localizable.strings b/ownCloud/Resources/cs.lproj/Localizable.strings index 8571090ba..d4a274111 100644 --- a/ownCloud/Resources/cs.lproj/Localizable.strings +++ b/ownCloud/Resources/cs.lproj/Localizable.strings @@ -666,16 +666,6 @@ "Preparing…" = "Preparing…"; "Save here" = "Save here"; -/* Data migration from legacy app */ -"Account Migration" = "Account Migration"; -"App Passcode" = "App Passcode"; -"Instant Upload Settings" = "Instant Upload Settings"; -"Migrating" = "Migrating"; -"Migrated" = "Migrated"; -"Failed to migrate" = "Failed to migrate"; -"Failed to access legacy user data" = "Failed to access legacy user data"; - - /* Misc. accessibility */ "Enter multiple selection" = "Enter multiple selection"; diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index db8e732dc..8f9781f04 100644 Binary files a/ownCloud/Resources/de.lproj/Localizable.strings and b/ownCloud/Resources/de.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index af44ecb4d..bf9ee963a 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -110,7 +110,7 @@ "Search space" = "Search space"; "Search {{space.name}}" = "Search {{space.name}}"; "Search account" = "Search account"; -"Save search" = "Save search"; +"Enter a search term" = "Enter a search term"; "Pending" = "Pending"; "Show parent paths" = "Show parent paths"; @@ -121,8 +121,24 @@ "%@ item | " = "%@ item | "; "%@ items | " = "%@ items | "; +"{{itemCount}} items with {{totalSize}} total ({{fileCount}} files, {{folderCount}} folders)" = "{{itemCount}} items with {{totalSize}} total ({{fileCount}} files, {{folderCount}} folders)"; +"{{remaining}} available" = "{{remaining}} available"; + "Show more results" = "Show more results"; +/* Saved searches */ +"Saved searches" = "Saved searches"; +"Saved search templates" = "Saved search templates"; +"Save search" = "Save search"; +"Save as search template" = "Save as search template"; + +"Name of saved search" = "Name of saved search"; +"Saved search" = "Saved search"; +"Saved searches" = "Saved searches"; +"Name of template" = "Name of template"; +"Search template" = "Search template"; +"Search templates" = "Search templates"; + /* Search scope */ "Search scope" = "Search scope"; "Toggle Search Scope" = "Toggle Search Scope"; @@ -147,9 +163,9 @@ "The Profile '%@' does not have a URL configured.\nPlease provide a URL via configuration or MDM." = "The Profile '%@' does not have a URL configured.\nPlease provide a URL via configuration or MDM."; /* Client Messages */ -"Empty folder" = "Empty folder"; "No contents" = "No contents"; "This folder contains no files or folders." = "This folder contains no files or folders."; +"This folder is empty." = "This folder is empty."; "This folder is empty. Fill it with content:" = "This folder is empty. Fill it with content:"; "Folder removed" = "Folder removed"; @@ -169,6 +185,8 @@ "No matches" = "No matches"; "The search term you entered did not match any item in the selected scope." = "The search term you entered did not match any item in the selected scope."; "There are no results for this search term" = "There are no results for this search term"; +"No items found matching the search criteria." = "No items found matching the search criteria."; + "Status" = "Status"; "Authorization failed" = "Authorization failed"; @@ -263,6 +281,8 @@ "Theme" = "Theme"; "User Interface" = "User Interface"; "Dark" = "Dark"; +"Dark Blue" = "Dark Blue"; +"Dark Web" = "Dark Web"; "Light" = "Light"; "Classic" = "Classic"; "System" = "System"; @@ -365,15 +385,18 @@ "Folder name" = "Folder name"; "Rename" ="Rename"; "Create folder" ="Create folder"; -"New document" = "New document"; -"Pick a name" = "Pick a name"; -"Pick a document type to create:" = "Pick a document type to create:"; "Duplicate" = "Duplicate"; "Move" = "Move"; +"Move {{itemCount}} items" = "Move {{itemCount}} items"; +"Move \"{{itemName}}\"" = "Move \"{{itemName}}\""; +"Move here" = "Move here"; +"Select target." = "Select target."; "Open in" = "Open in"; -"Open in {{appName}} (web)" = "Open in {{appName}} (web)"; "Copy" = "Copy"; +"Copy {{itemCount}} items" = "Copy {{itemCount}} items"; +"Copy \"{{itemName}}\"" = "Copy \"{{itemName}}\""; "Copy here" = "Copy here"; + "Cannot connect to " = "Cannot connect to "; " couldn't download file(s)" = " couldn't download file(s)"; "Actions" = "Actions"; @@ -386,6 +409,8 @@ "%ld Items were copied to the clipboard" = "%ld Items were copied to the clipboard"; "Please note: Folders can only be pasted into the %@ app and the same account." = "Please note: Folders can only be pasted into the %@ app and the same account."; +"Processing on server" = "Processing on server"; + "Preparing…" = "Preparing…"; "No actions available" = "No actions available"; @@ -400,8 +425,18 @@ "Enable" = "Enable"; "Exit Full Screen" = "Exit Full Screen"; -/* Directory Picker Messages */ -"Move here" = "Move here"; +"Select one or more items." = "Select one or more items."; + +"Add item" = "Add item"; + +/* App Provider */ +"New document" = "New document"; +"Pick a name" = "Pick a name"; +"Pick a document type to create:" = "Pick a document type to create:"; +"Error creating {{itemName}}" = "Error creating {{itemName}}"; + +"Open in {{appName}} (web)" = "Open in {{appName}} (web)"; +"Error opening {{itemName}} in {{appName}}" = "Error opening {{itemName}} in {{appName}}"; /* Preview */ "Open file" = "Open file"; @@ -563,6 +598,10 @@ "Share with user/group" = "Share with user/group"; "Share link" = "Share link"; +"Shared with me" = "Shared with me"; +"Shared by me" = "Shared by me"; +"Shared by link" = "Shared by link"; + /* Quick Access view */ "Quick Access" = "Quick Access"; "Collection" = "Collection"; @@ -574,6 +613,10 @@ "Documents" = "Documents"; "Audio" = "Audio"; +/* Favorites view */ +"No favorites found" = "No favorites found"; +"If you make an item a favorite, it will turn up here." = "If you make an item a favorite, it will turn up here."; + /* Media files settings */ "Media Files" = "Media Files"; "Download instead of streaming" = "Download instead of streaming"; @@ -656,10 +699,12 @@ "Make unavailable offline" = "Make unavailable offline"; "Available Offline" = "Available Offline"; -"No items have been selected for offline availability." = "No items have been selected for offline availability."; -"Overview" = "Overview"; -"All Files" = "All Files"; +"No files available offline" = "No files available offline"; +"Files selected and downloaded for offline availability will show up here." = "Files selected and downloaded for offline availability will show up here."; + +"Locations" = "Locations"; +"Downloaded Files" = "Downloaded Files"; /* Single Account */ "You are connected as\n%@" = "You are connected as\n%@"; @@ -744,7 +789,7 @@ /* Licensing: Settings */ "In-App Purchases" = "In-App Purchases"; -"Purchases" = "Purchases"; +"Purchases & Subscriptions" = "Purchases & Subscriptions"; "Error fetching transactions" = "Error fetching transactions"; "Document Scanner" = "Document Scanner"; "Scan documents and photos with your camera." = "Scan documents and photos with your camera."; @@ -784,8 +829,16 @@ "Error importing %@" = "Error importing %@"; "Error loading item" = "Error loading item"; "Preparing…" = "Preparing…"; +"Import \"{{itemName}}\"" = "Import \"{{itemName}}\""; +"Import {{itemCount}} files" = "Import {{itemCount}} files"; "Save here" = "Save here"; +/* FileProvider */ +/* - Disabled */ +"File Provider access has been disabled by the administrator.\n\nPlease use the app to access your files." = "File Provider access has been disabled by the administrator.\n\nPlease use the app to access your files."; +"File Provider access has been disabled by the administrator. Please use the app to create new folders." = "File Provider access has been disabled by the administrator. Please use the app to create new folders."; +"File Provider access has been disabled by the administrator. Please use the share extension to import files." = "File Provider access has been disabled by the administrator. Please use the share extension to import files."; + /* Disallowed Import Methods */ /* - Share Extension */ "Share Extension disabled" = "Share Extension disabled"; @@ -799,16 +852,6 @@ "Import not allowed" = "Import not allowed"; "Importing files through the File Provider is not allowed on this device." = "Importing files through the File Provider is not allowed on this device."; -/* Data migration from legacy app */ -"Account Migration" = "Account Migration"; -"App Passcode" = "App Passcode"; -"Instant Upload Settings" = "Instant Upload Settings"; -"Migrating" = "Migrating"; -"Migrated" = "Migrated"; -"Failed to migrate" = "Failed to migrate"; -"Failed to access legacy user data" = "Failed to access legacy user data"; -"The app was upgraded to a new version. Below there is an overview of all migrated accounts." = "The app was upgraded to a new version. Below there is an overview of all migrated accounts."; - /* Misc. accessibility */ "Enter multiple selection" = "Enter multiple selection"; @@ -943,3 +986,8 @@ "New settings received from MDM" = "New settings received from MDM"; "Tap to quit the app." = "Tap to quit the app."; "Tap to launch the app." = "Tap to launch the app."; + +/* Beta warning */ +"Beta Warning" = "Beta Warning"; +"\nThis is a BETA release that may - and likely will - still contain bugs.\n\nYOU SHOULD NOT USE THIS BETA VERSION WITH PRODUCTION SYSTEMS, PRODUCTION DATA OR DATA OF VALUE. YOU'RE USING THIS BETA AT YOUR OWN RISK.\n\nPlease let us know about any issues that come up via the \"Send Feedback\" option in the settings." = "\nThis is a BETA release that may - and likely will - still contain bugs.\n\nYOU SHOULD NOT USE THIS BETA VERSION WITH PRODUCTION SYSTEMS, PRODUCTION DATA OR DATA OF VALUE. YOU'RE USING THIS BETA AT YOUR OWN RISK.\n\nPlease let us know about any issues that come up via the \"Send Feedback\" option in the settings."; +"Agree" = "Agree"; diff --git a/ownCloud/Resources/es.lproj/Localizable.strings b/ownCloud/Resources/es.lproj/Localizable.strings index 257c36056..57cc643a6 100644 Binary files a/ownCloud/Resources/es.lproj/Localizable.strings and b/ownCloud/Resources/es.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/fr.lproj/Localizable.strings b/ownCloud/Resources/fr.lproj/Localizable.strings index cb12c0d0f..a53b8c87f 100644 Binary files a/ownCloud/Resources/fr.lproj/Localizable.strings and b/ownCloud/Resources/fr.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/gl.lproj/Localizable.strings b/ownCloud/Resources/gl.lproj/Localizable.strings index 65b801cc0..8108795e5 100644 Binary files a/ownCloud/Resources/gl.lproj/Localizable.strings and b/ownCloud/Resources/gl.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/he.lproj/Localizable.strings b/ownCloud/Resources/he.lproj/Localizable.strings index 0e771ebb0..3714e3e1d 100644 Binary files a/ownCloud/Resources/he.lproj/Localizable.strings and b/ownCloud/Resources/he.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/ko.lproj/InfoPlist.strings b/ownCloud/Resources/ko.lproj/InfoPlist.strings index e5df95aab..d478ca566 100644 Binary files a/ownCloud/Resources/ko.lproj/InfoPlist.strings and b/ownCloud/Resources/ko.lproj/InfoPlist.strings differ diff --git a/ownCloud/Resources/ko.lproj/Localizable.strings b/ownCloud/Resources/ko.lproj/Localizable.strings index 4da92cd4b..cbfbf99e3 100644 Binary files a/ownCloud/Resources/ko.lproj/Localizable.strings and b/ownCloud/Resources/ko.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/mk.lproj/Localizable.strings b/ownCloud/Resources/mk.lproj/Localizable.strings index 18a22480d..e176dacf0 100644 Binary files a/ownCloud/Resources/mk.lproj/Localizable.strings and b/ownCloud/Resources/mk.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/nb-NO.lproj/Localizable.strings b/ownCloud/Resources/nb-NO.lproj/Localizable.strings index dd39f4374..219032e64 100644 --- a/ownCloud/Resources/nb-NO.lproj/Localizable.strings +++ b/ownCloud/Resources/nb-NO.lproj/Localizable.strings @@ -667,16 +667,6 @@ "Preparing…" = "Preparing…"; "Save here" = "Save here"; -/* Data migration from legacy app */ -"Account Migration" = "Account Migration"; -"App Passcode" = "App Passcode"; -"Instant Upload Settings" = "Instant Upload Settings"; -"Migrating" = "Migrating"; -"Migrated" = "Migrated"; -"Failed to migrate" = "Failed to migrate"; -"Failed to access legacy user data" = "Failed to access legacy user data"; - - /* Misc. accessibility */ "Enter multiple selection" = "Enter multiple selection"; diff --git a/ownCloud/Resources/nn-NO.lproj/Localizable.strings b/ownCloud/Resources/nn-NO.lproj/Localizable.strings index f1758b6c4..aa29ae5e4 100644 --- a/ownCloud/Resources/nn-NO.lproj/Localizable.strings +++ b/ownCloud/Resources/nn-NO.lproj/Localizable.strings @@ -666,16 +666,6 @@ "Preparing…" = "Preparing…"; "Save here" = "Save here"; -/* Data migration from legacy app */ -"Account Migration" = "Account Migration"; -"App Passcode" = "App Passcode"; -"Instant Upload Settings" = "Instant Upload Settings"; -"Migrating" = "Migrating"; -"Migrated" = "Migrated"; -"Failed to migrate" = "Failed to migrate"; -"Failed to access legacy user data" = "Failed to access legacy user data"; - - /* Misc. accessibility */ "Enter multiple selection" = "Enter multiple selection"; diff --git a/ownCloud/Resources/pt-BR.lproj/Localizable.strings b/ownCloud/Resources/pt-BR.lproj/Localizable.strings index 6211b91cc..1a4f777c4 100644 Binary files a/ownCloud/Resources/pt-BR.lproj/Localizable.strings and b/ownCloud/Resources/pt-BR.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/pt-PT.lproj/Localizable.strings b/ownCloud/Resources/pt-PT.lproj/Localizable.strings index c0e698f03..76b37c478 100644 --- a/ownCloud/Resources/pt-PT.lproj/Localizable.strings +++ b/ownCloud/Resources/pt-PT.lproj/Localizable.strings @@ -666,16 +666,6 @@ "Preparing…" = "Preparing…"; "Save here" = "Save here"; -/* Data migration from legacy app */ -"Account Migration" = "Account Migration"; -"App Passcode" = "App Passcode"; -"Instant Upload Settings" = "Instant Upload Settings"; -"Migrating" = "Migrating"; -"Migrated" = "Migrated"; -"Failed to migrate" = "Failed to migrate"; -"Failed to access legacy user data" = "Failed to access legacy user data"; - - /* Misc. accessibility */ "Enter multiple selection" = "Enter multiple selection"; diff --git a/ownCloud/Resources/ru.lproj/Localizable.strings b/ownCloud/Resources/ru.lproj/Localizable.strings index 5eaf6914f..fb7f3dd1e 100644 Binary files a/ownCloud/Resources/ru.lproj/Localizable.strings and b/ownCloud/Resources/ru.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/sq.lproj/Localizable.strings b/ownCloud/Resources/sq.lproj/Localizable.strings index ddc4dda18..3749186c7 100644 Binary files a/ownCloud/Resources/sq.lproj/Localizable.strings and b/ownCloud/Resources/sq.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/th-TH.lproj/Localizable.strings b/ownCloud/Resources/th-TH.lproj/Localizable.strings index a3ece4f80..2ebf789e1 100644 Binary files a/ownCloud/Resources/th-TH.lproj/Localizable.strings and b/ownCloud/Resources/th-TH.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/tr.lproj/InfoPlist.strings b/ownCloud/Resources/tr.lproj/InfoPlist.strings index faa0cda5f..bc8b585ab 100644 Binary files a/ownCloud/Resources/tr.lproj/InfoPlist.strings and b/ownCloud/Resources/tr.lproj/InfoPlist.strings differ diff --git a/ownCloud/Resources/tr.lproj/Localizable.strings b/ownCloud/Resources/tr.lproj/Localizable.strings index 2ce2cbd36..2181c111a 100644 Binary files a/ownCloud/Resources/tr.lproj/Localizable.strings and b/ownCloud/Resources/tr.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/zh-Hans.lproj/InfoPlist.strings b/ownCloud/Resources/zh-Hans.lproj/InfoPlist.strings index 1f48b880c..76e7fe67b 100644 Binary files a/ownCloud/Resources/zh-Hans.lproj/InfoPlist.strings and b/ownCloud/Resources/zh-Hans.lproj/InfoPlist.strings differ diff --git a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings index 7ecdb0642..06d5f7335 100644 Binary files a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings and b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/zh_TW.lproj/Localizable.strings b/ownCloud/Resources/zh_TW.lproj/Localizable.strings index 745906f54..2ddb6c81d 100644 Binary files a/ownCloud/Resources/zh_TW.lproj/Localizable.strings and b/ownCloud/Resources/zh_TW.lproj/Localizable.strings differ diff --git a/ownCloud/SDK Extensions/OCBookmarkManager+Management.swift b/ownCloud/SDK Extensions/OCBookmarkManager+Management.swift new file mode 100644 index 000000000..4fb64d932 --- /dev/null +++ b/ownCloud/SDK Extensions/OCBookmarkManager+Management.swift @@ -0,0 +1,41 @@ +// +// OCBookmarkManager+Management.swift +// ownCloud +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudAppShared +import ownCloudSDK + +extension OCBookmarkManager { + public func manage(bookmark: OCBookmark, presentOn hostViewController: UIViewController, completion manageCompletion: (() -> Void)? = nil) { + if !OCBookmarkManager.attemptLock(bookmark: bookmark, presentErrorOn: hostViewController, action: { bookmark, lockActionCompletion in + let viewController = BookmarkInfoViewController(bookmark) + viewController.completionHandler = { + lockActionCompletion() + } + + let navigationController : ThemeNavigationController = ThemeNavigationController(rootViewController: viewController) + navigationController.modalPresentationStyle = .overFullScreen + + hostViewController.present(navigationController, animated: true, completion: { + manageCompletion?() + }) + }) { + manageCompletion?() + } + } +} diff --git a/ownCloud/SceneDelegate.swift b/ownCloud/SceneDelegate.swift index e43622786..51487ec75 100644 --- a/ownCloud/SceneDelegate.swift +++ b/ownCloud/SceneDelegate.swift @@ -19,47 +19,46 @@ import UIKit import ownCloudSDK import ownCloudAppShared -@available(iOS 13.0, *) class SceneDelegate: UIResponder, UIWindowSceneDelegate { - + // MARK: - Window var window: ThemeWindow? + weak var scene: UIScene? + + // MARK: - Scene Context + lazy var sceneClientContext: ClientContext = { + return ClientContext(scene: scene) + }() + + // MARK: - AppRootViewController + lazy var appRootViewController: AppRootViewController = { + return self.buildAppRootViewController() + }() + + func buildAppRootViewController() -> AppRootViewController { + return AppRootViewController(with: sceneClientContext) + } - // UIWindowScene delegate + // MARK: - UIWindowSceneDelegate + // MARK: Sessions func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + self.scene = scene + // Set up HTTP pipelines OCHTTPPipelineManager.setupPersistentPipelines() + // Window and AppRootViewController creation if let windowScene = scene as? UIWindowScene { window = ThemeWindow(windowScene: windowScene) - var navigationController: UINavigationController? - - if VendorServices.shared.isBranded { - let staticLoginViewController = StaticLoginViewController(with: StaticLoginBundle.defaultBundle) - navigationController = ThemeNavigationController(rootViewController: staticLoginViewController) - navigationController?.setNavigationBarHidden(true, animated: false) - } else { - var serverListTableViewController : ServerListTableViewController? - if OCBookmarkManager.shared.bookmarks.count == 1 { - serverListTableViewController = StaticLoginSingleAccountServerListViewController(style: .insetGrouped) - } else { - serverListTableViewController = ServerListTableViewController(style: .plain) - } - guard let serverListTableViewController = serverListTableViewController else { return } - - serverListTableViewController.restorationIdentifier = "ServerListTableViewController" - - navigationController = ThemeNavigationController(rootViewController: serverListTableViewController) - } - window?.rootViewController = navigationController - window?.addSubview((navigationController!.view)!) + window?.rootViewController = appRootViewController + window?.addSubview(appRootViewController.view) window?.makeKeyAndVisible() } // Was the app launched with registered URL scheme? if let urlContext = connectionOptions.urlContexts.first { if urlContext.url.matchesAppScheme { - openPrivateLink(url: urlContext.url, in: scene) + openAppSchemeLink(url: urlContext.url) } else { ImportFilesController.shared.importFile(ImportFile(url: urlContext.url, fileIsLocalCopy: urlContext.options.openInPlace)) } @@ -71,11 +70,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } else { configure(window: window, with: userActivity) } - } else if ServerListTableViewController.classSetting(forOCClassSettingsKey: .accountAutoConnect) as? Bool ?? false, let bookmark = OCBookmarkManager.shared.bookmarks.first { - connect(to: bookmark) } } + // MARK: Screen foreground/background events private func set(scene: UIScene, inForeground: Bool) { if let windowScene = scene as? UIWindowScene { for window in windowScene.windows { @@ -94,51 +92,48 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.set(scene: scene, inForeground: false) } + // MARK: - State restoration func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { - return scene.userActivity - } + var actions: [AppStateAction] = [] - @discardableResult func configure(window: ThemeWindow?, with activity: NSUserActivity) -> Bool { - if let bookmarkUUIDString = activity.userInfo?[OCBookmark.ownCloudOpenAccountAccountUuidKey] as? String, - let bookmarkUUID = UUID(uuidString: bookmarkUUIDString), - let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { - if activity.title == OCBookmark.ownCloudOpenAccountPath { - connect(to: bookmark) - - return true - } else if activity.title == OpenItemUserActivity.ownCloudOpenItemPath { - guard let itemLocalID = activity.userInfo?[OpenItemUserActivity.ownCloudOpenItemUuidKey] as? String else { - return false - } + let navigationBookmark = appRootViewController.contentBrowserController.history.currentItem?.navigationBookmark - // At first connect to the bookmark for the item - connect(to: bookmark, lastVisibleItemId: itemLocalID, activity: activity) + for activeConnection in AccountConnectionPool.shared.activeConnections { + let connectAction: AppStateAction = .connection(with: activeConnection.bookmark) - return true + if let navigationBookmark, activeConnection.bookmark.uuid == navigationBookmark.bookmarkUUID { + connectAction.children = [ + .navigate(to: navigationBookmark) + ] } - } else if activity.activityType == ServerListTableViewController.showServerListActivityType { - // Show server list - window?.windowScene?.userActivity = activity - return true + actions.append(connectAction) } - return false + return AppStateAction(with: actions).userActivity(with: sceneClientContext) } - func connect(to bookmark: OCBookmark, lastVisibleItemId: String? = nil, activity: NSUserActivity? = nil) { - if let navigationController = window?.rootViewController as? ThemeNavigationController, - let serverListController = navigationController.topViewController as? StateRestorationConnectProtocol { - serverListController.connect(to: bookmark, lastVisibleItemId: lastVisibleItemId, animated: false, present: nil) - window?.windowScene?.userActivity = activity ?? bookmark.openAccountUserActivity + @discardableResult func configure(window: ThemeWindow?, with activity: NSUserActivity) -> Bool { + if activity.isRestorableActivity { + OnMainThread(after: 0.5) { + activity.restore(with: [ + UserActivityOption.clientContext.rawValue : self.sceneClientContext + ]) + } + + return true } + + return false } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + self.scene = scene + if let firstURL = URLContexts.first?.url { // Ensure the set isn't empty if !OCAuthenticationBrowserSessionCustomScheme.handleOpen(firstURL), // No custom scheme URL handling for this URL firstURL.matchesAppScheme { // + URL matches app scheme - openPrivateLink(url: firstURL, in: scene) + openAppSchemeLink(url: firstURL) } else { if firstURL.isFileURL, // Ensure the URL is a file URL ImportFilesController.shared.importAllowed(alertUserOtherwise: true) { // Ensure import is allowed @@ -151,26 +146,35 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + self.scene = scene + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return } - guard let windowScene = scene as? UIWindowScene else { return } - - guard let window = windowScene.windows.first else { return } - - url.resolveAndPresent(in: window) + openAppSchemeLink(url: url) } - private func openPrivateLink(url:URL, in scene:UIScene?) { - if url.privateLinkItemID() != nil { - - guard let windowScene = scene as? UIWindowScene else { return } - - guard let window = windowScene.windows.first else { return } + private func openAppSchemeLink(url: URL) { + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + appDelegate.openAppSchemeLink(url: url, clientContext: sceneClientContext) + } + } +} - url.resolveAndPresent(in: window) +extension ClientContext: ClientContextProvider { + public func provideClientContext(for bookmarkUUID: UUID, completion: (Error?, ownCloudAppShared.ClientContext?) -> Void) { + if let sceneDelegate = scene?.delegate as? SceneDelegate, + let sections = sceneDelegate.appRootViewController.sidebarViewController?.allSections { + for section in sections { + if let accountControllerSection = section as? AccountControllerSection, accountControllerSection.clientContext?.accountConnection?.bookmark.uuid == bookmarkUUID { + completion(nil, section.clientContext) + return + } + } } + + completion(nil, nil) } } diff --git a/ownCloud/Server List/ServerListTableHeaderView.swift b/ownCloud/Server List/ServerListTableHeaderView.swift deleted file mode 100644 index c9bed6de9..000000000 --- a/ownCloud/Server List/ServerListTableHeaderView.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// ServerListTableHeaderView.swift -// ownCloud -// -// Created by Matthias Hühne on 27.01.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 ownCloudAppShared - -class ServerListTableHeaderView: UIView, Themeable { - - // MARK: - Constants - fileprivate let shadowHeight: CGFloat = 1.0 - fileprivate let textLabelTopMargin: CGFloat = 10.0 - fileprivate let textLabelHorizontalMargin: CGFloat = 16.0 - fileprivate let textLabelHeight: CGFloat = 18.0 - - // MARK: - Instance variables. - var messageThemeApplierToken : ThemeApplierToken? - var textLabel : UILabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - - Theme.shared.register(client: self, applyImmediately: true) - - textLabel.translatesAutoresizingMaskIntoConstraints = false - textLabel.text = "Accounts".localized - textLabel.font = UIFont.preferredFont(forTextStyle: .headline) - - self.addSubview(textLabel) - - let shadowView = UIView() - shadowView.backgroundColor = UIColor.init(white: 0.0, alpha: 0.1) - shadowView.translatesAutoresizingMaskIntoConstraints = false - - self.addSubview(shadowView) - - NSLayoutConstraint.activate([ - - shadowView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - shadowView.widthAnchor.constraint(equalTo: self.widthAnchor), - shadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - shadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - shadowView.heightAnchor.constraint(equalToConstant: shadowHeight), - - textLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: textLabelTopMargin), - textLabel.leftAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leftAnchor, constant: textLabelHorizontalMargin), - textLabel.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -textLabelHorizontalMargin) - ]) - - messageThemeApplierToken = Theme.shared.add(applier: { [weak self] (_, collection, _) in - self?.backgroundColor = collection.navigationBarColors.backgroundColor - self?.textLabel.textColor = collection.navigationBarColors.labelColor - }) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - Theme.shared.unregister(client: self) - - if messageThemeApplierToken != nil { - Theme.shared.remove(applierForToken: messageThemeApplierToken) - messageThemeApplierToken = nil - } - } - - // MARK: - Theme support - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - - self.backgroundColor = collection.navigationBarColors.backgroundColor - textLabel.textColor = collection.navigationBarColors.labelColor - } - -} diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift deleted file mode 100644 index 86cb874b4..000000000 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ /dev/null @@ -1,1071 +0,0 @@ -// -// ServerListTableViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 08.03.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 -import ownCloudApp -import ownCloudAppShared -import PocketSVG - -public protocol StateRestorationConnectProtocol : AnyObject { - func connect(to bookmark: OCBookmark, lastVisibleItemId: String?, animated: Bool, present message: OCMessage?) -} - -class ServerListTableViewController: UITableViewController, Themeable, StateRestorationConnectProtocol { - // MARK: - Views - @IBOutlet var welcomeOverlayView: UIView! - @IBOutlet var welcomeTitleLabel : UILabel! - @IBOutlet var welcomeMessageLabel : UILabel! - @IBOutlet var welcomeAddServerButton : ThemeButton! - @IBOutlet var welcomeLogoImageView : UIImageView! - @IBOutlet var welcomeLogoTVGView : VectorImageView! - // @IBOutlet var welcomeLogoSVGView : SVGImageView! - - // MARK: - User Activity - static let showServerListActivityType = "com.owncloud.ios-app.showServerList" - static let showServerListActivityTitle = "showServerList" - - static var showServerListActivity : NSUserActivity { - let userActivity = NSUserActivity(activityType: showServerListActivityType) - - userActivity.title = showServerListActivityTitle - userActivity.userInfo = [ : ] - - return userActivity - } - - // MARK: - Internals - var shownFirstTime = true - var hasToolbar : Bool = true - - var pushTransitionRecovery : PushTransitionRecovery? - weak var pushFromViewController : UIViewController? - - // MARK: - Init - override init(style: UITableView.Style) { - super.init(style: style) - - NotificationCenter.default.addObserver(self, selector: #selector(serverListChanged), name: .OCBookmarkManagerListChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(openAccount(notification:)), name: .NotificationAuthErrorForwarderOpenAccount, object: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - NotificationCenter.default.removeObserver(self, name: .OCBookmarkManagerListChanged, object: nil) - NotificationCenter.default.removeObserver(self, name: .NotificationAuthErrorForwarderOpenAccount, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) - } - - // MARK: - View controller events - override func viewDidLoad() { - super.viewDidLoad() - - OCItem.registerIcons() - - self.navigationController?.navigationBar.prefersLargeTitles = true - self.navigationController?.navigationBar.isTranslucent = false - self.navigationController?.toolbar.isTranslucent = false - self.tableView.register(ServerListBookmarkCell.self, forCellReuseIdentifier: "bookmark-cell") - self.tableView.rowHeight = UITableView.automaticDimension - self.tableView.estimatedRowHeight = 80 - self.tableView.allowsSelectionDuringEditing = true - self.tableView.dragDelegate = self - extendedLayoutIncludesOpaqueBars = true - - if VendorServices.shared.canAddAccount { - let addServerBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(addBookmark)) - addServerBarButtonItem.accessibilityLabel = "Add account".localized - addServerBarButtonItem.accessibilityIdentifier = "addAccount" - self.navigationItem.rightBarButtonItem = addServerBarButtonItem - } - - // This view is nil, when branded app version - if welcomeOverlayView != nil { - welcomeOverlayView.translatesAutoresizingMaskIntoConstraints = false - Theme.shared.add(tvgResourceFor: "owncloud-logo") - welcomeLogoTVGView.vectorImage = Theme.shared.tvgImage(for: "owncloud-logo") - } - - let logoImage = UIImage(named: "branding-login-logo") - let logoImageView = UIImageView(image: logoImage) - logoImageView.contentMode = .scaleAspectFit - logoImageView.translatesAutoresizingMaskIntoConstraints = false - if let logoImage = logoImage { - // Keep aspect ratio + scale logo to 90% of available height - logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor, multiplier: (logoImage.size.width / logoImage.size.height) * 0.9).isActive = true - } - - let logoLabel = UILabel() - logoLabel.translatesAutoresizingMaskIntoConstraints = false - logoLabel.text = VendorServices.shared.appName - logoLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) - logoLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - logoLabel.setContentCompressionResistancePriority(.required, for: .vertical) - - let logoContainer = UIView() - logoContainer.translatesAutoresizingMaskIntoConstraints = false - logoContainer.addSubview(logoImageView) - logoContainer.addSubview(logoLabel) - logoContainer.setContentHuggingPriority(.required, for: .horizontal) - logoContainer.setContentHuggingPriority(.required, for: .vertical) - - let logoWrapperView = ThemeView() - logoWrapperView.addSubview(logoContainer) - - NSLayoutConstraint.activate([ - logoImageView.topAnchor.constraint(greaterThanOrEqualTo: logoContainer.topAnchor), - logoImageView.bottomAnchor.constraint(lessThanOrEqualTo: logoContainer.bottomAnchor), - logoImageView.centerYAnchor.constraint(equalTo: logoContainer.centerYAnchor), - logoLabel.topAnchor.constraint(greaterThanOrEqualTo: logoContainer.topAnchor), - logoLabel.bottomAnchor.constraint(lessThanOrEqualTo: logoContainer.bottomAnchor), - logoLabel.centerYAnchor.constraint(equalTo: logoContainer.centerYAnchor), - - logoImageView.leadingAnchor.constraint(equalTo: logoContainer.leadingAnchor), - logoLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: logoImageView.trailingAnchor, multiplier: 1), - logoLabel.trailingAnchor.constraint(equalTo: logoContainer.trailingAnchor), - - logoContainer.topAnchor.constraint(equalTo: logoWrapperView.topAnchor), - logoContainer.bottomAnchor.constraint(equalTo: logoWrapperView.bottomAnchor), - logoContainer.centerXAnchor.constraint(equalTo: logoWrapperView.centerXAnchor) - ]) - - logoWrapperView.addThemeApplier({ (_, collection, _) in - logoLabel.applyThemeCollection(collection, itemStyle: .logo) - if !VendorServices.shared.isBranded { - logoImageView.image = logoImageView.image?.tinted(with: collection.navigationBarColors.labelColor) - } - }) - - self.navigationItem.largeTitleDisplayMode = .never - self.navigationItem.titleView = logoWrapperView - - if ReleaseNotesDatasource().shouldShowReleaseNotes { - let releaseNotesHostController = ReleaseNotesHostViewController() - releaseNotesHostController.modalPresentationStyle = .formSheet - self.present(releaseNotesHostController, animated: true, completion: nil) - } - - ReleaseNotesDatasource.setUserPreferenceValue(NSString(utf8String: VendorServices.shared.appVersion), forClassSettingsKey: .lastSeenAppVersion) - - if Migration.shared.legacyDataFound { - let migrationViewController = MigrationViewController() - let navigationController = ThemeNavigationController(rootViewController: migrationViewController) - migrationViewController.migrationFinishedHandler = { - Migration.shared.wipeLegacyData() - } - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: false) - } - - messageCountByBookmarkUUID = [:] // Initial update of app badge icon - - messageCountSelector = MessageSelector(filter: nil, handler: { [weak self] (messages, _, _) in - var countByBookmarkUUID : [UUID? : Int ] = [:] - - if let messages = messages { - for message in messages { - if !message.resolved { - if let count = countByBookmarkUUID[message.bookmarkUUID] { - countByBookmarkUUID[message.bookmarkUUID] = count + 1 - } else { - countByBookmarkUUID[message.bookmarkUUID] = 1 - } - } - } - } - - OnMainThread { - self?.messageCountByBookmarkUUID = countByBookmarkUUID - } - }) - } - - var messageCountSelector : MessageSelector? - - typealias ServerListTableMessageCountByUUID = [UUID? : Int ] - - var messageCountByBookmarkUUID : ServerListTableMessageCountByUUID = [:] { - didSet { - // Notify cells about changed counts - NotificationCenter.default.post(Notification(name: .BookmarkMessageCountChanged, object: messageCountByBookmarkUUID, userInfo: nil)) - - // Update app badge count - var totalNotificationCount = messageCountByBookmarkUUID[nil] ?? 0 // Global notifications - - for bookmark in OCBookmarkManager.shared.bookmarks { - if let count = messageCountByBookmarkUUID[bookmark.uuid] { - totalNotificationCount += count - } - } - - if !ProcessInfo.processInfo.arguments.contains("UI-Testing") { - NotificationManager.shared.requestAuthorization(options: .badge) { (granted, _) in - if granted { - OnMainThread { - UIApplication.shared.applicationIconBadgeNumber = totalNotificationCount - } - } - } - } - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if hasToolbar { - self.navigationController?.setToolbarHidden(false, animated: animated) - } - self.navigationController?.navigationBar.prefersLargeTitles = true - - Theme.shared.register(client: self) - - if welcomeOverlayView != nil { - welcomeOverlayView.layoutSubviews() - } - - self.tableView.reloadData() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - ClientSessionManager.shared.add(delegate: self) - - updateNoServerMessageVisibility() - - if hasToolbar { - let settingsBarButtonItem = UIBarButtonItem(title: "Settings".localized, style: UIBarButtonItem.Style.plain, target: self, action: #selector(settings)) - settingsBarButtonItem.accessibilityIdentifier = "settingsBarButtonItem" - - self.toolbarItems = [ - UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil), - settingsBarButtonItem - ] - } - - if AppLockManager.shared.passcode == nil && AppLockSettings.shared.isPasscodeEnforced { - PasscodeSetupCoordinator(parentViewController: self, action: .setup).start() - } else if let passcode = AppLockManager.shared.passcode, passcode.count < AppLockSettings.shared.requiredPasscodeDigits { - PasscodeSetupCoordinator(parentViewController: self, action: .upgrade).start() - } - - if VendorServices.shared.showBetaWarning, shownFirstTime { - considerBetaWarning() - } - - if !shownFirstTime { - VendorServices.shared.considerReviewPrompt() - } - - view.window?.windowScene?.userActivity = ServerListTableViewController.showServerListActivity - } - - func considerBetaWarning() { - let lastBetaWarningCommit = OCAppIdentity.shared.userDefaults?.string(forKey: "LastBetaWarningCommit") - - Log.log("Show beta warning: \(String(describing: VendorServices.classSetting(forOCClassSettingsKey: .showBetaWarning) as? Bool))") - - if VendorServices.classSetting(forOCClassSettingsKey: .showBetaWarning) as? Bool == true, - let lastGitCommit = LastGitCommit(), - (lastBetaWarningCommit == nil) || (lastBetaWarningCommit != lastGitCommit) { - // Beta warning has never been shown before - or has last been shown for a different release - let betaAlert = ThemedAlertController(with: "Beta Warning", message: "\nThis is a BETA release that may - and likely will - still contain bugs.\n\nYOU SHOULD NOT USE THIS BETA VERSION WITH PRODUCTION SYSTEMS, PRODUCTION DATA OR DATA OF VALUE. YOU'RE USING THIS BETA AT YOUR OWN RISK.\n\nPlease let us know about any issues that come up via the \"Send Feedback\" option in the settings.", okLabel: "Agree") { - OCAppIdentity.shared.userDefaults?.set(lastGitCommit, forKey: "LastBetaWarningCommit") - OCAppIdentity.shared.userDefaults?.set(NSDate(), forKey: "LastBetaWarningAcceptDate") - } - - self.showModal(viewController: betaAlert) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - self.navigationController?.setToolbarHidden(true, animated: animated) - - ClientSessionManager.shared.remove(delegate: self) - - Theme.shared.unregister(client: self) - } - - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - if welcomeAddServerButton != nil { - welcomeAddServerButton.themeColorCollection = collection.neutralColors - - welcomeTitleLabel.applyThemeCollection(collection, itemStyle: .title) - welcomeMessageLabel.applyThemeCollection(collection, itemStyle: .message) - } - - self.tableView.applyThemeCollection(collection) - } - - func updateNoServerMessageVisibility() { - guard welcomeOverlayView != nil else { - return - } - - if OCBookmarkManager.shared.bookmarks.count == 0 { - let safeAreaLayoutGuide : UILayoutGuide = self.tableView.safeAreaLayoutGuide - var constraint : NSLayoutConstraint - - if welcomeOverlayView.superview != self.view { - - welcomeOverlayView.alpha = 0 - - self.view.addSubview(welcomeOverlayView) - - UIView.animate(withDuration: 0.2, animations: { - self.welcomeOverlayView.alpha = 1 - }) - - welcomeOverlayView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor).isActive = true - welcomeOverlayView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true - - constraint = welcomeOverlayView.leftAnchor.constraint(greaterThanOrEqualTo: safeAreaLayoutGuide.leftAnchor, constant: 30) - constraint.isActive = true - constraint = welcomeOverlayView.rightAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.rightAnchor, constant: -30) - constraint.isActive = true - - self.tableView.tableHeaderView = nil - self.navigationController?.navigationBar.shadowImage = nil - - welcomeAddServerButton.setTitle("Add account".localized, for: .normal) - welcomeAddServerButton.accessibilityIdentifier = "addServer" - welcomeTitleLabel.text = "Welcome".localized - let welcomeMessage = "Thanks for choosing %@! \n Start by adding your account.".localized - welcomeMessageLabel.text = welcomeMessage.replacingOccurrences(of: "%@", with: VendorServices.shared.appName) - - tableView.separatorStyle = UITableViewCell.SeparatorStyle.none - tableView.reloadData() - tableView.isScrollEnabled = false - } - - if self.navigationItem.leftBarButtonItem != nil { - self.navigationItem.leftBarButtonItem = nil - } - - } else { - - if welcomeOverlayView.superview == self.view { - welcomeOverlayView.removeFromSuperview() - - tableView.separatorStyle = UITableViewCell.SeparatorStyle.singleLine - tableView.reloadData() - tableView.isScrollEnabled = true - } - - if self.navigationItem.leftBarButtonItem == nil { - self.navigationItem.leftBarButtonItem = self.editButtonItem - } - - // Add Header View - self.tableView.tableHeaderView = ServerListTableHeaderView(frame: CGRect(x: 0.0, y: 0.0, width: self.view.frame.size.width, height: 40.0)) - self.navigationController?.navigationBar.shadowImage = UIImage() - self.tableView.tableHeaderView?.applyThemeCollection(Theme.shared.activeCollection) - - self.addThemableBackgroundView() - } - } - - // MARK: - Actions - @IBAction func addBookmark() { - var attemptLoginOnSuccess = true - - // Prevent requesting and immediately returning a core for the newly generated bookmark if it is the first one and - // the conditions in didUpdateServerList() would lead to a replacement of this view controller. Because then, this would - // happen: - // - didUpdateServerList() replaces ServerListTableViewController with StaticLoginSingleAccountServerListViewController - // - showBookmarkUI(attemptLoginOnSuccess:true) starts a new connection on the replaced ServerListTableViewController, which requests a core from OCCoreManager - // - then immediately ClientRootViewController gets deallocated, which returns the core to OCCoreManager - // - the unusual *immediate* request + return might lead to an overlap with the next request for a core, so duplicate OCCore and OCConnection instances exist for a moment, - // possibly interfering event and request routing of one another - if !VendorServices.shared.isBranded, OCBookmarkManager.shared.bookmarks.count == 0 { - attemptLoginOnSuccess = false - } - - showBookmarkUI(attemptLoginOnSuccess: attemptLoginOnSuccess) - } - - func showBookmarkUI(edit bookmark: OCBookmark? = nil, performContinue: Bool = false, attemptLoginOnSuccess: Bool = false, autosolveErrorOnSuccess: NSError? = nil, removeAuthDataFromCopy: Bool = true) { - let bookmarkViewController : BookmarkViewController = BookmarkViewController(bookmark, removeAuthDataFromCopy: removeAuthDataFromCopy) - let navigationController : ThemeNavigationController = ThemeNavigationController(rootViewController: bookmarkViewController) - - navigationController.modalPresentationStyle = .overFullScreen - - // Prevent any in-progress connection from being shown - resetPreviousBookmarkSelection() - - // Exit editing mode (unfortunately, self.isEditing = false will not do the trick as it leaves the left bar button unchanged as "Done") - if self.tableView.isEditing, - let target = self.navigationItem.leftBarButtonItem?.target, - let action = self.navigationItem.leftBarButtonItem?.action { - _ = target.perform(action, with: self) - } - - bookmarkViewController.userActionCompletionHandler = { [weak self] (bookmark, success) in - if success, let bookmark = bookmark, let self = self { - self.didUpdateServerList() - - if let error = autosolveErrorOnSuccess as Error? { - OCMessageQueue.global.resolveIssues(forError: error, forBookmarkUUID: bookmark.uuid) - } - - if attemptLoginOnSuccess { - self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) - } - } - } - - self.showModal(viewController: navigationController, completion: { - OnMainThread { - if performContinue { - bookmarkViewController.showedOAuthInfoHeader = true // needed for HTTP+OAuth2 connections to really continue on .handleContinue() call - bookmarkViewController.handleContinue() - } - } - }) - } - - func showBookmarkInfoUI(_ bookmark: OCBookmark) { - let viewController = BookmarkInfoViewController(bookmark) - let navigationController : ThemeNavigationController = ThemeNavigationController(rootViewController: viewController) - navigationController.modalPresentationStyle = .overFullScreen - - // Prevent any in-progress connection from being shown - resetPreviousBookmarkSelection() - - self.showModal(viewController: navigationController) - } - - @available(iOS 13.0, *) - func openAccountInWindow(at indexPath: IndexPath) { - if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - let activity = bookmark.openAccountUserActivity - UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil) - } - } - - @available(iOS 13.0, *) - func dismissWindow() { - if let scene = view.window?.windowScene { - UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil) { (_) in - } - } - } - - func showModal(viewController: UIViewController, completion: (() -> Void)? = nil) { - self.present(viewController, animated: true, completion: completion) - } - - func delete(bookmark: OCBookmark, at indexPath: IndexPath, completion: (() -> Void)? = nil) { - var presentationStyle: UIAlertController.Style = .actionSheet - if UIDevice.current.isIpad { - presentationStyle = .alert - } - - var alertTitle = "Really delete '%@'?".localized - var destructiveTitle = "Delete".localized - var failureTitle = "Deletion of '%@' failed".localized - if VendorServices.shared.isBranded { - alertTitle = "Do you want to log out from '%@'?".localized - destructiveTitle = "Log out".localized - failureTitle = "Log out of '%@' failed".localized - } - - let alertController = ThemedAlertController(title: NSString(format: alertTitle as NSString, bookmark.shortName) as String, - message: "This will also delete all locally stored file copies.".localized, - preferredStyle: presentationStyle) - - alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) - - alertController.addAction(UIAlertAction(title: destructiveTitle, style: .destructive, handler: { (_) in - - OCBookmarkManager.lock(bookmark: bookmark) - - OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in - let vault : OCVault = OCVault(bookmark: bookmark) - - vault.erase(completionHandler: { (_, error) in - OnMainThread { - if error != nil { - // Inform user if vault couldn't be erased - let alertController = ThemedAlertController(title: NSString(format: failureTitle as NSString, bookmark.shortName as NSString) as String, - message: error?.localizedDescription, - preferredStyle: .alert) - - alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) - - self.present(alertController, animated: true, completion: nil) - } else { - // Success! We can now remove the bookmark - self.ignoreServerListChanges = true - - OCMessageQueue.global.dequeueAllMessages(forBookmarkUUID: bookmark.uuid) - - OCBookmarkManager.shared.removeBookmark(bookmark) - - completion?() - self.updateNoServerMessageVisibility() - } - - OCBookmarkManager.unlock(bookmark: bookmark) - - completionHandler() - } - }) - }, for: bookmark) - })) - - self.present(alertController, animated: true, completion: nil) - } - - var themeCounter : Int = 0 - - @IBAction func settings() { - let viewController : SettingsViewController = SettingsViewController(style: .grouped) - - // Prevent any in-progress connection from being shown - resetPreviousBookmarkSelection() - - self.navigationController?.pushViewController(viewController, animated: true) - } - - // MARK: - Track external changes - var ignoreServerListChanges : Bool = false - - @objc func serverListChanged() { - OnMainThread { - if !self.ignoreServerListChanges { - self.tableView.reloadData() - self.updateNoServerMessageVisibility() - } - } - } - - @objc func openAccount(notification: NSNotification) { - if let bookmarkUUID = notification.object as? UUID, - let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { - self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) - } - } - - // MARK: - Connect and locking - func isLocked(bookmark: OCBookmark, presentAlert: Bool = true) -> Bool { - return OCBookmarkManager.isLocked(bookmark: bookmark, presentAlertOn: presentAlert ? self : nil) - } - - weak var activeClientRootViewController : ClientRootViewController? - - func connect(to bookmark: OCBookmark, lastVisibleItemId: String?, animated: Bool, present message: OCMessage? = nil) { - if isLocked(bookmark: bookmark) { - return - } - - guard let indexPath = indexPath(for: bookmark) else { - return - } - - let clientRootViewController = ClientSessionManager.shared.startSession(for: bookmark)! - - let bookmarkRow = self.tableView.cellForRow(at: indexPath) as? ServerListBookmarkCell - let activityIndicator = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) - - var bookmarkRowAccessoryView : UIView? - var bookmarkDetailLabelContent : String? - - if bookmarkRow != nil { - bookmarkRowAccessoryView = bookmarkRow?.accessoryView - bookmarkRow?.accessoryView = activityIndicator - - activityIndicator.startAnimating() - } - - self.setLastSelectedBookmark(bookmark, openedBlock: { - activityIndicator.stopAnimating() - bookmarkRow?.accessoryView = bookmarkRowAccessoryView - }) - - clientRootViewController.authDelegate = self - clientRootViewController.modalPresentationStyle = .overFullScreen - - clientRootViewController.afterCoreStart(lastVisibleItemId, busyHandler: { (progress) in - OnMainThread { - if let bookmarkRow = self.tableView.cellForRow(at: indexPath) as? ServerListBookmarkCell { - if progress != nil { - let progressView = ProgressView(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) - progressView.progress = progress - - bookmarkDetailLabelContent = bookmarkRow.detailLabel.text - - activityIndicator.stopAnimating() - - bookmarkRow.detailLabel.text = progress?.localizedDescription - bookmarkRow.accessoryView = progressView - - bookmarkRow.detailLabel.beginPulsing() - } else { - bookmarkRow.detailLabel.endPulsing() - - bookmarkRow.detailLabel.text = bookmarkDetailLabelContent - bookmarkRow.accessoryView = activityIndicator - - activityIndicator.startAnimating() - } - } - } - }, completionHandler: { (error) in - if self.lastSelectedBookmark?.uuid == bookmark.uuid, // Make sure only the UI for the last selected bookmark is actually presented (in case of other bookmarks facing a huge delay and users selecting another bookmark in the meantime) - self.activeClientRootViewController == nil { // Make sure we don't present this ClientRootViewController while still presenting another - if let fromViewController = self.pushFromViewController ?? self.navigationController { - if let error = error { - let alert = UIAlertController(title: NSString(format: "Error opening %@".localized as NSString, bookmark.shortName) as String, message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) - - fromViewController.present(alert, animated: true) - - self.resetPreviousBookmarkSelection(bookmark) - } else { - OCBookmarkManager.lastBookmarkSelectedForConnection = bookmark - - self.activeClientRootViewController = clientRootViewController // save this ClientRootViewController as the active one (only weakly referenced) - - // Set up custom push transition for presentation - let transitionDelegate = PushTransitionDelegate(with: { (toViewController, window) in - window.addSubview(toViewController.view) - window.windowScene?.userActivity = ServerListTableViewController.showServerListActivity - }) - - clientRootViewController.pushTransition = transitionDelegate // Keep a reference, so it's still around on dismissal - clientRootViewController.transitioningDelegate = transitionDelegate - clientRootViewController.modalPresentationStyle = .custom - - fromViewController.present(clientRootViewController, animated: animated, completion: { - self.resetPreviousBookmarkSelection(bookmark) - - // Present message if one was provided - if let message = message { - self.presentInClient(message: message) - } - }) - } - } - } - self.didUpdateServerList() - }) - } - - func didUpdateServerList() { - // This is a hook for subclasses - - if !VendorServices.shared.isBranded { - if OCBookmarkManager.shared.bookmarks.count == 1 { - var serverListTableViewController : ServerListTableViewController? - serverListTableViewController = StaticLoginSingleAccountServerListViewController(style: .insetGrouped) - - guard let serverListTableViewController = serverListTableViewController else { return } - - self.navigationController?.setViewControllers([ serverListTableViewController ], animated: false) - } - } - } - - var clientViewController : ClientRootViewController? { - return self.presentedViewController as? ClientRootViewController - } - - func presentInClient(message: OCMessage) { - if let cardMessagePresenter = clientViewController?.cardMessagePresenter { - OnMainThread { // Wait for next runloop cycle - OCMessageQueue.global.present(message, with: cardMessagePresenter) - } - } - } - - // MARK: - Table view delegate - var lastSelectedBookmark : OCBookmark? - var lastSelectedBookmarkOpenedBlock : (() -> Void)? - - func setLastSelectedBookmark(_ bookmark: OCBookmark, openedBlock: (() -> Void)?) { - resetPreviousBookmarkSelection() - lastSelectedBookmark = bookmark - lastSelectedBookmarkOpenedBlock = openedBlock - } - - func resetPreviousBookmarkSelection(_ bookmark: OCBookmark? = nil) { - if (bookmark == nil) || ((bookmark != nil) && (bookmark?.uuid == lastSelectedBookmark?.uuid)) { - if lastSelectedBookmark != nil, lastSelectedBookmarkOpenedBlock != nil { - lastSelectedBookmarkOpenedBlock?() - lastSelectedBookmarkOpenedBlock = nil - - lastSelectedBookmark = nil - } - } - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - if self.isLocked(bookmark: bookmark) { - return - } - - if tableView.isEditing { - self.showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) - } else { - self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) - self.tableView.deselectRow(at: indexPath, animated: true) - } - - self.tableView.deselectRow(at: indexPath, animated: true) - } - } - - @available(iOS 13.0, *) - override func tableView(_ tableView: UITableView, - contextMenuConfigurationForRowAt indexPath: IndexPath, - point: CGPoint) -> UIContextMenuConfiguration? { - if !self.isEditing, let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in - return self.makeContextMenu(for: indexPath, with: bookmark) - }) - } - - return nil - } - - @available(iOS 13.0, *) - func makeContextMenu(for indexPath: IndexPath, with bookmark: OCBookmark) -> UIMenu { - var menuItems : [UIAction] = [] - - if UIDevice.current.isIpad { - let openWindow = UIAction(title: "Open in a new Window".localized, image: UIImage(systemName: "uiwindow.split.2x1")) { _ in - self.openAccountInWindow(at: indexPath) - } - menuItems.append(openWindow) - } - let edit = UIAction(title: "Edit".localized, image: UIImage(systemName: "gear")) { _ in - self.showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) - } - if VendorServices.shared.canEditAccount { - menuItems.append(edit) - } - let manage = UIAction(title: "Manage".localized, image: UIImage(systemName: "arrow.3.trianglepath")) { _ in - self.showBookmarkInfoUI(bookmark) - } - menuItems.append(manage) - - var destructiveTitle = "Delete".localized - if VendorServices.shared.isBranded { - destructiveTitle = "Log out".localized - } - let delete = UIAction(title: destructiveTitle, image: UIImage(systemName: "trash"), attributes: .destructive) { _ in - self.delete(bookmark: bookmark, at: indexPath ) { - OnMainThread { - self.tableView.performBatchUpdates({ - self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) - }, completion: { (_) in - self.ignoreServerListChanges = false - self.didUpdateServerList() - }) - } - } - } - menuItems.append(delete) - - return UIMenu(title: bookmark.shortName, children: menuItems) - } - - // MARK: - Table view data source - func indexPath(for bookmark: OCBookmark) -> IndexPath? { - var index = 0 - - for otherBookmark in OCBookmarkManager.shared.bookmarks { - if bookmark.uuid == otherBookmark.uuid { - return IndexPath(item: index, section: 0) - } - - index += 1 - } - - return nil - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return OCBookmarkManager.shared.bookmarks.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let bookmarkCell = self.tableView.dequeueReusableCell(withIdentifier: "bookmark-cell", for: indexPath) as? ServerListBookmarkCell else { - return ServerListBookmarkCell() - } - - if let bookmark : OCBookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - bookmarkCell.bookmark = bookmark - bookmarkCell.updateMessageBadge(count: messageCountByBookmarkUUID[bookmark.uuid] ?? 0) - } - - return bookmarkCell - } - - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - - if self.isEditing { - return nil - } - - var destructiveTitle = "Delete".localized - if VendorServices.shared.isBranded { - destructiveTitle = "Log out".localized - } - - let deleteRowAction = UIContextualAction(style: .destructive, title: destructiveTitle, handler: { (_, _, completionHandler) in - if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - self.delete(bookmark: bookmark, at: indexPath ) { - OnMainThread { - self.tableView.performBatchUpdates({ - if self.tableView.cellForRow(at: indexPath) != nil { - self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) - } - }, completion: { (_) in - self.ignoreServerListChanges = false - self.didUpdateServerList() - }) - } - completionHandler(true) - } - } - }) - - let editRowAction = UIContextualAction(style: .normal, title: "Edit".localized, handler: { [weak self] (_, _, completionHandler) in - if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - self?.showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) - } - completionHandler(true) - }) - editRowAction.backgroundColor = .blue - - let manageRowAction = UIContextualAction(style: .normal, - title: "Manage".localized, - handler: { [weak self] (_, _, completionHandler) in - if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - self?.showBookmarkInfoUI(bookmark) - } - completionHandler(true) - }) - - if UIDevice.current.isIpad { - let openAccountAction = UIContextualAction(style: .normal, title: "Open in Window".localized, handler: { (_, _, completionHandler) in - self.openAccountInWindow(at: indexPath) - completionHandler(true) - }) - openAccountAction.backgroundColor = .orange - - if VendorServices.shared.canEditAccount { - return UISwipeActionsConfiguration(actions: [deleteRowAction, editRowAction, manageRowAction, openAccountAction]) - } - return UISwipeActionsConfiguration(actions: [deleteRowAction, manageRowAction, openAccountAction]) - } - - if VendorServices.shared.canEditAccount { - return UISwipeActionsConfiguration(actions: [deleteRowAction, editRowAction, manageRowAction]) - } - return UISwipeActionsConfiguration(actions: [deleteRowAction, manageRowAction]) - } - - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - if self.isEditing { - return true - } - - return false - } - - override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) { - OCBookmarkManager.shared.moveBookmark(from: UInt(fromIndexPath.row), to: UInt(to.row)) - } -} - -extension OCBookmarkManager { - static private let lastConnectedBookmarkUUIDDefaultsKey = "last-connected-bookmark-uuid" - - // MARK: - Defaults Keys - static var lastBookmarkSelectedForConnection : OCBookmark? { - get { - if let bookmarkUUIDString = OCAppIdentity.shared.userDefaults?.string(forKey: OCBookmarkManager.lastConnectedBookmarkUUIDDefaultsKey), let bookmarkUUID = UUID(uuidString: bookmarkUUIDString) { - return OCBookmarkManager.shared.bookmark(for: bookmarkUUID) - } - - return nil - } - - set { - OCAppIdentity.shared.userDefaults?.set(newValue?.uuid.uuidString, forKey: OCBookmarkManager.lastConnectedBookmarkUUIDDefaultsKey) - } - } - - static var lockedBookmarks : [OCBookmark] = [] - - static func lock(bookmark: OCBookmark) { - OCSynchronized(self) { - self.lockedBookmarks.append(bookmark) - } - } - - static func unlock(bookmark: OCBookmark) { - OCSynchronized(self) { - if let removeIndex = self.lockedBookmarks.firstIndex(of: bookmark) { - self.lockedBookmarks.remove(at: removeIndex) - } - } - } - - static func isLocked(bookmark: OCBookmark, presentAlertOn viewController: UIViewController? = nil, completion: ((_ isLocked: Bool) -> Void)? = nil) -> Bool { - if self.lockedBookmarks.contains(bookmark) { - if viewController != nil { - let alertController = ThemedAlertController(title: NSString(format: "'%@' is currently locked".localized as NSString, bookmark.shortName as NSString) as String, - message: NSString(format: "An operation is currently performed that prevents connecting to '%@'. Please try again later.".localized as NSString, bookmark.shortName as NSString) as String, - preferredStyle: .alert) - - alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: { (_) in - completion?(true) - })) - - viewController?.present(alertController, animated: true, completion: nil) - } - - return true - } - - completion?(false) - - return false - } -} - -extension ServerListTableViewController : ClientRootViewControllerAuthenticationDelegate { - func handleAuthError(for clientViewController: ClientRootViewController, error: NSError, editBookmark: OCBookmark?, preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?) { - clientViewController.closeClient(completion: { [weak self] in - if let editBookmark = editBookmark { - // Bring up bookmark editing UI - self?.showBookmarkUI(edit: editBookmark, - performContinue: (editBookmark.isTokenBased == true), - attemptLoginOnSuccess: true, - removeAuthDataFromCopy: true) - } - }) - } -} - -extension ServerListTableViewController : ClientSessionManagerDelegate { - func canPresent(bookmark: OCBookmark, message: OCMessage?) -> OCMessagePresentationPriority { - if let themeWindow = self.viewIfLoaded?.window as? ThemeWindow, themeWindow.themeWindowInForeground { - if !isLocked(bookmark: bookmark) { - if presentedViewController == nil { - return .high - } else { - if let clientViewController = self.clientViewController { - if clientViewController.bookmark.uuid == bookmark.uuid { - return .high - } else { - return .default - } - } - } - } - - return .low - } - - return .wontPresent - } - - func present(bookmark: OCBookmark, message: OCMessage?) { - OnMainThread { - if self.presentedViewController == nil { - self.connect(to: bookmark, lastVisibleItemId: nil, animated: true, present: message) - } else { - if self.clientViewController != nil, let message = message { - self.presentInClient(message: message) - } - } - } - } -} - -extension ServerListTableViewController: UITableViewDragDelegate { - - func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - if !self.isEditing, let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - let userActivity = bookmark.openAccountUserActivity - let itemProvider = NSItemProvider(item: bookmark, typeIdentifier: "com.owncloud.ios-app.ocbookmark") - itemProvider.registerObject(userActivity, visibility: .all) - - let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = bookmark - - return [dragItem] - } - - return [] - } -} - -extension NSNotification.Name { - static let BookmarkMessageCountChanged = NSNotification.Name("boomark.message-count.changed") -} - -// MARK: - OCClassSettings support -extension OCClassSettingsIdentifier { - static let account = OCClassSettingsIdentifier("account") -} - -extension OCClassSettingsKey { - static let accountAutoConnect = OCClassSettingsKey("auto-connect") -} - -extension ServerListTableViewController : OCClassSettingsSupport { - static let classSettingsIdentifier : OCClassSettingsIdentifier = .account - - static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? { - if identifier == .account { - return [ - .accountAutoConnect : false - ] - } - - return nil - } - - static func classSettingsMetadata() -> [OCClassSettingsKey : [OCClassSettingsMetadataKey : Any]]? { - return [ - .accountAutoConnect : [ - .type : OCClassSettingsMetadataType.boolean, - .description : "Skip \"Account\" screen / automatically open \"Files\" screen after login", - .category : "Account", - .status : OCClassSettingsKeyStatus.supported - ] - ] - } -} diff --git a/ownCloud/Server List/ServerListTableViewController.xib b/ownCloud/Server List/ServerListTableViewController.xib deleted file mode 100644 index 4a8ea31d0..000000000 --- a/ownCloud/Server List/ServerListTableViewController.xib +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ownCloud/Settings/AutoUploadSettingsSection.swift b/ownCloud/Settings/AutoUploadSettingsSection.swift index fb6d0ff6e..cfcd949fa 100644 --- a/ownCloud/Settings/AutoUploadSettingsSection.swift +++ b/ownCloud/Settings/AutoUploadSettingsSection.swift @@ -28,14 +28,19 @@ extension UserDefaults { enum AutoUploadKeys : String { case InstantUploadPhotosKey = "instant-upload-photos" case InstantUploadVideosKey = "instant-upload-videos" - case InstantLegacyUploadBookmarkUUIDKey = "instant-upload-bookmark-uuid" - case InstantPhotoUploadBookmarkUUIDKey = "instant-photo-upload-bookmark-uuid" - case InstantVideoUploadBookmarkUUIDKey = "instant-video-upload-bookmark-uuid" - case InstantLegacyUploadPathKey = "instant-upload-path" - case InstantPhotoUploadPathKey = "instant-photo-upload-path" - case InstantVideoUploadPathKey = "instant-video-upload-path" + + case InstantPhotoUploadLocation = "instant-photo-upload-location" + case InstantVideoUploadLocation = "instant-video-upload-location" + case InstantUploadPhotosAfterDateKey = "instant-upload-photos-after-date" case InstantUploadVideosAfterDateKey = "instant-upload-videos-after-date" + + case LegacyInstantLegacyUploadBookmarkUUIDKey = "instant-upload-bookmark-uuid" + case LegacyInstantPhotoUploadBookmarkUUIDKey = "instant-photo-upload-bookmark-uuid" + case LegacyInstantVideoUploadBookmarkUUIDKey = "instant-video-upload-bookmark-uuid" + case LegacyInstantLegacyUploadPathKey = "instant-upload-path" + case LegacyInstantPhotoUploadPathKey = "instant-photo-upload-path" + case LegacyInstantVideoUploadPathKey = "instant-video-upload-path" } public var instantUploadPhotos: Bool { @@ -58,88 +63,143 @@ extension UserDefaults { } } - public var instantPhotoUploadBookmarkUUID: UUID? { + public var instantPhotoUploadLocation: OCLocation? { set { - self.set(newValue?.uuidString, forKey: AutoUploadKeys.InstantPhotoUploadBookmarkUUIDKey.rawValue) + set(newValue?.data, forKey: AutoUploadKeys.InstantPhotoUploadLocation.rawValue) } get { - var uuidString = self.string(forKey: AutoUploadKeys.InstantPhotoUploadBookmarkUUIDKey.rawValue) - if uuidString == nil { - uuidString = self.string(forKey: AutoUploadKeys.InstantLegacyUploadBookmarkUUIDKey.rawValue) + if let oldBookmarkUUID = legacyInstantPhotoUploadBookmarkUUID, + let oldBookmarkPath = legacyInstantPhotoUploadPath { + // Migrate and remove old setting + legacyInstantPhotoUploadBookmarkUUID = nil + legacyInstantPhotoUploadPath = nil + + let location = OCLocation(bookmarkUUID: oldBookmarkUUID, driveID: nil, path: oldBookmarkPath) + set(location.data, forKey: AutoUploadKeys.InstantPhotoUploadLocation.rawValue) + return location } - guard let uuid = uuidString else { return nil } - return UUID(uuidString: uuid) + + if let locationData = data(forKey: AutoUploadKeys.InstantPhotoUploadLocation.rawValue) { + return OCLocation.fromData(locationData) + } + + return nil } } - public var instantVideoUploadBookmarkUUID: UUID? { + public var instantVideoUploadLocation: OCLocation? { set { - self.set(newValue?.uuidString, forKey: AutoUploadKeys.InstantVideoUploadBookmarkUUIDKey.rawValue) + set(newValue?.data, forKey: AutoUploadKeys.InstantVideoUploadLocation.rawValue) } get { - var uuidString = self.string(forKey: AutoUploadKeys.InstantVideoUploadBookmarkUUIDKey.rawValue) - if uuidString == nil { - uuidString = self.string(forKey: AutoUploadKeys.InstantLegacyUploadBookmarkUUIDKey.rawValue) + if let oldBookmarkUUID = legacyInstantVideoUploadBookmarkUUID, + let oldBookmarkPath = legacyInstantVideoUploadPath { + // Migrate and remove old setting + legacyInstantVideoUploadBookmarkUUID = nil + legacyInstantVideoUploadPath = nil + + let location = OCLocation(bookmarkUUID: oldBookmarkUUID, driveID: nil, path: oldBookmarkPath) + set(location.data, forKey: AutoUploadKeys.InstantVideoUploadLocation.rawValue) + return location } - guard let uuid = uuidString else { return nil } - return UUID(uuidString: uuid) + + if let locationData = data(forKey: AutoUploadKeys.InstantVideoUploadLocation.rawValue) { + return OCLocation.fromData(locationData) + } + + return nil } } - public var instantPhotoUploadPath: String? { + public var instantUploadPhotosAfter: Date? { + set { + self.set(newValue, forKey: AutoUploadKeys.InstantUploadPhotosAfterDateKey.rawValue) + } + + get { + return self.value(forKey: AutoUploadKeys.InstantUploadPhotosAfterDateKey.rawValue) as? Date + } + } + public var instantUploadVideosAfter: Date? { set { - self.set(newValue, forKey: AutoUploadKeys.InstantPhotoUploadPathKey.rawValue) + self.set(newValue, forKey: AutoUploadKeys.InstantUploadVideosAfterDateKey.rawValue) } get { - return self.string(forKey: AutoUploadKeys.InstantPhotoUploadPathKey.rawValue) ?? self.string(forKey: AutoUploadKeys.InstantLegacyUploadPathKey.rawValue) + return self.value(forKey: AutoUploadKeys.InstantUploadVideosAfterDateKey.rawValue) as? Date } } - public var instantVideoUploadPath: String? { + public func resetInstantPhotoUploadConfiguration() { + self.legacyInstantPhotoUploadBookmarkUUID = nil + self.legacyInstantPhotoUploadPath = nil + self.instantPhotoUploadLocation = nil + self.instantUploadPhotos = false + } + + public func resetInstantVideoUploadConfiguration() { + self.legacyInstantVideoUploadBookmarkUUID = nil + self.legacyInstantVideoUploadPath = nil + self.instantVideoUploadLocation = nil + self.instantUploadVideos = false + } +} +// Legacy bookmark UUID + path settings from the pre-OCLocation era +extension UserDefaults { + public var legacyInstantPhotoUploadBookmarkUUID: UUID? { set { - self.set(newValue, forKey: AutoUploadKeys.InstantVideoUploadPathKey.rawValue) + self.set(newValue?.uuidString, forKey: AutoUploadKeys.LegacyInstantPhotoUploadBookmarkUUIDKey.rawValue) } get { - return self.string(forKey: AutoUploadKeys.InstantVideoUploadPathKey.rawValue) ?? self.string(forKey: AutoUploadKeys.InstantLegacyUploadPathKey.rawValue) + var uuidString = self.string(forKey: AutoUploadKeys.LegacyInstantPhotoUploadBookmarkUUIDKey.rawValue) + if uuidString == nil { + uuidString = self.string(forKey: AutoUploadKeys.LegacyInstantLegacyUploadBookmarkUUIDKey.rawValue) + } + guard let uuid = uuidString else { return nil } + return UUID(uuidString: uuid) } } - public var instantUploadPhotosAfter: Date? { + public var legacyInstantVideoUploadBookmarkUUID: UUID? { set { - self.set(newValue, forKey: AutoUploadKeys.InstantUploadPhotosAfterDateKey.rawValue) + self.set(newValue?.uuidString, forKey: AutoUploadKeys.LegacyInstantVideoUploadBookmarkUUIDKey.rawValue) } get { - return self.value(forKey: AutoUploadKeys.InstantUploadPhotosAfterDateKey.rawValue) as? Date + var uuidString = self.string(forKey: AutoUploadKeys.LegacyInstantVideoUploadBookmarkUUIDKey.rawValue) + if uuidString == nil { + uuidString = self.string(forKey: AutoUploadKeys.LegacyInstantLegacyUploadBookmarkUUIDKey.rawValue) + } + guard let uuid = uuidString else { return nil } + return UUID(uuidString: uuid) } } - public var instantUploadVideosAfter: Date? { + public var legacyInstantPhotoUploadPath: String? { + set { - self.set(newValue, forKey: AutoUploadKeys.InstantUploadVideosAfterDateKey.rawValue) + self.set(newValue, forKey: AutoUploadKeys.LegacyInstantPhotoUploadPathKey.rawValue) } get { - return self.value(forKey: AutoUploadKeys.InstantUploadVideosAfterDateKey.rawValue) as? Date + return self.string(forKey: AutoUploadKeys.LegacyInstantPhotoUploadPathKey.rawValue) ?? self.string(forKey: AutoUploadKeys.LegacyInstantLegacyUploadPathKey.rawValue) } } - public func resetInstantPhotoUploadConfiguration() { - self.instantPhotoUploadBookmarkUUID = nil - self.instantPhotoUploadPath = nil - self.instantUploadPhotos = false - } + public var legacyInstantVideoUploadPath: String? { - public func resetInstantVideoUploadConfiguration() { - self.instantVideoUploadBookmarkUUID = nil - self.instantVideoUploadPath = nil - self.instantUploadVideos = false + set { + self.set(newValue, forKey: AutoUploadKeys.LegacyInstantVideoUploadPathKey.rawValue) + } + + get { + return self.string(forKey: AutoUploadKeys.LegacyInstantVideoUploadPathKey.rawValue) ?? self.string(forKey: AutoUploadKeys.LegacyInstantLegacyUploadPathKey.rawValue) + } } } @@ -171,7 +231,7 @@ class AutoUploadSettingsSection: SettingsSection { self?.setupPhotoAutoUpload(enabled: switchState) }) } - }, title: "Auto Upload Photos".localized, value: self.userDefaults.instantUploadPhotos, identifier: "auto-upload-photos") + }, title: "Auto Upload Photos".localized, value: self.userDefaults.instantUploadPhotos, identifier: "auto-upload-photos") instantUploadVideosRow = StaticTableViewRow(switchWithAction: { [weak self] (_, sender) in if let convertSwitch = sender as? UISwitch { @@ -179,15 +239,15 @@ class AutoUploadSettingsSection: SettingsSection { self?.setupVideoAutoUpload(enabled: switchState) }) } - }, title: "Auto Upload Videos".localized, value: self.userDefaults.instantUploadVideos, identifier: "auto-upload-videos") + }, title: "Auto Upload Videos".localized, value: self.userDefaults.instantUploadVideos, identifier: "auto-upload-videos") photoBookmarkAndPathSelectionRow = StaticTableViewRow(subtitleRowWithAction: { [weak self] (_, _) in self?.showAccountSelectionViewController(for: .photo) - }, title: "Photo upload path".localized, subtitle: "", accessoryType: .disclosureIndicator, identifier: AutoUploadSettingsSection.photoUploadBookmarkAndPathSelectionRowIdentifier) + }, title: "Photo upload path".localized, subtitle: "", accessoryType: .disclosureIndicator, identifier: AutoUploadSettingsSection.photoUploadBookmarkAndPathSelectionRowIdentifier) videoBookmarkAndPathSelectionRow = StaticTableViewRow(subtitleRowWithAction: { [weak self] (_, _) in self?.showAccountSelectionViewController(for: .video) - }, title: "Video upload path".localized, subtitle: "", accessoryType: .disclosureIndicator, identifier: AutoUploadSettingsSection.videoUploadBookmarkAndPathSelectionRowIdentifier) + }, title: "Video upload path".localized, subtitle: "", accessoryType: .disclosureIndicator, identifier: AutoUploadSettingsSection.videoUploadBookmarkAndPathSelectionRowIdentifier) self.add(row: instantUploadPhotosRow!) self.add(row: instantUploadVideosRow!) @@ -204,7 +264,7 @@ class AutoUploadSettingsSection: SettingsSection { } else { userDefaults.instantUploadPhotos = true userDefaults.instantUploadPhotosAfter = Date() - if userDefaults.instantPhotoUploadPath == nil || userDefaults.instantPhotoUploadBookmarkUUID == nil { + if userDefaults.instantPhotoUploadLocation == nil { showAccountSelectionViewController(for: .photo) } else { updateDynamicUI() @@ -222,11 +282,11 @@ class AutoUploadSettingsSection: SettingsSection { } else { userDefaults.instantUploadVideos = true userDefaults.instantUploadVideosAfter = Date() - if userDefaults.instantVideoUploadPath == nil || userDefaults.instantVideoUploadBookmarkUUID == nil { + if userDefaults.instantVideoUploadLocation == nil { showAccountSelectionViewController(for: .video) } else { - updateDynamicUI() - } + updateDynamicUI() + } } NotificationCenter.default.post(name: .OCBookmarkManagerListChanged, object: nil) @@ -237,10 +297,10 @@ class AutoUploadSettingsSection: SettingsSection { var bookmarkUUID: UUID? switch mediaType { - case .photo: - bookmarkUUID = self.userDefaults.instantPhotoUploadBookmarkUUID - case .video: - bookmarkUUID = self.userDefaults.instantVideoUploadBookmarkUUID + case .photo: + bookmarkUUID = self.userDefaults.instantPhotoUploadLocation?.bookmarkUUID + case .video: + bookmarkUUID = self.userDefaults.instantVideoUploadLocation?.bookmarkUUID } if let selectedBookmarkUUID = bookmarkUUID { @@ -255,14 +315,14 @@ class AutoUploadSettingsSection: SettingsSection { self.remove(rowWithIdentifier: AutoUploadSettingsSection.photoUploadBookmarkAndPathSelectionRowIdentifier) self.remove(rowWithIdentifier: AutoUploadSettingsSection.videoUploadBookmarkAndPathSelectionRowIdentifier) - if let bookmark = getSelectedBookmark(for: .photo), let path = userDefaults.instantPhotoUploadPath, userDefaults.instantUploadPhotos == true { - OCItemTracker(for: bookmark, at: .legacyRootPath(path)) { (error, _, pathItem) in + if let bookmark = getSelectedBookmark(for: .photo), let location = userDefaults.instantPhotoUploadLocation, userDefaults.instantUploadPhotos == true { + OCItemTracker(for: bookmark, at: location) { (error, _, pathItem) in guard error == nil else { return } OnMainThread { if pathItem != nil { self.add(row: self.photoBookmarkAndPathSelectionRow!) - let directory = URL(fileURLWithPath: path).lastPathComponent + let directory = location.lastPathComponent ?? "?" self.photoBookmarkAndPathSelectionRow?.value = "\(bookmark.shortName)/\(directory)" } else { self.userDefaults.resetInstantPhotoUploadConfiguration() @@ -279,14 +339,14 @@ class AutoUploadSettingsSection: SettingsSection { changeHandler?() } - if let bookmark = getSelectedBookmark(for: .video), let path = userDefaults.instantVideoUploadPath, userDefaults.instantUploadVideos == true { - OCItemTracker(for: bookmark, at: .legacyRootPath(path)) { (error, _, pathItem) in + if let bookmark = getSelectedBookmark(for: .video), let location = userDefaults.instantVideoUploadLocation, userDefaults.instantUploadVideos == true { + OCItemTracker(for: bookmark, at: location) { (error, _, pathItem) in guard error == nil else { return } OnMainThread { if pathItem != nil { self.add(row: self.videoBookmarkAndPathSelectionRow!) - let directory = URL(fileURLWithPath: path).lastPathComponent + let directory = location.lastPathComponent ?? "?" self.videoBookmarkAndPathSelectionRow?.value = "\(bookmark.shortName)/\(directory)" } else { self.userDefaults.resetInstantVideoUploadConfiguration() @@ -322,105 +382,44 @@ class AutoUploadSettingsSection: SettingsSection { } private func showAccountSelectionViewController(for mediaType:MediaType) { + var prompt: String - let accountSelectionViewController = StaticTableViewController(style: .grouped) - let navigationController = ThemeNavigationController(rootViewController: accountSelectionViewController) + switch mediaType { + case .photo: + prompt = "Pick a destination for photo uploads".localized - accountSelectionViewController.navigationItem.title = "Select account".localized - accountSelectionViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, - target: accountSelectionViewController, - action: #selector(accountSelectionViewController.dismissAnimated)) - accountSelectionViewController.didDismissAction = { [weak self] (viewController) in - self?.updateDynamicUI() + case .video: + prompt = "Pick a destination for video uploads".localized } - let accountsSection = StaticTableViewSection(headerTitle: "Accounts".localized) - - var bookmarkRows: [StaticTableViewRow] = [] - let bookmarks = OCBookmarkManager.shared.bookmarks - - guard bookmarks.count > 0 else { return } - - var bookmarkDictionary = [StaticTableViewRow : OCBookmark]() - - for bookmark in bookmarks { - let row = StaticTableViewRow(buttonWithAction: { [weak self] (_ row, _ sender) in - - // Store selected bookmark - let selectedBookmark = bookmarkDictionary[row]! - switch mediaType { - case .photo: - self?.userDefaults.instantPhotoUploadBookmarkUUID = selectedBookmark.uuid - self?.userDefaults.instantPhotoUploadPath = nil - case .video: - self?.userDefaults.instantVideoUploadBookmarkUUID = selectedBookmark.uuid - self?.userDefaults.instantVideoUploadPath = nil + let locationPicker = ClientLocationPicker(location: .accounts, selectButtonTitle: "Select Destination".localized, selectPrompt: prompt, requiredPermissions: [ .createFile ], avoidConflictsWith: nil, choiceHandler: { [weak self] (chosenItem, location, _, cancelled) in + if let chosenItem, !chosenItem.permissions.contains(.createFile) { + OnMainThread { [weak self] in + let alert = ThemedAlertController(title: "Missing permissions".localized, message: "This permission is needed to upload photos and videos from your photo library.".localized, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + self?.viewController?.present(alert, animated: true, completion: nil) } - - // Proceed with upload path selection - self?.selectUploadPath(for: selectedBookmark, pushIn: navigationController, completion: { (directoryItem) in - let path = self?.getDirectoryPath(from: directoryItem) - switch mediaType { + } else { + switch mediaType { case .photo: - self?.userDefaults.instantPhotoUploadPath = path - if path == nil { + self?.userDefaults.instantPhotoUploadLocation = location + if location == nil { self?.userDefaults.resetInstantPhotoUploadConfiguration() } + case .video: - self?.userDefaults.instantVideoUploadPath = path - if path == nil { + self?.userDefaults.instantVideoUploadLocation = location + if location == nil { self?.userDefaults.resetInstantVideoUploadConfiguration() } - } - - navigationController.dismiss(animated: true, completion: nil) - self?.postSettingsChangedNotification() - self?.updateDynamicUI() - }) - - }, title: bookmark.shortName, style: .plain, image: Theme.shared.image(for: "owncloud-logo", size: CGSize(width: 25, height: 25)), imageWidth: 25, alignment: .left) - - bookmarkRows.append(row) - bookmarkDictionary[row] = bookmark - } - - accountsSection.add(rows: bookmarkRows) - accountSelectionViewController.addSection(accountsSection) - - self.viewController?.present(navigationController, animated: true) - } - - private func selectUploadPath(for bookmark:OCBookmark, pushIn navigationController:UINavigationController, completion:@escaping (_ directoryItem:OCItem?) -> Void) { - - OCCoreManager.shared.requestCore(for: bookmark, setup: { (_, _) in }, - completionHandler: { [weak navigationController] (core, error) in - - guard let core = core, error == nil else { return } + } - OnMainThread { - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, location: .legacyRoot, selectButtonTitle: "Select Upload Path".localized, avoidConflictsWith: [], choiceHandler: { (selectedDirectory, _) in - OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) - completion(selectedDirectory) - }) - navigationController?.pushViewController(directoryPickerViewController, animated: true) - } + self?.postSettingsChangedNotification() + self?.updateDynamicUI() + } }) - } - - private func getDirectoryPath(from directoryItem:OCItem?) -> String? { - guard let item = directoryItem else { return nil } - - if item.permissions.contains(.createFile) { - return item.path - } else { - OnMainThread { - let alert = ThemedAlertController(title: "Missing permissions".localized, message: "This permission is needed to upload photos and videos from your photo library.".localized, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) - self.viewController?.present(alert, animated: true, completion: nil) - } - } - return nil + locationPicker.present(in: ClientContext(originatingViewController: self.viewController)) } private func postSettingsChangedNotification() { @@ -429,7 +428,7 @@ class AutoUploadSettingsSection: SettingsSection { private func showAutoUploadDisabledAlert() { let alertController = ThemedAlertController(with: "Auto upload disabled".localized, - message: "Auto upload of media was disabled since configured account / folder was not found".localized) + message: "Auto upload of media was disabled since configured account / folder was not found".localized) self.viewController?.present(alertController, animated: true, completion: nil) } } diff --git a/ownCloud/Settings/DisplaySettingsSection.swift b/ownCloud/Settings/DisplaySettingsSection.swift index 35147b304..8277a05ea 100644 --- a/ownCloud/Settings/DisplaySettingsSection.swift +++ b/ownCloud/Settings/DisplaySettingsSection.swift @@ -50,5 +50,13 @@ class DisplaySettingsSection: SettingsSection { DiagnosticManager.shared.enabled = diagnosticsEnabled } }, title: "Enable diagnostics".localized, value: DiagnosticManager.shared.enabled, identifier: "diagnostics-enabled")) + + if OCLicenseQAProvider.isQAUnlockPossible { + self.add(row: StaticTableViewRow(switchWithAction: { (row, _) in + if let qaUnlockedProFeatures = row.value as? Bool { + OCLicenseQAProvider.isQAUnlockEnabled = qaUnlockedProFeatures + } + }, title: "Enable Pro Features (QA)".localized, value: OCLicenseQAProvider.isQAUnlockEnabled, identifier: "enable-pro-features")) + } } } diff --git a/ownCloud/Settings/PurchasesSettingsSection.swift b/ownCloud/Settings/PurchasesSettingsSection.swift index d2b6936ad..1c162057a 100644 --- a/ownCloud/Settings/PurchasesSettingsSection.swift +++ b/ownCloud/Settings/PurchasesSettingsSection.swift @@ -45,7 +45,7 @@ class PurchasesSettingsSection: SettingsSection { transactionsRow = StaticTableViewRow(rowWithAction: { (row, _) in row.viewController?.navigationController?.pushViewController(LicenseTransactionsViewController(), animated: true) - }, title: "Purchases".localized, accessoryType: .disclosureIndicator, identifier: "Purchases") + }, title: "Purchases & Subscriptions".localized, accessoryType: .disclosureIndicator, identifier: "purchases") } // MARK: - Update UI diff --git a/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift b/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift index c68e653a4..7d1f7a47b 100644 --- a/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift @@ -118,7 +118,7 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { onboardingSection = StaticTableViewSection(headerTitle: nil, identifier: "onboardingSection") if let message = profile.promptForHelpURL, let title = profile.helpURLButtonString { - let (proceedButton, _) = onboardingSection.addButtonFooter(message: message, messageItemStyle: .welcomeMessage, proceedLabel: title, proceedItemStyle: .informal, cancelLabel: nil) + let (proceedButton, _) = onboardingSection.addButtonFooter(message: message, messageItemStyle: .welcomeMessage, proceedLabel: title, proceedItemStyle: .welcomeInformal, cancelLabel: nil) proceedButton?.addTarget(self, action: #selector(self.helpAction), for: .touchUpInside) } @@ -419,64 +419,104 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { connection.generateAuthenticationData(withMethod: authMethodIdentifier, options: options, completionHandler: { [weak self] (error, authMethodIdentifier, authMethodData) in guard let self = self else { return } OnMainThread { + // HUD dismissal and error presentation + func dismissHUDandShowError(error: Error?, issue inIssue: OCIssue?) { + // Error + OnMainThread { + hud?.dismiss(completion: { + var issue : OCIssue? = inIssue + let nsError = error as NSError? + + if issue == nil { + if let embeddedIssue = nsError?.embeddedIssue() { + issue = embeddedIssue + } else if let error = error { + issue = OCIssue(forError: error, level: .error, issueHandler: nil) + } + } + + if nsError?.isOCError(withCode: .authorizationFailed) == true { + // Shake + self.navigationController?.view.shakeHorizontally() + OnMainThread { + self.passwordRow?.textField?.becomeFirstResponder() + } + } else { + if let loginViewController = self.loginViewController, let issue = issue { + + if let busySection = self.busySection, busySection.attached { + self.removeSection(busySection) + } + + IssuesCardViewController.present(on: loginViewController, issue: issue, completion: { [weak self, weak issue] (response) in + switch response { + case .cancel: + issue?.reject() + + case .approve: + issue?.approve() + self?.startAuthentication(nil) + + case .dismiss: break + } + }) + } + } + }) + } + } + self.isAuthenticating = false if let button = sender as? ThemeButton { spinner.removeFromSuperview() button.setTitle("Login".localized, for: .normal) button.isEnabled = true } - hud?.dismiss(completion: { - if error == nil { - bookmark.authenticationMethodIdentifier = authMethodIdentifier - bookmark.authenticationData = authMethodData - bookmark.name = self.profile.bookmarkName - bookmark.userInfo[StaticLoginProfile.staticLoginProfileIdentifierKey] = self.profile.identifier - - OCBookmarkManager.shared.addBookmark(bookmark) - - self.loginViewController?.showFirstScreen() - if ServerListTableViewController.classSetting(forOCClassSettingsKey: .accountAutoConnect) as? Bool ?? false { - self.loginViewController?.openBookmark(bookmark) - } - } else { - var issue : OCIssue? - let nsError = error as NSError? - - if let embeddedIssue = nsError?.embeddedIssue() { - issue = embeddedIssue - } else if let error = error { - issue = OCIssue(forError: error, level: .error, issueHandler: nil) - } - if nsError?.isOCError(withCode: .authorizationFailed) == true { - // Shake - self.navigationController?.view.shakeHorizontally() - OnMainThread { - self.passwordRow?.textField?.becomeFirstResponder() - } - } else { - if let loginViewController = self.loginViewController, let issue = issue { + if let error = error { + // Handle auth data generation error + dismissHUDandShowError(error: error, issue: nil) + } else { + // Auth successful, proceed with generation of bookmark + bookmark.authenticationMethodIdentifier = authMethodIdentifier + bookmark.authenticationData = authMethodData + bookmark.name = self.profile.bookmarkName + bookmark.userInfo[StaticLoginProfile.staticLoginProfileIdentifierKey] = self.profile.identifier + + // Connect to enrich bookmark with data on instance and user's displayName + OnMainThread { + hud?.updateLabel(with: "Fetching user information…".localized) + } - if let busySection = self.busySection, busySection.attached { - self.removeSection(busySection) - } + bookmark.authenticationDataStorage = .keychain // Commit auth changes to keychain + let connection = self.instantiateConnection(for: bookmark) - IssuesCardViewController.present(on: loginViewController, issue: issue, completion: { [weak self, weak issue] (response) in - switch response { - case .cancel: - issue?.reject() + connection.connect { [weak self] (error, issue) in + if error == nil { + // Add userDisplayName to bookmark + bookmark.userDisplayName = connection.loggedInUser?.displayName - case .approve: - issue?.approve() - self?.startAuthentication(nil) + connection.disconnect(completionHandler: { + OnMainThread { + // Add bookmark to OCBookmarkManager + OCBookmarkManager.shared.addBookmark(bookmark) - case .dismiss: break - } - }) - } + // Dismiss HUD and show login screen + hud?.dismiss(completion: { + self?.loginViewController?.showFirstScreen() + + if ServerListTableViewController.classSetting(forOCClassSettingsKey: .accountAutoConnect) as? Bool ?? false { + self?.loginViewController?.openBookmark(bookmark) + } + }) + } + }) + } else { + // Handle connection error + dismissHUDandShowError(error: error, issue: issue) } } - }) + } } }) } diff --git a/ownCloud/Static Login/Interface/StaticLoginViewController.swift b/ownCloud/Static Login/Interface/StaticLoginViewController.swift index 3562c556e..81961f2e8 100644 --- a/ownCloud/Static Login/Interface/StaticLoginViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginViewController.swift @@ -366,7 +366,7 @@ class StaticLoginViewController: UIViewController, Themeable, StateRestorationCo // Set up custom push transition for presentation if let navigationController = self.navigationController { if let error = error { - let alert = UIAlertController(title: NSString(format: "Error opening %@".localized as NSString, bookmark.shortName) as String, message: error.localizedDescription, preferredStyle: .alert) + let alert = ThemedAlertController(title: NSString(format: "Error opening %@".localized as NSString, bookmark.shortName) as String, message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) navigationController.present(alert, animated: true) diff --git a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift index 60f482aab..b74e434e5 100644 --- a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift +++ b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift @@ -42,9 +42,9 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { var enqueuedAssetCount = 0 if userDefaults.instantUploadPhotos == true { - if let bookmarkUUID = userDefaults.instantPhotoUploadBookmarkUUID, let path = userDefaults.instantPhotoUploadPath { + if let location = userDefaults.instantPhotoUploadLocation, let bookmarkUUID = location.bookmarkUUID { if let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { - enqueuedAssetCount += uploadPhotoAssets(for: bookmark, at: OCLocation.legacyRootPath(path)) + enqueuedAssetCount += uploadPhotoAssets(for: bookmark, at: location) } } else { Log.warning(tagged: ["INSTANT_MEDIA_UPLOAD"], "Instant photo upload enabled, but bookmark or path not configured") @@ -52,9 +52,9 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { } if userDefaults.instantUploadVideos == true { - if let bookmarkUUID = userDefaults.instantVideoUploadBookmarkUUID, let path = userDefaults.instantVideoUploadPath { + if let location = userDefaults.instantVideoUploadLocation, let bookmarkUUID = location.bookmarkUUID { if let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { - enqueuedAssetCount += uploadVideoAssets(for: bookmark, at: OCLocation.legacyRootPath(path)) + enqueuedAssetCount += uploadVideoAssets(for: bookmark, at: location) } } else { Log.warning(tagged: ["INSTANT_MEDIA_UPLOAD"], "Instant video upload enabled, but bookmark or path not configured") diff --git a/ownCloud/Tools/URL+Extensions.swift b/ownCloud/Tools/URL+Extensions.swift index 7388d3ac2..ce06cca55 100644 --- a/ownCloud/Tools/URL+Extensions.swift +++ b/ownCloud/Tools/URL+Extensions.swift @@ -13,6 +13,7 @@ import ownCloudAppShared typealias UploadHandler = (OCItem?, Error?) -> Void extension URL { + // MARK: - App scheme matching var matchesAppScheme : Bool { guard let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [Any], @@ -26,6 +27,7 @@ extension URL { return false } + // MARK: - File upload func upload(with core:OCCore?, at rootItem:OCItem, alternativeName:String? = nil, modificationDate:Date? = nil, importByCopy:Bool = false, cellularSwitchIdentifier:OCCellularSwitchIdentifier? = nil, placeholderHandler:UploadHandler? = nil, completionHandler:UploadHandler? = nil) -> Progress? { let fileName = alternativeName != nil ? alternativeName! : self.lastPathComponent var importOptions : [OCCoreOption : Any] = [.importByCopying : importByCopy, .automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue] @@ -67,8 +69,8 @@ extension URL { return progress } - func privateLinkItemID() -> String? { - + // MARK: - Private link handling (OC10) + var privateLinkItemID: String? { // Check if the link URL has format https:///f/ if self.pathComponents.count > 2 { if self.pathComponents[self.pathComponents.count - 2] == "f" { @@ -79,9 +81,9 @@ extension URL { return nil } - @discardableResult func retrieveLinkedItem(with completion: @escaping (_ item:OCItem?, _ bookmark:OCBookmark?, _ error:Error?, _ connected:Bool) -> Void) -> Bool { + @discardableResult func retrieveLinkedItem(with completion: @escaping (_ item: OCItem?, _ bookmark: OCBookmark?, _ error: Error?, _ connected: Bool) -> Void) -> Bool { // Check if the link is private ones and has item ID - guard self.privateLinkItemID() != nil else { + guard self.privateLinkItemID != nil else { return false } @@ -135,23 +137,44 @@ extension URL { return true } - func resolveAndPresent(in window:UIWindow) { + func resolveAndPresentPrivateLink(with clientContext: ClientContext) { + guard let window = (clientContext.scene as? UIWindowScene)?.windows.first else { + return + } let hud : ProgressHUDViewController? = ProgressHUDViewController(on: nil) hud?.present(on: window.rootViewController?.topMostViewController, label: "Resolving link…".localized) self.retrieveLinkedItem(with: { (item, bookmark, _, internetReachable) in - let completion = { if item == nil { let isOffline = internetReachable == false let accountFound = bookmark != nil - let alertController = ThemedAlertController.alertControllerForUnresolvedLink(offline: isOffline, accountFound: accountFound) + var message = "" + + if !accountFound { + message = "Link points to an account bookmark which is not configured in the app.".localized + } else if isOffline { + message = "Couldn't resolve a private link since you are offline and corresponding item is not cached locally.".localized + } else { + message = "Couldn't resolve a private link since the item is not known to the server.".localized + } + + let alertController = ThemedAlertController(title: "Link resolution failed".localized, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default)) + window.rootViewController?.topMostViewController.present(alertController, animated: true) } else { - if let itemID = item?.localID, let bookmark = bookmark { - window.display(itemWithID: itemID, in: bookmark) + if let item, let bookmark = bookmark { + let stateAction = AppStateAction(with: [ + .connection(with: bookmark, children: [ + .reveal(item: item) + ]) + ]) + + stateAction.run(in: clientContext, completion: { error, clientContext in + }) } } } diff --git a/ownCloud/UIKit Extensions/UIAlertController+UniversalLinks.swift b/ownCloud/UIKit Extensions/UIAlertController+UniversalLinks.swift deleted file mode 100644 index 5346beb83..000000000 --- a/ownCloud/UIKit Extensions/UIAlertController+UniversalLinks.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// UIAlertController+UniversalLinks.swift -// ownCloud -// -// Created by Michael Neuwert on 20.04.20. -// Copyright © 2020 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 ownCloudAppShared - -extension ThemedAlertController { - - class func alertControllerForUnresolvedLink(offline:Bool, accountFound:Bool) -> ThemedAlertController { - - var message = "" - - if !accountFound { - message = "Link points to an account bookmark which is not configured in the app.".localized - } else if offline { - message = "Couldn't resolve a private link since you are offline and corresponding item is not cached locally.".localized - } else { - message = "Couldn't resolve a private link since the item is not known to the server.".localized - } - - let alert = ThemedAlertController(title: "Link resolution failed".localized, message: message, preferredStyle: .alert) - - let okAction = UIAlertAction(title: "OK", style: .default) - - alert.addAction(okAction) - - return alert - } -} diff --git a/ownCloud/UIKit Extensions/UIWindow+Extension.swift b/ownCloud/UIKit Extensions/UIWindow+Extension.swift deleted file mode 100644 index d802293c7..000000000 --- a/ownCloud/UIKit Extensions/UIWindow+Extension.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// UIWindow+Extension.swift -// ownCloud -// -// Created by Michael Neuwert on 31.01.20. -// Copyright © 2020 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 ownCloudAppShared - -extension UIWindow { - func display(itemWithID Identifier:String, in bookmark:OCBookmark) { - if let rootViewController = self.rootViewController as? ThemeNavigationController { - rootViewController.popToRootViewController(animated: false) - if let serverListController = rootViewController.topViewController as? StateRestorationConnectProtocol { - if let viewController = serverListController as? UIViewController, viewController.presentedViewController != nil { - viewController.dismiss(animated: false, completion: { - serverListController.connect(to: bookmark, lastVisibleItemId: Identifier, animated: false, present: nil) - }) - } else { - serverListController.connect(to: bookmark, lastVisibleItemId: Identifier, animated: false, present: nil) - } - } - } - } -} diff --git a/ownCloudAppFramework/AppLock Settings/AppLockSettings.h b/ownCloudAppFramework/AppLock Settings/AppLockSettings.h index 13fc01a6c..941153565 100644 --- a/ownCloudAppFramework/AppLock Settings/AppLockSettings.h +++ b/ownCloudAppFramework/AppLock Settings/AppLockSettings.h @@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN @property(assign,nonatomic) BOOL lockEnabled; @property(assign,nonatomic) NSInteger lockDelay; @property(assign,nonatomic) BOOL biometricalSecurityEnabled; +@property(assign,nonatomic) BOOL biometricalSecurityEnabledinShareSheet; +@property(readonly,nonatomic,nullable) NSURL *biometricalAuthenticationRedirectionTargetURL; @property(readonly,nonatomic) BOOL isPasscodeEnforced; @property(readonly,nonatomic) NSInteger requiredPasscodeDigits; @@ -46,5 +48,6 @@ extern OCClassSettingsKey OCClassSettingsKeyRequiredPasscodeDigits; extern OCClassSettingsKey OCClassSettingsKeyMaximumPasscodeDigits; extern OCClassSettingsKey OCClassSettingsKeyPasscodeLockDelay; extern OCClassSettingsKey OCClassSettingsKeyPasscodeUseBiometricalUnlock; +extern OCClassSettingsKey OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp; NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/AppLock Settings/AppLockSettings.m b/ownCloudAppFramework/AppLock Settings/AppLockSettings.m index 46622d520..25c57542d 100644 --- a/ownCloudAppFramework/AppLock Settings/AppLockSettings.m +++ b/ownCloudAppFramework/AppLock Settings/AppLockSettings.m @@ -17,6 +17,7 @@ */ #import "AppLockSettings.h" +#import "Branding.h" @implementation AppLockSettings @@ -54,7 +55,19 @@ + (OCClassSettingsIdentifier)classSettingsIdentifier OCClassSettingsKeyPasscodeEnforced : @(NO), OCClassSettingsKeyRequiredPasscodeDigits : @(4), OCClassSettingsKeyMaximumPasscodeDigits : @(6), - OCClassSettingsKeyPasscodeUseBiometricalUnlock : @(NO) + OCClassSettingsKeyPasscodeUseBiometricalUnlock : @(NO), + OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp : @{ + @"default" : @{ + @"allow" : @(YES) + }, + + // For unknown reasons invoking biometric authentication from the + // share sheet in Boxer leads to dismissal of the entire share sheet, + // so (as of July 2022) we hardcode it as an exception here + @"com.air-watch.boxer" : @{ + @"allow" : @(NO) + } + } }); } @@ -89,9 +102,16 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata OCClassSettingsMetadataKeyCategory : @"Passcode" }, - OCClassSettingsKeyPasscodeUseBiometricalUnlock : @{ - OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, - OCClassSettingsMetadataKeyDescription : @"Controls wether the biometrical unlock will be enabled automatically.", + OCClassSettingsKeyPasscodeUseBiometricalUnlock : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, + OCClassSettingsMetadataKeyDescription : @"Controls wether the biometrical unlock will be enabled automatically.", + OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusAdvanced, + OCClassSettingsMetadataKeyCategory : @"Passcode" + }, + + OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeDictionary, + OCClassSettingsMetadataKeyDescription : @"Controls the biometrical unlock availability in the share sheet, with per-app level control.", OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusAdvanced, OCClassSettingsMetadataKeyCategory : @"Passcode" } @@ -128,14 +148,28 @@ - (void)setLockDelay:(NSInteger)lockDelay - (BOOL)biometricalSecurityEnabled { - NSNumber *useBiometricalUnlock; + NSNumber *useBiometricalUnlockNumber; + BOOL useBiometricalUnlock = NO; - if ((useBiometricalUnlock = [_userDefaults objectForKey:@"security-settings-use-biometrical"]) != nil) + if ((useBiometricalUnlockNumber = [_userDefaults objectForKey:@"security-settings-use-biometrical"]) != nil) { - return (useBiometricalUnlock.boolValue); + useBiometricalUnlock = useBiometricalUnlockNumber.boolValue; + } + else + { + useBiometricalUnlock = [[self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeUseBiometricalUnlock] boolValue]; } - return ([[self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeUseBiometricalUnlock] boolValue]); + if (useBiometricalUnlock) + { + // Apple share extension specific settings + if ([OCAppIdentity.sharedAppIdentity.componentIdentifier isEqual:OCAppComponentIdentifierShareExtension]) + { + return ([self biometricalSecurityEnabledinShareSheet]); + } + } + + return (useBiometricalUnlock); } - (void)setBiometricalSecurityEnabled:(BOOL)biometricalSecurityEnabled @@ -143,6 +177,102 @@ - (void)setBiometricalSecurityEnabled:(BOOL)biometricalSecurityEnabled [_userDefaults setBool:biometricalSecurityEnabled forKey:@"security-settings-use-biometrical"]; } +- (NSDictionary *)_shareSheetBiometricalAttributesForApp:(NSString *)hostAppID +{ + NSDictionary *shareSheetBiometricalUnlockByApp = [self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp]; + NSDictionary *attributesForApp = nil; + + if ([shareSheetBiometricalUnlockByApp isKindOfClass:NSDictionary.class]) + { + if (shareSheetBiometricalUnlockByApp[hostAppID] != nil) + { + attributesForApp = OCTypedCast(shareSheetBiometricalUnlockByApp[hostAppID], NSDictionary); + } + else + { + attributesForApp = OCTypedCast(shareSheetBiometricalUnlockByApp[@"default"], NSDictionary); + } + } + + return (attributesForApp); +} + +- (NSDictionary *)_shareSheetBiometricalAttributes +{ + NSString *hostAppID; + + if ((hostAppID = OCAppIdentity.sharedAppIdentity.hostAppBundleIdentifier) == nil) + { + hostAppID = @"default"; + } + + return ([self _shareSheetBiometricalAttributesForApp:hostAppID]); +} + +- (BOOL)biometricalSecurityEnabledinShareSheet +{ + NSNumber *useBiometricalUnlock; + + if ((useBiometricalUnlock = [_userDefaults objectForKey:@"security-settings-use-biometrical-share-sheet"]) != nil) + { + return (useBiometricalUnlock.boolValue); + } + + NSDictionary *shareSheetAttributesForApp = nil; + + if ((shareSheetAttributesForApp = [self _shareSheetBiometricalAttributes]) != nil) + { + NSNumber *enabled; + + if ((enabled = OCTypedCast(shareSheetAttributesForApp[@"allow"], NSNumber)) != nil) + { + return (enabled.boolValue); + } + } + + return (YES); +} + +- (void)setBiometricalSecurityEnabledinShareSheet:(BOOL)biometricalSecurityEnabledinShareSheet +{ + [_userDefaults setBool:biometricalSecurityEnabledinShareSheet forKey:@"security-settings-use-biometrical-share-sheet"]; +} + +- (NSURL *)biometricalAuthenticationRedirectionTargetURL +{ + if ([OCAppIdentity.sharedAppIdentity.componentIdentifier isEqual:OCAppComponentIdentifierShareExtension]) + { + // Only in share extension + NSDictionary *shareSheetAttributesForApp = nil; + + if ((shareSheetAttributesForApp = [self _shareSheetBiometricalAttributes]) != nil) + { + NSString *trampolineURLString; + + // For apps with a trampoline URL, determine the target URL to initiate the authentication trampoline + if ((trampolineURLString = shareSheetAttributesForApp[@"trampoline-url"]) != nil) + { + NSString *toAppURLScheme; + + if ((toAppURLScheme = [Branding.sharedBranding appURLSchemesForBundleURLName:nil].firstObject) != nil) + { + NSString *targetURLString = [NSString stringWithFormat:@"%@://?authenticateForApp=%@", toAppURLScheme, OCAppIdentity.sharedAppIdentity.hostAppBundleIdentifier]; + + return ([NSURL URLWithString:targetURLString]); + } + } + } + } + + return (nil); +} + +// Counterpart to .biometricalAuthenticationRedirectionTargetURL for use in the app (not implemented) +//- (NSURL *)biometricalAuthenticationReturnURL +//{ +// return (nil); +//} + - (BOOL)isPasscodeEnforced { NSNumber *isPasscodeEnforced = [self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeEnforced]; @@ -190,3 +320,4 @@ - (BOOL)lockDelayUserSettable OCClassSettingsKey OCClassSettingsKeyMaximumPasscodeDigits = @"maximumPasscodeDigits"; OCClassSettingsKey OCClassSettingsKeyPasscodeLockDelay = @"lockDelay"; OCClassSettingsKey OCClassSettingsKeyPasscodeUseBiometricalUnlock = @"use-biometrical-unlock"; +OCClassSettingsKey OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp = @"share-sheet-biometrical-unlock-by-app"; diff --git a/ownCloudAppFramework/Branding/Branding.h b/ownCloudAppFramework/Branding/Branding.h index 20ddc801f..8c7a00faa 100644 --- a/ownCloudAppFramework/Branding/Branding.h +++ b/ownCloudAppFramework/Branding/Branding.h @@ -44,6 +44,8 @@ typedef NSString* BrandingImageName NS_TYPED_EXTENSIBLE_ENUM; @property(strong,nullable,nonatomic,readonly) NSBundle *appBundle; //!< Bundle of the main app +- (NSArray *)appURLSchemesForBundleURLName:(nullable NSString *)bundleURLName; //!< URL schemes from the app's Info.plist matching the provided CFBundleURLName. + @property(strong) NSDictionary *legacyKeyPathsByClassSettingsKeys; - (void)registerLegacyKeyPath:(BrandingLegacyKeyPath)keyPath forClassSettingsKey:(OCClassSettingsKey)classSettingsKey; diff --git a/ownCloudAppFramework/Branding/Branding.m b/ownCloudAppFramework/Branding/Branding.m index 52b26d5b4..58f0bd5b7 100644 --- a/ownCloudAppFramework/Branding/Branding.m +++ b/ownCloudAppFramework/Branding/Branding.m @@ -163,6 +163,35 @@ - (NSDictionary *)userDefaultsDefaultValues return ([self computedValueForClassSettingsKey:BrandingKeyUserDefaultsDefaultValues]); } +- (NSArray *)appURLSchemesForBundleURLName:(nullable NSString *)bundleURLName +{ + NSBundle *appBundle; + NSMutableArray *appURLSchemes = [NSMutableArray new]; + + if ((appBundle = self.appBundle) != nil) + { + NSArray *urlSchemeDictionaries; + + if ((urlSchemeDictionaries = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) != nil) + { + for (NSDictionary *urlSchemesDict in urlSchemeDictionaries) + { + if ((bundleURLName == nil) || [bundleURLName isEqual:urlSchemesDict[@"CFBundleURLName"]]) + { + NSArray *urlSchemes; + + if ((urlSchemes = urlSchemesDict[@"CFBundleURLSchemes"]) != nil) + { + [appURLSchemes addObjectsFromArray:urlSchemes]; + } + } + } + } + } + + return (appURLSchemes); +} + - (NSArray *)disabledImportMethods { return ([self computedValueForClassSettingsKey:BrandingKeyDisabledImportMethods]); diff --git a/ownCloudAppFramework/Display Settings/DisplaySettings.h b/ownCloudAppFramework/Display Settings/DisplaySettings.h index e31c755dd..6cd7392cd 100644 --- a/ownCloudAppFramework/Display Settings/DisplaySettings.h +++ b/ownCloudAppFramework/Display Settings/DisplaySettings.h @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN @interface DisplaySettings : NSObject #pragma mark - Singleton -@property(class,retain,nonatomic,readonly) DisplaySettings *sharedDisplaySettings; +@property(class,strong,nonatomic,readonly) DisplaySettings *sharedDisplaySettings; #pragma mark - Show hidden files @property(assign,nonatomic) BOOL showHiddenFiles; @@ -35,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Drag files @property(assign,nonatomic) BOOL preventDraggingFiles; +#pragma mark - Query condition +@property(nonatomic,readonly,nullable) OCQueryCondition *queryConditionForDisplaySettings; + #pragma mark - Query updating - (void)updateQueryWithDisplaySettings:(OCQuery *)query; diff --git a/ownCloudAppFramework/Display Settings/DisplaySettings.m b/ownCloudAppFramework/Display Settings/DisplaySettings.m index dd4384101..078522f15 100644 --- a/ownCloudAppFramework/Display Settings/DisplaySettings.m +++ b/ownCloudAppFramework/Display Settings/DisplaySettings.m @@ -206,6 +206,24 @@ - (void)updateQueryWithDisplaySettings:(OCQuery *)query } } +#pragma mark - Query condition +- (OCQueryCondition *)queryConditionForDisplaySettings +{ + if (!_showHiddenFiles) + { + return ([OCQueryCondition require:@[ + // Exclude root folder as item + [OCQueryCondition where:OCItemPropertyNamePath isNotEqualTo:@"/"], + + // Exclude hidden files + [OCQueryCondition negating:YES condition:[OCQueryCondition where:OCItemPropertyNamePath contains:@"/."]] + ]]); + } + + // Exclude root folder as item + return ([OCQueryCondition where:OCItemPropertyNamePath isNotEqualTo:@"/"]); +} + #pragma mark - Query filter - (BOOL)query:(OCQuery *)query shouldIncludeItem:(OCItem *)item { diff --git a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.h b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.h new file mode 100644 index 000000000..6e218867a --- /dev/null +++ b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.h @@ -0,0 +1,33 @@ +// +// OCFileProviderSettings.h +// ownCloudApp +// +// Created by Felix Schwarz on 25.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OCFileProviderSettings : NSObject + +@property(class,readonly,nonatomic) BOOL browseable; + +@end + +extern OCClassSettingsIdentifier OCClassSettingsIdentifierFileProvider; +extern OCClassSettingsKey OCClassSettingsKeyFileProviderBrowseable; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m new file mode 100644 index 000000000..88869cd39 --- /dev/null +++ b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m @@ -0,0 +1,55 @@ +// +// OCFileProviderSettings.m +// ownCloudApp +// +// Created by Felix Schwarz on 25.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 "OCFileProviderSettings.h" + +@implementation OCFileProviderSettings + ++ (OCClassSettingsIdentifier)classSettingsIdentifier +{ + return (OCClassSettingsIdentifierFileProvider); +} + ++ (nullable NSDictionary *)defaultSettingsForIdentifier:(nonnull OCClassSettingsIdentifier)identifier { + return (@{ + OCClassSettingsKeyFileProviderBrowseable : @(YES) + }); +} + ++ (OCClassSettingsMetadataCollection)classSettingsMetadata +{ + return (@{ + // FileProvider + OCClassSettingsKeyFileProviderBrowseable : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, + OCClassSettingsMetadataKeyDescription : @"Controls whether the account content is available to other apps via File Provider / Files.app.", + OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusSupported, + OCClassSettingsMetadataKeyCategory : @"FileProvider", + } + }); +} + ++ (BOOL)browseable +{ + return ([([self classSettingForOCClassSettingsKey:OCClassSettingsKeyFileProviderBrowseable]) boolValue]); +} + +@end + +OCClassSettingsIdentifier OCClassSettingsIdentifierFileProvider = @"fileprovider"; +OCClassSettingsKey OCClassSettingsKeyFileProviderBrowseable = @"browseable"; diff --git a/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.h b/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.h index 6ceecfa63..310635cf8 100644 --- a/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.h +++ b/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN @interface NSObject (AnnotatedProperties) +- (id)valueForAnnotatedProperty:(NSString *)annotatedPropertyName withGenerator:(id(^)(void))generator; + - (nullable id)valueForAnnotatedProperty:(NSString *)annotatedPropertyName; - (void)setValue:(nullable id)value forAnnotatedProperty:(NSString *)annotatedPropertyName; diff --git a/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.m b/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.m index 5210721bf..27d1cf5d2 100644 --- a/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.m +++ b/ownCloudAppFramework/Foundation Extensions/NSObject+AnnotatedProperties.m @@ -53,4 +53,21 @@ - (void)setValue:(nullable id)value forAnnotatedProperty:(NSString *)annotatedPr } } +- (id)valueForAnnotatedProperty:(NSString *)annotatedPropertyName withGenerator:(id(^)(void))generator +{ + id value = nil; + + @synchronized(self) + { + if ((value = [self valueForAnnotatedProperty:annotatedPropertyName]) == nil) + { + value = generator(); + + [self setValue:value forAnnotatedProperty:annotatedPropertyName]; + } + } + + return (value); +} + @end diff --git a/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m b/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m index d31b12c3a..5057a2018 100644 --- a/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m +++ b/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m @@ -24,7 +24,7 @@ - (OCLicenseEnvironment *)licenseEnvironment { OCLicenseEnvironment *environment = nil; - environment = [OCLicenseEnvironment environmentWithIdentifier:nil hostname:self.bookmark.url.host certificate:self.bookmark.certificate attributes:nil]; + environment = [OCLicenseEnvironment environmentWithIdentifier:nil hostname:self.bookmark.url.host certificate:self.bookmark.primaryCertificate attributes:nil]; environment.bookmarkUUID = self.bookmark.uuid; environment.core = self; diff --git a/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m b/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m index 74003b661..ec4c81474 100644 --- a/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m +++ b/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m @@ -40,7 +40,7 @@ + (instancetype)environmentWithBookmark:(OCBookmark *)bookmark environment.bookmarkUUID = bookmark.uuid; environment.bookmark = bookmark; environment.hostname = bookmark.url.host; - environment.certificate = bookmark.certificate; + environment.certificate = bookmark.primaryCertificate; return (environment); } diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h index eb349e971..501fcac86 100644 --- a/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h +++ b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h @@ -43,6 +43,8 @@ typedef NS_ENUM(NSInteger, OCLicenseAppStoreProviderError) @property(nullable,strong,readonly,nonatomic) OCLicenseAppStoreReceipt *receipt; +@property(strong,readonly,class) NSURL *appStoreManagementURL; + @property(strong) NSArray *items; @property(nonatomic,readonly) BOOL purchasesAllowed; diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m index 75a7bf723..14a4e2ff8 100644 --- a/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m +++ b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m @@ -53,6 +53,11 @@ @interface OCLicenseAppStoreProvider () *)items { @@ -187,7 +192,7 @@ - (void)retrieveTransactionsWithCompletionHandler:(void (^)(NSError * _Nullable, cancellationDate:iap.cancellationDate]; if ((transaction.type == OCLicenseTypeSubscription) && (iap.subscriptionExpirationDate.timeIntervalSinceNow > 0) && ((iap.cancellationDate==nil) || (iap.cancellationDate.timeIntervalSinceNow > 0))) { - transaction.links = @{ OCLocalized(@"Manage subscription") : [NSURL URLWithString:@"https://apps.apple.com/account/subscriptions"] }; + transaction.links = @{ OCLocalized(@"Manage subscription") : OCLicenseAppStoreProvider.appStoreManagementURL }; } [transactions addObject:transaction]; diff --git a/ownCloudAppFramework/Licensing/Providers/EMM/OCLicenseEMMProvider.m b/ownCloudAppFramework/Licensing/Providers/EMM/OCLicenseEMMProvider.m index 3fd080a54..232cdfb9e 100644 --- a/ownCloudAppFramework/Licensing/Providers/EMM/OCLicenseEMMProvider.m +++ b/ownCloudAppFramework/Licensing/Providers/EMM/OCLicenseEMMProvider.m @@ -23,7 +23,7 @@ @implementation OCLicenseEMMProvider #pragma mark - Init - (instancetype)initWithUnlockedProductIdentifiers:(NSArray *)unlockedProductIdentifiers { - if ((self = [super initWithIdentifier:OCLicenseProviderIdentifierEnterprise]) != nil) + if ((self = [super initWithIdentifier:OCLicenseProviderIdentifierEMM]) != nil) { _unlockedProductIdentifiers = unlockedProductIdentifiers; self.localizedName = OCLocalized(@"EMM"); @@ -32,26 +32,25 @@ - (instancetype)initWithUnlockedProductIdentifiers:(NSArray. + * + */ + +#import "OCLicenseProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol OCLicenseQAProviderDelegate +@property(readonly) BOOL isQALicenseUnlockPossible; +@end + +@interface OCLicenseQAProvider : OCLicenseProvider + +@property(class,strong,readonly,nonatomic) OCLicenseQAProvider *sharedProvider; //!< Set to the first instantiated instance + +@property(class,nonatomic) BOOL isQAUnlockEnabled; +@property(class,readonly,nonatomic) BOOL isQAUnlockPossible; + +@property(strong,readonly) NSArray *unlockedProductIdentifiers; + +- (instancetype)initWithUnlockedProductIdentifiers:(NSArray *)unlockedProductIdentifiers delegate:(id)delegate; + +- (void)updateEntitlements; + +@end + +extern OCLicenseProviderIdentifier OCLicenseProviderIdentifierQA; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/QA/OCLicenseQAProvider.m b/ownCloudAppFramework/Licensing/Providers/QA/OCLicenseQAProvider.m new file mode 100644 index 000000000..80ea94091 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/QA/OCLicenseQAProvider.m @@ -0,0 +1,160 @@ +// +// OCLicenseQAProvider.m +// ownCloudAppShared +// +// Created by Felix Schwarz on 23.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 +#import "OCLicenseQAProvider.h" +#import "OCLicenseEntitlement.h" +#import "OCLicenseProduct.h" +#import "OCLicenseFeature.h" +#import "OCLicenseManager.h" + +static OCLicenseQAProvider *sharedProvider = nil; + +@implementation OCLicenseQAProvider +{ + id _delegate; +} + +#pragma mark - Shared instance ++ (void)setSharedProvider:(OCLicenseQAProvider *)provider +{ + sharedProvider = provider; +} + ++ (OCLicenseQAProvider *)sharedProvider +{ + return (sharedProvider); +} + +#pragma mark - Init +- (instancetype)initWithUnlockedProductIdentifiers:(NSArray *)unlockedProductIdentifiers delegate:(id)delegate +{ + if ((self = [super initWithIdentifier:OCLicenseProviderIdentifierQA]) != nil) + { + _unlockedProductIdentifiers = unlockedProductIdentifiers; + _delegate = delegate; + + self.localizedName = @"QA"; + + [OCLicenseQAProvider setSharedProvider:self]; + } + + return (self); +} + +#pragma mark - Providing and updating entitlements +- (void)startProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + [self updateEntitlements]; + + completionHandler(self, nil); +} + +- (void)updateEntitlements +{ + NSMutableArray *entitlements = [NSMutableArray new]; + + if (OCLicenseQAProvider.isQAUnlockEnabled && OCLicenseQAProvider.isQAUnlockPossible) + { + for (OCLicenseProductIdentifier productIdentifier in self.unlockedProductIdentifiers) + { + OCLicenseEntitlement *entitlement; + + entitlement = [OCLicenseEntitlement entitlementWithIdentifier:nil forProduct:productIdentifier type:OCLicenseTypePurchase valid:YES expiryDate:nil applicability:nil]; // Valid entitlement for all environments + + [entitlements addObject:entitlement]; + } + } + + self.entitlements = (entitlements.count > 0) ? entitlements : nil; +} + +#pragma mark - Unlock message +- (nullable OCLicenseProduct *)_unlockedProductForFeature:(OCLicenseFeatureIdentifier)featureIdentifier +{ + for (OCLicenseProductIdentifier productIdentifier in self.unlockedProductIdentifiers) + { + OCLicenseProduct *product; + + if ((product = [self.manager productWithIdentifier:productIdentifier]) != nil) + { + if (featureIdentifier != nil) + { + if ([product.contents containsObject:featureIdentifier]) + { + return (product); + } + } + else + { + return (product); + } + } + } + + return (nil); +} + +- (NSString *)inAppPurchaseMessageForFeature:(OCLicenseFeatureIdentifier)featureIdentifier +{ + NSString *iapMessage = nil; + OCLicenseProduct *unlockedProduct = nil; + OCLicenseFeature *feature = nil; + + if (featureIdentifier != nil) + { + feature = [self.manager featureWithIdentifier:featureIdentifier]; + } + + if ((unlockedProduct = [self _unlockedProductForFeature:featureIdentifier]) != nil) + { + if (OCLicenseQAProvider.isQAUnlockEnabled && OCLicenseQAProvider.isQAUnlockPossible) + { + NSString *subject = (feature.localizedName != nil) ? feature.localizedName : unlockedProduct.localizedName; + iapMessage = [NSString stringWithFormat:OCLocalized(@"%@ unlocked for QA."), subject]; + } + } + + return (iapMessage); +} + +#pragma mark - ++ (BOOL)isQAUnlockEnabled +{ + return ([OCAppIdentity.sharedAppIdentity.userDefaults boolForKey:@"qa.license-unlock-enabled"]); +} + ++ (void)setIsQAUnlockEnabled:(BOOL)isQAUnlockEnabled +{ + [OCAppIdentity.sharedAppIdentity.userDefaults setBool:isQAUnlockEnabled forKey:@"qa.license-unlock-enabled"]; + [OCLicenseQAProvider.sharedProvider updateEntitlements]; +} + +- (BOOL)isQAUnlockPossible +{ + return ([_delegate isQALicenseUnlockPossible]); +} + ++ (BOOL)isQAUnlockPossible +{ + return ([OCLicenseQAProvider.sharedProvider isQAUnlockPossible]); +} + +@end + +OCLicenseProviderIdentifier OCLicenseProviderIdentifierQA = @"qa"; diff --git a/ownCloudAppFramework/Resources/de.lproj/Localizable.strings b/ownCloudAppFramework/Resources/de.lproj/Localizable.strings index bd8c0df0f..d3c3daaab 100644 --- a/ownCloudAppFramework/Resources/de.lproj/Localizable.strings +++ b/ownCloudAppFramework/Resources/de.lproj/Localizable.strings @@ -8,6 +8,8 @@ "App Store" = "App Store"; "Purchases are not allowed on this device." = "Käufe sind auf diesem Gerät nicht erlaubt."; +"In-app purchases are not supported for copies purchased through the Volume Purchase Program. For access to additional features, please purchase the EMM version." = "In-App Käufe werden von Kopien die über das Volume Purchase Program bestellt wurden nicht unterstützt. Kaufen Sie bitte die EMM-Version um Zugriff auf zusätzliche Features zu erhalten."; + "Purchased App Version" = "Gekaufte App-Version"; "Receipt Date" = "Rechnungsdatum"; "Manage subscription" = "Abonnement verwalten"; @@ -54,6 +56,11 @@ "keyword_folder" = "ordner"; "keyword_image" = "bild"; "keyword_video" = "video"; +"keyword_audio" = "audio"; +"keyword_document" = "dokument"; +"keyword_spreadsheet" = "tabelle"; +"keyword_presentation" = "präsentation"; +"keyword_pdf" = "pdf"; "keyword_today" = "heute"; "keyword_week" = "woche"; "keyword_month" = "monat"; @@ -62,3 +69,66 @@ "keyword_w" = "w"; /* short-form for "week" */ "keyword_m" = "m"; /* short-form for "month" */ "keyword_y" = "j"; /* short-form for "year" */ + +/* Search token labels */ +"No folder" = "Kein Ordner"; +"Folder" = "Ordner"; + +"No file" = "Keine Datei"; +"File" = "Datei"; + +"No image" = "Kein Bild"; +"Image" = "Bild"; + +"No video" = "Kein Video"; +"Video" = "Video"; + +"No audio" = "Kein Audio"; +"Audio" = "Audio"; + +"Document" = "Dokument"; +"No Document" = "Kein Dokument"; + +"Spreadsheet" = "Tabelle"; +"No spreadsheet" = "Keine Tabelle"; + +"Presentation" = "Präsentation"; +"No presentation" = "Keine Präsentation"; + +"PDF" = "PDF"; +"No PDF" = "Kein PDF"; + +"Before" = "Vor"; +"After" = "nach"; + +"Not on" = "Nicht am"; +"On" = "Am"; +"Not" = "Nicht"; + +"Before today" = "Vor heute"; +"Before yesterday" = "Vor gestern"; +">%d days ago" = "Vor >%d Tagen"; +"Today" = "Heute"; +"Since yesterday" = "Seit gestern"; +"Last %d days" = "Letzte %d Tage"; + +"Before this week" = "Vor dieser Woche"; +"Before last week" = "Vor letzter Woche"; +">%d weeks ago" = "Vor >%d Wochen"; +"This week" = "Diese Woche"; +"Since last week" = "Seit letzter WOche"; +"Last %d weeks" = "Letzte %d Wochen"; + +"Before this month" = "Vor diesem Monat"; +"Before last month" = "Vor letztem Monat"; +"> %d months ago" = "Vor > %d Monaten"; +"This month" = "Diesen Monat"; +"Since last month" = "Seit letztem Monat"; +"Last %d months" = "Letzte %d Monate"; + +"Before this year" = "Vor diesem Jahr"; +"Before last year" = "Vor letztem Jahr"; +"> %d years ago" = "Vor > %d Jahren"; +"This year" = "Dieses Jahr"; +"Since last year" = "Seit letztem Jahr"; +"Last %d years" = "Letzte %d Jahre"; diff --git a/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.h b/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.h index 1cdcc802f..864dbfe09 100644 --- a/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.h +++ b/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.h @@ -37,7 +37,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OCQueryCondition (SearchSegmentDescription) -@property(strong,nonatomic,nullable) NSString *symbolName; //!< Optional, name of symbol to use +@property(strong,nonatomic,nullable) OCSymbolName symbolName; //!< Optional, name of symbol to use @property(strong,nonatomic,nullable) NSString *localizedDescription; //!< Optional, localized description @property(strong,nonatomic,nullable) NSString *searchSegment; //!< Optional, search segment from which this condition was created diff --git a/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.m index b2268690a..3c6ad3a5f 100644 --- a/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/Search/OCQueryCondition+SearchSegmenter.m @@ -623,12 +623,12 @@ - (void)setValue:(id)value forMutableUserInfoKey:(OCQueryConditionUserInfoKey)ke } -- (NSString *)symbolName +- (OCSymbolName)symbolName { return (self.userInfo[OCQueryConditionUserInfoKeySymbolName]); } -- (void)setSymbolName:(NSString *)symbolName +- (void)setSymbolName:(OCSymbolName)symbolName { [self setValue:symbolName forMutableUserInfoKey:OCQueryConditionUserInfoKeySymbolName]; } diff --git a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h index 4b1cc5068..91aeb1e75 100644 --- a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h +++ b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OCSavedSearch : NSObject -@property(readonly,strong) OCSavedSearchUUID uuid; //!< Unique ID of the saved search +@property(strong) OCSavedSearchUUID uuid; //!< Unique ID of the saved search @property(strong) OCSavedSearchScope scope; //!< The scope of the saved search @@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN @property(strong,nullable) OCLocation *location; //!< The location for saved searches with folder or drive scope @property(strong,nonatomic) NSString *name; //!< User-chosen or autogenerated name of the saved search. Falls back to .searchTerm if none was provided. +@property(readonly,nonatomic) BOOL isNameUserDefined; //!< User defined the name @property(strong) NSString *searchTerm; //!< Search term parseable by OCQueryCondition+SearchSegmenter @property(strong,nullable) NSDictionary *userInfo; //!< Userinfo for richer UI presentation, only use types from OCEvent.safeClasses! diff --git a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m index edc64f4e3..d4cfa8f51 100644 --- a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m +++ b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m @@ -41,6 +41,11 @@ - (NSString *)name return ((_name != nil) ? _name : _searchTerm); } +- (BOOL)isNameUserDefined +{ + return (_name != nil); +} + #pragma mark - Data item & Data item versioning - (OCDataItemType)dataItemType { diff --git a/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h new file mode 100644 index 000000000..5b081780c --- /dev/null +++ b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h @@ -0,0 +1,29 @@ +// +// UIViewController+HostBundleID.h +// ownCloud +// +// Created by Felix Schwarz on 12.07.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 UIViewController (HostBundleID) + +@property(nullable,readonly) NSString *oc_hostAppBundleIdentifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m new file mode 100644 index 000000000..7a8711c95 --- /dev/null +++ b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m @@ -0,0 +1,34 @@ +// +// UIViewController+HostBundleID.m +// ownCloud +// +// Created by Felix Schwarz on 12.07.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 "UIViewController+HostBundleID.h" +#import + +@implementation UIViewController (HostBundleID) + +- (NSString *)oc_hostAppBundleIdentifier +{ + @try { + return ([self valueForKey:@"_hostBundleID"]); + } @catch (NSException *exception) { + } + + return (nil); +} + +@end diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 543e790eb..983c3a59a 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -37,12 +37,15 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import #import +#import + #import #import #import #import #import #import +#import #import #import @@ -66,6 +69,8 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import +#import + #import #import diff --git a/ownCloudAppShared/AppLock/AppLockManager.swift b/ownCloudAppShared/AppLock/AppLockManager.swift index 7f7e9e202..3031e6d28 100644 --- a/ownCloudAppShared/AppLock/AppLockManager.swift +++ b/ownCloudAppShared/AppLock/AppLockManager.swift @@ -37,8 +37,16 @@ public class AppLockManager: NSObject { // MARK: - State private var lastApplicationBackgroundedDate : Date? { - didSet { - if let date = lastApplicationBackgroundedDate { + get { + if let archivedData = self.keychain?.readDataFromKeychainItem(forAccount: keychainAccount, path: keychainLockedDate) { + guard let value = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSDate.self, from: archivedData) else { return nil } + return value as Date + } + + return nil + } + set(newValue) { + if let date = newValue { let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: date as NSDate, requiringSecureCoding: true) self.keychain?.write(archivedData, toKeychainItemForAccount: keychainAccount, path: keychainLockedDate) } else { @@ -47,9 +55,17 @@ public class AppLockManager: NSObject { } } - public var unlocked: Bool = false { - didSet { - let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: unlocked as NSNumber, requiringSecureCoding: true) + public var unlocked: Bool { + get { + if let archivedData = self.keychain?.readDataFromKeychainItem(forAccount: keychainAccount, path: keychainUnlocked) { + guard let value = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: archivedData)?.boolValue else { return false} + return value + } + + return false + } + set(newValue) { + let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: newValue as NSNumber, requiringSecureCoding: true) self.keychain?.write(archivedData, toKeychainItemForAccount: keychainAccount, path: keychainUnlocked) } } @@ -124,6 +140,10 @@ public class AppLockManager: NSObject { // Set a view controller only, if you want to use it in an extension, when UIWindow is not working public var passwordViewHostViewController: UIViewController? + private var biometricalSecurityEnabled: Bool { + return AppLockSettings.shared.biometricalSecurityEnabled + } + // MARK: - Init public static var shared = AppLockManager() @@ -148,17 +168,26 @@ public class AppLockManager: NSObject { } // MARK: - Show / Dismiss Passcode View - public func showLockscreenIfNeeded(forceShow: Bool = false, setupMode: Bool = false, context: LAContext = LAContext()) { + public func showLockscreenIfNeeded(forceShow: Bool = false, setupMode: Bool = false, context: LAContext? = nil) { if self.shouldDisplayLockscreen || forceShow || setupMode { lockscreenOpenForced = forceShow lockscreenOpen = true - // Show biometrical - if !forceShow, !self.shouldDisplayCountdown, self.biometricalAuthenticationSucceeded { - showBiometricalAuthenticationInterface(context: context) - } else if setupMode { - showBiometricalAuthenticationInterface(context: context) - } + // The following code needs to be executed after a short delay, because in the share sheet the biometrical unlock UI can block adding the PasscodeViewController UI + var delay = 0.0 + if self.passwordViewHostViewController != nil { + delay = 0.5 + } + OnMainThread(after: delay) { + // Show biometrical + if !forceShow, !self.shouldDisplayCountdown, self.biometricalAuthenticationSucceeded { + self.showBiometricalAuthenticationInterface(context: context) + } else if setupMode { + self.showBiometricalAuthenticationInterface(context: context) + } + } + } else { + dismissLockscreen(animated: true) } } @@ -293,15 +322,14 @@ public class AppLockManager: NSObject { passcodeViewController = PasscodeViewController(biometricalHandler: { (passcodeViewController) in if !self.shouldDisplayCountdown { - let context = LAContext() - self.showBiometricalAuthenticationInterface(context: context) + self.showBiometricalAuthenticationInterface() } }, completionHandler: { (viewController: PasscodeViewController, passcode: String) in self.attemptUnlock(with: passcode, passcodeViewController: viewController) }, requiredLength: AppLockManager.shared.passcode?.count ?? AppLockSettings.shared.requiredPasscodeDigits) passcodeViewController.message = "Enter code".localized - passcodeViewController.cancelButtonHidden = false + passcodeViewController.cancelButtonAvailable = false passcodeViewController.screenBlurringEnabled = lockscreenOpenForced && !self.shouldDisplayLockscreen @@ -309,15 +337,20 @@ public class AppLockManager: NSObject { } // MARK: - App Events - @objc func appDidEnterBackground() { - lastApplicationBackgroundedDate = Date() + @objc public func appDidEnterBackground() { + if unlocked { + lastApplicationBackgroundedDate = Date() + } else { + lastApplicationBackgroundedDate = nil + } showLockscreenIfNeeded(forceShow: true) } @objc func appWillEnterForeground() { if self.shouldDisplayLockscreen { - showLockscreenIfNeeded() + dismissLockscreen(animated: false) + self.showLockscreenIfNeeded() } else { dismissLockscreen(animated: false) } @@ -327,11 +360,11 @@ public class AppLockManager: NSObject { func attemptUnlock(with testPasscode: String?, customErrorMessage: String? = nil, passcodeViewController: PasscodeViewController? = nil) { if testPasscode == self.passcode { unlocked = true - lastApplicationBackgroundedDate = nil failedPasscodeAttempts = 0 dismissLockscreen(animated: true) } else { unlocked = false + lastApplicationBackgroundedDate = nil passcodeViewController?.errorMessage = (customErrorMessage != nil) ? customErrorMessage! : "Incorrect code".localized failedPasscodeAttempts += 1 @@ -452,8 +485,20 @@ public class AppLockManager: NSObject { // MARK: - Biometrical Unlock private var biometricalAuthenticationInterfaceShown : Bool = false - func showBiometricalAuthenticationInterface(context: LAContext) { - if shouldDisplayLockscreen, AppLockSettings.shared.biometricalSecurityEnabled, !biometricalAuthenticationInterfaceShown { + func showBiometricalAuthenticationInterface(context inContext: LAContext? = nil) { + + if shouldDisplayLockscreen, biometricalSecurityEnabled, !biometricalAuthenticationInterfaceShown { + // Check if we should perform biometrical authentication - or redirect + if let targetURL = AppLockSettings.shared.biometricalAuthenticationRedirectionTargetURL { + // Unfortunately, opening the URL closes the share sheet just like invoking + // biometric auth - so in those instances where we'd want to use it to work around + // that. + self.passwordViewHostViewController?.openURL(targetURL) + return + } + + // Perform biometrical authentication + let context = inContext ?? LAContext() var evaluationError: NSError? // Check if the device can evaluate the policy. @@ -514,7 +559,7 @@ public class AppLockManager: NSObject { } } } else { - if let error = evaluationError, AppLockSettings.shared.biometricalSecurityEnabled { + if let error = evaluationError, biometricalSecurityEnabled { OnMainThread { self.performPasscodeViewControllerUpdates { (passcodeViewController) in passcodeViewController.errorMessage = error.localizedDescription diff --git a/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift b/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift index ee445cd4a..6bb511841 100644 --- a/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift +++ b/ownCloudAppShared/AppLock/PasscodeSetupCoordinator.swift @@ -162,7 +162,7 @@ public class PasscodeSetupCoordinator { AppLockManager.shared.showLockscreenIfNeeded(setupMode: true) } } else { - let alertController = UIAlertController(title: biometricalSecurityName, message: String(format:"Unlock using %@?".localized, biometricalSecurityName), preferredStyle: .alert) + let alertController = ThemedAlertController(title: biometricalSecurityName, message: String(format:"Unlock using %@?".localized, biometricalSecurityName), preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Enable".localized, style: .default, handler: { _ in PasscodeSetupCoordinator.isBiometricalSecurityEnabled = true diff --git a/ownCloudAppShared/AppLock/PasscodeViewController.swift b/ownCloudAppShared/AppLock/PasscodeViewController.swift index b1e047450..a6390db1e 100644 --- a/ownCloudAppShared/AppLock/PasscodeViewController.swift +++ b/ownCloudAppShared/AppLock/PasscodeViewController.swift @@ -107,18 +107,18 @@ public class PasscodeViewController: UIViewController, Themeable { } } - var cancelButtonHidden: Bool { + var cancelButtonAvailable: Bool { didSet { - cancelButton?.isEnabled = cancelButtonHidden - cancelButton?.isHidden = !cancelButtonHidden + cancelButton?.isEnabled = cancelButtonAvailable + cancelButton?.isHidden = !cancelButtonAvailable } } var biometricalButtonHidden: Bool = false { didSet { - biometricalButton?.isEnabled = biometricalButtonHidden - biometricalButton?.isHidden = !biometricalButtonHidden - biometricalImageView?.isHidden = !biometricalButtonHidden + biometricalButton?.isEnabled = !biometricalButtonHidden + biometricalButton?.isHidden = biometricalButtonHidden + biometricalImageView?.isHidden = biometricalButtonHidden biometricalImageView?.image = LAContext().biometricsAuthenticationImage() } } @@ -142,7 +142,7 @@ public class PasscodeViewController: UIViewController, Themeable { self.biometricalHandler = biometricalHandler self.completionHandler = completionHandler self.keypadButtonsEnabled = keypadButtonsEnabled - self.cancelButtonHidden = hasCancelButton + self.cancelButtonAvailable = hasCancelButton self.keypadButtonsHidden = false self.screenBlurringEnabled = false self.passcodeLength = requiredLength @@ -167,13 +167,13 @@ public class PasscodeViewController: UIViewController, Themeable { self.errorMessage = { self.errorMessage }() self.timeoutMessage = { self.timeoutMessage }() - self.cancelButtonHidden = { self.cancelButtonHidden }() + self.cancelButtonAvailable = { self.cancelButtonAvailable }() self.keypadButtonsEnabled = { self.keypadButtonsEnabled }() self.keypadButtonsHidden = { self.keypadButtonsHidden }() self.screenBlurringEnabled = { self.screenBlurringEnabled }() self.errorMessageLabel?.minimumScaleFactor = 0.5 self.errorMessageLabel?.adjustsFontSizeToFitWidth = true - self.biometricalButtonHidden = !((!AppLockSettings.shared.biometricalSecurityEnabled || !AppLockSettings.shared.lockEnabled) || self.cancelButtonHidden) + self.biometricalButtonHidden = (!AppLockSettings.shared.biometricalSecurityEnabled || !AppLockSettings.shared.lockEnabled || cancelButtonAvailable) // cancelButtonAvailable is true for setup tasks/settings changes only updateKeypadButtons() if let biometricalSecurityName = LAContext().supportedBiometricsAuthenticationName() { self.biometricalButton?.accessibilityLabel = biometricalSecurityName @@ -336,7 +336,7 @@ public class PasscodeViewController: UIViewController, Themeable { biometricalImageView?.tintColor = collection.tintColor - cancelButton?.applyThemeCollection(collection, itemStyle: .defaultForItem) + cancelButton?.applyThemeCollection(collection, itemStyle: .neutral) } } diff --git a/ownCloudAppShared/Client/Account/Connection/AccountConnection+ItemActions.swift b/ownCloudAppShared/Client/Account/Connection/AccountConnection+ItemActions.swift new file mode 100644 index 000000000..9be3aed67 --- /dev/null +++ b/ownCloudAppShared/Client/Account/Connection/AccountConnection+ItemActions.swift @@ -0,0 +1,58 @@ +// +// AccountController+ItemActions.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 AccountConnection : InlineMessageCenter { + public func hasInlineMessage(for dataItem: OCDataItem) -> Bool { + guard let item = dataItem as? OCItem, let activeSyncRecordIDs = item.activeSyncRecordIDs, let syncRecordIDsWithMessages = self.syncRecordIDsWithMessages else { + return false + } + + return syncRecordIDsWithMessages.contains { (syncRecordID) -> Bool in + return activeSyncRecordIDs.contains(syncRecordID) + } + } + + public func showInlineMessage(for dataItem: OCDataItem) { + if let item = dataItem as? OCItem, + let messages = self.messageSelector?.selection, + let firstMatchingMessage = messages.first(where: { (message) -> Bool in + guard let syncRecordID = message.syncIssue?.syncRecordID, let containsSyncRecordID = item.activeSyncRecordIDs?.contains(syncRecordID) else { + return false + } + + return containsSyncRecordID + }) { + firstMatchingMessage.showInApp() + } + } +} + +extension AccountConnection : ActionProgressHandlerProvider { + public func makeActionProgressHandler() -> ActionProgressHandler { + return { [weak self] (progress, publish) in + if publish { + self?.progressSummarizer.startTracking(progress: progress) + } else { + self?.progressSummarizer.stopTracking(progress: progress) + } + } + } +} diff --git a/ownCloudAppShared/Client/Account/Connection/AccountConnection.swift b/ownCloudAppShared/Client/Account/Connection/AccountConnection.swift new file mode 100644 index 000000000..5aeda1fbc --- /dev/null +++ b/ownCloudAppShared/Client/Account/Connection/AccountConnection.swift @@ -0,0 +1,596 @@ +// +// AccountConnection.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 16.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ownCloudApp +import ownCloudSDK + +open class AccountConnection: NSObject { + public struct AuthFailure { + var bookmark: OCBookmark + + var error: NSError? + + var title: String + var message: String? + + var ignoreLabel: String + var ignoreStyle: UIAlertAction.Style + + var hasEditOption: Bool + + var failureResolver: ((_ authFailure: AuthFailure, _ context: ClientContext) -> Void)? + + func resolve(context: ClientContext) { + failureResolver?(self, context) + } + } + + public enum Status { + case noCore + case offline + case connecting + case coreAvailable + case online + + case busy + case authenticationError(failure: AuthFailure) + } + + public static let StatusChangedNotification = NSNotification.Name("AccountConnectionStatusChanged") + + open var bookmark: OCBookmark + open weak var core: OCCore? + + var consumers: [AccountConnectionConsumer] = [] + + public dynamic var status: Status = .noCore { + didSet { + Log.debug("Account connection status: \(status)") + + let newStatus = status + + if let richStatus = richStatus { + richStatus.status = newStatus + self.richStatus = richStatus + } else { + self.richStatus = AccountConnectionRichStatus(kind: .status, status: newStatus) + } + + OnMainThread { + self.enumerateConsumers { consumer in + consumer.statusObserver?.account(connection: self, changedStatusTo: newStatus, initial: false) + } + + NotificationCenter.default.post(name: AccountConnection.StatusChangedNotification, object: self) + } + } + } + @objc public dynamic var richStatus: AccountConnectionRichStatus? + + var skipAuthorizationFailure : Bool = false + + public typealias CompletionHandler = (_ error: Error?) -> Void + + public init(bookmark: OCBookmark) { + self.bookmark = bookmark + self.taskQueue = OCAsyncSequentialQueue(queue: AccountConnectionPool.shared.serialQueue) + self.progressSummarizer = ProgressSummarizer.shared(forBookmark: bookmark) + + super.init() + + setupProgressSummarizer() + setupMessageSelector() + } + + deinit { + shutdownMessageSelector() + shutdownProgressSummarizer() + } + + // MARK: - Queue + private var taskQueue: OCAsyncSequentialQueue + + func queue(completion: CompletionHandler? = nil, _ block: @escaping (_ connection: AccountConnection, _ jobDone: @escaping () -> Void) -> Void) { + AccountConnectionPool.shared.taskQueue.async({ [weak self] (jobDone) in + guard let self = self else { + completion?(NSError(ocError: .internal)) + jobDone() + return + } + + block(self, jobDone) + }) + } + + // MARK: - Add/remove fixed components from consumers + @discardableResult func addFixedComponents(from consumer: AccountConnectionConsumer) -> Bool { + guard let core = self.core else { return false } + + if let messagePresenter = consumer.messagePresenter { + core.messageQueue.add(presenter: messagePresenter) + } + + if let progressSummarizerNotificationHandler = consumer.progressSummarizerNotificationHandler { + progressSummarizer.addObserver(consumer, notificationBlock: progressSummarizerNotificationHandler) + } + + return true + } + + @discardableResult func removeFixedComponents(from consumer: AccountConnectionConsumer) -> Bool { + guard let core = self.core else { return false } + + if consumer.progressSummarizerNotificationHandler != nil { + progressSummarizer.removeObserver(consumer) + } + + if let messagePresenter = consumer.messagePresenter { + core.messageQueue.remove(presenter: messagePresenter) + } + + return true + } + + // MARK: - API + public func add(consumer: AccountConnectionConsumer, completion: CompletionHandler?=nil) { + queue(completion: completion) { (connection, jobDone) in + OCSynchronized(connection.consumers) { + connection.consumers.append(consumer) + } + + // Add fixed components + connection.addFixedComponents(from: consumer) + + // Initial calls to dynamic components + consumer.statusObserver?.account(connection: connection, changedStatusTo: connection.status, initial: true) + + completion?(nil) + jobDone() + } + } + + public func remove(consumer: AccountConnectionConsumer, completion: CompletionHandler?=nil) { + queue(completion: completion) { (connection, jobDone) in + // Remove fixed components + self.removeFixedComponents(from: consumer) + + OCSynchronized(connection.consumers) { + if let idx = connection.consumers.firstIndex(of: consumer) { + connection.consumers.remove(at: idx) + } + } + + completion?(nil) + jobDone() + } + } + + public func connect(consumer: AccountConnectionConsumer? = nil, completion: CompletionHandler? = nil) { + queue(completion: completion) { (connection, jobDone) in + guard connection.core == nil else { + // Already has a core - nothing to do + OnMainThread { + completion?(nil) + } + jobDone() + return + } + + // No core yet - request one + connection.status = .connecting + + OCCoreManager.shared.requestCore(for: connection.bookmark, setup: { (core, error) in + // Setup core for AccountConnection + if core != nil { + connection.core = core + + // Install hooks + core?.delegate = connection + core?.busyStatusHandler = { [weak connection] (progress) in + connection?.handleBusyStatus(progress: progress) + } + + // Add fixed components from consumers + connection.enumerateConsumers { consumer in + connection.addFixedComponents(from: consumer) + } + + // Observe .appProvider property + if OCAppIdentity.shared.componentIdentifier == .app { // Only in the app + connection.appProviderObservation = core?.observe(\OCCore.appProvider, options: .initial, changeHandler: { [weak connection] (core, change) in + connection?.appProviderChanged(to: core.appProvider) + }) + } + + // Add shareJailQueryCustomizer + core?.shareJailQueryCustomizer = { (query) in + DisplaySettings.shared.updateQuery(withDisplaySettings: query) + query.sortComparator = SortMethod.alphabetically.comparator(direction: .ascendant) + } + + // Remove skip available offline when user opens the bookmark + core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) + } + }, completionHandler: { (core, error) in + if error == nil { + // Start FP standby in 5 seconds regardless of connnection status + // (or below: after it's clear that authentication worked) + OnBackgroundQueue(async: true, after: 5.0) { [weak connection] in + connection?.startFPServiceStandbyIfNotRunning() + } + + // Add default icons source to core's resource manager + OnMainThread { [weak core] in + if let core = core { + core.vault.resourceManager?.add(ResourceSourceItemIcons(core: core)) + } + } + + // Connected + connection.status = .coreAvailable + + // Start showing connection status + OnMainThread { [weak connection] () in + connection?.connectionStatusObservation = core?.observe(\OCCore.connectionStatus, options: [.initial], changeHandler: { [weak connection] (_, _) in + connection?.updateConnectionStatusSummary() + + if let connectionStatus = connection?.core?.connectionStatus, + connectionStatus == .online { + // Start FP service standby after it's clear that authentication worked + // (or above: after 5 seconds regardless of connnection status) + connection?.startFPServiceStandbyIfNotRunning() + } + }) + } + } else { + connection.core = nil + + Log.error("Error requesting/starting core: \(String(describing: error))") + } + + // Done + OnMainThread { + completion?(error) + } + jobDone() + }) + } + } + + public func disconnect(consumer: AccountConnectionConsumer? = nil, completion: CompletionHandler? = nil) { + queue(completion: completion) { (connection, jobDone) in + guard connection.core != nil else { + // Has no core - nothing to do + OnMainThread { + completion?(nil) + } + jobDone() + return + } + + // Remove fixed components from consumers + connection.enumerateConsumers { consumer in + connection.removeFixedComponents(from: consumer) + } + + // Remove App Provider action extensions + connection.appProviderActionExtensions = nil + + connection.fpServiceStandby?.stop() + + // Return core + OCCoreManager.shared.returnCore(for: self.bookmark, completionHandler: { + connection.richStatus = nil + connection.core = nil + connection.status = .noCore + + OnMainThread { + completion?(nil) + } + jobDone() + }) + } + } + + public typealias ConsumerEnumerator = (_ consumer: AccountConnectionConsumer) -> Void + public typealias ConsumerConditionalEnumerator = (_ consumer: AccountConnectionConsumer) -> Bool // Return false to stop enumeration + + public func enumerateConsumers(with enumerator: ConsumerEnumerator) { + OCSynchronized(self.consumers) { + for consumer in self.consumers { + enumerator(consumer) + } + } + } + + public func enumerateConsumers(withConditional enumerator: ConsumerConditionalEnumerator) { + OCSynchronized(self.consumers) { + for consumer in self.consumers { + if !enumerator(consumer) { + // Enumerator returned false - stop enumeration + break + } + } + } + } + + // MARK: - Delegation to consumers + func handleBusyStatus(progress: Progress?) { + OnMainThread(inline: true) { + // Build rich status + if progress != nil { // nil value indicates the busy status has ended + self.status = .busy + self.richStatus = AccountConnectionRichStatus(kind: .status, progress: progress, status: .busy) + } + + // Distribute event to consumers + self.enumerateConsumers { consumer in + consumer.busyHandler?(progress) + } + } + } + + // MARK: - Progress Summarizer + var progressSummarizer : ProgressSummarizer + + func setupProgressSummarizer() { + // Set up progress summarizer + progressSummarizer.addObserver(self) { [weak self] (summarizer, summary) in + var useSummary : ProgressSummary = summary + let prioritySummary : ProgressSummary? = summarizer.prioritySummary + + if (summary.progress == 1), (summarizer.fallbackSummary != nil) { + useSummary = summarizer.fallbackSummary ?? summary + } + + if let prioritySummary = prioritySummary { + useSummary = prioritySummary + } + + let autoCollapse = (((summarizer.fallbackSummary == nil) || (useSummary.progressCount == 0)) && (prioritySummary == nil)) // || (self?.allowProgressBarAutoCollapse ?? false) + + if let self = self { + self.enumerateConsumers(with: { (consumer) in + consumer.progressUpdateHandler?.account(connection: self, progressSummary: useSummary, autoCollapse: autoCollapse) + }) + + if autoCollapse { + self.richStatus = nil + } else { + self.richStatus = AccountConnectionRichStatus(kind: .status, progressSummary: useSummary, status: self.status) + } + } + } + } + + func shutdownProgressSummarizer() { + progressSummarizer.removeObserver(self) + } + + // MARK: - App Provider updates + var appProviderObservation: NSKeyValueObservation? + + var appProviderActionExtensions : [OCExtension]? { + willSet { + if let extensions = appProviderActionExtensions { + for ext in extensions { + OCExtensionManager.shared.removeExtension(ext) + } + } + } + + didSet { + if let extensions = appProviderActionExtensions { + for ext in extensions { + OCExtensionManager.shared.addExtension(ext) + } + } + } + } + + func appProviderChanged(to appProvider: OCAppProvider?) { + var actionExtensions : [OCExtension] = [] + + if let core = core { + if let apps = core.appProvider?.apps { + for app in apps { + // Pre-load app icon + if let appIconRequest = app.iconResourceRequest { + core.vault.resourceManager?.start(appIconRequest) + } + + // Create app-specific open-in-web-app action + let openInWebAction = OpenInWebAppAction.createActionExtension(for: app, core: core) + actionExtensions.append(openInWebAction) + } + } + + if let types = core.appProvider?.types { + let creationTypes = types.filter({ type in + return type.allowCreation + }) + + if creationTypes.count > 0 { + // Pre-load document icons + for type in creationTypes { + if let typeIconRequest = type.iconResourceRequest { + core.vault.resourceManager?.start(typeIconRequest) + } + } + + // Log.debug("Creation Types: \(String(describing: creationTypes))") + } + } + } + + appProviderActionExtensions = actionExtensions + } + + // MARK: - FileProvider Service pinging + var fpServiceStandby : OCFileProviderServiceStandby? + + func startFPServiceStandbyIfNotRunning() { + // Set up FP standby + OCSynchronized(self) { + if let core = core, + core.state == .starting || core.state == .running, + self.fpServiceStandby == nil { + self.fpServiceStandby = OCFileProviderServiceStandby(core: core) + self.fpServiceStandby?.start() + } + } + } + + // MARK: - Connection status observation + var connectionStatusObservation : NSKeyValueObservation? + open var connectionStatus: OCCoreConnectionStatus? + var connectionStatusSummary : ProgressSummary? { + willSet { + if newValue != nil { + progressSummarizer.pushPrioritySummary(summary: newValue!) + } + } + + didSet { + if oldValue != nil { + progressSummarizer.popPrioritySummary(summary: oldValue!) + } + } + } + + func updateConnectionStatusSummary() { + var summary : ProgressSummary? = ProgressSummary(indeterminate: true, progress: 1.0, message: nil, progressCount: 1) + + connectionStatus = core?.connectionStatus + + if let connectionStatus = connectionStatus { + var connectionShortDescription = core?.connectionStatusShortDescription + + connectionShortDescription = connectionShortDescription != nil ? (connectionShortDescription!.hasSuffix(".") ? connectionShortDescription! + " " : connectionShortDescription! + ". ") : "" + + switch connectionStatus { + case .online: + summary = nil + status = .online + + case .connecting: + summary?.message = "Connecting…".localized + + case .offline, .unavailable: + summary?.message = String(format: "%@%@", connectionShortDescription!, "Contents from cache.".localized) + } + + if connectionStatus == .online { + // Connection switched to online - perform actions + updateUserAvatar() + } + } + + connectionStatusSummary = summary + } + + // MARK: - Actions to perform on connect + private var userAvatarUpdated : Bool = false + + func updateUserAvatar() { + if !userAvatarUpdated, let user = core?.connection.loggedInUser { + // Update avatar on every connect + userAvatarUpdated = true + + let avatarRequest = OCResourceRequestAvatar(for: user, maximumSize: OCAvatar.defaultSize, scale: 0, waitForConnectivity: true, changeHandler: { [weak self] request, error, ongoing, previousResource, newResource in + if !ongoing, + let bookmarkUUID = self?.bookmark.uuid, + let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID), + let newResource = newResource as? OCViewProvider { + bookmark.avatar = newResource + OCBookmarkManager.shared.updateBookmark(bookmark) + } + }) + avatarRequest.lifetime = .singleRun + + core?.vault.resourceManager?.start(avatarRequest) + } + } + + // MARK: - Inline Message Center + public var messageSelector : MessageSelector? + @objc public dynamic var messageCount: Int = 0 + + func setupMessageSelector() { + // Setup message selector + let bookmarkUUID = bookmark.uuid + + messageSelector = MessageSelector(from: .global, filter: { (message) in + return (message.bookmarkUUID == bookmarkUUID) && !message.resolved + }, provideGroupedSelection: true, provideSyncRecordIDs: true, handler: { [weak self] (messages, groups, syncRecordIDs) in + self?.updateMessageSelectionWith(messages: messages, groups: groups, syncRecordIDs: syncRecordIDs) + OnMainThread { + self?.messageCount = messages?.count ?? 0 + } + }) + } + + func shutdownMessageSelector() { + messageSelector = nil + } + + func updateMessageSelectionWith(messages: [OCMessage]?, groups : [MessageGroup]?, syncRecordIDs : Set?) { + OnMainThread { + let messageCount = messages?.count ?? 0 + + self.enumerateConsumers(with: { consumer in + consumer.messageUpdateHandler?.handleMessageCountChanged?(to: messageCount) + consumer.messageUpdateHandler?.handleMessagesUpdates?(messages: messages, groups: groups) + }) + + if syncRecordIDs != self.syncRecordIDsWithMessages { + self.syncRecordIDsWithMessages = syncRecordIDs + } + } + } + + var syncRecordIDsWithMessages : Set? { + didSet { + if let core = core { + NotificationCenter.default.post(name: .ClientSyncRecordIDsWithMessagesChanged, object: core) + } + } + } +} + +// MARK: - Core Delegate +extension AccountConnection : OCCoreDelegate { + public func core(_ core: OCCore, handleError error: Error?, issue: OCIssue?) { + // Distribute event to consumers + self.enumerateConsumers { consumer in + // Send to all consumers until the first consumer indicates + if consumer.coreErrorHandler?.account(connnection: self, handleError: error, issue: issue) == true { + return false + } + + return true + } + } +} + +public extension NSNotification.Name { + static let ClientSyncRecordIDsWithMessagesChanged = NSNotification.Name(rawValue: "client-sync-record-ids-with-messages-changed") +} + +public typealias ClientActionCompletionHandler = (_ actionPerformed: Bool) -> Void diff --git a/ownCloudAppShared/Client/Account/Connection/AccountConnectionConsumer.swift b/ownCloudAppShared/Client/Account/Connection/AccountConnectionConsumer.swift new file mode 100644 index 000000000..70bf16871 --- /dev/null +++ b/ownCloudAppShared/Client/Account/Connection/AccountConnectionConsumer.swift @@ -0,0 +1,67 @@ +// +// AccountConnectionConsumer.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 16.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +public protocol AccountConnectionCoreErrorHandler: AnyObject { + func account(connnection: AccountConnection, handleError: Error?, issue: OCIssue?) -> Bool //!< Return true if you handled the error/issue - otherwise false to allow propagation to the next consumer +} + +public protocol AccountConnectionStatusObserver: AnyObject { + func account(connection: AccountConnection, changedStatusTo: AccountConnection.Status, initial: Bool) + // func account(connection: AccountConnection, changedConnectionStatusTo: OCCoreConnectionStatus?) +} + +public protocol AccountConnectionProgressUpdates: AnyObject { + func account(connection: AccountConnection, progressSummary: ProgressSummary, autoCollapse: Bool) +} + +@objc public protocol AccountConnectionMessageUpdates: AnyObject { + @objc optional func handleMessageCountChanged(to count: Int) + @objc optional func handleMessagesUpdates(messages: [OCMessage]?, groups : [MessageGroup]?) +} + +public class AccountConnectionConsumer: NSObject { + open weak var owner: AnyObject? + + // Fixed components - have to remain identical across the lifetime of an AccountConnectionConsumer object + open var messagePresenter : OCMessagePresenter? // f.ex. a CardIssueMessagePresenter + open var progressSummarizerNotificationHandler: ProgressSummarizerNotificationBlock? + + // Dynamic components - called as needed, allowed to change over time + open var busyHandler: OCCoreBusyStatusHandler? + + open weak var coreErrorHandler: AccountConnectionCoreErrorHandler? + open weak var statusObserver: AccountConnectionStatusObserver? + + open weak var progressUpdateHandler: AccountConnectionProgressUpdates? + open weak var messageUpdateHandler: AccountConnectionMessageUpdates? + + public init(owner: AnyObject? = nil, messagePresenter: OCMessagePresenter? = nil, progressSummarizerNotificationHandler: ProgressSummarizerNotificationBlock? = nil, busyHandler: OCCoreBusyStatusHandler? = nil, coreErrorHandler: AccountConnectionCoreErrorHandler? = nil, statusObserver: AccountConnectionStatusObserver? = nil, progressUpdateHandler: AccountConnectionProgressUpdates? = nil, messageUpdateHandler: AccountConnectionMessageUpdates? = nil) { + self.owner = owner + self.messagePresenter = messagePresenter + self.progressSummarizerNotificationHandler = progressSummarizerNotificationHandler + self.busyHandler = busyHandler + self.coreErrorHandler = coreErrorHandler + self.statusObserver = statusObserver + self.progressUpdateHandler = progressUpdateHandler + self.messageUpdateHandler = messageUpdateHandler + } +} diff --git a/ownCloudAppShared/Client/Account/Connection/AccountConnectionPool.swift b/ownCloudAppShared/Client/Account/Connection/AccountConnectionPool.swift new file mode 100644 index 000000000..5c879e5bb --- /dev/null +++ b/ownCloudAppShared/Client/Account/Connection/AccountConnectionPool.swift @@ -0,0 +1,102 @@ +// +// AccountConnectionPool.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 16.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class AccountConnectionPool: NSObject { + public typealias CompletionHandler = () -> Void + + public static var shared: AccountConnectionPool = AccountConnectionPool() + + var connectionsByBookmarkUUID: [String:AccountConnection] = [:] + + var taskQueue: OCAsyncSequentialQueue + let serialQueue = DispatchQueue(label: "com.owncloud.connection-pool") + + public override init() { + taskQueue = OCAsyncSequentialQueue(queue: serialQueue) + + super.init() + } + + public func connection(for bookmark: OCBookmark) -> AccountConnection? { + var connection: AccountConnection? + let bookmarkUUID = bookmark.uuid.uuidString + + OCSynchronized(self) { + OCSynchronized(connectionsByBookmarkUUID) { + if let existingConnection = connectionsByBookmarkUUID[bookmarkUUID] { + connection = existingConnection + } else { + connection = AccountConnection(bookmark: bookmark) + connectionsByBookmarkUUID[bookmarkUUID] = connection + } + } + + if let connection { + connection.add(consumer: AccountConnectionAuthErrorConsumer(for: connection)) + } + } + + return connection + } + + public func disconnectAll(_ completion: CompletionHandler?) { + let waitGroup = DispatchGroup() + + OCSynchronized(self) { + var connections: [AccountConnection] = [] + OCSynchronized(connectionsByBookmarkUUID) { + connections = Array(connectionsByBookmarkUUID.values) + } + + for connection in connections { + waitGroup.enter() + + connection.disconnect { error in + waitGroup.leave() + } + } + } + + waitGroup.notify(queue: .main, execute: { + completion?() + }) + } + + public var activeConnections: [AccountConnection] { + var connections: [AccountConnection] = [] + OCSynchronized(connectionsByBookmarkUUID) { + connections = Array(connectionsByBookmarkUUID.values) + } + + var activeConnections: [AccountConnection] = [] + + for connection in connections { + switch connection.status { + case .offline, .noCore: break + + default: + activeConnections.append(connection) + } + } + + return activeConnections + } +} diff --git a/ownCloudAppShared/Client/Account/Connection/AccountConnectionRichStatus.swift b/ownCloudAppShared/Client/Account/Connection/AccountConnectionRichStatus.swift new file mode 100644 index 000000000..4b15a9bff --- /dev/null +++ b/ownCloudAppShared/Client/Account/Connection/AccountConnectionRichStatus.swift @@ -0,0 +1,71 @@ +// +// AccountConnectionRichStatus.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 18.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class AccountConnectionRichStatus: NSObject { + public enum Kind { + case status + case error + } + + public typealias Interaction = (_ viewControllerToPresentOn: UIViewController) -> Void + + public var kind: Kind + + public var icon: UIImage? + public var text: String? + public var progress: Progress? + public var progressSummary: ProgressSummary? + + public var status: AccountConnection.Status? + + public var interaction: Interaction? + public var interactionLabel: String? + + init(kind: Kind, icon: UIImage? = nil, text: String? = nil, progress: Progress? = nil, progressSummary: ProgressSummary? = nil, status: AccountConnection.Status? = nil, interaction: Interaction? = nil, interactionLabel: String? = nil) { + self.kind = kind + self.icon = icon + self.text = text + self.progress = progress + self.progressSummary = progressSummary + self.status = status + self.interaction = interaction + + if text == nil, let progress = progress, let localizedProgressDescription = progress.localizedDescription { + self.text = localizedProgressDescription + } + + if let progressSummary = progressSummary { + if text == nil, let message = progressSummary.message { + self.text = message + } + + if progress == nil { + if progressSummary.indeterminate { + self.progress = .indeterminate() + } else if progressSummary.progressCount > 0 { + let percentageProgress = Progress() + percentageProgress.totalUnitCount = 100 + percentageProgress.completedUnitCount = Int64(progressSummary.progress * 100) + self.progress = percentageProgress + } + } + } + } +} diff --git a/ownCloud/Client/ClientAuthenticationUpdater.swift b/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdater.swift similarity index 85% rename from ownCloud/Client/ClientAuthenticationUpdater.swift rename to ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdater.swift index 34478fbdd..e96355658 100644 --- a/ownCloud/Client/ClientAuthenticationUpdater.swift +++ b/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdater.swift @@ -1,5 +1,5 @@ // -// ClientAuthenticationUpdater.swift +// AccountAuthenticationUpdater.swift // ownCloud // // Created by Felix Schwarz on 25.04.20. @@ -18,20 +18,19 @@ import UIKit import ownCloudSDK -import ownCloudAppShared -class ClientAuthenticationUpdater: NSObject { +public class AccountAuthenticationUpdater: NSObject { var bookmark : OCBookmark var preferredAuthenticationMethodIdentifiers: [OCAuthenticationMethodIdentifier]? - init(with inBookmark: OCBookmark, preferredAuthenticationMethods authMethodIDs: [OCAuthenticationMethodIdentifier]?) { + public init(with inBookmark: OCBookmark, preferredAuthenticationMethods authMethodIDs: [OCAuthenticationMethodIdentifier]?) { bookmark = inBookmark preferredAuthenticationMethodIdentifiers = authMethodIDs super.init() } - var authenticationMethodIdentifier : OCAuthenticationMethodIdentifier? { + open var authenticationMethodIdentifier : OCAuthenticationMethodIdentifier? { if let methods = preferredAuthenticationMethodIdentifiers, methods.count > 0, let existingAuthMethod = bookmark.authenticationMethodIdentifier, !methods.contains(existingAuthMethod) { @@ -49,11 +48,11 @@ class ClientAuthenticationUpdater: NSObject { return false } - var canUpdateInline : Bool { + open var canUpdateInline : Bool { return (isTokenBased || (!isTokenBased && (bookmark.userName != nil))) && (preferredAuthenticationMethodIdentifiers != nil) && ((preferredAuthenticationMethodIdentifiers?.count ?? 0) > 0) } - func updateAuthenticationData(on viewController: UIViewController, completion: ((Error?) -> Void)? = nil) { + open func updateAuthenticationData(on viewController: UIViewController, completion: ((Error?) -> Void)? = nil) { if let url = bookmark.url, let authenticationMethodID = self.authenticationMethodIdentifier, self.canUpdateInline { let tempBookmark = OCBookmark(for: url) let tempConnection = OCConnection(bookmark: tempBookmark) @@ -84,7 +83,7 @@ class ClientAuthenticationUpdater: NSObject { } } } else { - let updateViewcontroller = ClientAuthenticationUpdaterViewController(passwordHeaderText: bookmark.shortName, passwordValidationHandler: { (password, errorHandler) in + let updateViewController = AccountAuthenticationUpdaterPasswordPromptViewController(passwordHeaderText: bookmark.shortName, passwordValidationHandler: { (password, errorHandler) in // Password Validation + Update if let userName = self.bookmark.userName { var options : [OCAuthenticationMethodKey : Any] = [:] @@ -109,7 +108,7 @@ class ClientAuthenticationUpdater: NSObject { } }) - viewController.present(asCard: ThemeNavigationController(rootViewController: updateViewcontroller), animated: true, completion: nil) + viewController.present(asCard: ThemeNavigationController(rootViewController: updateViewController), animated: true, completion: nil) } } else { completion?(NSError(ocError: .internal)) diff --git a/ownCloud/Client/ClientAuthenticationUpdaterViewController.swift b/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdaterPasswordPromptViewController.swift similarity index 90% rename from ownCloud/Client/ClientAuthenticationUpdaterViewController.swift rename to ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdaterPasswordPromptViewController.swift index 319f8a5f7..f9730def0 100644 --- a/ownCloud/Client/ClientAuthenticationUpdaterViewController.swift +++ b/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdaterPasswordPromptViewController.swift @@ -1,5 +1,5 @@ // -// ClientAuthenticationUpdaterViewController.swift +// AccountAuthenticationUpdaterPasswordPromptViewController.swift // ownCloud // // Created by Felix Schwarz on 26.04.20. @@ -18,15 +18,15 @@ import UIKit import ownCloudSDK -import ownCloudAppShared -class ClientAuthenticationUpdaterViewController: StaticTableViewController { +class AccountAuthenticationUpdaterPasswordPromptViewController: StaticTableViewController { var headerText : String - typealias PasswordValidationHandler = (_ password: String, _ completion: @escaping (_ error: Error?) -> Void) -> Void var validationHandler : PasswordValidationHandler? var validationQueue : OCAsyncSequentialQueue - init(passwordHeaderText: String, passwordValidationHandler : @escaping PasswordValidationHandler) { + typealias PasswordValidationHandler = (_ password: String, _ completion: @escaping (_ error: Error?) -> Void) -> Void + + required init(passwordHeaderText: String, passwordValidationHandler : @escaping PasswordValidationHandler) { self.headerText = passwordHeaderText self.validationHandler = passwordValidationHandler @@ -109,7 +109,7 @@ class ClientAuthenticationUpdaterViewController: StaticTableViewController { } } -extension ClientAuthenticationUpdaterViewController : UITextFieldDelegate { +extension AccountAuthenticationUpdaterPasswordPromptViewController : UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { startValidation(textField) diff --git a/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountConnectionAuthErrorConsumer.swift b/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountConnectionAuthErrorConsumer.swift new file mode 100644 index 000000000..702895d11 --- /dev/null +++ b/ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountConnectionAuthErrorConsumer.swift @@ -0,0 +1,277 @@ +// +// AccountConnectionAuthErrorConsumer.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 Error { + var isAccountConnectionAuthenticationError: Bool { + if let nsError = self as NSError? { + if nsError.isOCError(withCode: .authorizationFailed) { + return true + } + + if nsError.isOCError(withCode: .authorizationNoMethodData) || nsError.isOCError(withCode: .authorizationMissingData) { + return true + } + + if nsError.isOCError(withCode: .authorizationMethodNotAllowed) { + return true + } + } + + return false + } +} + +class AccountConnectionAuthErrorConsumer: AccountConnectionConsumer, AccountConnectionCoreErrorHandler, AccountConnectionStatusObserver { + weak var connection: AccountConnection? + + init(for connection: AccountConnection) { + self.connection = connection + super.init() + self.coreErrorHandler = self + self.statusObserver = self + } + + var skipAuthorizationFailure: Bool = false + + var bookmark: OCBookmark? { + return connection?.bookmark + } + + public var authenticationFailure: AccountConnection.AuthFailure? { + didSet { + if let authenticationFailure { + connection?.status = .authenticationError(failure: authenticationFailure) + } + } + } + + public func account(connection: AccountConnection, changedStatusTo status: AccountConnection.Status, initial: Bool) { + if case .noCore = status { + skipAuthorizationFailure = false + } + } + + public func account(connnection: AccountConnection, handleError error: Error?, issue inIssue: OCIssue?) -> Bool { + guard let connection = connection, let core = connection.core else { + return false + } + + var issue = inIssue + var isAuthFailure : Bool = false + var authFailureMessage : String? + var authFailureTitle : String = "Authorization failed".localized + var authFailureHasEditOption : Bool = true + var authFailureIgnoreLabel = "Continue offline".localized + var authFailureIgnoreStyle = UIAlertAction.Style.destructive + let editBookmark = connection.bookmark + var nsError = error as NSError? + + Log.debug("Received error \(nsError?.description ?? "nil")), issue \(issue?.description ?? "nil")") + + if let authError = issue?.authenticationError { + // Turn issues that are just converted authorization errors back into errors and discard the issue + nsError = authError + issue = nil + } + + Log.debug("Received error \(nsError?.description ?? "nil")), issue \(issue?.description ?? "nil")") + + if let nsError = nsError { + if nsError.isOCError(withCode: .authorizationFailed) { + if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError, underlyingError.isDAVException, underlyingError.davExceptionMessage == "User disabled" { + authFailureHasEditOption = false + authFailureIgnoreStyle = .cancel + authFailureIgnoreLabel = "Continue offline".localized + authFailureMessage = "The account has been disabled." + } else { + if connection.bookmark.isTokenBased == true { + authFailureTitle = "Access denied".localized + authFailureMessage = "The connection's access token has expired or become invalid. Sign in again to re-gain access.".localized + + if let localizedDescription = nsError.userInfo[NSLocalizedDescriptionKey] { + authFailureMessage = "\(authFailureMessage!)\n\n(\(localizedDescription))" + } + } else { + authFailureMessage = "The server declined access with the credentials stored for this connection.".localized + } + } + + isAuthFailure = true + } + + if nsError.isOCError(withCode: .authorizationNoMethodData) || nsError.isOCError(withCode: .authorizationMissingData) { + authFailureMessage = "No authentication data has been found for this connection.".localized + + isAuthFailure = true + } + + if nsError.isOCError(withCode: .authorizationMethodNotAllowed) { + authFailureMessage = NSString(format: "Authentication with %@ is no longer allowed. Re-authentication needed.".localized as NSString, core.connection.authenticationMethod?.name ?? "??") as String + + isAuthFailure = true + } + + if isAuthFailure { + // Make sure only the first auth failure will actually lead to an alert + // (otherwise alerts could keep getting enqueued while the first alert is being shown, + // and then be presented even though they're no longer relevant). It's ok to only show + // an alert for the first auth failure, because the options are "Continue offline" (=> no longer show them) + // and "Edit" (=> log out, go to bookmark editing) + var doSkip = false + + OCSynchronized(self) { + doSkip = skipAuthorizationFailure // Keep in mind OCSynchronized() contents is running as a block, so "return" in here wouldn't have the desired effect + skipAuthorizationFailure = true + } + + if doSkip { + Log.debug("Skip authorization failure") + return false + } + } + } + + Log.debug("Handling error \(String(describing: error)) / \(String(describing: issue)) with isAuthFailure=\(isAuthFailure), bookmarkURL= \(String(describing: connection.bookmark.url)), authFailureHasEditOption=\(authFailureHasEditOption), authFailureIgnoreStyle=\(authFailureIgnoreStyle), authFailureIgnoreLabel=\(authFailureIgnoreLabel), authFailureMessage=\(String(describing: authFailureMessage))") + + if isAuthFailure { + let authFailure = AccountConnection.AuthFailure(bookmark: editBookmark, error: nsError, title: authFailureTitle, message: authFailureMessage, ignoreLabel: authFailureIgnoreLabel, ignoreStyle: authFailureIgnoreStyle, hasEditOption: authFailureHasEditOption, failureResolver: { [weak self] (authFailure, context) in + self?.attemptLogin(for: authFailure, context: context) + }) + + authenticationFailure = authFailure + + return true + } + + return false + } + + func attemptLogin(for authFailure: AccountConnection.AuthFailure, context: ClientContext) { + guard let bookmark = context.accountConnection?.bookmark, let bookmarkURL = bookmark.url else { + return + } + + // Clone bookmark + let clonedBookmark = OCBookmark(for: bookmarkURL) + + // Carry over permission for plain HTTP connections + clonedBookmark.userInfo[OCBookmarkUserInfoKey.allowHTTPConnection] = bookmark.userInfo[OCBookmarkUserInfoKey.allowHTTPConnection] + + // Create connection + let connection = OCConnection(bookmark: clonedBookmark) + + if let cookieSupportEnabled = OCCore.classSetting(forOCClassSettingsKey: .coreCookieSupportEnabled) as? Bool, cookieSupportEnabled == true { + connection.cookieStorage = OCHTTPCookieStorage() + Log.debug("Created cookie storage \(String(describing: connection.cookieStorage)) for client root view auth method detection") + } + + connection.prepareForSetup(options: nil, completionHandler: { [weak self] (issue, suggestedURL, supportedMethods, preferredMethods, generationOptions) in + Log.debug("Preparing for handling authentication error: issue=\(issue?.description ?? "nil"), suggestedURL=\(suggestedURL?.absoluteString ?? "nil"), supportedMethods: \(supportedMethods?.description ?? "nil"), preferredMethods: \(preferredMethods?.description ?? "nil"), existingAuthMethod: \(context.accountConnection?.bookmark.authenticationMethodIdentifier?.rawValue ?? "nil"))") + + if let preferredMethods = preferredMethods, preferredMethods.count > 0 { + if let existingAuthMethod = context.accountConnection?.bookmark.authenticationMethodIdentifier, !preferredMethods.contains(existingAuthMethod), let bookmark = context.accountConnection?.bookmark { + // Authentication method no longer supported + bookmark.scanForAuthenticationMethodsRequired = true // Mark bookmark as requiring a scan for available authentication methods before editing + OCBookmarkManager.shared.updateBookmark(bookmark) + } + } else { + // Supported authentication methods unclear -> rescan + if let bookmark = context.accountConnection?.bookmark { + bookmark.scanForAuthenticationMethodsRequired = true // Mark bookmark as requiring a scan for available authentication methods before editing + OCBookmarkManager.shared.updateBookmark(bookmark) + } + } + + context.alertQueue?.async { [weak self] (queueCompletionHandler) in + self?.presentAuthAlert(for: authFailure, preferredAuthenticationMethods: preferredMethods, context: context, completionHandler: queueCompletionHandler) + } + }) + } + + func presentAuthAlert(for authFailure: AccountConnection.AuthFailure, preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?, context: ClientContext, completionHandler: @escaping () -> Void) { + let alertController = ThemedAlertController(title: authFailure.title, + message: authFailure.message, + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: authFailure.ignoreLabel, style: authFailure.ignoreStyle, handler: { (_) in + completionHandler() + })) + + if authFailure.hasEditOption { + let action = UIAlertAction(title: "Sign in".localized, style: .default, handler: { [weak self] (_) in + completionHandler() + + var notifyAuthDelegate = true + + if let bookmark = self?.connection?.bookmark { + // var authenticationUpdater: AccountConnectionAuthenticationUpdater.Type? + // let updater = authenticationUpdater?.init(with: bookmark, preferredAuthenticationMethods: preferredAuthenticationMethods) + let updater = AccountAuthenticationUpdater(with: bookmark, preferredAuthenticationMethods: preferredAuthenticationMethods) + + if updater.canUpdateInline, let self = self, let viewController = context.presentationViewController { + notifyAuthDelegate = false + + updater.updateAuthenticationData(on: viewController, completion: { (error) in + if error == nil { + OCSynchronized(self) { + self.skipAuthorizationFailure = false // Auth failure fixed -> allow new failures to prompt for sign in again + } + } else if let nsError = error as NSError?, !nsError.isOCError(withCode: .authorizationCancelled) { + // Error updating authentication -> inform the user and provide option to retry + context.alertQueue?.async { [weak self] (queueCompletionHandler) in + let newAuthFail = AccountConnection.AuthFailure(bookmark: authFailure.bookmark, error: error as NSError?, title: "Error".localized, message: error?.localizedDescription, ignoreLabel: authFailure.ignoreLabel, ignoreStyle: authFailure.ignoreStyle, hasEditOption: authFailure.hasEditOption) + + self?.presentAuthAlert(for: newAuthFail, preferredAuthenticationMethods: preferredAuthenticationMethods, context: context, completionHandler: queueCompletionHandler) + } + } + }) + } + } + + if notifyAuthDelegate { + if let authDelegate = context.bookmarkEditingHandler, let presentationViewController = context.presentationViewController, let nsError = authFailure.error { + self?.connection?.disconnect(consumer: nil, completion: { error in + authDelegate.handleAuthError(for: presentationViewController, error: nsError, editBookmark: authFailure.bookmark, preferredAuthenticationMethods: preferredAuthenticationMethods) + }) + } else { + context.alertQueue?.async({ [weak context] (queueCompletionHandler) in + let alertController = ThemedAlertController(title: "Authentication failed".localized, + message: "Please open the app and select the account to re-authenticate.".localized, + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: { _ in + queueCompletionHandler() + })) + + context?.present(alertController, animated: true) + }) + } + + completionHandler() + } + }) + + alertController.addAction(action) + } + + context.present(alertController, animated: true, completion: nil) + } +} diff --git a/ownCloudAppShared/Client/Account/Controller/AccountConnectionErrorHandler.swift b/ownCloudAppShared/Client/Account/Controller/AccountConnectionErrorHandler.swift new file mode 100644 index 000000000..515640b2f --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/AccountConnectionErrorHandler.swift @@ -0,0 +1,118 @@ +// +// AccountConnectionErrorHandler.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 28.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public protocol AccountAuthenticationHandlerBookmarkEditingHandler: AnyObject { + func handleAuthError(for viewController: UIViewController, error: NSError, editBookmark: OCBookmark?, preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]?) +} + +open class AccountConnectionErrorHandler: NSObject, AccountConnectionCoreErrorHandler { + var connection: AccountConnection + var consumer: AccountConnectionConsumer? + var context: ClientContext + + init(for context: ClientContext, connection: AccountConnection? = nil) { + self.context = context + self.connection = connection ?? context.accountConnection! + + super.init() + + consumer = AccountConnectionConsumer(owner: self, coreErrorHandler: self) + self.connection.add(consumer: consumer!) + } + + deinit { + connection.remove(consumer: consumer!) + } + + public func account(connnection: AccountConnection, handleError error: Error?, issue inIssue: OCIssue?) -> Bool { + var issue = inIssue + var nsError = error as NSError? + + Log.debug("Received error \(nsError?.description ?? "nil")), issue \(issue?.description ?? "nil")") + + if let authError = issue?.authenticationError { + // Turn issues that are just converted authorization errors back into errors and discard the issue + nsError = authError + issue = nil + } + + Log.debug("Received error \(nsError?.description ?? "nil")), issue \(issue?.description ?? "nil")") + + if nsError?.isAccountConnectionAuthenticationError == true { + return false + } else { + context.alertQueue?.async { [weak self] (queueCompletionHandler) in + var presentIssue : OCIssue? = issue + var queueCompletionHandlerScheduled : Bool = false + + if issue == nil, let error = error { + presentIssue = OCIssue(forError: error, level: .error, issueHandler: nil) + } + + if presentIssue != nil { + var presentViewController : UIViewController? + var onViewController : UIViewController? + + if let startViewController = self?.context.presentationViewController { + var hostViewController : UIViewController = startViewController + + while hostViewController.presentedViewController != nil, + hostViewController.presentedViewController?.isBeingDismissed == false { + hostViewController = hostViewController.presentedViewController! + } + + onViewController = hostViewController + } + + if let presentIssue = presentIssue, presentIssue.type == .multipleChoice { + presentViewController = ThemedAlertController(with: presentIssue, completion: queueCompletionHandler) + } else if let onViewController = onViewController, let presentIssue = presentIssue { + IssuesCardViewController.present(on: onViewController, issue: presentIssue, bookmark: self?.connection.bookmark, completion: { [weak presentIssue] (response) in + switch response { + case .cancel: + presentIssue?.reject() + + case .approve: + presentIssue?.approve() + + case .dismiss: break + } + queueCompletionHandler() + }) + + queueCompletionHandlerScheduled = true + } + + if let presentViewController = presentViewController, let onViewController = onViewController { + queueCompletionHandlerScheduled = true + onViewController.present(presentViewController, animated: true, completion: nil) + } + } + + if !queueCompletionHandlerScheduled { + queueCompletionHandler() + } + } + } + + return true + } +} diff --git a/ownCloudAppShared/Client/Account/Controller/AccountController.swift b/ownCloudAppShared/Client/Account/Controller/AccountController.swift new file mode 100644 index 000000000..2b6c116a4 --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/AccountController.swift @@ -0,0 +1,684 @@ +// +// AccountController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 10.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +public protocol AccountControllerExtraItems: AccountController { + func updateExtraItems(dataSource: OCDataSourceArray) + func provideExtraItemViewController(for specialItem: SpecialItem, in context: ClientContext) -> UIViewController? +} + +public extension OCDataItemType { + static let accountController = OCDataItemType(rawValue: "accountController") +} + +public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, AccountConnectionStatusObserver, AccountConnectionMessageUpdates { + public struct Configuration { + public var showAccountPill: Bool + public var showShared: Bool + public var showSavedSearches: Bool + public var showQuickAccess: Bool + public var showActivity: Bool + public var autoSelectPersonalFolder: Bool + + public var sectionAppearance: UICollectionLayoutListConfiguration.Appearance = .sidebar + + public static var defaultConfiguration: Configuration { + return Configuration() + } + + public static var pickerConfiguration: Configuration { + var config = Configuration() + + config.showSavedSearches = false + config.showQuickAccess = false + config.showActivity = false + + config.sectionAppearance = .insetGrouped + + config.autoSelectPersonalFolder = false + + return config + } + + public init() { + showAccountPill = true + showShared = true + showSavedSearches = true + showQuickAccess = true + showActivity = true + + autoSelectPersonalFolder = true + } + } + + public enum SpecialItem: String, CaseIterable { + case sharingFolder + case sharedWithMe + case sharedByMe + case sharedByLink + + case spacesFolder + + case savedSearchesFolder + + case quickAccessFolder + case favoriteItems + case availableOfflineItems + + case searchPDFDocuments + case searchDocuments + // case searchText + case searchImages + case searchVideos + case searchAudios + + case activity + } + + open var clientContext: ClientContext + open var configuration: Configuration + + open var connectionErrorHandler: AccountConnectionCoreErrorHandler? + + weak var accountControllerSection: AccountControllerSection? + + open var bookmark: OCBookmark? { // Convenience accessor + return connection?.bookmark + } + + public init(bookmark: OCBookmark, context: ClientContext, configuration: Configuration) { + let accountConnection = AccountConnectionPool.shared.connection(for: bookmark) + + self.clientContext = ClientContext(with: context, modifier: { context in + context.accountConnection = accountConnection + context.progressSummarizer = accountConnection?.progressSummarizer + context.actionProgressHandlerProvider = accountConnection + context.inlineMessageCenter = accountConnection + }) + + self.configuration = configuration + + itemsDataSource = OCDataSourceComposition(sources: []) + controllerDataSource = OCDataSourceArray(items: []) + + consumer = AccountConnectionConsumer() + + let bookmarkUUID = bookmark.uuid + + for specialItem in SpecialItem.allCases { + if let representationSideBarItemRef = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: bookmarkUUID, specialItem: specialItem).representationSideBarItemRef { + specialItemsDataReferences[specialItem] = representationSideBarItemRef + } + } + + legacyAccountRootLocation = OCLocation.legacyRoot + legacyAccountRootLocation.bookmarkUUID = bookmark.uuid + + super.init() + + controllerDataSource.setVersionedItems([ self ]) + + consumer.owner = self + consumer.statusObserver = self + consumer.messageUpdateHandler = self + + connection = accountConnection + connection?.add(consumer: consumer) + + addErrorHandler() + } + + func destroy() { + // Break retain cycles + controllerDataSource.setVersionedItems([]) + } + + deinit { + connection?.remove(consumer: consumer) + } + + // MARK: - Connection + open weak var connection: AccountConnection? + var consumer: AccountConnectionConsumer + + // MARK: - Connect & Disconnect + public typealias CompletionHandler = (_ error: Error?) -> Void + + public func connect(completion: CompletionHandler?) { + if let bookmark = connection?.bookmark, + !OCBookmarkManager.isLocked(bookmark: bookmark, presentAlertOn: clientContext.rootViewController) { + // Add controller's error handler + addErrorHandler() + + connection?.connect(consumer: consumer, completion: completion) + } else { + completion?(NSError.init(ocError: .internal)) + } + } + + public func disconnect(completion: CompletionHandler?) { + connection?.disconnect(consumer: consumer, completion: completion) + } + + func addErrorHandler() { + if connectionErrorHandler == nil { + self.connectionErrorHandler = AccountConnectionErrorHandler(for: clientContext) + } + } + + func removeErrorHandler() { + connectionErrorHandler = nil + } + + // MARK: - Status handling + public func account(connection: AccountConnection, changedStatusTo status: AccountConnection.Status, initial: Bool) { + if let vault = connection.core?.vault { + // Create savedSearchesDataSource if wanted + if configuration.showSavedSearches, savedSearchesDataSource == nil { + savedSearchesDataSource = OCDataSourceKVO(object: vault, keyPath: "savedSearches", versionedItemUpdateHandler: { [weak self] obj, keypath, newValue in + if let savedSearches = newValue as? [OCSavedSearch] { + let searches = savedSearches.filter { savedSearch in return !savedSearch.isTemplate } + self?.savedSearchesVisible = searches.count > 0 + return searches + } + + return nil + }) + } + } else { + savedSearchesDataSource = nil + } + + switch status { + case .authenticationError(failure: let failure): + // Authentication failure + authFailure = failure + + case .coreAvailable, .online: + // Begin to show account items + showAccountItems = true + showDisconnectButton = true + authFailure = nil + + default: + // Do not show account items + showAccountItems = false + showDisconnectButton = false + authFailure = nil + } + + if case .noCore = status, !initial { + // Remove controller's error handler + removeErrorHandler() + + // Send connection closed navigation event + NavigationRevocationEvent.connectionClosed(bookmarkUUID: connection.bookmark.uuid).send() + } + } + + // MARK: - Authentication failures + var authFailure: AccountConnection.AuthFailure? { + didSet { + if let authFailure = authFailure { + let authFailureResolveAction = OCAction(title: authFailure.title, icon: OCSymbol.icon(forSymbolName: "person.crop.circle.badge.exclamationmark"), action: { [weak self] _, _, completion in + if let self = self { + self.authFailure?.resolve(context: self.clientContext) + } + completion(nil) + }) + + authFailureResolveAction.selectable = false + authFailureResolveAction.buttonLabel = "More".localized + authFailureResolveAction.type = .warning + + controllerDataSource.setVersionedItems([ + self, + authFailureResolveAction + ]) + } else { + if authFailure == nil, oldValue == nil { + return + } + + controllerDataSource.setVersionedItems([ + self + ]) + } + } + } + + // MARK: - Account items + var showAccountItems: Bool = false { + didSet { + if showAccountItems != oldValue { + if showAccountItems { + composeItemsDataSource() + } else { + authFailure = nil + itemsDataSource.sources = [] + } + } + } + } + + @objc dynamic var showDisconnectButton: Bool = false + + var savedSearchesDataSource: OCDataSourceKVO? + var savedSearchesVisible: Bool = true { + didSet { + if oldValue != savedSearchesVisible, let savedSearchesFolderDatasource = specialItemsDataSources[.savedSearchesFolder] { + itemsDataSource.setInclude(savedSearchesVisible, for: savedSearchesFolderDatasource) + } + } + } + var savedSearchesCondition: DataSourceCondition? + + open var specialItems: [SpecialItem : OCDataItem & OCDataItemVersioning] = [:] + open var specialItemsDataReferences: [SpecialItem : OCDataItemReference] = [:] + open var specialItemsDataSources: [SpecialItem : OCDataSource] = [:] + + open var sharingItemsDataSource: OCDataSourceArray = OCDataSourceArray(items: []) + + open var quickAccessItemsDataSource: OCDataSourceArray = OCDataSourceArray(items: []) + + open var extraItemsDataSource: OCDataSourceArray = OCDataSourceArray(items: []) + + open var personalSpaceDataItemRef: OCDataItemReference? { + var personalSpaceItemRef: OCDataItemReference? + + if connection?.core?.useDrives == true { + personalSpaceItemRef = connection?.core?.drives.first(where: { drive in + return drive.specialType == .personal + })?.dataItemReference + } else { + personalSpaceItemRef = legacyAccountRootLocation.dataItemReference + } + + return personalSpaceItemRef + } + + open var sharesFolderDataItemRef: OCDataItemReference? { + return connection?.core?.drives.first(where: { drive in + return drive.specialType == .shares + })?.dataItemReference + } + + private var legacyAccountRootLocation: OCLocation + + func composeItemsDataSource() { + if let core = connection?.core { + var sources : [OCDataSource] = [] + + // Personal Folder, Shared Files + Drives + if core.useDrives { + // Spaces + let spacesDataSource = self.buildTopFolder(with: core.projectDrivesDataSource, title: "Spaces".localized, icon: OCSymbol.icon(forSymbolName: "square.grid.2x2"), topItem: .spacesFolder, viewControllerProvider: { [weak self] context, action in + return self?.provideViewController(for: .spacesFolder, in: context) + }) + + if let accountControllerSection = accountControllerSection, + let expandedItemRefs = accountControllerSection.collectionViewController?.wrap(references: [ /* specialItemsDataReferences[.spacesFolder]! */ ], forSection: accountControllerSection.identifier) { + accountControllerSection.expandedItemRefs = expandedItemRefs + } + + sources = [ + core.personalDriveDataSource, + spacesDataSource + ] + } else { + // OC10 Root folder + sources = [ + OCDataSourceArray(items: [legacyAccountRootLocation]) + ] + } + + // Sharing + if configuration.showShared { + let (sharingFolderDataSource, sharingFolderItem) = self.buildFolder(with: sharingItemsDataSource, title: "Shares".localized, icon: OCSymbol.icon(forSymbolName: "arrowshape.turn.up.left"), folderItemRef: specialItemsDataReferences[.sharingFolder]!) + + specialItems[.sharingFolder] = sharingFolderItem + specialItemsDataSources[.sharingFolder] = sharingFolderDataSource + + if specialItems[.sharedWithMe] == nil { + specialItems[.sharedWithMe] = CollectionSidebarAction(with: "Shared with me".localized, icon: OCSymbol.icon(forSymbolName: "arrowshape.turn.up.left"), identifier: specialItemsDataReferences[.sharedWithMe], viewControllerProvider: { [weak self] context, action in + return self?.provideViewController(for: .sharedWithMe, in: context) + }, cacheViewControllers: false) + } + + if specialItems[.sharedByMe] == nil { + specialItems[.sharedByMe] = CollectionSidebarAction(with: "Shared by me".localized, icon: OCSymbol.icon(forSymbolName: "arrowshape.turn.up.right"), identifier: specialItemsDataReferences[.sharedByMe], viewControllerProvider: { [weak self] context, action in + return self?.provideViewController(for: .sharedByMe, in: context) + }, cacheViewControllers: false) + } + + if specialItems[.sharedByLink] == nil { + specialItems[.sharedByLink] = CollectionSidebarAction(with: "Shared by link".localized, icon: OCSymbol.icon(forSymbolName: "link"), identifier: specialItemsDataReferences[.sharedByLink], viewControllerProvider: { [weak self] context, action in + return self?.provideViewController(for: .sharedByLink, in: context) + }, cacheViewControllers: false) + } + + var sharingItems : [OCDataItem & OCDataItemVersioning] = [] + + if let sharingItem = specialItems[.sharedWithMe] { sharingItems.append(sharingItem) } + if let sharingItem = specialItems[.sharedByMe] { sharingItems.append(sharingItem) } + if let sharingItem = specialItems[.sharedByLink] { sharingItems.append(sharingItem) } + + sharingItemsDataSource.setVersionedItems(sharingItems) + + sources.insert(sharingFolderDataSource, at: 1) + } + + // Saved searches + if configuration.showSavedSearches, let savedSearchesDataSource = savedSearchesDataSource { + savedSearchesCondition = DataSourceCondition(.empty, with: savedSearchesDataSource, initial: true, action: { [weak self] condition in + self?.savedSearchesVisible = condition.fulfilled == false + }) + + let (savedSearchesFolderDataSource, savedSearchesFolderItem) = self.buildFolder(with: savedSearchesDataSource, title: "Saved searches".localized, icon: OCSymbol.icon(forSymbolName: "magnifyingglass"), folderItemRef:specialItemsDataReferences[.savedSearchesFolder]!) + + specialItems[.savedSearchesFolder] = savedSearchesFolderItem + specialItemsDataSources[.savedSearchesFolder] = savedSearchesFolderDataSource + + sources.append(savedSearchesFolderDataSource) + } + + // Quick access + if configuration.showQuickAccess { + var quickAccessItems: [OCDataItem & OCDataItemVersioning] = [] + + // Favorites + if bookmark?.hasCapability(.favorites) == true { + if specialItems[.favoriteItems] == nil { + specialItems[.favoriteItems] = buildSidebarSpecialItem(with: "Favorites".localized, icon: OCSymbol.icon(forSymbolName: "star"), for: .favoriteItems) + } + if let sideBarItem = specialItems[.favoriteItems] { + quickAccessItems.append(sideBarItem) + } + } + + // Available offline + if specialItems[.availableOfflineItems] == nil { + specialItems[.availableOfflineItems] = buildSidebarSpecialItem(with: "Available Offline".localized, icon: UIImage(named: "cloud-available-offline"), for: .availableOfflineItems) + } + if let sideBarItem = specialItems[.availableOfflineItems] { + quickAccessItems.append(sideBarItem) + } + + // Convenience searches + if specialItems[.searchPDFDocuments] == nil { + specialItems[.searchPDFDocuments] = OCSavedSearch(scope: .account, location: nil, name: "PDF Documents".localized, isTemplate: false, searchTerm: ":pdf").withCustomIcon(name: "doc.richtext").useNameAsTitle(true) + } + if specialItems[.searchDocuments] == nil { + specialItems[.searchDocuments] = OCSavedSearch(scope: .account, location: nil, name: "Documents".localized, isTemplate: false, searchTerm: ":document").withCustomIcon(name: "doc").useNameAsTitle(true) + } + if specialItems[.searchImages] == nil { + specialItems[.searchImages] = OCSavedSearch(scope: .account, location: nil, name: "Images".localized, isTemplate: false, searchTerm: ":image").withCustomIcon(name: "photo").useNameAsTitle(true) + } + if specialItems[.searchVideos] == nil { + specialItems[.searchVideos] = OCSavedSearch(scope: .account, location: nil, name: "Videos".localized, isTemplate: false, searchTerm: ":video").withCustomIcon(name: "film").useNameAsTitle(true) + } + if specialItems[.searchAudios] == nil { + specialItems[.searchAudios] = OCSavedSearch(scope: .account, location: nil, name: "Audios".localized, isTemplate: false, searchTerm: ":audio").withCustomIcon(name: "waveform").useNameAsTitle(true) + } + + let addSpecialItemsTypes: [SpecialItem] = [ .searchPDFDocuments, .searchDocuments, .searchImages, .searchVideos, .searchAudios ] + + for specialItemType in addSpecialItemsTypes { + if let item = specialItems[specialItemType] as? OCSavedSearch { + if let representationUUID = specialItemsDataReferences[specialItemType] as? String { + item.uuid = representationUUID + } + quickAccessItems.append(item) + } + } + + quickAccessItemsDataSource.setVersionedItems(quickAccessItems) + + // Quick access folder + if specialItems[.quickAccessFolder] == nil { + let (quickAccessFolderDataSource, quickAccessFolderItem) = self.buildFolder(with: quickAccessItemsDataSource, title: "Quick Access".localized, icon: OCSymbol.icon(forSymbolName: "speedometer"), folderItemRef:specialItemsDataReferences[.quickAccessFolder]!) + + specialItems[.quickAccessFolder] = quickAccessFolderItem + specialItemsDataSources[.quickAccessFolder] = quickAccessFolderDataSource + } + + if let quickAccessFolderDataSource = specialItemsDataSources[.quickAccessFolder] { + sources.append(quickAccessFolderDataSource) + } + } + + // Extra items (Activity & Co via class extension in the app) + if let extraItemsSupport = self as? AccountControllerExtraItems { + extraItemsSupport.updateExtraItems(dataSource: extraItemsDataSource) + + sources.append(extraItemsDataSource) + } + + itemsDataSource.sources = sources + + if let savedSearchesFolderDataSource = specialItemsDataSources[.savedSearchesFolder], !savedSearchesVisible { + itemsDataSource.setInclude(savedSearchesVisible, for: savedSearchesFolderDataSource) + } + } + } + + func buildActionFolder(with contentsDataSource: OCDataSource, title: String, icon: UIImage?, folderItemRef: OCDataItemReference = "_folder_\(UUID().uuidString)" as NSString, viewControllerProvider: @escaping CollectionSidebarAction.ViewControllerProvider) -> (OCDataSource, CollectionSidebarAction) { + let folderAction = CollectionSidebarAction(with: title, icon: icon, identifier: folderItemRef, viewControllerProvider: viewControllerProvider) + folderAction.childrenDataSource = contentsDataSource + + let titleSource = OCDataSourceArray() + titleSource.setVersionedItems([ folderAction ]) + + return (titleSource, folderAction) + } + + func buildTopFolder(with contentsDataSource: OCDataSource, title: String, icon: UIImage?, topItem: SpecialItem, viewControllerProvider: @escaping CollectionSidebarAction.ViewControllerProvider) -> OCDataSource { + let (titleSource, folderAction) = buildActionFolder(with: contentsDataSource, title: title, icon: icon, folderItemRef: specialItemsDataReferences[topItem]!, viewControllerProvider: viewControllerProvider) + + specialItems[topItem] = folderAction + specialItemsDataSources[topItem] = titleSource + specialItemsDataReferences[topItem] = folderAction.dataItemReference + + return titleSource + } + + func buildFolder(with contentsDataSource: OCDataSource, title: String, icon: UIImage?, folderItemRef: OCDataItemReference = "_folder_\(UUID().uuidString)" as NSString) -> (OCDataSource, OCDataItemPresentable) { + let folderItem = OCDataItemPresentable(reference: folderItemRef, originalDataItemType: .presentable, version: "1" as NSString) + folderItem.title = title + folderItem.image = icon + + folderItem.hasChildrenProvider = { (dataSource, item) in + return true + } + + folderItem.childrenDataSourceProvider = { (parentItemDataSource, parentItem) in + return contentsDataSource + } + + let titleSource = OCDataSourceArray() + titleSource.setVersionedItems([ folderItem ]) + + return (titleSource, folderItem) + } + + // MARK: - View controller construction + open func provideViewController(for specialItem: SpecialItem, in context: ClientContext?) -> UIViewController? { + guard let context else { return nil } + + var viewController: UIViewController? + + switch specialItem { + case .sharedWithMe: + viewController = ClientSharedWithMeViewController(context: context) + + case .sharedByMe: + viewController = ClientSharedByMeViewController(context: context, byMe: true) + + case .sharedByLink: + viewController = ClientSharedByMeViewController(context: context, byLink: true) + + case .spacesFolder: + viewController = AccountControllerSpacesGridViewController(with: context) + + case .availableOfflineItems: + if let core = context.core { + let availableOfflineFilesDataSource = core.availableOfflineFilesDataSource + let sortedDataSource = SortedItemDataSource(itemDataSource: availableOfflineFilesDataSource) + + let availableOfflineViewController = ClientItemViewController(context: context, query: nil, itemsDatasource: sortedDataSource, showRevealButtonForItems: true, emptyItemListIcon: UIImage(named: "cloud-available-offline"), emptyItemListTitleLocalized: "No files available offline".localized, emptyItemListMessageLocalized: "Files selected and downloaded for offline availability will show up here.".localized) + availableOfflineViewController.navigationTitle = "Available Offline".localized + + sortedDataSource.sortingFollowsContext = availableOfflineViewController.clientContext + + let availableOfflineItemPoliciesDataSource = core.availableOfflineItemPoliciesDataSource + + let locationsSection = CollectionViewSection(identifier: "locations", dataSource: availableOfflineItemPoliciesDataSource, cellStyle: .init(with: .tableCell), cellLayout: .list(appearance: .plain, contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 30, trailing: 0)), clientContext: context) + + locationsSection.hideIfEmptyDataSource = availableOfflineFilesDataSource + locationsSection.boundarySupplementaryItems = [ + .title("Locations".localized, pinned: true) + ] + locationsSection.hidden = true + + let downloadedFilesHeaderSection = CollectionViewSection(identifier: "downloadedFilesHeader", dataSource: nil, cellStyle: .init(with: .tableCell), cellLayout: .list(appearance: .plain), clientContext: context) + downloadedFilesHeaderSection.hideIfEmptyDataSource = sortedDataSource + downloadedFilesHeaderSection.boundarySupplementaryItems = [ + .title("Downloaded Files".localized) + ] + downloadedFilesHeaderSection.hidden = true + + availableOfflineViewController.insert(sections: [ locationsSection, downloadedFilesHeaderSection ], at: 0) + + availableOfflineViewController.revoke(in: context, when: [ .connectionClosed ]) + + viewController = availableOfflineViewController + } + + case .favoriteItems: + if let favoritesDataSource = context.core?.favoritesDataSource { + let favoritesContext = ClientContext(with: context, modifier: { context in + context.queryDatasource = favoritesDataSource + }) + + let sortedDataSource = SortedItemDataSource(itemDataSource: favoritesDataSource) + + let favoritesViewController = ClientItemViewController(context: favoritesContext, query: nil, itemsDatasource: sortedDataSource, showRevealButtonForItems: true, emptyItemListIcon: OCSymbol.icon(forSymbolName: "star.fill"), emptyItemListTitleLocalized: "No favorites found".localized, emptyItemListMessageLocalized: "If you make an item a favorite, it will turn up here.".localized) + favoritesViewController.navigationTitle = "Favorites".localized + + sortedDataSource.sortingFollowsContext = favoritesViewController.clientContext + + favoritesViewController.revoke(in: favoritesContext, when: [ .connectionClosed ]) + viewController = favoritesViewController + } + + default: + if let extraItemsProvider = self as? AccountControllerExtraItems, + let extraViewController = extraItemsProvider.provideExtraItemViewController(for: specialItem, in: context) { + viewController = extraViewController + } + } + + if viewController?.navigationBookmark == nil { + viewController?.navigationBookmark = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: context.accountConnection?.bookmark.uuid, specialItem: specialItem) + } + + return viewController + } + + // MARK: - Data sources + open var controllerDataSource: OCDataSourceArray + open var itemsDataSource: OCDataSourceComposition + private weak var _accountSectionDataSource: OCDataSourceComposition? + open var accountSectionDataSource: OCDataSource? { + if let dataSource = _accountSectionDataSource { + return dataSource + } + + let dataSource = OCDataSourceComposition(sources: [ + controllerDataSource, + itemsDataSource + ]) + + if !configuration.showAccountPill { + dataSource.setInclude(false, for: controllerDataSource) + } + + _accountSectionDataSource = dataSource + + return dataSource + } + + // MARK: - OCDataItem & OCDataItemVersioning + open var dataItemType: OCDataItemType = .accountController + open var dataItemReference: OCDataItemReference = NSString(string: NSUUID().uuidString) + open var dataItemVersion: OCDataItemVersion { + let bookmark = self.connection?.bookmark + return "\(bookmark?.shortName ?? "")-#_#-\(bookmark?.displayName ?? "")" as NSObject + } +} + +// MARK: - Selection handling +extension AccountController: DataItemSelectionInteraction { + public func allowSelection(in viewController: UIViewController?, section: CollectionViewSection?, with context: ClientContext?) -> Bool { + func revealPersonalItem() { + if let personalSpaceDataItemRef = self.personalSpaceDataItemRef, + let sectionID = section?.identifier, + let personalFolderItemRef = section?.collectionViewController?.wrap(references: [personalSpaceDataItemRef], forSection: sectionID).first, + let /* spacesFolderItemRef */ _ = section?.collectionViewController?.wrap(references: [specialItemsDataReferences[.spacesFolder]!], forSection: sectionID).first { + section?.collectionViewController?.addActions([ + CollectionViewAction(kind: .select(animated: false, scrollPosition: .centeredVertically), itemReference: personalFolderItemRef) + // CollectionViewAction(kind: .expand(animated: true), itemReference: spacesFolderItemRef) + ]) + } + } + + if let bookmark = bookmark { + self.connect(completion: { error in + if let error = error { + Log.error("Connected with \(error)") + + let alert = ThemedAlertController(title: NSString(format: "Error opening %@".localized as NSString, bookmark.shortName) as String, message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + + context?.rootViewController?.present(alert, animated: true) + } else { + if self.configuration.autoSelectPersonalFolder { + revealPersonalItem() + } + } + }) + } + return false + } +} + +// MARK: - Special Side Bar Items +extension AccountController { + func buildSidebarSpecialItem(with title: String, icon: UIImage?, for specialItem: SpecialItem) -> OCDataItem & OCDataItemVersioning { + let item = CollectionSidebarAction(with: title, icon: icon, viewControllerProvider: { [weak self] (context, action) in + return self?.provideViewController(for: specialItem, in: context) + }, cacheViewControllers: false) + + item.identifier = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: connection?.bookmark.uuid, specialItem: specialItem).representationSideBarItemRef as? String + + return item + } +} diff --git a/ownCloudAppShared/Client/Account/Controller/AccountControllerSection.swift b/ownCloudAppShared/Client/Account/Controller/AccountControllerSection.swift new file mode 100644 index 000000000..656d457ca --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/AccountControllerSection.swift @@ -0,0 +1,31 @@ +// +// AccountControllerSection.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 15.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class AccountControllerSection: CollectionViewSection { + open var accountController: AccountController + + public init(with accountController: AccountController) { + self.accountController = accountController + let uuid = accountController.connection?.bookmark.uuid.uuidString ?? "_missing_bookmark_" + super.init(identifier: "account.\(uuid)", dataSource: accountController.accountSectionDataSource, cellStyle: CollectionViewCellStyle(with: .sideBar), cellLayout: .list(appearance: accountController.configuration.sectionAppearance), clientContext: accountController.clientContext) + accountController.accountControllerSection = self + } +} diff --git a/ownCloudAppShared/Client/Account/Controller/AccountControllerSpacesGridViewController.swift b/ownCloudAppShared/Client/Account/Controller/AccountControllerSpacesGridViewController.swift new file mode 100644 index 000000000..347bd870c --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/AccountControllerSpacesGridViewController.swift @@ -0,0 +1,71 @@ +// +// AccountControllerSpacesGridViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 AccountControllerSpacesGridViewController: CollectionViewController, ViewControllerPusher { + var spacesSection: CollectionViewSection + var noSpacesCondition: DataSourceCondition? + + init(with context: ClientContext) { + let gridContext = ClientContext(with: context) + + gridContext.postInitializationModifier = { (owner, context) in + context.viewControllerPusher = owner as? ViewControllerPusher + } + + spacesSection = CollectionViewSection(identifier: "spaces", dataSource: context.core?.projectDrivesDataSource, cellStyle: .init(with: .gridCell), cellLayout: .grid(itemWidthDimension: .fractionalWidth(0.33), itemHeightDimension: .absolute(200), contentInsets: NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))) + + super.init(context: gridContext, sections: [ spacesSection ], useStackViewRoot: true, hierarchic: false) + + self.revoke(in: gridContext, when: [ .connectionClosed ]) + + navigationItem.title = "Spaces".localized + + if let projectDrivesDataSource = context.core?.projectDrivesDataSource { + let noSpacesMessage = ComposedMessageView(elements: [ + .image(OCSymbol.icon(forSymbolName: "square.grid.2x2")!, size: CGSize(width: 64, height: 48), alignment: .centered), + .text("No spaces".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered) + ]) + + noSpacesCondition = DataSourceCondition(.empty, with: projectDrivesDataSource, initial: true, action: { [weak self] condition in + let coverView = (condition.fulfilled == true) ? noSpacesMessage : nil + self?.setCoverView(coverView, layout: .top) + }) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func pushViewController(context: ClientContext?, provider: (ClientContext) -> UIViewController?, push: Bool, animated: Bool) -> UIViewController? { + var viewController: UIViewController? + + if let context { + viewController = provider(context) + } + + if push, let viewController { + navigationController?.pushViewController(viewController, animated: animated) + } + + return viewController + } +} diff --git a/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift b/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift new file mode 100644 index 000000000..4a7621390 --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift @@ -0,0 +1,83 @@ +// +// BrowserNavigationBookmark+AccountController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 09.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension BrowserNavigationBookmark { + var representationSideBarItemRef: OCDataItemReference? { + return representationSideBarItemRefs?.first + } + + var representationSideBarItemRefs: [OCDataItemReference]? { + // Returns the OCDataItemReference of the sidebar item that best represents the BrowserNavigationBookmark + var itemRefs: [OCDataItemReference] = [] + + func composedItemRef(for specialItem: AccountController.SpecialItem) -> OCDataItemReference { + return ":B:\(bookmarkUUID?.uuidString ?? ""):I:\(specialItem.rawValue)" as NSString + } + + switch type { + case .dataItem: + if let driveID = location?.driveID as? NSString { + // Respective driveID (OCDrive.dataItemReference) + if let bookmarkUUID, + let spacesFolderID = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: bookmarkUUID, specialItem: .spacesFolder).representationSideBarItemRef { + // Provide spaces folder as fallback + return [driveID, spacesFolderID] + } else { + return [driveID] + } + } else if let locationItemRef = location?.dataItemReference { + // OCLocation.dataItemReference + // Legacy account (for lack of drives) + let rootLocation = OCLocation.legacyRoot + rootLocation.bookmarkUUID = bookmarkUUID + + return [locationItemRef, rootLocation.dataItemReference] + } else if let savedSearchUUID = savedSearch?.uuid { + // OCSavedSearch.uuid + itemRefs.append(savedSearchUUID as NSString) + + switch specialItem { + case .searchPDFDocuments, .searchDocuments, .searchImages, .searchVideos, .searchAudios: + itemRefs.append(composedItemRef(for: .quickAccessFolder)) + + default: break + + } + } + + case .specialItem: + if let specialItem { + itemRefs.append(composedItemRef(for: specialItem)) + + switch specialItem { + case .sharedByMe, .sharedWithMe, .sharedByLink: + itemRefs.append(composedItemRef(for: .sharingFolder)) + + default: break + } + } + + default: break + } + + return itemRefs.count > 0 ? itemRefs : nil + } +} diff --git a/ownCloud/Messages/MessageGroup.swift b/ownCloudAppShared/Client/Account/Messages/MessageGroup.swift similarity index 82% rename from ownCloud/Messages/MessageGroup.swift rename to ownCloudAppShared/Client/Account/Messages/MessageGroup.swift index f64862a47..53b4fb350 100644 --- a/ownCloud/Messages/MessageGroup.swift +++ b/ownCloudAppShared/Client/Account/Messages/MessageGroup.swift @@ -19,11 +19,11 @@ import UIKit import ownCloudSDK -class MessageGroup: NSObject { - var identifier : OCMessageCategoryIdentifier? +public class MessageGroup: NSObject { + public var identifier : OCMessageCategoryIdentifier? private var _groupTitle : String? - var groupTitle : String? { + public var groupTitle : String? { get { // Derive group title if let identifier = identifier, let issueTemplate = OCMessageTemplate(forIdentifier: OCMessageTemplateIdentifier(rawValue: identifier.rawValue)) { @@ -37,9 +37,9 @@ class MessageGroup: NSObject { } } - var messages : [OCMessage] = [] + public var messages : [OCMessage] = [] - init(with message: OCMessage) { + public init(with message: OCMessage) { identifier = message.categoryIdentifier messages = [message] } diff --git a/ownCloud/Messages/MessageSelector.swift b/ownCloudAppShared/Client/Account/Messages/MessageSelector.swift similarity index 81% rename from ownCloud/Messages/MessageSelector.swift rename to ownCloudAppShared/Client/Account/Messages/MessageSelector.swift index 8de9042f0..def7baf71 100644 --- a/ownCloud/Messages/MessageSelector.swift +++ b/ownCloudAppShared/Client/Account/Messages/MessageSelector.swift @@ -19,33 +19,33 @@ import UIKit import ownCloudSDK -typealias MessageSelectorFilter = (_ message: OCMessage) -> Bool -typealias MessageSelectorChangeHandler = (_ messages: [OCMessage]?, _ groups : [MessageGroup]?, _ syncRecordIDs : Set?) -> Void +public typealias MessageSelectorFilter = (_ message: OCMessage) -> Bool +public typealias MessageSelectorChangeHandler = (_ messages: [OCMessage]?, _ groups : [MessageGroup]?, _ syncRecordIDs : Set?) -> Void -extension OCMessageCategoryIdentifier { +public extension OCMessageCategoryIdentifier { static let other : OCMessageCategoryIdentifier = OCMessageCategoryIdentifier(rawValue: "_other") } -class MessageSelector: NSObject { +public class MessageSelector: NSObject { private var observer : NSKeyValueObservation? private var rateLimiter : OCRateLimiter private var filter : MessageSelectorFilter? - var queue : OCMessageQueue? - var handler : MessageSelectorChangeHandler? + public var queue : OCMessageQueue? + public var handler : MessageSelectorChangeHandler? private var provideGroupedSelection : Bool = false private var provideSyncRecordIDs : Bool = false private var selectionUUIDs : [OCMessageUUID] = [] - var selection : [OCMessage]? { + public var selection : [OCMessage]? { didSet { self.handler?(selection, groupedSelection, syncRecordIDsInSelection) } } - var groupedSelection : [MessageGroup]? - var syncRecordIDsInSelection : Set? + public var groupedSelection : [MessageGroup]? + public var syncRecordIDsInSelection : Set? - init(from messageQueue: OCMessageQueue = .global, filter messageFilter: MessageSelectorFilter?, provideGroupedSelection: Bool = false, provideSyncRecordIDs: Bool = false, handler: MessageSelectorChangeHandler?) { + public init(from messageQueue: OCMessageQueue = .global, filter messageFilter: MessageSelectorFilter?, provideGroupedSelection: Bool = false, provideSyncRecordIDs: Bool = false, handler: MessageSelectorChangeHandler?) { rateLimiter = OCRateLimiter(minimumTime: 0.2) filter = messageFilter diff --git a/ownCloudAppShared/Client/Account/README.md b/ownCloudAppShared/Client/Account/README.md new file mode 100644 index 000000000..58b0967b8 --- /dev/null +++ b/ownCloudAppShared/Client/Account/README.md @@ -0,0 +1,23 @@ +# Account +The `Account` set of classes manage different aspects of an account and its connection. + +## AccountConnection +The `AccountConnection` set of classes is used to manage a shared connection. + +### AccountConnectionPool +The pool keeps record of existing `AccountConnection` instances and creates new ones as needed. For every `OCBookmark`, only one instance of `AccountConnection` is created. + +### AccountConnection +The connection is responsible for connecting to and disconnecting from a server, keeping track of the OCCore and distributing access to it. + +### AccountConnectionConsumer +An `AccountConnection` is consumed by `AccountConnectionConsumer`s. Consumers group everything that is linked to an OCCore, such as `OCMessagePresenter`s, `OCFileProviderServiceStandby` instances, delegates, error handlers, … +Consumers allow to cleanly plug and unplug consumers of the account connection in a single step, so that UI elements gain consistent access to events and more, regardless of whether they were the initial element to first use a connection - or not. + +## AccountController +The `AccountController` set of classes provide easy access to everything needed to provide a rich representation of an account and its contents in the UI. + +### AccountController + +### AccountControllerSection +`AccountControllerSection` is a convenience wrapper for `AccountController`. It's content is derived from the `AccountController.accountSectionDataSource`. diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift index 1b8a30b38..b011311e0 100644 --- a/ownCloudAppShared/Client/Actions/Action.swift +++ b/ownCloudAppShared/Client/Actions/Action.swift @@ -1,6 +1,6 @@ // // Action.swift -// ownCloud +// ownCloudAppShared // // Created by Pablo Carrascal on 30/10/2018. // Copyright © 2018 ownCloud GmbH. All rights reserved. @@ -67,6 +67,8 @@ public extension OCExtensionLocationIdentifier { static let keyboardShortcut: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("keyboardShortcut") //!< Currently used for UIKeyCommand static let contextMenuItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("contextMenuItem") //!< Used in UIMenu static let contextMenuSharingItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("contextMenuSharingItem") //!< Used in UIMenu + static let unviewableFileType: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("unviewableFileType") //!< Used in PreviewController for unviewable file types + static let locationPickerBar: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("locationPickerBar") //!< Used in ClientLocationPicker } public class ActionExtension: OCExtension { @@ -499,12 +501,19 @@ open class Action : NSObject { ocAction.identifier = actionExtension.identifier.rawValue if singleVersion { ocAction.version = actionExtension.identifier.rawValue + ocAction.supportsDrop = true } ocAction.type = (actionExtension.category == .destructive) ? .destructive : .regular return ocAction } + open func provideBarButtonItem() -> UIBarButtonItem { + return UIBarButtonItem(title: actionExtension.name, image: icon, primaryAction: UIAction(handler: { (_) in + self.perform() + })) + } + // MARK: - Action metadata class open func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { return nil @@ -531,6 +540,7 @@ public extension OCClassSettingsIdentifier { public extension OCClassSettingsKey { static let allowedActions = OCClassSettingsKey("allowed") static let disallowedActions = OCClassSettingsKey("disallowed") + static let excludedSystemActivities = OCClassSettingsKey("excludedSystemActivities") } extension Action : OCClassSettingsSupport { @@ -576,6 +586,54 @@ extension Action : OCClassSettingsSupport { .description : "List of all disallowed actions. If provided, actions not listed here are allowed.", .category : "Actions", .status : OCClassSettingsKeyStatus.advanced + ], + .excludedSystemActivities : [ + .type : OCClassSettingsMetadataType.stringArray, + .description : "List of all operating system activities that should be excluded from OS share sheets in actions such as Open In.", + .category : "Actions", + .status : OCClassSettingsKeyStatus.advanced, + .possibleValues : [ + [ + OCClassSettingsMetadataKey.description : "Add to reading list", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.addToReadingList.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Copy to pasteboard", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.copyToPasteboard.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Print", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.print.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Save to camera roll", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.saveToCameraRoll.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Mail", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.mail.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Message", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.message.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Assign to contact", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.assignToContact.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "AirDrop", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.airDrop.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Open in (i)Books", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.openInIBooks.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Markup as PDF", + OCClassSettingsMetadataKey.value : UIActivity.ActivityType.markupAsPDF.rawValue + ] + ] ] ] diff --git a/ownCloudAppShared/Client/Actions/ClientItemResolvingCell.swift b/ownCloudAppShared/Client/Actions/ClientItemResolvingCell.swift deleted file mode 100644 index 9a22f4063..000000000 --- a/ownCloudAppShared/Client/Actions/ClientItemResolvingCell.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// ClientItemResolvingCell.swift -// ownCloud -// -// Created by Felix Schwarz on 18.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 ownCloudSDK - -open class ClientItemResolvingCell: ClientItemCell { - var itemTracker : OCCoreItemTracking? - - // MARK: - Resolve item from path - public var itemResolutionLocation : OCLocation? { - didSet { - self.item = nil - - if let itemLocation = itemResolutionLocation { - self.itemResolutionLocalID = nil - self.itemTracker = core?.trackItem(at: itemLocation, trackingHandler: { (error, item, isInitial) in - if error == nil, let item = item, isInitial { - OnMainThread { - self.item = item - self.itemTracker = nil // unless isInitial is removed from above "if", that's it - we can stop tracking as any updates won't be used anyway - } - } else if (error != nil) || (item == nil) { - OnMainThread { - self.resolutionFailed(error: error) - } - } - }) - } else { - self.itemTracker = nil - } - } - } - - public var itemResolutionLocalID : String? { - didSet { - if let itemResolutionLocalID = itemResolutionLocalID { - self.item = nil - self.itemResolutionLocation = nil - - core?.retrieveItemFromDatabase(forLocalID: itemResolutionLocalID, completionHandler: { (error, _, item) in - if let item = item, item.localID == self.itemResolutionLocalID { - OnMainThread { - self.item = item - } - } else { - OnMainThread { - self.resolutionFailed(error: error) - } - } - }) - } - } - } - - func resolutionFailed(error: Error?) { - - } - - deinit { - itemTracker = nil - } -} diff --git a/ownCloud/Client/Actions/Actions+Extensions/ClientWebAppViewController.swift b/ownCloudAppShared/Client/Actions/Implementations/ClientWebAppViewController.swift similarity index 56% rename from ownCloud/Client/Actions/Actions+Extensions/ClientWebAppViewController.swift rename to ownCloudAppShared/Client/Actions/Implementations/ClientWebAppViewController.swift index e3bb9a8cf..78ca342be 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/ClientWebAppViewController.swift +++ b/ownCloudAppShared/Client/Actions/Implementations/ClientWebAppViewController.swift @@ -1,6 +1,6 @@ // // ClientWebAppViewController.swift -// ownCloud +// ownCloudAppShared // // Created by Felix Schwarz on 19.09.22. // Copyright © 2022 ownCloud GmbH. All rights reserved. @@ -17,16 +17,16 @@ */ import UIKit +import ownCloudSDK import WebKit -import ownCloudAppShared -class ClientWebAppViewController: UIViewController, WKUIDelegate { - var urlRequest: URLRequest - var webView: WKWebView? +open class ClientWebAppViewController: UIViewController, WKUIDelegate { + public var urlRequest: URLRequest + public var webView: WKWebView? - var shouldSendCloseEvent: Bool = true + public var shouldSendCloseEvent: Bool = true - init(with urlRequest: URLRequest) { + public init(with urlRequest: URLRequest) { self.urlRequest = urlRequest super.init(nibName: nil, bundle: nil) @@ -34,11 +34,11 @@ class ClientWebAppViewController: UIViewController, WKUIDelegate { self.isModalInPresentation = true } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - var webViewConfiguration: WKWebViewConfiguration { + public var webViewConfiguration: WKWebViewConfiguration { let configuration = WKWebViewConfiguration() let webSiteDataStore = WKWebsiteDataStore.nonPersistent() @@ -48,7 +48,7 @@ class ClientWebAppViewController: UIViewController, WKUIDelegate { return configuration } - override func loadView() { + override public func loadView() { let rootView = UIView() webView = WKWebView(frame: .zero, configuration: webViewConfiguration) @@ -67,31 +67,38 @@ class ClientWebAppViewController: UIViewController, WKUIDelegate { view = rootView } - override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() - navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction(handler: { [weak self] _ in + navigationItem.rightBarButtonItem = UIBarButtonItem(title: nil, image: OCSymbol.icon(forSymbolName: "xmark.circle.fill"), primaryAction: UIAction(handler: { [weak self] _ in if self?.shouldSendCloseEvent == true { - // Close via window.close(), which is calling dismissSecurely() once done - self?.closeWebWindow() - - // Call dismissOnce() after 10 seconds regardless - OnMainThread(after: 10) { - self?.dismissOnce() + if let strongSelf = self { + // Close via window.close(), which is calling dismissSecurely() once done + strongSelf.closeWebWindow() + + // Call dismissOnce() + strongSelf.dismissOnce({ + // Dispose after 10 seconds automatically (or earlier if web view signals close event) + OnMainThread(after: 10) { + strongSelf.disposeWebViewOnce() + } + }) } } else { // Close directly - self?.closeWebWindow() + self?.dismissOnce({ [weak self] in + self?.disposeWebViewOnce() + }) } })) } - override func viewWillAppear(_ animated: Bool) { + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) webView?.load(urlRequest) } - override func viewDidDisappear(_ animated: Bool) { + override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) // Drop web view @@ -101,20 +108,33 @@ class ClientWebAppViewController: UIViewController, WKUIDelegate { } private var isDismissed = false - func dismissOnce() { + public func dismissOnce(_ completion: (() -> Void)? = nil) { if !isDismissed { isDismissed = true - self.dismiss(animated: true) + self.dismiss(animated: true, completion: completion) + } else { + completion?() + } + } + + private var hasDisposedWebView = false + func disposeWebViewOnce() { + if !hasDisposedWebView { + hasDisposedWebView = true + webView?.removeFromSuperview() + webView = nil } } // window.close() handling - func closeWebWindow() { + public func closeWebWindow() { webView?.evaluateJavaScript("window.close();") } // UI delegate - func webViewDidClose(_ webView: WKWebView) { - dismissOnce() + public func webViewDidClose(_ webView: WKWebView) { + dismissOnce({ [weak self] in + self?.disposeWebViewOnce() + }) } } diff --git a/ownCloudAppShared/Client/Actions/CreateFolderAction.swift b/ownCloudAppShared/Client/Actions/Implementations/CreateFolderAction.swift similarity index 84% rename from ownCloudAppShared/Client/Actions/CreateFolderAction.swift rename to ownCloudAppShared/Client/Actions/Implementations/CreateFolderAction.swift index 4b0e6e488..08eef2e5d 100644 --- a/ownCloudAppShared/Client/Actions/CreateFolderAction.swift +++ b/ownCloudAppShared/Client/Actions/Implementations/CreateFolderAction.swift @@ -1,20 +1,20 @@ // // CreateFolderAction.swift -// ownCloud +// ownCloudAppShared // // Created by Pablo Carrascal on 20/11/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 . -* -*/ + * 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 ownCloudSDK @@ -22,7 +22,7 @@ open class CreateFolderAction : Action { override open class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.createFolder") } override open class var category : ActionCategory? { return .normal } override open class var name : String? { return "Create folder".localized } - override open class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction, .keyboardShortcut, .emptyFolder] } + override open class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction, .keyboardShortcut, .emptyFolder, .locationPickerBar] } override open class var keyCommand : String? { return "N" } override open class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } @@ -66,6 +66,7 @@ open class CreateFolderAction : Action { OnMainThread { let createFolderVC = NamingViewController( with: self.core, defaultName: suggestedName, stringValidator: { name in + // return (true, nil, nil) // Uncomment to allow errors (for f.ex. testing) if name.contains("/") || name.contains("\\") { return (false, nil, "File name cannot contain / or \\".localized) } else { @@ -106,6 +107,6 @@ open class CreateFolderAction : Action { } override open class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { - return Theme.shared.image(for: "folder-create", size: CGSize(width: 30.0, height: 30.0))!.withRenderingMode(.alwaysTemplate) + return Theme.shared.image(for: "folder-create", size: CGSize(width: 30.0, height: 30.0))?.withRenderingMode(.alwaysTemplate) } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInWebAppAction.swift b/ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift similarity index 74% rename from ownCloud/Client/Actions/Actions+Extensions/OpenInWebAppAction.swift rename to ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift index f6fc62faa..2f93f4d4f 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInWebAppAction.swift +++ b/ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift @@ -1,6 +1,6 @@ // // OpenInWebAppAction.swift -// ownCloud +// ownCloudAppShared // // Created by Felix Schwarz on 06.09.22. // Copyright © 2022 ownCloud GmbH. All rights reserved. @@ -18,21 +18,20 @@ import UIKit import ownCloudSDK -import ownCloudAppShared public extension OCClassSettingsKey { static let openInWebAppMode = OCClassSettingsKey("open-in-web-app-mode") } -enum OpenInWebAppActionMode: String { +public enum OpenInWebAppActionMode: String { case defaultBrowser = "default-browser" case inApp = "in-app" case inAppWithDefaultBrowserOption = "in-app-with-default-browser-option" } -class OpenInWebAppAction: Action { +public class OpenInWebAppAction: Action { private static var _classSettingsRegistered: Bool = false - override class var actionExtension: ActionExtension { + override public class var actionExtension: ActionExtension { if !_classSettingsRegistered { _classSettingsRegistered = true @@ -66,9 +65,11 @@ class OpenInWebAppAction: Action { return super.actionExtension } - override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openinwebapp") } - override class var category : ActionCategory? { return .normal } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreDetailItem, .contextMenuItem] } + override public class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openinwebapp") } + override public class var category : ActionCategory? { return .normal } + override public class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreDetailItem, .contextMenuItem] } + override open class var keyCommand : String? { return "E" } + override open class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .shift] } class open func createActionExtension(for app: OCAppProviderApp, core: OCCore) -> ActionExtension { let objectProvider : OCExtensionObjectProvider = { (_ rawExtension, _ context, _ error) -> Any? in @@ -132,10 +133,10 @@ class OpenInWebAppAction: Action { return .nearFirst } - var app : OCAppProviderApp? + public var app : OCAppProviderApp? // MARK: - Action implementation - override func run() { + override public func run() { var openMode : OpenInWebAppActionMode = .inApp if let openInWebAppMode = classSetting(forOCClassSettingsKey: .openInWebAppMode) as? String, let configuredOpenMode = OpenInWebAppActionMode(rawValue: openInWebAppMode) { @@ -155,20 +156,39 @@ class OpenInWebAppAction: Action { } func openInInAppBrowser(withDefaultBrowserOption defaultBrowserOption: Bool) { - guard context.items.count == 1, let item = context.items.first, let core = context.core else { + guard context.items.count == 1, let item = context.items.first, let core = context.core, let viewController = context.viewController else { self.completed(with: NSError(ocError: .insufficientParameters)) return } // Open in in-app browser core.connection.open(inApp: item, with: app, viewMode: nil, completionHandler: { (error, url, method, headers, parameters, urlRequest) in + if let error = error { + OnMainThread { + let appName = self.app?.name ?? "app" + let itemName = item.name ?? "item" + + let alertController = ThemedAlertController( + with: "Error opening {{itemName}} in {{appName}}".localized(["itemName" : itemName, "appName" : appName]), + message: error.localizedDescription, + okLabel: "OK".localized, + action: nil) + + viewController.present(alertController, animated: true) + + self.completed(with: error) + } + + return + } + if let urlRequest = urlRequest as? URLRequest { OnMainThread { let webAppViewController = ClientWebAppViewController(with: urlRequest) webAppViewController.navigationItem.title = item.name if defaultBrowserOption { - webAppViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: nil, image: UIImage(systemName: "safari"), primaryAction: UIAction(handler: { [weak webAppViewController] _ in + webAppViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: nil, image: OCSymbol.icon(forSymbolName: "safari"), primaryAction: UIAction(handler: { [weak webAppViewController] _ in webAppViewController?.parent?.dismiss(animated: true, completion: { self.openInExternalBrowser() }) @@ -176,6 +196,7 @@ class OpenInWebAppAction: Action { } let navigationController = ThemeNavigationController(rootViewController: webAppViewController) + navigationController.modalPresentationStyle = .overFullScreen // Fill the screen with the webapp on iPad self.context.viewController?.present(navigationController, animated: true) } @@ -192,10 +213,13 @@ class OpenInWebAppAction: Action { } // Open in external browser - core.connection.open(inWeb: item, with: app) { (error, url) in + core.connection.open(inWeb: item, with: app) { (inError, url) in + var error = inError + if let url = url { - OnMainThread { - UIApplication.shared.open(url) + if !OCAuthenticationBrowserSessionCustomScheme.open(url) { // calls UIApplication.shared.open where available. That API can't be called directly from extension-ready frameworks + // openURL not available -> return an error + error = NSError(ocError: .internal) } } @@ -203,7 +227,7 @@ class OpenInWebAppAction: Action { } } - override var position: ActionPosition { + override public var position: ActionPosition { if let app = app { return type(of: self).applicablePosition(forContext: context, app: app) } @@ -211,7 +235,7 @@ class OpenInWebAppAction: Action { return .none } - override var icon: UIImage? { + override public var icon: UIImage? { if let remoteIcon = (app?.iconResourceRequest?.resource as? OCResourceImage)?.image?.image { return remoteIcon } @@ -219,7 +243,7 @@ class OpenInWebAppAction: Action { return super.icon } - override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { - return UIImage(systemName: "globe")?.withRenderingMode(.alwaysTemplate) + override public class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return OCSymbol.icon(forSymbolName: "globe") } } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/AccountControllerCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/AccountControllerCell.swift new file mode 100644 index 000000000..7edf2c0c4 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/AccountControllerCell.swift @@ -0,0 +1,348 @@ +// +// AccountControllerCell.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 10.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +class AccountControllerCell: ThemeableCollectionViewListCell { + static let avatarSideLength : CGFloat = 45 + + public var titleLabel: UILabel = UILabel() + public var detailLabel: UILabel = UILabel() + public var logoFallbackView: UIImageView = UIImageView() + public var iconView: ResourceViewHost = ResourceViewHost(fallbackSize: CGSize(width: AccountControllerCell.avatarSideLength, height: AccountControllerCell.avatarSideLength)) + public var infoView: UIView = UIView() + public var statusIconView: UIImageView = UIImageView() + public var disconnectButton: UIButton = UIButton() + + func configure() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.translatesAutoresizingMaskIntoConstraints = false + iconView.translatesAutoresizingMaskIntoConstraints = false + logoFallbackView.translatesAutoresizingMaskIntoConstraints = false + infoView.translatesAutoresizingMaskIntoConstraints = false + statusIconView.translatesAutoresizingMaskIntoConstraints = false + disconnectButton.translatesAutoresizingMaskIntoConstraints = false + + logoFallbackView.contentMode = .scaleAspectFit + logoFallbackView.image = Branding.shared.brandedImageNamed(.bookmarkIcon) + + iconView.fallbackView = logoFallbackView + + titleLabel.font = UIFont.preferredFont(forTextStyle: .title3, with: .bold) + titleLabel.adjustsFontForContentSizeCategory = true + + detailLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + detailLabel.adjustsFontForContentSizeCategory = true + + detailLabel.textColor = UIColor.gray + + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 10) + + var buttonConfig = UIButton.Configuration.gray() + buttonConfig.image = UIImage(systemName: "eject.fill", withConfiguration: symbolConfig) + buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6) + buttonConfig.buttonSize = .mini + buttonConfig.cornerStyle = .capsule + + // disconnectButton.setImage(UIImage(systemName: "eject.fill"), for: .normal) + disconnectButton.configuration = buttonConfig + disconnectButton.addAction(UIAction(handler: { [weak self] _ in + self?.accountController?.disconnect(completion: nil) + }), for: .primaryActionTriggered) + disconnectButton.isHidden = true + + contentView.addSubview(titleLabel) + contentView.addSubview(detailLabel) + contentView.addSubview(iconView) + contentView.addSubview(statusIconView) + contentView.addSubview(infoView) + + infoView.addSubview(disconnectButton) + } + + func configureLayout() { + NSLayoutConstraint.activate([ + iconView.widthAnchor.constraint(equalToConstant: AccountControllerCell.avatarSideLength), + iconView.heightAnchor.constraint(equalToConstant: AccountControllerCell.avatarSideLength), + iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), + iconView.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -10), + iconView.trailingAnchor.constraint(equalTo: detailLabel.leadingAnchor, constant: -10), + + statusIconView.trailingAnchor.constraint(equalTo: iconView.trailingAnchor), + statusIconView.bottomAnchor.constraint(equalTo: iconView.bottomAnchor), + statusIconView.widthAnchor.constraint(equalToConstant: 16), + statusIconView.heightAnchor.constraint(equalToConstant: 16), + + titleLabel.trailingAnchor.constraint(equalTo: infoView.leadingAnchor), + titleLabel.topAnchor.constraint(equalTo: iconView.topAnchor), + + detailLabel.trailingAnchor.constraint(equalTo: infoView.leadingAnchor), + detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2), + detailLabel.bottomAnchor.constraint(equalTo: iconView.bottomAnchor), + + infoView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), + infoView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5), + infoView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + disconnectButton.leadingAnchor.constraint(greaterThanOrEqualTo: infoView.leadingAnchor), + disconnectButton.trailingAnchor.constraint(lessThanOrEqualTo: infoView.trailingAnchor), + disconnectButton.centerYAnchor.constraint(equalTo: infoView.centerYAnchor), + + contentView.heightAnchor.constraint(equalToConstant: AccountControllerCell.avatarSideLength + 20) + + // separatorLayoutGuide.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor) + ]) + + infoView.setContentHuggingPriority(.required, for: .horizontal) + logoFallbackView.setContentHuggingPriority(.required, for: .vertical) + titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + configureLayout() + } + + deinit { + if observingStatusChangeNotifications { + NotificationCenter.default.removeObserver(self, name: AccountConnection.StatusChangedNotification, object: nil) + } + } + + required init?(coder: NSCoder) { + fatalError() + } + + var title: String? { + didSet { + titleLabel.text = title + } + } + var detail: String? { + didSet { + detailLabel.text = detail + } + } + + var avatarViewProvider: OCViewProvider? { + didSet { + iconView.activeViewProvider = avatarViewProvider + } + } + + var showDisconnectButtonObserver: NSKeyValueObservation? + var richStatusObserver: NSKeyValueObservation? + var messageCountObserver: NSKeyValueObservation? + var observingStatusChangeNotifications: Bool = false + + weak var accountController: AccountController? { + willSet { + if observingStatusChangeNotifications { + NotificationCenter.default.removeObserver(self, name: AccountConnection.StatusChangedNotification, object: nil) + observingStatusChangeNotifications = false + } + + showDisconnectButtonObserver?.invalidate() + showDisconnectButtonObserver = nil + + messageCountObserver?.invalidate() + messageCountObserver = nil + + richStatusObserver?.invalidate() + richStatusObserver = nil + + } + didSet { + if let accountController = accountController { + showDisconnectButtonObserver = accountController.observe(\.showDisconnectButton, options: .initial, changeHandler: { [weak self] (accountController, change) in + let showDisconnectButton = accountController.showDisconnectButton + + Log.debug("\(accountController) reports showDisconnectButton \(accountController.showDisconnectButton) \(showDisconnectButton)") + + OnMainThread { [weak self] in + if accountController == self?.accountController { + self?.disconnectButton.isHidden = !showDisconnectButton + } + } + }) + + richStatusObserver = accountController.connection?.observe(\.richStatus, options: .initial, changeHandler: { [weak self] (accountConnection, change) in + let richStatus = accountConnection.richStatus + + OnMainThread { [weak self] in + if let self = self, accountConnection == self.accountController?.connection { + self.updateStatus(from: richStatus) + } + } + }) + + messageCountObserver = accountController.connection?.observe(\.messageCount, options: .initial, changeHandler: { [weak self] (accountConnection, change) in + let messageCount = accountConnection.messageCount + + OnMainThread { [weak self] in + if let self = self { + self.updateMessageBadge(count: messageCount) + } + } + }) + + observingStatusChangeNotifications = true + NotificationCenter.default.addObserver(forName: AccountConnection.StatusChangedNotification, object: accountController.connection, queue: .main, using: { [weak self] (notification) in + if let self = self, let connection = notification.object as? AccountConnection, connection === self.accountController?.connection { + self.updateStatus(iconFor: connection.status) + } + }) + } + + self.updateStatus(iconFor: accountController?.connection?.status) + } + } + + func updateStatus(iconFor status: AccountConnection.Status?) { + var color: UIColor? + + if let status = status { + switch status { + case .noCore: break + + case .offline: + color = .systemGray + + case .connecting, .coreAvailable: + color = .systemYellow + + case .online: + color = .systemGreen + + case .busy: + color = .systemBlue + + case .authenticationError: + color = .systemRed + } + } + + if let color = color { + var symbolConfig = UIImage.SymbolConfiguration(paletteColors: [ color ]) + symbolConfig = symbolConfig.applying(UIImage.SymbolConfiguration(font: .systemFont(ofSize: 16))) + + statusIconView.preferredSymbolConfiguration = symbolConfig + statusIconView.contentMode = .scaleAspectFit + statusIconView.image = OCSymbol.icon(forSymbolName: "circlebadge.fill") + } else { + statusIconView.image = nil + } + } + + func updateStatus(from richStatus: AccountConnectionRichStatus?) { + if let richStatus { + updateStatus(iconFor: richStatus.status) + } + + var notOfflineNotCore: Bool = true + if case .offline = richStatus?.status { notOfflineNotCore = false } + if case .noCore = richStatus?.status { notOfflineNotCore = false } + + if let richStatus, let richStatusText = richStatus.text, notOfflineNotCore { + detailLabel.text = richStatusText + } else { + detailLabel.text = detail + } + } + + // MARK: - Message Badge + private var badgeLabel : RoundedLabel? + + func updateMessageBadge(count: Int) { + if count > 0 { + if badgeLabel == nil { + badgeLabel = RoundedLabel(text: "", style: .token) + badgeLabel?.translatesAutoresizingMaskIntoConstraints = false + + if let badgeLabel = badgeLabel { + contentView.addSubview(badgeLabel) + + NSLayoutConstraint.activate([ + badgeLabel.trailingAnchor.constraint(equalTo: iconView.trailingAnchor), + badgeLabel.topAnchor.constraint(equalTo: iconView.topAnchor) + ]) + } + } + + badgeLabel?.labelText = "\(count)" + } else { + badgeLabel?.removeFromSuperview() + badgeLabel = nil + } + } + + override func prepareForReuse() { + super.prepareForReuse() + disconnectButton.isHidden = true + badgeLabel?.removeFromSuperview() + updateStatus(iconFor: nil) + } + + open override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + var backgroundConfig = UIBackgroundConfiguration.listSidebarCell() + backgroundConfig.cornerRadius = 10 + backgroundConfig.backgroundColor = UIColor(white: 1.0, alpha: 0.8) + + backgroundConfiguration = backgroundConfig + } +} + +extension AccountControllerCell { + static func registerCellProvider() { + let accountControllerListCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + var title : String? + var detail : String? + var avatarViewProvider : OCViewProvider? + // var expandable = false + weak var controller: AccountController? + + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let accountController = item as? AccountController, let bookmark = accountController.connection?.bookmark { + title = bookmark.displayName + detail = bookmark.shortName + avatarViewProvider = bookmark.avatar + + controller = accountController + } + }) + + cell.title = title + cell.detail = detail + cell.avatarViewProvider = avatarViewProvider + cell.accountController = controller + } + + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .accountController, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in + switch cellConfiguration?.style.type { + default: + return collectionView.dequeueConfiguredReusableCell(using: accountControllerListCellRegistration, for: indexPath, item: itemRef) + } + })) + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift index f84f3866c..bb54f1df5 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift @@ -142,9 +142,23 @@ class ActionCell: ThemeableCollectionViewCell { var backgroundConfig = backgroundConfiguration?.updated(for: state) if state.isHighlighted || state.isSelected || (state.cellDropState == .targeted) { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.highlighted.background : UIColor(white: 0, alpha: 0.10) + switch type { + case .regular: + backgroundConfig?.backgroundColor = UIColor(white: 0, alpha: 0.10) + case .warning: + backgroundConfig?.backgroundColor = collection.warningColors.highlighted.background + case .destructive: + backgroundConfig?.backgroundColor = collection.destructiveColors.highlighted.background + } } else { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.normal.background : UIColor(white: 0, alpha: 0.05) + switch type { + case .regular: + backgroundConfig?.backgroundColor = UIColor(white: 0, alpha: 0.05) + case .warning: + backgroundConfig?.backgroundColor = collection.warningColors.normal.background + case .destructive: + backgroundConfig?.backgroundColor = collection.destructiveColors.normal.background + } } backgroundConfig?.cornerRadius = 8 @@ -155,6 +169,20 @@ class ActionCell: ThemeableCollectionViewCell { override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { super.applyThemeCollectionToCellContents(theme: theme, collection: collection, state: state) + switch type { + case .regular: + titleLabel.textColor = collection.tintColor + iconView.tintColor = collection.tintColor + + case .warning: + titleLabel.textColor = collection.warningColors.normal.foreground + iconView.tintColor = collection.warningColors.normal.foreground + + case .destructive: + titleLabel.textColor = collection.destructiveColors.normal.foreground + iconView.tintColor = collection.destructiveColors.normal.foreground + } + titleLabel.textColor = (type == .destructive) ? collection.destructiveColors.normal.foreground : collection.tintColor iconView.tintColor = (type == .destructive) ? collection.destructiveColors.normal.foreground : collection.tintColor @@ -186,11 +214,91 @@ extension ActionCell { }) } + let actionSideBarCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + var accessories: [UICellAccessory] = [] + var content = cell.defaultContentConfiguration() + var backgroundConfiguration: UIBackgroundConfiguration? + + if let action = item as? OCAction { + content.text = action.title + content.image = action.icon + + switch action.type { + case .warning: + content.textProperties.font = UIFont.systemFont(ofSize: UIFont.labelFontSize, weight: .semibold) + + content.textProperties.color = Theme.shared.activeCollection.warningColors.normal.foreground + content.imageProperties.tintColor = Theme.shared.activeCollection.warningColors.normal.foreground + backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell() + backgroundConfiguration?.backgroundColor = Theme.shared.activeCollection.warningColors.normal.background + + case .destructive: + content.textProperties.font = UIFont.systemFont(ofSize: UIFont.labelFontSize, weight: .semibold) + + content.textProperties.color = Theme.shared.activeCollection.destructiveColors.normal.foreground + content.imageProperties.tintColor = Theme.shared.activeCollection.destructiveColors.normal.foreground + backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell() + backgroundConfiguration?.backgroundColor = Theme.shared.activeCollection.destructiveColors.normal.background + + default: break + } + + if let buttonLabel = action.buttonLabel { + let context = cellConfiguration.clientContext + + var buttonConfig = UIButton.Configuration.filled() + buttonConfig.title = buttonLabel + buttonConfig.buttonSize = .mini + buttonConfig.cornerStyle = .capsule + + let button: UIButton = UIButton() + button.configuration = buttonConfig + button.addAction(UIAction(handler: { [weak action, weak context] _ in + var options: [OCActionRunOptionKey:Any] = [:] + + if let context { + options[.clientContext] = context + } + + action?.run(options: options) + }), for: .primaryActionTriggered) + + accessories.append(.customView(configuration: UICellAccessory.CustomViewConfiguration(customView: button, placement: .trailing()))) + } + } + + if let sidebarAction = item as? CollectionSidebarAction { + if let badgeCount = sidebarAction.badgeCount { + accessories.append(.customView(configuration: UICellAccessory.CustomViewConfiguration(customView: RoundedLabel(text: "\(badgeCount)", style: .token), placement: .trailing()))) + } + if sidebarAction.childrenDataSource != nil { + let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .cell) + accessories.append(.outlineDisclosure(options: headerDisclosureOption)) + } + } + +// if cellConfiguration.highlight { +// if backgroundConfiguration == nil { +// backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell() +// backgroundConfiguration?.backgroundColor = Theme.shared.activeCollection.darkBrandColor +// } +// } + + cell.accessories = accessories + cell.contentConfiguration = content + cell.backgroundConfiguration = backgroundConfiguration + }) + } + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .action, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in switch cellConfiguration?.style.type { case .gridCell: return collectionView.dequeueConfiguredReusableCell(using: gridActionCellRegistration, for: indexPath, item: itemRef) + case .sideBar: + return collectionView.dequeueConfiguredReusableCell(using: actionSideBarCellRegistration, for: indexPath, item: itemRef) + default: return collectionView.dequeueConfiguredReusableCell(using: wideActionCellRegistration, for: indexPath, item: itemRef) } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift new file mode 100644 index 000000000..73220d5f6 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift @@ -0,0 +1,41 @@ +// +// DriveGridCell.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 DriveGridCell: DriveHeaderCell { + override var suggestedCellHeight: CGFloat? { + return nil + } + + override func configure() { + super.configure() + + contentView.layer.cornerRadius = 8 + titleLabel.numberOfLines = 1 + titleLabel.lineBreakMode = .byTruncatingTail + subtitleLabel.numberOfLines = 1 + subtitleLabel.lineBreakMode = .byTruncatingTail + } + + override var subtitle: String? { + didSet { + subtitleLabel.text = subtitle ?? " " // Ensure the grid cells' titles align by always showing a subtitle - if necessary, an empty one + } + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift index 9ab37ecde..4a1d2f9da 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift @@ -31,6 +31,8 @@ class DriveHeaderCell: DriveListCell { weak var collectionViewController: CollectionViewController? var collectionItemRef: CollectionViewController.ItemRef? + var coverImageHeightConstraint : NSLayoutConstraint? + deinit { Theme.shared.unregister(client: self) coverObservation?.invalidate() @@ -53,7 +55,9 @@ class DriveHeaderCell: DriveListCell { textOuterSpacing = 16 coverImageResourceView.fallbackView = nil - coverImageHeightConstraint = coverImageResourceView.heightAnchor.constraint(greaterThanOrEqualToConstant: 160) + if let suggestedCellHeight { + coverImageHeightConstraint = coverImageResourceView.heightAnchor.constraint(greaterThanOrEqualToConstant: suggestedCellHeight) + } contentView.insertSubview(darkBackgroundView, belowSubview: titleLabel) @@ -65,18 +69,25 @@ class DriveHeaderCell: DriveListCell { }) } - func recomputeHeight() { + var suggestedCellHeight: CGFloat? { var newHeight : CGFloat = 160 if !isRequestingCoverImage && (coverImageResourceView.contentStatus == .none) { newHeight = 80 } - if let constantHeight = coverImageHeightConstraint?.constant, constantHeight != newHeight { + return newHeight + } + + func recomputeHeight() { + if let newHeight = suggestedCellHeight, let constantHeight = coverImageHeightConstraint?.constant, constantHeight != newHeight { coverImageHeightConstraint?.constant = newHeight if let collectionViewController = collectionViewController, let collectionItemRef = collectionItemRef { - collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef], animated: false) + collectionViewController.performDataSourceUpdate(with: { updateDone in + collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef], animated: false) + updateDone() + }) } } } @@ -92,10 +103,7 @@ class DriveHeaderCell: DriveListCell { } override func configureLayout() { - - NSLayoutConstraint.activate([ - coverImageHeightConstraint!, - + var constraints = [ coverImageResourceView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), coverImageResourceView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), coverImageResourceView.topAnchor.constraint(equalTo: contentView.topAnchor), @@ -115,12 +123,12 @@ class DriveHeaderCell: DriveListCell { darkBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), darkBackgroundView.topAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -textOuterSpacing), darkBackgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } + ] - var coverImageHeightConstraint : NSLayoutConstraint? + if let coverImageHeightConstraint { + constraints.append(coverImageHeightConstraint) + } - @objc func growHeight() { - coverImageHeightConstraint?.constant += 64 + NSLayoutConstraint.activate(constraints) } } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift index 7ced88c96..3fd493d78 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift @@ -104,20 +104,12 @@ class DriveListCell: ThemeableCollectionViewListCell { ]) } -// func updateWith(item: OCDataItem?, cellConfiguration: CollectionViewCellConfiguration?) { -// var coverImageRequest : OCResourceRequest? -// -// if let item = item, -// let cellConfiguration = cellConfiguration, -// let presentable = OCDataRenderer.default.renderItem(item, asType: .presentable, error: nil, withOptions: nil) as? OCDataItemPresentable { -// title = presentable.title -// subtitle = presentable.subtitle -// -// if let resourceManager = cellConfiguration.core?.vault.resourceManager { -// coverImageRequest = try? presentable.provideResourceRequest(.coverImage, withOptions: nil) -// } -// } -// } + override func prepareForReuse() { + super.prepareForReuse() + coverImageResourceView.activeViewProvider = nil + title = "" + subtitle = "" + } } extension DriveListCell { @@ -182,11 +174,83 @@ extension DriveListCell { } } + let driveGridCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + var coverImageRequest : OCResourceRequest? + var resourceManager : OCResourceManager? + var title : String? + var subtitle : String? + + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let presentable = OCDataRenderer.default.renderItem(item, asType: .presentable, error: nil, withOptions: nil) as? OCDataItemPresentable { + title = presentable.title + subtitle = presentable.subtitle + + resourceManager = cellConfiguration.core?.vault.resourceManager + + coverImageRequest = try? presentable.provideResourceRequest(.coverImage, withOptions: nil) + } + }) + + cell.title = title + cell.subtitle = subtitle + + cell.coverImageResourceView.request = coverImageRequest + cell.isRequestingCoverImage = (coverImageRequest != nil) + + cell.collectionItemRef = collectionItemRef + cell.collectionViewController = collectionItemRef.ocCellConfiguration?.hostViewController + + if let coverImageRequest = coverImageRequest { + resourceManager?.start(coverImageRequest) + } + } + + let driveSideBarCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + var title : String? + // var subtitle : String? + var icon: UIImage? + + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let drive = item as? OCDrive, let specialType = drive.specialType { + switch specialType { + case .personal: + icon = OCSymbol.icon(forSymbolName: "person") + + case .shares: + icon = OCSymbol.icon(forSymbolName: "arrowshape.turn.up.left") + + case .space: + icon = OCSymbol.icon(forSymbolName: "square.grid.2x2") + + default: + icon = OCSymbol.icon(forSymbolName: "square.grid.2x2") + } + } + + if let presentable = OCDataRenderer.default.renderItem(item, asType: .presentable, error: nil, withOptions: nil) as? OCDataItemPresentable { + title = presentable.title + } + }) + + var content = cell.defaultContentConfiguration() + + content.text = title + content.image = icon + + cell.contentConfiguration = content + } + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .drive, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in switch cellConfiguration?.style.type { case .header: return collectionView.dequeueConfiguredReusableCell(using: driveHeaderCellRegistration, for: indexPath, item: itemRef) + case .sideBar: + return collectionView.dequeueConfiguredReusableCell(using: driveSideBarCellRegistration, for: indexPath, item: itemRef) + + case .gridCell: + return collectionView.dequeueConfiguredReusableCell(using: driveGridCellRegistration, for: indexPath, item: itemRef) + default: return collectionView.dequeueConfiguredReusableCell(using: driveListCellRegistration, for: indexPath, item: itemRef) } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ExpandableResourceCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ExpandableResourceCell.swift index 375903e7f..33e4da846 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ExpandableResourceCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ExpandableResourceCell.swift @@ -134,7 +134,10 @@ class ExpandableResourceCell: UICollectionViewListCell, Themeable { } if let collectionViewController = collectionViewController, let collectionItemRef = collectionItemRef { - collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef], animated: false) + collectionViewController.performDataSourceUpdate(with: { updateDone in + collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef], animated: false) + updateDone() + }) } } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ItemListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ItemListCell.swift deleted file mode 100644 index c7a0092a4..000000000 --- a/ownCloudAppShared/Client/Collection Views/Cells/ItemListCell.swift +++ /dev/null @@ -1,647 +0,0 @@ -// -// ItemListCell.swift -// ownCloudAppShared -// -// Created by Felix Schwarz on 20.04.22. -// Copyright © 2022 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2022, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import UIKit -import ownCloudSDK -import ownCloudApp - -open class ItemListCell: ThemeableCollectionViewListCell { - private let horizontalMargin : CGFloat = 15 - private let verticalLabelMargin : CGFloat = 10 - private let verticalIconMargin : CGFloat = 10 - private let horizontalSmallMargin : CGFloat = 10 - private let spacing : CGFloat = 15 - private let smallSpacing : CGFloat = 2 - private let iconViewWidth : CGFloat = 40 - private let detailIconViewHeight : CGFloat = 15 - private let accessoryButtonWidth : CGFloat = 35 - private let verticalLabelMarginFromCenter : CGFloat = 2 - private let iconSize : CGSize = CGSize(width: 40, height: 40) - private let thumbnailSize : CGSize = CGSize(width: 60, height: 60) // when changing size, also update .iconView/fallbackSize - - open weak var clientContext: ClientContext? { - didSet { - isMoreButtonPermanentlyHidden = (clientContext?.moreItemHandler as? MoreItemAction == nil) - } - } - - open var titleLabel : UILabel = UILabel() - open var detailLabel : UILabel = UILabel() - open var iconView : ResourceViewHost = ResourceViewHost(fallbackSize: CGSize(width: 60, height: 60)) // when changing size, also update .thumbnailSize - open var cloudStatusIconView : UIImageView = UIImageView() - open var sharedStatusIconView : UIImageView = UIImageView() - open var publicLinkStatusIconView : UIImageView = UIImageView() - - open var actionViewContainer : UIView = UIView() - open var actionAccessory: UICellAccessory? - open var moreButton : UIButton = UIButton() - open var messageButton : UIButton = UIButton() - open var progressView : ProgressView? - - open var revealButtonContainer: UIView = UIView() - open var revealButton : UIButton = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) - open var revealButtonAccessory: UICellAccessory? - - open var sharedStatusIconViewZeroWidthConstraint : NSLayoutConstraint? - open var publicLinkStatusIconViewZeroWidthConstraint : NSLayoutConstraint? - open var cloudStatusIconViewZeroWidthConstraint : NSLayoutConstraint? - - open var sharedStatusIconViewRightMarginConstraint : NSLayoutConstraint? - open var publicLinkStatusIconViewRightMarginConstraint : NSLayoutConstraint? - open var cloudStatusIconViewRightMarginConstraint : NSLayoutConstraint? - - open var activeThumbnailRequest : OCResourceRequestItemThumbnail? - - open var hasMessageForItem : Bool = false - - open var isMoreButtonPermanentlyHidden = false { - didSet { - updateAccessories() - } - } - - open var isActive = true { - didSet { - let alpha : CGFloat = self.isActive ? 1.0 : 0.5 - titleLabel.alpha = alpha - detailLabel.alpha = alpha - iconView.alpha = alpha - cloudStatusIconView.alpha = alpha - } - } - - open weak var core : OCCore? - - override init(frame: CGRect) { - super.init(frame: frame) - prepareViewAndConstraints() - - NotificationCenter.default.addObserver(self, selector: #selector(updateAvailableOfflineStatus(_:)), name: .OCCoreItemPoliciesChanged, object: OCItemPolicyKind.availableOffline) - - NotificationCenter.default.addObserver(self, selector: #selector(updateHasMessage(_:)), name: .ClientSyncRecordIDsWithMessagesChanged, object: nil) - - PointerEffect.install(on: self.contentView, effectStyle: .hover) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - deinit { - NotificationCenter.default.removeObserver(self, name: .OCCoreItemPoliciesChanged, object: OCItemPolicyKind.availableOffline) - - NotificationCenter.default.removeObserver(self, name: .ClientSyncRecordIDsWithMessagesChanged, object: nil) - - self.localID = nil - self.core = nil - } - - func prepareViewAndConstraints() { - // cell.content setup - titleLabel.translatesAutoresizingMaskIntoConstraints = false - detailLabel.translatesAutoresizingMaskIntoConstraints = false - - iconView.translatesAutoresizingMaskIntoConstraints = false - iconView.contentMode = .scaleAspectFit - - cloudStatusIconView.translatesAutoresizingMaskIntoConstraints = false - cloudStatusIconView.contentMode = .center - cloudStatusIconView.contentMode = .scaleAspectFit - - sharedStatusIconView.translatesAutoresizingMaskIntoConstraints = false - sharedStatusIconView.contentMode = .center - sharedStatusIconView.contentMode = .scaleAspectFit - - publicLinkStatusIconView.translatesAutoresizingMaskIntoConstraints = false - publicLinkStatusIconView.contentMode = .center - publicLinkStatusIconView.contentMode = .scaleAspectFit - - titleLabel.font = UIFont.preferredFont(forTextStyle: .callout) - titleLabel.adjustsFontForContentSizeCategory = true - titleLabel.lineBreakMode = .byTruncatingMiddle - - detailLabel.font = UIFont.preferredFont(forTextStyle: .footnote) - detailLabel.adjustsFontForContentSizeCategory = true - - self.contentView.addSubview(titleLabel) - self.contentView.addSubview(detailLabel) - self.contentView.addSubview(iconView) - self.contentView.addSubview(sharedStatusIconView) - self.contentView.addSubview(publicLinkStatusIconView) - self.contentView.addSubview(cloudStatusIconView) - - sharedStatusIconView.setContentHuggingPriority(.required, for: .vertical) - sharedStatusIconView.setContentHuggingPriority(.required, for: .horizontal) - sharedStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) - sharedStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) - - publicLinkStatusIconView.setContentHuggingPriority(.required, for: .vertical) - publicLinkStatusIconView.setContentHuggingPriority(.required, for: .horizontal) - publicLinkStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) - publicLinkStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) - - cloudStatusIconView.setContentHuggingPriority(.required, for: .vertical) - cloudStatusIconView.setContentHuggingPriority(.required, for: .horizontal) - cloudStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) - cloudStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) - - iconView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - - cloudStatusIconViewZeroWidthConstraint = cloudStatusIconView.widthAnchor.constraint(equalToConstant: 0) - sharedStatusIconViewZeroWidthConstraint = sharedStatusIconView.widthAnchor.constraint(equalToConstant: 0) - publicLinkStatusIconViewZeroWidthConstraint = publicLinkStatusIconView.widthAnchor.constraint(equalToConstant: 0) - - cloudStatusIconViewRightMarginConstraint = sharedStatusIconView.leadingAnchor.constraint(equalTo: cloudStatusIconView.trailingAnchor) - sharedStatusIconViewRightMarginConstraint = publicLinkStatusIconView.leadingAnchor.constraint(equalTo: sharedStatusIconView.trailingAnchor) - publicLinkStatusIconViewRightMarginConstraint = detailLabel.leadingAnchor.constraint(equalTo: publicLinkStatusIconView.trailingAnchor) - - NSLayoutConstraint.activate([ - iconView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: horizontalMargin), - iconView.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -spacing), - iconView.widthAnchor.constraint(equalToConstant: iconViewWidth), - iconView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalIconMargin), - iconView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalIconMargin), - - titleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: 0), - detailLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: 0), - - cloudStatusIconViewZeroWidthConstraint!, - sharedStatusIconViewZeroWidthConstraint!, - publicLinkStatusIconViewZeroWidthConstraint!, - - cloudStatusIconView.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: spacing), - cloudStatusIconViewRightMarginConstraint!, - sharedStatusIconViewRightMarginConstraint!, - publicLinkStatusIconViewRightMarginConstraint!, - - titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalLabelMargin), - titleLabel.bottomAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: -verticalLabelMarginFromCenter), - detailLabel.topAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: verticalLabelMarginFromCenter), - detailLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalLabelMargin), - - cloudStatusIconView.centerYAnchor.constraint(equalTo: detailLabel.centerYAnchor), - sharedStatusIconView.centerYAnchor.constraint(equalTo: detailLabel.centerYAnchor), - publicLinkStatusIconView.centerYAnchor.constraint(equalTo: detailLabel.centerYAnchor), - - cloudStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), - sharedStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), - publicLinkStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight) - ]) - - // actionViewContainer setup - moreButton.translatesAutoresizingMaskIntoConstraints = false - revealButton.translatesAutoresizingMaskIntoConstraints = false - messageButton.translatesAutoresizingMaskIntoConstraints = false - - actionViewContainer.addSubview(moreButton) - actionViewContainer.addSubview(messageButton) - - revealButtonContainer.addSubview(revealButton) - - moreButton.setImage(UIImage(named: "more-dots"), for: .normal) - moreButton.contentMode = .center - moreButton.isPointerInteractionEnabled = true - - revealButton.setImage(UIImage(systemName: "arrow.right.circle.fill"), for: .normal) - revealButton.isPointerInteractionEnabled = true - revealButton.contentMode = .center - revealButton.accessibilityLabel = "Reveal in folder".localized - - messageButton.setTitle("⚠️", for: .normal) - messageButton.contentMode = .center - messageButton.isPointerInteractionEnabled = true - messageButton.isHidden = true - - moreButton.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside) - revealButton.addTarget(self, action: #selector(revealButtonTapped), for: .touchUpInside) - messageButton.addTarget(self, action: #selector(messageButtonTapped), for: .touchUpInside) - - NSLayoutConstraint.activate([ - revealButton.topAnchor.constraint(equalTo: revealButtonContainer.topAnchor), - revealButton.bottomAnchor.constraint(equalTo: revealButtonContainer.bottomAnchor), - revealButton.leadingAnchor.constraint(equalTo: revealButtonContainer.leadingAnchor), - revealButton.trailingAnchor.constraint(equalTo: revealButtonContainer.trailingAnchor), - - revealButton.widthAnchor.constraint(equalToConstant: accessoryButtonWidth), - - moreButton.topAnchor.constraint(equalTo: actionViewContainer.topAnchor), - moreButton.bottomAnchor.constraint(equalTo: actionViewContainer.bottomAnchor), - moreButton.leadingAnchor.constraint(equalTo: actionViewContainer.leadingAnchor), - moreButton.trailingAnchor.constraint(equalTo: actionViewContainer.trailingAnchor), - - moreButton.widthAnchor.constraint(equalToConstant: accessoryButtonWidth), - - messageButton.topAnchor.constraint(equalTo: moreButton.topAnchor), - messageButton.bottomAnchor.constraint(equalTo: moreButton.bottomAnchor), - messageButton.leadingAnchor.constraint(equalTo: moreButton.leadingAnchor), - messageButton.trailingAnchor.constraint(equalTo: moreButton.trailingAnchor) - ]) - - let backgroundConfig = UIBackgroundConfiguration.listPlainCell() - backgroundConfiguration = backgroundConfig - - self.accessibilityElements = [titleLabel, detailLabel, moreButton, revealButton, messageButton] - - actionAccessory = .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: actionViewContainer, placement: .trailing(displayed: .whenNotEditing))) - - revealButtonAccessory = .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: revealButtonContainer, placement: .trailing(displayed: .whenNotEditing))) - - updateAccessories() - } - - // MARK: - Present item - open var item : OCItem? { - didSet { - localID = item?.localID as NSString? - - if let newItem = item { - updateWith(newItem) - } - } - } - - open func titleLabelString(for item: OCItem?) -> NSAttributedString { - guard let item = item else { return NSAttributedString(string: "") } - - if item.type == .file, let itemName = item.baseName, let itemExtension = item.fileExtension { - return NSMutableAttributedString() - .appendBold(itemName) - .appendNormal(".") - .appendNormal(itemExtension) - } else if item.type == .collection, let itemName = item.name { - return NSMutableAttributedString() - .appendBold(itemName) - } - - return NSAttributedString(string: "") - } - - open func detailLabelString(for item: OCItem?) -> String { - if let item = item { - var size: String = item.sizeLocalized - - if item.size < 0 { - size = "Pending".localized - } - - return size + " - " + item.lastModifiedLocalized - } - - return "" - } - - open func updateWith(_ item: OCItem) { - // Cancel any already active request - if let activeThumbnailRequest = activeThumbnailRequest { - core?.vault.resourceManager?.stop(activeThumbnailRequest) - self.activeThumbnailRequest = nil - } - - // Set has message - self.hasMessageForItem = clientContext?.inlineMessageCenter?.hasInlineMessage(for: item) ?? false - - // Set the icon and initiate thumbnail generation - let thumbnailRequest = OCResourceRequestItemThumbnail.request(for: item, maximumSize: self.thumbnailSize, scale: 0, waitForConnectivity: true, changeHandler: nil) - - iconView.request = thumbnailRequest - - // Start new thumbnail request - core?.vault.resourceManager?.start(thumbnailRequest) - - if item.isSharedWithUser || item.sharedByUserOrGroup { - sharedStatusIconView.image = UIImage(named: "group") - sharedStatusIconViewRightMarginConstraint?.constant = smallSpacing - sharedStatusIconViewZeroWidthConstraint?.isActive = false - } else { - sharedStatusIconView.image = nil - sharedStatusIconViewRightMarginConstraint?.constant = 0 - sharedStatusIconViewZeroWidthConstraint?.isActive = true - } - sharedStatusIconView.invalidateIntrinsicContentSize() - - if item.sharedByPublicLink { - publicLinkStatusIconView.image = UIImage(named: "link") - publicLinkStatusIconViewRightMarginConstraint?.constant = smallSpacing - publicLinkStatusIconViewZeroWidthConstraint?.isActive = false - } else { - publicLinkStatusIconView.image = nil - publicLinkStatusIconViewRightMarginConstraint?.constant = 0 - publicLinkStatusIconViewZeroWidthConstraint?.isActive = true - } - publicLinkStatusIconView.invalidateIntrinsicContentSize() - - self.updateCloudStatusIcon(with: item) - - self.updateLabels(with: item) - - self.iconView.alpha = item.isPlaceholder ? 0.5 : 1.0 - self.moreButton.isHidden = (item.isPlaceholder || (progressView != nil)) ? true : !showMoreButton - - self.moreButton.accessibilityLabel = "Actions".localized - self.moreButton.accessibilityIdentifier = (item.name != nil) ? (item.name! + " " + "Actions".localized) : "Actions".localized - - self.updateStatus() - } - - open func updateCloudStatusIcon(with item: OCItem?) { - var cloudStatusIcon : UIImage? - var cloudStatusIconAlpha : CGFloat = 1.0 - - if let item = item { - let availableOfflineCoverage : OCCoreAvailableOfflineCoverage = core?.availableOfflinePolicyCoverage(of: item) ?? .none - - switch availableOfflineCoverage { - case .direct, .none: cloudStatusIconAlpha = 1.0 - case .indirect: cloudStatusIconAlpha = 0.5 - } - - if item.type == .file { - switch item.cloudStatus { - case .cloudOnly: - cloudStatusIcon = UIImage(named: "cloud-only") - cloudStatusIconAlpha = 1.0 - - case .localCopy: - cloudStatusIcon = (item.downloadTriggerIdentifier == OCItemDownloadTriggerID.availableOffline) ? UIImage(named: "cloud-available-offline") : nil - - case .locallyModified, .localOnly: - cloudStatusIcon = UIImage(named: "cloud-local-only") - cloudStatusIconAlpha = 1.0 - } - } else { - if availableOfflineCoverage == .none { - cloudStatusIcon = nil - } else { - cloudStatusIcon = UIImage(named: "cloud-available-offline") - } - } - } - - cloudStatusIconView.image = cloudStatusIcon - cloudStatusIconView.alpha = cloudStatusIconAlpha - - cloudStatusIconViewZeroWidthConstraint?.isActive = (cloudStatusIcon == nil) - cloudStatusIconViewRightMarginConstraint?.constant = (cloudStatusIcon == nil) ? 0 : smallSpacing - - cloudStatusIconView.invalidateIntrinsicContentSize() - } - - open func updateLabels(with item: OCItem?) { - self.titleLabel.attributedText = titleLabelString(for: item) - self.detailLabel.text = detailLabelString(for: item) - } - - // MARK: - Available offline tracking - @objc open func updateAvailableOfflineStatus(_ notification: Notification) { - OnMainThread { [weak self] in - self?.updateCloudStatusIcon(with: self?.item) - } - } - - // MARK: - Has Message tracking - @objc open func updateHasMessage(_ notification: Notification) { - if let notificationCore = notification.object as? OCCore, let core = self.core, notificationCore === core { - OnMainThread { [weak self] in - let oldMessageForItem = self?.hasMessageForItem ?? false - - if let item = self?.item, let hasMessage = self?.clientContext?.inlineMessageCenter?.hasInlineMessage(for: item) { - self?.hasMessageForItem = hasMessage - } else { - self?.hasMessageForItem = false - } - - if oldMessageForItem != self?.hasMessageForItem { - self?.updateStatus() - } - } - } - } - - // MARK: - Progress - open var localID : OCLocalID? { - willSet { - if localID != nil { - NotificationCenter.default.removeObserver(self, name: .OCCoreItemChangedProgress, object: nil) - } - } - - didSet { - if localID != nil { - NotificationCenter.default.addObserver(self, selector: #selector(progressChangedForItem(_:)), name: .OCCoreItemChangedProgress, object: nil) - } - } - } - - @objc open func progressChangedForItem(_ notification : Notification) { - if notification.object as? NSString == localID { - OnMainThread { - self.updateStatus() - } - } - } - - open func updateStatus() { - var progress : Progress? - - if let item = item, (item.syncActivity.rawValue & (OCItemSyncActivity.downloading.rawValue | OCItemSyncActivity.uploading.rawValue) != 0), !hasMessageForItem { - progress = self.core?.progress(for: item, matching: .none)?.first - - if progress == nil { - progress = Progress.indeterminate() - } - } - - if progress != nil { - if progressView == nil { - let progressView = ProgressView() - progressView.contentMode = .center - progressView.translatesAutoresizingMaskIntoConstraints = false - - actionViewContainer.addSubview(progressView) - - NSLayoutConstraint.activate([ - progressView.leftAnchor.constraint(equalTo: moreButton.leftAnchor), - progressView.rightAnchor.constraint(equalTo: moreButton.rightAnchor), - progressView.topAnchor.constraint(equalTo: moreButton.topAnchor), - progressView.bottomAnchor.constraint(equalTo: moreButton.bottomAnchor) - ]) - - self.progressView = progressView - } - - self.progressView?.progress = progress - - moreButton.isHidden = true - messageButton.isHidden = true - } else { - moreButton.isHidden = hasMessageForItem || !showMoreButton - messageButton.isHidden = !hasMessageForItem - - progressView?.removeFromSuperview() - progressView = nil - } - } - - // MARK: - Themeing - open var revealHighlight : Bool = false { - didSet { - setNeedsUpdateConfiguration() - } - } - - open override func updateConfiguration(using state: UICellConfigurationState) { - let collection = Theme.shared.activeCollection - var backgroundConfig = backgroundConfiguration?.updated(for: state) - - if state.isHighlighted || state.isSelected || (state.cellDropState == .targeted) || revealHighlight { - backgroundConfig?.backgroundColor = collection.tableRowHighlightColors.backgroundColor?.withAlphaComponent(0.5) - } else { - backgroundConfig?.backgroundColor = collection.tableBackgroundColor - } - - backgroundConfiguration = backgroundConfig - } - - open override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { - titleLabel.applyThemeCollection(collection, itemStyle: .title, itemState: state) - detailLabel.applyThemeCollection(collection, itemStyle: .message, itemState: state) - - sharedStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor - publicLinkStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor - cloudStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor - detailLabel.textColor = collection.tableRowColors.secondaryLabelColor - - moreButton.tintColor = collection.tableRowColors.secondaryLabelColor - - setNeedsUpdateConfiguration() - } - - // MARK: - Editing mode - var showMoreButton: Bool = true { - didSet { - updateAccessories() - } - } - - var showRevealButton: Bool = false { - didSet { - updateAccessories() - } - } - - func updateAccessories() { - var updatedAccessories : [UICellAccessory] = [ - .multiselect() - ] - - if (showMoreButton || (item?.isPlaceholder == true) || (progressView != nil)) && !isMoreButtonPermanentlyHidden, let actionAccessory = actionAccessory { - updatedAccessories.append(actionAccessory) - } - - if showRevealButton, let revealButtonAccessory = revealButtonAccessory { - updatedAccessories.append(revealButtonAccessory) - } - - accessories = updatedAccessories - } - - // MARK: - Actions - @objc open func moreButtonTapped() { - guard let item = item, let clientContext = clientContext else { - return - } - - if let moreItemHandling = clientContext.moreItemHandler { - moreItemHandling.moreOptions(for: item, at: .moreItem, context: clientContext, sender: self) - } - } - - @objc open func messageButtonTapped() { - guard let item = item, let clientContext = clientContext else { - return - } - - clientContext.inlineMessageCenter?.showInlineMessageFor(item: item) - } - - @objc open func revealButtonTapped() { - guard let item = item, let clientContext = clientContext else { - return - } - - clientContext.revealItemHandler?.reveal(item: item, context: clientContext, sender: self) - } -} - -public extension CollectionViewCellStyle.StyleOptionKey { - static let showRevealButton = CollectionViewCellStyle.StyleOptionKey(rawValue: "showRevealButton") - static let showMoreButton = CollectionViewCellStyle.StyleOptionKey(rawValue: "showMoreButton") -} - -public extension CollectionViewCellStyle { - var showRevealButton : Bool { - get { - return options[.showRevealButton] as? Bool ?? false - } - - set { - options[.showRevealButton] = newValue - } - } - - var showMoreButton : Bool { - get { - return options[.showMoreButton] as? Bool ?? true - } - - set { - options[.showMoreButton] = newValue - } - } -} - -extension ItemListCell { - static func registerCellProvider() { - let itemListCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in - collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in - cell.clientContext = cellConfiguration.clientContext - cell.core = cellConfiguration.core - - if let ocItem = item as? OCItem { - cell.item = ocItem - } - - cell.showRevealButton = cellConfiguration.style.showRevealButton - cell.showMoreButton = cellConfiguration.style.showMoreButton - }) - } - - CollectionViewCellProvider.register(CollectionViewCellProvider(for: .item, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in - let cell = collectionView.dequeueConfiguredReusableCell(using: itemListCellRegistration, for: indexPath, item: itemRef) - - if cellConfiguration?.highlight == true { - cell.revealHighlight = true - } - - return cell - })) - } -} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift index ea6db0172..9bf49da2b 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift @@ -33,21 +33,28 @@ class SavedSearchCell: ThemeableCollectionViewCell { let iconView = UIImageView() let titleLabel = UILabel() + let segmentView = SegmentView(with: [], truncationMode: .truncateTail) - var iconInsets : UIEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 0, right: 5) - var titleInsets : UIEdgeInsets = UIEdgeInsets(top: 5, left: 3, bottom: 5, right: 3) + var iconInsets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 0, right: 5) + var titleInsets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 3, bottom: 5, right: 3) + var titleSegmentSpacing: CGFloat = 5 - var title : String? { + var title: String? { didSet { titleLabel.text = title } } - var icon : UIImage? { + var icon: UIImage? { didSet { iconView.image = icon } } - var type : OCActionType = .regular { + var items: [SegmentViewItem]? { + didSet { + segmentView.items = items ?? [] + } + } + var type: OCActionType = .regular { didSet { if superview != nil { applyThemeCollectionToCellContents(theme: Theme.shared, collection: Theme.shared.activeCollection, state: .normal) @@ -58,9 +65,11 @@ class SavedSearchCell: ThemeableCollectionViewCell { func configure() { iconView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false + segmentView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(titleLabel) contentView.addSubview(iconView) + contentView.addSubview(segmentView) iconView.image = icon iconView.contentMode = .scaleAspectFit @@ -79,7 +88,7 @@ class SavedSearchCell: ThemeableCollectionViewCell { } func configureLayout() { - iconInsets = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 5) + iconInsets = UIEdgeInsets(top: 11, left: 10, bottom: 13, right: 5) titleInsets = UIEdgeInsets(top: 13, left: 3, bottom: 13, right: 10) titleLabel.textAlignment = .left @@ -90,14 +99,20 @@ class SavedSearchCell: ThemeableCollectionViewCell { self.configuredConstraints = [ iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: iconInsets.left), iconView.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -(iconInsets.right + titleInsets.left)), - iconView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: iconInsets.top), - iconView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -iconInsets.bottom), - - iconView.widthAnchor.constraint(equalTo: iconView.heightAnchor), + iconView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + // iconView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: iconInsets.top), + // iconView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -iconInsets.bottom), + // iconView.widthAnchor.constraint(equalTo: iconView.heightAnchor), + iconView.widthAnchor.constraint(equalToConstant: 24), + iconView.heightAnchor.constraint(equalToConstant: 24), titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -titleInsets.right), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: titleInsets.top), - titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -titleInsets.bottom) + titleLabel.bottomAnchor.constraint(equalTo: segmentView.topAnchor, constant: -titleSegmentSpacing), + + segmentView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + segmentView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + segmentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -titleInsets.bottom) ] } @@ -106,9 +121,9 @@ class SavedSearchCell: ThemeableCollectionViewCell { var backgroundConfig = backgroundConfiguration?.updated(for: state) if state.isHighlighted || state.isSelected || (state.cellDropState == .targeted) { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.highlighted.background : UIColor(white: 0, alpha: 0.10) + backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.highlighted.background : collection.tableRowButtonColors.filledColorPairCollection.highlighted.background } else { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.normal.background : UIColor(white: 0, alpha: 0.05) + backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.normal.background : collection.tableRowButtonColors.filledColorPairCollection.normal.background } backgroundConfig?.cornerRadius = 8 @@ -126,22 +141,94 @@ class SavedSearchCell: ThemeableCollectionViewCell { } } +extension OCSavedSearch { + private var queryConditionsForDisplay: [OCQueryCondition] { + let searchSegments = (searchTerm as NSString).segmentedForSearch(withQuotationMarks: true) + var queryConditions: [OCQueryCondition] = [] + + for searchSegment in searchSegments { + if let queryCondition = OCQueryCondition.forSearchSegment(searchSegment) { + queryConditions.append(queryCondition) + } + } + + return queryConditions + } + + public var segmentViewItemsForDisplay: [SegmentViewItem] { + let conditions = queryConditionsForDisplay + var items: [SegmentViewItem] = [] + + for condition in conditions { + var item: SegmentViewItem? + + if condition.property == .name || (condition.localizedDescription == nil) { + item = SegmentViewItem(with: nil, title: condition.searchSegment, style: .plain, titleTextStyle: .footnote) + item?.insets = .zero + } else { + item = SegmentViewItem(with: OCSymbol.icon(forSymbolName: condition.symbolName), title: condition.localizedDescription, style: .token, titleTextStyle: .caption1) + item?.cornerStyle = .round(points: 3) + } + + if let item = item { + items.append(item) + } + } + + return items + } +} + +extension OCSavedSearch { + var displayName: String { + return (isNameUserDefined && name.count > 0) ? name : (isTemplate ? "Search template".localized : "Saved search".localized) + } + + var sideBarDisplayName: String { + return (isNameUserDefined && name.count > 0) ? name : searchTerm + } +} + extension SavedSearchCell { - static let savedTemplateIcon = UIImage(systemName: "square.dashed.inset.filled")?.withRenderingMode(.alwaysTemplate) - static let savedSearchIcon = UIImage(systemName: "gearshape.fill")?.withRenderingMode(.alwaysTemplate) + static let savedTemplateIcon = OCSymbol.icon(forSymbolName: "square.dashed.inset.filled") + static let savedSearchIcon = OCSymbol.icon(forSymbolName: "gearshape.fill") static func registerCellProvider() { let savedSearchCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in if let savedSearch = OCDataRenderer.default.renderItem(item, asType: .savedSearch, error: nil, withOptions: nil) as? OCSavedSearch { - cell.title = savedSearch.name + cell.title = savedSearch.displayName cell.icon = savedSearch.isTemplate ? savedTemplateIcon : savedSearchIcon + cell.items = savedSearch.segmentViewItemsForDisplay + } + }) + } + + let savedSearchSidebarCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + var content = cell.defaultContentConfiguration() + + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let savedSearch = OCDataRenderer.default.renderItem(item, asType: .savedSearch, error: nil, withOptions: nil) as? OCSavedSearch { + content.text = savedSearch.sideBarDisplayName + if let customIconName = savedSearch.customIconName { + content.image = OCSymbol.icon(forSymbolName: customIconName) + } else { + content.image = savedSearch.isTemplate ? savedTemplateIcon : savedSearchIcon + } } }) + + cell.contentConfiguration = content } CollectionViewCellProvider.register(CollectionViewCellProvider(for: .savedSearch, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in - return collectionView.dequeueConfiguredReusableCell(using: savedSearchCellRegistration, for: indexPath, item: itemRef) + switch cellConfiguration?.style.type { + case .sideBar: + return collectionView.dequeueConfiguredReusableCell(using: savedSearchSidebarCellRegistration, for: indexPath, item: itemRef) + + default: + return collectionView.dequeueConfiguredReusableCell(using: savedSearchCellRegistration, for: indexPath, item: itemRef) + } })) } } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift new file mode 100644 index 000000000..363d34392 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift @@ -0,0 +1,306 @@ +// +// OCItem+UniversalItemListCellContentProvider.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 05.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 OCItemUniversalItemListCellHelper { + weak var clientContext: ClientContext? + + typealias ContentRefresher = (_ helper: OCItemUniversalItemListCellHelper, _ fields: UniversalItemListCell.Content.Fields?) -> Bool + + var contentRefresher: ContentRefresher + var hasMessageForItem: Bool + var item: OCItem + + init(with item: OCItem, hasMessageForItem: Bool, context: ClientContext?, contentRefresher: @escaping ContentRefresher) { + self.item = item + self.clientContext = context + self.hasMessageForItem = hasMessageForItem + self.contentRefresher = contentRefresher + + addObservers() + } + + deinit { + removeObservers() + } + + func addObservers() { + localID = item.localID as NSString? + + NotificationCenter.default.addObserver(self, selector: #selector(updateAvailableOfflineStatus(_:)), name: .OCCoreItemPoliciesChanged, object: OCItemPolicyKind.availableOffline) + NotificationCenter.default.addObserver(self, selector: #selector(updateHasMessage(_:)), name: .ClientSyncRecordIDsWithMessagesChanged, object: nil) + } + func removeObservers() { + NotificationCenter.default.removeObserver(self, name: .OCCoreItemPoliciesChanged, object: OCItemPolicyKind.availableOffline) + NotificationCenter.default.removeObserver(self, name: .ClientSyncRecordIDsWithMessagesChanged, object: nil) + + localID = nil + } + + // MARK: - Progress + var localID: OCLocalID? { + willSet { + if localID != nil { + NotificationCenter.default.removeObserver(self, name: .OCCoreItemChangedProgress, object: localID) + } + } + + didSet { + if localID != nil { + NotificationCenter.default.addObserver(self, selector: #selector(progressChangedForItem(_:)), name: .OCCoreItemChangedProgress, object: localID) + } + } + } + + @objc open func progressChangedForItem(_ notification : Notification) { + self.refreshContent(fields: .progress) + } + + // MARK: - Available offline tracking + @objc open func updateAvailableOfflineStatus(_ notification: Notification) { + // TODO: Improve change detection, trigger refresh only when changed + self.refreshContent(fields: .details) + } + + // MARK: - Has Message tracking + @objc open func updateHasMessage(_ notification: Notification) { + if let notificationCore = notification.object as? OCCore, let core = clientContext?.core, notificationCore === core { + OnMainThread { [weak self] in + guard let self else { return } + + let oldMessageForItem = self.hasMessageForItem + let newMessageForItem = self.clientContext?.inlineMessageCenter?.hasInlineMessage(for: self.item) + + if let newMessageForItem, oldMessageForItem != newMessageForItem { + self.hasMessageForItem = newMessageForItem + + self.refreshContent(fields: .accessories) + } else { + Log.debug("Skipped message center update") + } + } + } + } + + func refreshContent(fields: UniversalItemListCell.Content.Fields?) { + OnMainThread { [weak self] in + if let self { + _ = self.contentRefresher(self, fields) + } + } + } +} + +extension OCItem: UniversalItemListCellContentProvider { + func cloudStatus(in core: OCCore?) -> (icon: UIImage?, iconAlpha: CGFloat) { + var cloudStatusIcon : UIImage? + var cloudStatusIconAlpha : CGFloat = 1.0 + let availableOfflineCoverage : OCCoreAvailableOfflineCoverage = core?.availableOfflinePolicyCoverage(of: self) ?? .none + + switch availableOfflineCoverage { + case .direct, .none: cloudStatusIconAlpha = 1.0 + case .indirect: cloudStatusIconAlpha = 0.5 + } + + if type == .file { + switch cloudStatus { + case .cloudOnly: + cloudStatusIcon = UIImage(named: "cloud-only") + cloudStatusIconAlpha = 1.0 + + case .localCopy: + cloudStatusIcon = (downloadTriggerIdentifier == OCItemDownloadTriggerID.availableOffline) ? UIImage(named: "cloud-available-offline") : nil + + case .locallyModified, .localOnly: + cloudStatusIcon = UIImage(named: "cloud-local-only") + cloudStatusIconAlpha = 1.0 + } + } else { + if availableOfflineCoverage == .none { + cloudStatusIcon = nil + } else { + cloudStatusIcon = UIImage(named: "cloud-available-offline") + } + } + + return (cloudStatusIcon, cloudStatusIconAlpha) + } + + func content(for cell: UniversalItemListCell?, thumbnailSize: CGSize, context: ClientContext?, configuration: CollectionViewCellConfiguration?) -> (content: UniversalItemListCell.Content, hasMessageForItem: Bool) { + let content = UniversalItemListCell.Content(with: self) + let isFile = (type == .file) + + // Disabled + content.disabled = (state == .serverSideProcessing) + + var itemAppearance: ClientItemAppearance = .regular + if let context, let itemStyler = context.itemStyler { + itemAppearance = itemStyler(context, nil, self) + + if itemAppearance == .disabled { + content.disabled = true + } + } + + // Icon + content.icon = .resource(request: OCResourceRequestItemThumbnail.request(for: self, maximumSize: thumbnailSize, scale: 0, waitForConnectivity: true, changeHandler: nil)) + content.iconDisabled = isPlaceholder + + // Title + if let name = self.name { + content.title = isFile ? .file(name: name) : .folder(name: name) + } + + // Details + var detailItems: [SegmentViewItem] = [] + + // - Cloud status + let (cloudStatusIcon, cloudStatusIconAlpha) = cloudStatus(in: context?.core) + + if let cloudStatusIcon { + let segmentItem = SegmentViewItem(with: cloudStatusIcon, style: .plain) + segmentItem.insets = .zero + segmentItem.alpha = cloudStatusIconAlpha + detailItems.append(segmentItem) + } + + // - Sharing + if isSharedWithUser || sharedByUserOrGroup { + let segmentItem = SegmentViewItem(with: UIImage(named: "group"), style: .plain) + segmentItem.insets = .zero + detailItems.append(segmentItem) + } + + if sharedByPublicLink { + let segmentItem = SegmentViewItem(with: UIImage(named: "link"), style: .plain) + segmentItem.insets = .zero + detailItems.append(segmentItem) + } + + // - Description + var detailString: String = sizeLocalized + + if size < 0 { + detailString = "Pending".localized + } + if state == .serverSideProcessing { + detailString = "Processing on server".localized + } + detailString += " - " + lastModifiedLocalized + + let detailSegment = SegmentViewItem(with: nil, title: detailString, style: .plain, titleTextStyle: .footnote) + detailSegment.insets = .zero + + detailItems.append(detailSegment) + + // /Details + content.details = detailItems + + // Message + let hasMessageForItem = context?.inlineMessageCenter?.hasInlineMessage(for: self) ?? false + + // Progress + var progress : Progress? + + if (syncActivity.rawValue & (OCItemSyncActivity.downloading.rawValue | OCItemSyncActivity.uploading.rawValue) != 0), !hasMessageForItem { + progress = context?.core?.progress(for: self, matching: .none)?.first + + if progress == nil { + progress = Progress.indeterminate() + } + + content.progress = progress + } + + // Accessories + var accessories: [UICellAccessory] = [ + .multiselect() + ] + var includeMoreButton: Bool = false + + if !((context?.moreItemHandler as? MoreItemAction == nil) || context?.hasPermission(for: .moreOptions) == false) { + includeMoreButton = configuration?.style.showMoreButton == true + } + + if let cell { + if hasMessageForItem { + accessories.append(cell.messageButtonAccessory) + } else if progress != nil { + accessories.append(cell.progressAccessory) + } else if includeMoreButton { + accessories.append(cell.moreButtonAccessory) + } + + if configuration?.style.showRevealButton == true { + accessories.append(cell.revealButtonAccessory) + } + } + + content.accessories = accessories // [ cell.moreButtonAccessory, cell.progressAccessory, cell.messageButtonAccessory, cell.revealButtonAccessory ] + + return (content, hasMessageForItem) + } + + // MARK: - UniversalItemListCellContentProvider implementation + public func provideContent(for cell: UniversalItemListCell, context: ClientContext?, configuration: CollectionViewCellConfiguration?, updateContent: @escaping UniversalItemListCell.ContentUpdater) { + // Assemble content + let (content, hasMessageForItem) = content(for: cell, thumbnailSize: cell.thumbnailSize, context: context, configuration: configuration) + + // Install helper object listening for changes that don't propagate through OCItem changes + cell.contentProviderUserInfo = OCItemUniversalItemListCellHelper(with: self, hasMessageForItem: hasMessageForItem, context: context, contentRefresher: { [weak self, weak cell, weak context] (helper, fields) in + if let cell, let (content, hasMessageForItem) = self?.content(for: cell, thumbnailSize: cell.thumbnailSize, context: context, configuration: configuration) { + content.onlyFields = fields + helper.hasMessageForItem = hasMessageForItem + + return updateContent(content) == true + } + + return false + }) + + // Return composed content + _ = updateContent(content) + } +} + +extension OCItem { + static func registerUniversalCellProvider() { + let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let item = OCDataRenderer.default.renderItem(item, asType: .item, error: nil, withOptions: nil) as? OCItem { + cell.fill(from: item, context: cellConfiguration.clientContext, configuration: cellConfiguration) + } + }) + } + + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .item, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in + switch cellConfiguration?.style.type { + default: + let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemRef) + + if cellConfiguration?.highlight == true { + cell.revealHighlight = true + } + + return cell + } + })) + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItemPolicy+UniversalItemListCellContentProvider.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItemPolicy+UniversalItemListCellContentProvider.swift new file mode 100644 index 000000000..3b1b94e54 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItemPolicy+UniversalItemListCellContentProvider.swift @@ -0,0 +1,116 @@ +// +// OCItemPolicy+UniversalItemListCellContentProvider.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 20.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 OCItemPolicy: UniversalItemListCellContentProvider { + public func provideContent(for cell: UniversalItemListCell, context: ClientContext?, configuration: CollectionViewCellConfiguration?, updateContent: @escaping UniversalItemListCell.ContentUpdater) { + let content = UniversalItemListCell.Content(with: self) + let isFile = location?.type == .file + + // Icon + content.icon = isFile ? .file : .folder + + // Title + if let name = location?.lastPathComponent { + content.title = isFile ? .file(name: name) : .folder(name: name) + } + + // Details + if let context, let location = isFile ? location?.parent : location { + let breadcrumbs = location.breadcrumbs(in: context, includeServerName: context.core?.useDrives == false) + var breadcrumbSegments = OCLocation.composeSegments(breadcrumbs: breadcrumbs, in: context) + + // More compact breadcrumbs + for breadcrumbSegment in breadcrumbSegments { + breadcrumbSegment.insets.trailing = 0 + breadcrumbSegment.insets.leading = 0 + } + + let availableOfflineInfoSegment = SegmentViewItem(with: UIImage(named: "cloud-available-offline"), title: "", style: .plain, titleTextStyle: .footnote) + availableOfflineInfoSegment.insets = .zero + availableOfflineInfoSegment.insets.trailing = 5 + availableOfflineInfoSegment.iconTitleSpacing = 3 + + breadcrumbSegments.insert(availableOfflineInfoSegment, at: 0) + + content.details = breadcrumbSegments + } + + // Accessories + content.accessories = [ cell.revealButtonAccessory ] + + // Icon retrieval for files + if let itemLocation = location { + let tokenArray: NSMutableArray = NSMutableArray() + + if let trackItemToken = context?.core?.trackItem(at: itemLocation, trackingHandler: { [weak cell] error, item, isInitial in + if let item, let cell { + let updatedContent = UniversalItemListCell.Content(with: content) + + updatedContent.details?.first?.title = item.sizeLocalized + + OnMainThread { + if isFile { + updatedContent.icon = .resource(request: OCResourceRequestItemThumbnail.request(for: item, maximumSize: cell.thumbnailSize, scale: 0, waitForConnectivity: true, changeHandler: nil)) + updatedContent.onlyFields = [.icon, .details] + } else { + updatedContent.onlyFields = [.details] + } + + if !updateContent(updatedContent) { + tokenArray.removeAllObjects() // Drop token, end tracking + } + } + } + }) { + tokenArray.add(trackItemToken) + } + + cell.contentProviderUserInfo = tokenArray + } + + _ = updateContent(content) + } +} + +extension OCItemPolicy { + static func registerUniversalCellProvider() { + let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let itemPolicy = OCDataRenderer.default.renderItem(item, asType: .itemPolicy, error: nil, withOptions: nil) as? OCItemPolicy { + cell.fill(from: itemPolicy, context: cellConfiguration.clientContext, configuration: cellConfiguration) + } + }) + } + + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .itemPolicy, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in + switch cellConfiguration?.style.type { + default: + let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemRef) + + if cellConfiguration?.highlight == true { + cell.revealHighlight = true + } + + return cell + } + })) + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCShare+UniversalItemListCellContentProvider.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCShare+UniversalItemListCellContentProvider.swift new file mode 100644 index 000000000..4dfe56fb0 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCShare+UniversalItemListCellContentProvider.swift @@ -0,0 +1,186 @@ +// +// OCShare+UniversalItemListCellContentProvider.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 05.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 OCShare: UniversalItemListCellContentProvider { + public func provideContent(for cell: UniversalItemListCell, context: ClientContext?, configuration: CollectionViewCellConfiguration?, updateContent: @escaping UniversalItemListCell.ContentUpdater) { + let content = UniversalItemListCell.Content(with: self) + let isFile = (itemType == .file) + + // Icon + if let mimeType = itemMIMEType, isFile { + content.icon = .mime(type: mimeType) + } else { + content.icon = isFile ? .file : .folder + } + + // Title + if let name = itemLocation.lastPathComponent { + content.title = isFile ? .file(name: name) : .folder(name: name) + } + + // Details + var detailText: String? + + switch category { + case .withMe: + let ownerName = owner?.displayName ?? owner?.userName ?? "" + detailText = "Shared by {{owner}}".localized([ "owner" : ownerName ]) + + case .byMe: + if type != .link { + let recipientName = recipient?.displayName ?? "" + var recipients: String + if let otherItemShares, otherItemShares.count > 0 { + var recipientNames : [String] = [ recipientName ] + for otherItemShare in otherItemShares { + if let otherRecipientName = otherItemShare.recipient?.displayName { + recipientNames.append(otherRecipientName) + } + } + recipients = "Shared with {{recipients}}".localized([ "recipients" : recipientNames.joined(separator: ", ") ]) + } else { + recipients = "Shared with {{recipient}}".localized([ "recipient" : recipientName ]) + } + detailText = recipients + } else { + if let urlString = url?.absoluteString, urlString.count > 0 { + if let name, name.count > 0 { + detailText = "\(name) | \(urlString)" + } else { + detailText = urlString + } + } + } + + default: break + } + + if let detailText { + let detailTextSegment = SegmentViewItem(with: nil, title: detailText, style: .plain, titleTextStyle: .footnote) + detailTextSegment.insets = .zero + + content.details = [ + detailTextSegment + ] + } + + if ((category == .withMe) && (state == .accepted)) || + ((category == .byMe) && isFile) { + let tokenArray: NSMutableArray = NSMutableArray() + + if let trackItemToken = context?.core?.trackItem(at: itemLocation, trackingHandler: { [weak cell] error, item, isInitial in + if let item, let cell { + let updatedContent = UniversalItemListCell.Content(with: content) + + OnMainThread { + updatedContent.icon = .resource(request: OCResourceRequestItemThumbnail.request(for: item, maximumSize: cell.thumbnailSize, scale: 0, waitForConnectivity: true, changeHandler: nil)) + updatedContent.onlyFields = .icon + + if !updateContent(updatedContent) { + tokenArray.removeAllObjects() // Drop token, end tracking + } + } + } + }) { + tokenArray.add(trackItemToken) + } + + cell.contentProviderUserInfo = tokenArray + } + + if category == .byMe { + if type == .link { + let (_, copyToClipboardAccessory) = cell.makeAccessoryButton(image: OCSymbol.icon(forSymbolName: "list.clipboard"), title: "Copy".localized, accessibilityLabel: "Copy to clipboard".localized, action: UIAction(handler: { [weak self, weak context] action in + if let self { + if self.copyToClipboard(), let presentationViewController = context?.presentationViewController { + _ = NotificationHUDViewController(on: presentationViewController, title: self.name ?? "Public Link".localized, subtitle: "URL was copied to the clipboard".localized) + } + } + })) + + content.accessories = [ + copyToClipboardAccessory, + cell.revealButtonAccessory + ] + } else { + content.accessories = [ + cell.revealButtonAccessory + ] + } + } + + if category == .withMe, let state, state != .accepted { + var accessories: [UICellAccessory] = [] + + if state == .pending || state == .declined { + let (button, accessory) = cell.makeAccessoryButton(image: OCSymbol.icon(forSymbolName: "checkmark.circle"), title: "Accept".localized, accessibilityLabel: "Accept share".localized, action: UIAction(handler: { [weak self, weak context] action in + if let self, let context, let core = context.core { + core.makeDecision(on: self, accept: true, completionHandler: { error in + }) + } + })) + + button.tintColor = UIColor.systemGreen + + accessories.append(accessory) + } + + if state == .pending { + let (button, accessory) = cell.makeAccessoryButton(image: OCSymbol.icon(forSymbolName: "minus.circle"), title: "Decline".localized, accessibilityLabel: "Decline share".localized, action: UIAction(handler: { [weak self, weak context] action in + if let self, let context, let core = context.core { + core.makeDecision(on: self, accept: false, completionHandler: { error in + }) + } + })) + + button.tintColor = UIColor.systemRed + + accessories.append(accessory) + } + + content.accessories = accessories + } + + _ = updateContent(content) + } +} + +extension OCShare { + static func registerUniversalCellProvider() { + let shareCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let share = OCDataRenderer.default.renderItem(item, asType: .share, error: nil, withOptions: nil) as? OCShare { + cell.fill(from: share, context: cellConfiguration.clientContext, configuration: cellConfiguration) + } + }) + } + + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .share, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in + switch cellConfiguration?.style.type { +// case .sideBar: +// return collectionView.dequeueConfiguredReusableCell(using: savedSearchSidebarCellRegistration, for: indexPath, item: itemRef) +// + default: + return collectionView.dequeueConfiguredReusableCell(using: shareCellRegistration, for: indexPath, item: itemRef) + } + })) + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift new file mode 100644 index 000000000..509ae14c7 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift @@ -0,0 +1,535 @@ +// +// UniversalItemListCell.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 04.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 ownCloudApp +import ownCloudSDK + +public protocol UniversalItemListCellContentProvider: AnyObject { + func provideContent(for cell: UniversalItemListCell, context: ClientContext?, configuration: CollectionViewCellConfiguration?, updateContent: @escaping UniversalItemListCell.ContentUpdater) //!< Provides content for cell, completion must be called immediately or - if delayed - via main thread. Helper objects must be stored in .contentProviderUserInfo immediately upon function invocation. If new content is set, .contentProviderUserInfo may be overwritten, so this should only be used to keep a reference to a helper object, but not to retrieve the helper object again. Updates to content can be provided by calling completion repeatedly. However, updates should stop if completion returns false, which indicates that the cell is now presenting other content. +} + +open class UniversalItemListCell: ThemeableCollectionViewListCell { + public class Content { + public struct Fields: OptionSet { + public let rawValue: Int + public init(rawValue: Int) { + self.rawValue = rawValue + } + + static public let title = Fields(rawValue: 1) + static public let icon = Fields(rawValue: 2) + static public let details = Fields(rawValue: 4) + static public let progress = Fields(rawValue: 8) + static public let accessories = Fields(rawValue: 16) + static public let disabled = Fields(rawValue: 32) + } + + init(with inDataItem: OCDataItem?) { + dataItem = inDataItem + } + + init(with content: Content) { + title = content.title + + icon = content.icon + iconDisabled = content.iconDisabled + + details = content.details + + progress = content.progress + accessories = content.accessories + + disabled = content.disabled + + dataItem = content.dataItem + + onlyFields = content.onlyFields + } + + public enum Title { + case text(_ string: String) + case file(name: String) + case folder(name: String) + } + + public enum Icon { + case file + case folder + case mime(type: String) + case resource(request: OCResourceRequest) + } + + var title: Title? + var icon: Icon? + var iconDisabled: Bool = false + + var details: [SegmentViewItem]? + + var progress: Progress? + var accessories: [UICellAccessory]? + + var disabled: Bool = false + + var dataItem: OCDataItem? + + var onlyFields: Fields? + } + + public typealias ContentUpdater = (UniversalItemListCell.Content) -> Bool + + open var titleLabel: UILabel = UILabel() + open var detailSegmentView: SegmentView = SegmentView(with: [], truncationMode: .truncateTail) + + private let iconSize : CGSize = CGSize(width: 40, height: 40) + public let thumbnailSize : CGSize = CGSize(width: 60, height: 60) // when changing size, also update .iconView.fallbackSize + open var iconView: ResourceViewHost = ResourceViewHost(fallbackSize: CGSize(width: 60, height: 60)) // when changing size, also update .thumbnailSize + + open weak var clientContext: ClientContext? + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + + prepareViews() + updateLayoutConstraints() + + PointerEffect.install(on: self.contentView, effectStyle: .hover) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + var cellConstraints: [NSLayoutConstraint]? + + open func prepareViews() { + detailSegmentView.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + iconView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(titleLabel) + contentView.addSubview(detailSegmentView) + contentView.addSubview(iconView) + } + + open func updateLayoutConstraints() { + if let cellConstraints { + NSLayoutConstraint.deactivate(cellConstraints) + self.cellConstraints = nil + } + + let horizontalMargin : CGFloat = 15 + let verticalLabelMargin : CGFloat = 10 + let verticalIconMargin : CGFloat = 10 +// let horizontalSmallMargin : CGFloat = 10 + let spacing : CGFloat = 15 +// let smallSpacing : CGFloat = 2 + let iconViewWidth : CGFloat = 40 +// let detailIconViewHeight : CGFloat = 15 +// let accessoryButtonWidth : CGFloat = 35 + let verticalLabelMarginFromCenter : CGFloat = 1 + + let constraints: [NSLayoutConstraint] = [ + iconView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: horizontalMargin), + iconView.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -spacing), + iconView.widthAnchor.constraint(equalToConstant: iconViewWidth), + iconView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalIconMargin), + iconView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalIconMargin), + + titleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor), + detailSegmentView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor), + detailSegmentView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + + titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalLabelMargin), + titleLabel.bottomAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: -verticalLabelMarginFromCenter), + detailSegmentView.topAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: verticalLabelMarginFromCenter), + detailSegmentView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalLabelMargin), + + separatorLayoutGuide.leadingAnchor.constraint(equalTo: iconView.leadingAnchor) + ] + + if constraints.count > 0 { + cellConstraints = constraints + NSLayoutConstraint.activate(constraints) + } + } + + // MARK: - Content + var title: NSAttributedString? { + didSet { + titleLabel.attributedText = title + } + } + var detailSegments: [SegmentViewItem]? { + didSet { + detailSegmentView.items = detailSegments ?? [] + } + } + + open func set(title: String?, isFileName: Bool = false) { + self.title = attributedTitle(for: title, isFileName: isFileName) + } + + func attributedTitle(for title: String?, isFileName: Bool) -> NSAttributedString { + guard let title = title as? NSString else { + return NSAttributedString(string: "") + } + + let pathExtension = title.pathExtension + + if isFileName, pathExtension.count > 0 { + let baseName = title.deletingPathExtension + + return NSMutableAttributedString() + .appendBold(baseName) + .appendNormal(".") + .appendNormal(pathExtension) + } else { + return NSMutableAttributedString() + .appendBold(title as String) + } + } + + var content: Content? { + willSet { + let onlyFields = content?.onlyFields + + // Icon + if onlyFields == nil || onlyFields?.contains(.icon) == true, let content { + // Cancel any already active request + if let icon = content.icon { + switch icon { + case .resource(request: let iconRequest): + clientContext?.core?.vault.resourceManager?.stop(iconRequest) + content.icon = nil + + default: break + } + } + } + } + didSet { + let onlyFields = content?.onlyFields + + // Icon + if onlyFields == nil || onlyFields?.contains(.icon) == true { + var iconRequest: OCResourceRequest? + var iconViewProvider: OCViewProvider? + + if let icon = content?.icon { + switch icon { + case .file: + iconViewProvider = ResourceItemIcon.file + + case .folder: + iconViewProvider = ResourceItemIcon.folder + + case .mime(type: let type): + iconViewProvider = ResourceItemIcon.iconFor(mimeType: type) + + case .resource(request: let request): + iconRequest = request + } + } + + iconView.activeViewProvider = iconViewProvider + iconView.request = iconRequest + + if let iconRequest { + // Start new resource request + clientContext?.core?.vault.resourceManager?.start(iconRequest) + } + } + + // Title + if onlyFields == nil || onlyFields?.contains(.title) == true { + if let title = content?.title { + switch title { + case .text(let text): + set(title: text) + + case .file(name: let name): + set(title: name, isFileName: true) + + case .folder(name: let name): + set(title: name) + } + } else { + set(title: nil) + } + } + + // Details + if onlyFields == nil || onlyFields?.contains(.details) == true { + if let details = content?.details { + detailSegments = details + } else { + detailSegments = nil + } + } + + // Disabled + if onlyFields == nil || onlyFields?.contains(.disabled) == true { + // - Content + if let disabled = content?.disabled, disabled { + contentView.alpha = 0.5 + } else { + contentView.alpha = 1.0 + } + + // - Icon + if let disabled = content?.iconDisabled, disabled { + iconView.alpha = 0.5 + } else { + iconView.alpha = 1.0 + } + } + + // Accessories + if onlyFields == nil || onlyFields?.contains(.accessories) == true { + if let accessories = content?.accessories { + self.accessories = accessories + } else { + self.accessories = [] + } + } + + // Progress + if onlyFields == nil || onlyFields?.contains(.progress) == true { + progressView?.progress = content?.progress + } + } + } + + // MARK: - Content provider + private var _contentSeed: UInt = 0 + public var contentProviderUserInfo: AnyObject? //!< Convenience property for use by ItemListCellContentProvider, to easily establish a strong reference to a helper object. Can be released or overwritten when the content changes or the cell is released. + func fill(from contentProvider: UniversalItemListCellContentProvider, context: ClientContext? = nil, configuration: CollectionViewCellConfiguration? = nil) { + _contentSeed += 1 + let fillSeed = _contentSeed + + clientContext = context + + contentProviderUserInfo = nil + + contentProvider.provideContent(for: self, context: context ?? configuration?.clientContext ?? clientContext, configuration: configuration) { [weak self] (content) in + if fillSeed == self?._contentSeed { + self?.content = content + return true // Content presented + } + return false // Cell is presenting different content + } + } + + // MARK: - Accessories + // - More ... + open var moreButton: UIButton? + open lazy var moreButtonAccessory: UICellAccessory = { + let button = UIButton() + + button.setImage(UIImage(named: "more-dots"), for: .normal) + button.contentMode = .center + button.isPointerInteractionEnabled = true + button.accessibilityLabel = "More".localized + button.addTarget(self, action: #selector(moreButtonTapped), for: .primaryActionTriggered) + + button.frame = CGRect(x: 0, y: 0, width: 32, height: 42) // Avoid _UITemporaryLayoutWidths auto-layout warnings + button.widthAnchor.constraint(equalToConstant: 32).isActive = true + button.heightAnchor.constraint(equalToConstant: 42).isActive = true + + moreButton = button + + return .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: button, placement: .trailing(displayed: .whenNotEditing))) + }() + @objc open func moreButtonTapped() { + guard let item = content?.dataItem, let clientContext = clientContext else { + return + } + + if let moreItemHandling = clientContext.moreItemHandler { + moreItemHandling.moreOptions(for: item, at: .moreItem, context: clientContext, sender: self) + } + } + + // - Reveal > + open var revealButton: UIButton? + open lazy var revealButtonAccessory: UICellAccessory = { + let button = UIButton() + + button.setImage(OCSymbol.icon(forSymbolName: "arrow.right.circle.fill"), for: .normal) + button.contentMode = .center + button.isPointerInteractionEnabled = true + button.accessibilityLabel = "Reveal".localized + button.addTarget(self, action: #selector(revealButtonTapped), for: .primaryActionTriggered) + + button.frame = CGRect(x: 0, y: 0, width: 32, height: 42) // Avoid _UITemporaryLayoutWidths auto-layout warnings + button.widthAnchor.constraint(equalToConstant: 32).isActive = true + button.heightAnchor.constraint(equalToConstant: 42).isActive = true + + revealButton = button + + return .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: button, placement: .trailing(displayed: .whenNotEditing))) + }() + + @objc open func revealButtonTapped() { + guard let item = content?.dataItem, let clientContext = clientContext else { + return + } + + if let revealItemHandler = clientContext.revealItemHandler { + revealItemHandler.reveal(item: item, context: clientContext, sender: self) + } else if let revealInteraction = item as? DataItemSelectionInteraction { + _ = revealInteraction.revealItem?(from: clientContext.originatingViewController, with: clientContext, animated: true, pushViewController: true, completion: nil) + } + } + + // - Inline message + open var messageButton: UIButton? + open lazy var messageButtonAccessory: UICellAccessory = { + let button = UIButton() + button.contentMode = .center + button.isPointerInteractionEnabled = true + button.accessibilityLabel = "Show message".localized + button.setTitle("⚠️", for: .normal) + button.addTarget(self, action: #selector(messageButtonTapped), for: .touchUpInside) + + messageButton = button + + return .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: button, placement: .trailing(displayed: .whenNotEditing))) + }() + + @objc open func messageButtonTapped() { + guard let item = content?.dataItem as? OCItem, let clientContext = clientContext else { + return + } + + clientContext.inlineMessageCenter?.showInlineMessage(for: item) + } + + // - Progress View + open var progressView: ProgressView? + open lazy var progressAccessory: UICellAccessory = { + let progressView = ProgressView() + progressView.contentMode = .center + + progressView.progress = Progress(totalUnitCount: 100) + + self.progressView = progressView + + return .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: progressView, placement: .trailing(displayed: .whenNotEditing))) + }() + + // - Make custom accessory buttons + open func makeAccessoryButton(image: UIImage? = nil, title: String? = nil, accessibilityLabel: String? = nil, action: UIAction? = nil) -> (UIButton, UICellAccessory) { + let button = UIButton() + + button.setTitle(title, for: .normal) + button.setImage(image, for: .normal) + button.contentMode = .center + button.isPointerInteractionEnabled = true + button.accessibilityLabel = accessibilityLabel + + if let action { + button.addAction(action, for: .primaryActionTriggered) + } + + button.applyThemeCollection(Theme.shared.activeCollection) + + if image != nil, title != nil { + var configuration = UIButton.Configuration.borderedTinted() + configuration.buttonSize = .small + configuration.imagePadding = 5 + configuration.cornerStyle = .large + + button.configuration = configuration.updated(for: button) + } + + return (button, .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: button, placement: .trailing(displayed: .whenNotEditing)))) + } + + // MARK: - Prepare for reuse + open override func prepareForReuse() { + super.prepareForReuse() + + detailSegments = nil + title = nil + + iconView.request = nil + iconView.activeViewProvider = nil + } + + // MARK: - Themeing + open var revealHighlight : Bool = false { + didSet { + setNeedsUpdateConfiguration() + } + } + + open override func updateConfiguration(using state: UICellConfigurationState) { + let collection = Theme.shared.activeCollection + var backgroundConfig = backgroundConfiguration?.updated(for: state) + + if state.isHighlighted || state.isSelected || (state.cellDropState == .targeted) || revealHighlight { + backgroundConfig?.backgroundColor = collection.tableRowHighlightColors.backgroundColor?.withAlphaComponent(0.5) + } else { + backgroundConfig?.backgroundColor = collection.tableBackgroundColor + } + + backgroundConfiguration = backgroundConfig + } + + open override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { + titleLabel.applyThemeCollection(collection, itemStyle: .title, itemState: state) + + moreButton?.tintColor = collection.tableRowColors.secondaryLabelColor + revealButton?.tintColor = collection.tableRowColors.secondaryLabelColor + messageButton?.tintColor = collection.tableRowColors.secondaryLabelColor + + setNeedsUpdateConfiguration() + } +} + +// MARK: - Additional CollectionViewCellStyle.StyleOptions +public extension CollectionViewCellStyle.StyleOptionKey { + static let showRevealButton = CollectionViewCellStyle.StyleOptionKey(rawValue: "showRevealButton") + static let showMoreButton = CollectionViewCellStyle.StyleOptionKey(rawValue: "showMoreButton") +} + +public extension CollectionViewCellStyle { + var showRevealButton : Bool { + get { + return options[.showRevealButton] as? Bool ?? false + } + + set { + options[.showRevealButton] = newValue + } + } + + var showMoreButton : Bool { + get { + return options[.showMoreButton] as? Bool ?? true + } + + set { + options[.showMoreButton] = newValue + } + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ViewCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ViewCell.swift index 5a833bf5b..d3537fddd 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ViewCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ViewCell.swift @@ -19,8 +19,14 @@ import UIKit class ViewCell: ThemeableCollectionViewListCell { + private var _previousSeparatorLayoutGuideConstraints: [NSLayoutConstraint]? var hostedView: UIView? { willSet { + if let previousSperatorLayoutGuideConstraints = _previousSeparatorLayoutGuideConstraints { + NSLayoutConstraint.deactivate(previousSperatorLayoutGuideConstraints) + _previousSeparatorLayoutGuideConstraints = nil + } + if hostedView != newValue { hostedView?.removeFromSuperview() } @@ -32,7 +38,7 @@ class ViewCell: ThemeableCollectionViewListCell { contentView.addSubview(hostedView) - NSLayoutConstraint.activate([ + var constraints : [NSLayoutConstraint] = [ // Fill cell.contentView // -> these constraints are applied with .defaultHigh priority (not the default of .required) to not trigger // an unsatisfiable constraints warning in case a cell is re-used and the new view's size conflicts with the @@ -40,11 +46,26 @@ class ViewCell: ThemeableCollectionViewListCell { hostedView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).with(priority: .defaultHigh), hostedView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).with(priority: .defaultHigh), hostedView.topAnchor.constraint(equalTo: contentView.topAnchor).with(priority: .defaultHigh), - hostedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).with(priority: .defaultHigh), + hostedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).with(priority: .defaultHigh) + ] + var layoutGuideConstraints: [NSLayoutConstraint]? + + if let customizer = hostedView.separatorLayoutGuideCustomizer { + // Use custom constraints + layoutGuideConstraints = customizer.customizer(self, hostedView) + } else { + layoutGuideConstraints = [ + // Extend cell seperator to contentView.leadingAnchor + separatorLayoutGuide.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) + ] + } - // Extend cell seperator to contentView.leadingAnchor - separatorLayoutGuide.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) - ]) + if let layoutGuideConstraints = layoutGuideConstraints { + _previousSeparatorLayoutGuideConstraints = layoutGuideConstraints + constraints += layoutGuideConstraints + } + + NSLayoutConstraint.activate(constraints) } } } @@ -66,3 +87,30 @@ class ViewCell: ThemeableCollectionViewListCell { })) } } + +class SeparatorLayoutGuideCustomizer : NSObject { + typealias Customizer = (_ viewCell: ViewCell, _ view: UIView) -> [NSLayoutConstraint] + + var customizer: Customizer + + init(with customizer: @escaping Customizer) { + self.customizer = customizer + super.init() + } +} + +extension UIView { + private struct AssociatedKeys { + static var separatorLayoutGuideCustomizerKey = "separatorLayoutGuideCustomizerKey" + } + + var separatorLayoutGuideCustomizer: SeparatorLayoutGuideCustomizer? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.separatorLayoutGuideCustomizerKey) as? SeparatorLayoutGuideCustomizer + } + + set { + objc_setAssociatedObject(self, &AssociatedKeys.separatorLayoutGuideCustomizerKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) + } + } +} diff --git a/ownCloudAppShared/Client/Collection Views/CollectionSidebarAction.swift b/ownCloudAppShared/Client/Collection Views/CollectionSidebarAction.swift new file mode 100644 index 000000000..4baf1641d --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionSidebarAction.swift @@ -0,0 +1,146 @@ +// +// CollectionSidebarAction.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 23.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public extension OCActionRunOptionKey { + static let clientContext = OCActionRunOptionKey(rawValue: "clientContext") +} + +public extension OCActionPropertyKey { + static let supportsDrop = OCActionPropertyKey(rawValue: "supportsDrop") + static let buttonLabel = OCActionPropertyKey(rawValue: "buttonLabel") + static let selectable = OCActionPropertyKey(rawValue: "selectable") +} + +extension OCAction { + var supportsDrop: Bool { + get { + return properties[.supportsDrop] as? Bool ?? false + } + + set { + properties[.supportsDrop] = newValue + } + } + + var selectable: Bool { + get { + return properties[.selectable] as? Bool ?? true + } + + set { + properties[.selectable] = newValue + } + } + + var buttonLabel: String? { + get { + return properties[.buttonLabel] as? String + } + + set { + properties[.buttonLabel] = newValue + } + } +} + +open class CollectionSidebarAction: OCAction { + open override var dataItemVersion: OCDataItemVersion { + return "\(dataItemReference)\(title)\(badgeCount ?? 0)" as NSObject + } + + public typealias ViewControllerProvider = (_ context: ClientContext?, _ action: CollectionSidebarAction) -> UIViewController? + + var cacheViewControllers: Bool = false + var clearCachedViewControllerOnConnectionClose: Bool = true + var viewControllerProvider: ViewControllerProvider? + var viewControllersByRootViewController: NSMapTable = NSMapTable.weakToStrongObjects() + + public var badgeCount: Int? + + public init(with title: String, icon: UIImage?, identifier: OCDataItemReference? = nil, viewControllerProvider: @escaping ViewControllerProvider, cacheViewControllers: Bool = true, clearCachedViewControllerOnConnectionClose: Bool = true) { + self.viewControllerProvider = viewControllerProvider + self.cacheViewControllers = cacheViewControllers + self.clearCachedViewControllerOnConnectionClose = clearCachedViewControllerOnConnectionClose + super.init() + if let identifier = identifier as? String { + self.identifier = identifier + } + self.title = title + self.icon = icon + } + + open override func run(options: [OCActionRunOptionKey : Any]? = nil, completionHandler: ((Error?) -> Void)? = nil) { + _ = openItem(from: nil, with: options?[.clientContext] as? ClientContext, animated: true, pushViewController: true, completion: { success in + completionHandler?(nil) + }) + } + + public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + var viewController: UIViewController? + + if let clientContext = context { + if cacheViewControllers, let rootViewController = clientContext.rootViewController { + viewController = viewControllersByRootViewController.object(forKey: rootViewController) + } + + if viewController == nil { + viewController = viewControllerProvider?(clientContext, self) + + if cacheViewControllers, let rootViewController = clientContext.rootViewController { + viewControllersByRootViewController.setObject(viewController, forKey: rootViewController) + + if let bookmarkUUID = context?.accountConnection?.bookmark.uuid, clearCachedViewControllerOnConnectionClose { + // Clear from cache if connection is closed (otherwise navigation revocation won't work after a disconnect/reconnect) + NavigationRevocationAction(triggeredBy: [.connectionClosed(bookmarkUUID: bookmarkUUID)], action: { [weak rootViewController, weak self] event, action in + if let self, let rootViewController { + self.viewControllersByRootViewController.removeObject(forKey: rootViewController) + } + }).register(for: viewController, globally: true) + } + } + } + + if viewController != nil { + viewController = clientContext.pushViewControllerToNavigation(context: clientContext, provider: { context in + return viewController + }, push: pushViewController, animated: animated) + + completion?(true) + + return viewController + } + } + + completion?(false) + + return nil + } + + open var childrenDataSource: OCDataSource? + + open override func hasChildren(using source: OCDataSource) -> Bool { + return childrenDataSource != nil + } + + open override func dataSourceForChildren(using source: OCDataSource) -> OCDataSource? { + return childrenDataSource + } +} diff --git a/ownCloudAppShared/Client/Collection Views/CollectionSidebarViewController.swift b/ownCloudAppShared/Client/Collection Views/CollectionSidebarViewController.swift new file mode 100644 index 000000000..356390f2c --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionSidebarViewController.swift @@ -0,0 +1,117 @@ +// +// ClientSidebarViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 07.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +open class CollectionSidebarViewController: CollectionViewController { + open var originalContext: ClientContext + open var sidebarContext: ClientContext + + public typealias ViewControllerNavigationPusher = (_ sideBarViewController: CollectionSidebarViewController, _ viewController: UIViewController, _ animated: Bool) -> Void + + open var navigationPusher: ViewControllerNavigationPusher? + + public init(context inContext: ClientContext, sections: [CollectionViewSection]?, navigationPusher: ViewControllerNavigationPusher? = nil, highlightItemReference: OCDataItemReference? = nil) { + originalContext = inContext + + if let navigationPusher = navigationPusher { + sidebarContext = ClientContext(with: originalContext) + + sidebarContext.postInitializationModifier = { (owner, context) in + context.viewControllerPusher = owner as? ViewControllerPusher + context.navigationRevocationHandler = owner as? NavigationRevocationHandler + } + + self.navigationPusher = navigationPusher + } else { + sidebarContext = inContext + } + + super.init(context: sidebarContext, sections: sections, useStackViewRoot: false, hierarchic: true, highlightItemReference: highlightItemReference) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func configureLayout() { + collectionView.translatesAutoresizingMaskIntoConstraints = false + + if usesStackViewRoot, let stackView = stackView { + stackView.addArrangedSubview(collectionView) + } else if let collectionView = collectionView { + view.embed(toFillWith: collectionView, enclosingAnchors: view.safeAreaAnchorSet) + } + } + + public override func shouldDeselect(record: OCDataItemRecord, at indexPath: IndexPath, afterInteraction: ClientItemInteraction, clientContext: ClientContext) -> Bool { + return false + } + + public var sectionOfCurrentSelection: CollectionViewSection? { + if let selectedItemIndexPaths = self.collectionView.indexPathsForSelectedItems, let selectedIndexPath = selectedItemIndexPaths.first { + return self.section(at: selectedIndexPath.section) + } + + return nil + } + + public override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + view.backgroundColor = collection.tableGroupBackgroundColor + } +} + +extension CollectionSidebarViewController: ViewControllerPusher { + public func pushViewController(context: ClientContext?, provider: (ClientContext) -> UIViewController?, push: Bool, animated: Bool) -> UIViewController? { + var effectiveContext: ClientContext? = context + + if effectiveContext == sidebarContext { + // Pass original context instead of sidebar context + effectiveContext = originalContext + } else { + // Create new context without viewControllerPusher if CollectionSidebarViewController is set as pusher of the current context + if effectiveContext?.viewControllerPusher === self { + effectiveContext = ClientContext(with: context, modifier: { context in + context.viewControllerPusher = nil + }) + } + } + + if let effectiveContext = effectiveContext, + let viewController = provider(effectiveContext) { + if push { + // Push view controller + if let navigationPusher = navigationPusher { + // Use pusher + navigationPusher(self, viewController, animated) + } else { + // Use navigation controller + if let navigationController = effectiveContext.navigationController { + navigationController.pushViewController(viewController, animated: animated) + } + } + } + + return viewController + } + + return nil + } +} diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewAction.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewAction.swift new file mode 100644 index 000000000..7b2179484 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewAction.swift @@ -0,0 +1,133 @@ +// +// CollectionViewAction.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 24.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class CollectionViewAction: NSObject { + public enum Kind { + case select(animated: Bool, scrollPosition: UICollectionView.ScrollPosition) + case highlight(animated: Bool, scrollPosition: UICollectionView.ScrollPosition) + case unhighlightAll(animated: Bool) + case expand(animated: Bool) + } + + public var kind: Kind + public var itemReference: CollectionViewController.ItemRef? + public var itemReferences: [CollectionViewController.ItemRef]? + + public init(kind: Kind, itemReference: CollectionViewController.ItemRef? = nil) { + self.kind = kind + self.itemReference = itemReference + } + + convenience public init?(kind: Kind, itemReferences: [CollectionViewController.ItemRef]) { + guard itemReferences.count > 0, let itemReference = itemReferences.first else { return nil } + + self.init(kind: kind, itemReference: itemReference) + self.itemReferences = itemReferences + } + + public func apply(on viewController: CollectionViewController, completion: (() -> Void)?) -> Bool { + if let itemReferences { + for itemRef in itemReferences { + if apply(on: viewController, for: itemRef, completion: completion) { + return true + } + } + + return false + } + + return apply(on: viewController, for: itemReference, completion: completion) + } + + func apply(on viewController: CollectionViewController, for itemRef: CollectionViewController.ItemRef?, completion: (() -> Void)?) -> Bool { + guard let collectionView = viewController.collectionView else { + return false + } + + if let itemRef, let indexPath = viewController.collectionViewDataSource?.indexPath(for: itemRef) { + switch kind { + case .select(animated: let animated, scrollPosition: let scrollPosition): + viewController.performDataSourceUpdate { updateDone in + if viewController.collectionView(collectionView, shouldSelectItemAt: indexPath) { + collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: scrollPosition) + viewController.collectionView(collectionView, didSelectItemAt: indexPath) + } + + completion?() + updateDone() + } + return true + + case .highlight(animated: let animated, scrollPosition: let scrollPosition): + viewController.performDataSourceUpdate { updateDone in + if viewController.collectionView(collectionView, shouldSelectItemAt: indexPath) { + collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: scrollPosition) + // viewController.recordSelection(ofItemAt: indexPath, operation: .replace) + } + + completion?() + updateDone() + } + return true + + case .expand(animated: let animated): + let (_, sectionID) = viewController.unwrap(itemRef) + + viewController.performDataSourceUpdate { updateDone in + if let datasource = viewController.collectionViewDataSource, let sectionID { + var sectionSnapshot = datasource.snapshot(for: sectionID) + sectionSnapshot.expand([ itemRef ]) + datasource.apply(sectionSnapshot, to: sectionID, animatingDifferences: animated, completion: { + completion?() + updateDone() + }) + } else { + completion?() + updateDone() + } + } + + return true + + default: break + } + } else { + switch kind { + case .unhighlightAll(animated: let animated): + viewController.performDataSourceUpdate { updateDone in + if let selectedIndexPaths = collectionView.indexPathsForSelectedItems { + for selectedIndexPath in selectedIndexPaths { + collectionView.deselectItem(at: selectedIndexPath, animated: animated) + } + } + + completion?() + updateDone() + } + + return true + + default: break + } + } + + return false + } +} diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewCellConfiguration.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewCellConfiguration.swift index d6f7584dd..3d51367b4 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewCellConfiguration.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewCellConfiguration.swift @@ -26,6 +26,7 @@ open class CollectionViewCellStyle: NSObject { case tableCell case gridCell case fillSpace + case sideBar } public struct StyleOptionKey : Hashable { @@ -40,7 +41,9 @@ open class CollectionViewCellStyle: NSObject { super.init() } - public convenience init(from style: CollectionViewCellStyle, changing: (CollectionViewCellStyle) -> Void) { + public typealias Modifier = (CollectionViewCellStyle) -> Void + + public convenience init(from style: CollectionViewCellStyle, changing: Modifier) { self.init(with: style.type) self.options = style.options @@ -97,7 +100,10 @@ public class CollectionViewCellConfiguration: NSObject { // Request reconfiguration of cell itemRecord.retrieveItem(completionHandler: { error, itemRecord in if let collectionViewController = self.hostViewController { - collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef]) + collectionViewController.performDataSourceUpdate { updateDone in + collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef]) + updateDone() + } } }) } diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift index c44dc0466..629f95c97 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift @@ -21,14 +21,20 @@ import ownCloudSDK public extension CollectionViewCellProvider { static func registerStandardImplementations() { - // Register cell providers for .drive and .presentable - DriveListCell.registerCellProvider() - ItemListCell.registerCellProvider() - ExpandableResourceCell.registerCellProvider() - ActionCell.registerCellProvider() - SavedSearchCell.registerCellProvider() - ViewCell.registerCellProvider() - + // Register cell providers + DriveListCell.registerCellProvider() // Cell providers for .drive + ExpandableResourceCell.registerCellProvider() // Cell providers for .textResource + ActionCell.registerCellProvider() // Cell providers for .action + AccountControllerCell.registerCellProvider() // Cell providers for .accountController + SavedSearchCell.registerCellProvider() // Cell providers for .savedSearch + ViewCell.registerCellProvider() // Cell providers for .view + + // Register UniversalItemListCell based cell providers + OCItem.registerUniversalCellProvider() // Cell providers for .item + OCShare.registerUniversalCellProvider() // Cell providers for .share + OCItemPolicy.registerUniversalCellProvider() // Cell providers for .itemPolicy + + // Register cell providers for .presentable registerPresentableCellProvider() } @@ -91,7 +97,10 @@ public extension CollectionViewCellProvider { // Request reconfiguration of cell itemRecord.retrieveItem(completionHandler: { error, itemRecord in if let collectionViewController = cellConfiguration.hostViewController { - collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef]) + collectionViewController.performDataSourceUpdate(with: { updateDone in + collectionViewController.collectionViewDataSource.requestReconfigurationOfItems([collectionItemRef]) + updateDone() + }) } }) } @@ -102,8 +111,50 @@ public extension CollectionViewCellProvider { cell.accessories = hasDisclosureIndicator ? [ .disclosureIndicator() ] : [ ] } + let presentableSidebarCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + var title: String? + var image: UIImage? + var hasChildren: Bool = false + + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let presentable = OCDataRenderer.default.renderItem(item, asType: .presentable, error: nil, withOptions: nil) as? OCDataItemPresentable { + title = presentable.title + image = presentable.image + if let source = cellConfiguration.source { + hasChildren = presentable.hasChildren(using: source) + } + } + }) + + var content = cell.defaultContentConfiguration() + + content.text = title + if let image = image { + content.image = image + } + + cell.contentConfiguration = content + + if hasChildren { + let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header) + // let hostViewController = collectionItemRef.ocCellConfiguration?.hostViewController + + cell.accessories = [.outlineDisclosure(options: headerDisclosureOption)] /* , actionHandler: { [weak hostViewController] in + hostViewController?.expandCollapse(collectionItemRef) + })]*/ + } else { + cell.accessories = [] + } + } + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .presentable, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in - return collectionView.dequeueConfiguredReusableCell(using: presentableCellRegistration, for: indexPath, item: itemRef) + switch cellConfiguration?.style.type { + case .sideBar: + return collectionView.dequeueConfiguredReusableCell(using: presentableSidebarCellRegistration, for: indexPath, item: itemRef) + + default: + return collectionView.dequeueConfiguredReusableCell(using: presentableCellRegistration, for: indexPath, item: itemRef) + } })) // This registration performs conversion to .presentable where necessary, so it can also be used for other types OCDataItemTypes. Example: diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift index 36d9e3e20..f686ee6c1 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift @@ -25,15 +25,18 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public var supportsHierarchicContent: Bool public var usesStackViewRoot: Bool + public var useWrappedIdentifiers: Bool var highlightItemReference: OCDataItemReference? var didHighlightItemReference: Bool = false + var hideNavigationBar: Bool? var emptyCellRegistration: UICollectionView.CellRegistration? - public init(context inContext: ClientContext?, sections inSections: [CollectionViewSection]?, useStackViewRoot: Bool = false, hierarchic: Bool = false, highlightItemReference: OCDataItemReference? = nil) { + public init(context inContext: ClientContext?, sections inSections: [CollectionViewSection]?, useStackViewRoot: Bool = false, hierarchic: Bool = false, useWrappedIdentifiers: Bool = false, highlightItemReference: OCDataItemReference? = nil) { supportsHierarchicContent = hierarchic usesStackViewRoot = useStackViewRoot + self.useWrappedIdentifiers = hierarchic ? hierarchic : useWrappedIdentifiers self.highlightItemReference = highlightItemReference super.init(nibName: nil, bundle: nil) @@ -79,7 +82,12 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public func configureLayout() { if usesStackViewRoot, let stackView = stackView { collectionView.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(collectionView) + + let safeAreaView = UIView() + safeAreaView.translatesAutoresizingMaskIntoConstraints = false + safeAreaView.embed(toFillWith: collectionView, enclosingAnchors: safeAreaView.safeAreaAnchorSet) + + stackView.addArrangedSubview(safeAreaView) } else { collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(collectionView) @@ -153,15 +161,17 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, var collectionViewDataSource: UICollectionViewDiffableDataSource! = nil public override func loadView() { + super.loadView() + if usesStackViewRoot { createStackView() - view = stackView - } else { - super.loadView() + if let stackView = stackView { + view.embed(toFillWith: stackView, enclosingAnchors: view.defaultAnchorSet) + } } } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() configureViews() configureDataSource() @@ -169,7 +179,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, Theme.shared.register(client: self, applyImmediately: true) } - public func createCollectionViewLayout() -> UICollectionViewLayout { + open func createCollectionViewLayout() -> UICollectionViewLayout { let configuration = UICollectionViewCompositionalLayoutConfiguration() configuration.interSectionSpacing = 0 @@ -191,7 +201,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, }, configuration: configuration) } - public func createCollectionView() { + open func createCollectionView() { if collectionView == nil { collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout()) collectionView.contentInsetAdjustmentBehavior = .never @@ -202,69 +212,355 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } } + // MARK: - Cover view + var coverRootView: UIView? { + willSet { + coverRootView?.removeFromSuperview() + } + + didSet { + if let coverRootView { + view.embed(toFillWith: coverRootView) + } + } + } + + public enum CoverViewLayout { + case fill + case center + case top + } + + open func setCoverView(_ coverView: UIView?, layout: CoverViewLayout) { + if view != nil { + if let coverView { + let rootView = UIView() + rootView.translatesAutoresizingMaskIntoConstraints = false + rootView.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + + switch layout { + case .fill: + rootView.embed(toFillWith: coverView) + + case .center: + rootView.embed(centered: coverView, enclosingAnchors: rootView.safeAreaAnchorSet) + + case .top: + rootView.addSubview(coverView) + NSLayoutConstraint.activate([ + coverView.leadingAnchor.constraint(greaterThanOrEqualTo: rootView.safeAreaLayoutGuide.leadingAnchor, constant: 20), + coverView.trailingAnchor.constraint(greaterThanOrEqualTo: rootView.safeAreaLayoutGuide.trailingAnchor, constant: 20), + coverView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor), + coverView.topAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.topAnchor, constant: 20), + coverView.bottomAnchor.constraint(lessThanOrEqualTo: rootView.safeAreaLayoutGuide.bottomAnchor, constant: -20) + ]) + } + + self.coverRootView = rootView + } else { + self.coverRootView = nil + } + } + } + // MARK: - Collection View Datasource - public func configureDataSource() { + open func configureDataSource() { + dataSourceWorkQueue.executor = { (job, completionHandler) in + job(completionHandler) + } + collectionViewDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak self] (collectionView: UICollectionView, indexPath: IndexPath, collectionItemRef: CollectionViewController.ItemRef) -> UICollectionViewCell? in if let sectionIdentifier = self?.collectionViewDataSource.sectionIdentifier(for: indexPath.section), let section = self?.sectionsByID[sectionIdentifier] { - return section.provideReusableCell(for: collectionView, collectionItemRef: collectionItemRef, indexPath: indexPath) + var cell = section.provideReusableCell(for: collectionView, collectionItemRef: collectionItemRef, indexPath: indexPath) + + if let cell { + return cell + } else if let self, !self.useWrappedIdentifiers { + // # UICollectionViewDiffableDataSource quirk workaround: + // If a cell moves from one section to another, the cell is requeested from the indexPath / section that it was PREVIOUSLY located at/in. + // At that point, however, the data source for that section no longer contains the item, so no cell can be returned. + // For this case, all other sections are asked to provide the cell before falling through. + + for otherSection in self.sections { + if !otherSection.hidden, otherSection != section { + cell = otherSection.provideReusableCell(for: collectionView, collectionItemRef: collectionItemRef, indexPath: indexPath) + + if let cell { + return cell + } + } + } + } } - return self?.provideEmptyFallbackCell(for: indexPath, item: collectionItemRef) + return self?.provideUnknownCell(for: indexPath, item: collectionItemRef) + } + + if supportsHierarchicContent { + collectionViewDataSource.sectionSnapshotHandlers.willExpandItem = { [weak self] (parentItemRef) in + if let (_, sectionIdentifier) = self?.unwrap(parentItemRef) { + if let sectionIdentifier = sectionIdentifier, + let section = self?.sectionsByID[sectionIdentifier] { + section.addExpanded(item: parentItemRef) + } + } + } + collectionViewDataSource.sectionSnapshotHandlers.willCollapseItem = { [weak self] (parentItemRef) in + if let (_, sectionIdentifier) = self?.unwrap(parentItemRef) { + if let sectionIdentifier = sectionIdentifier, + let section = self?.sectionsByID[sectionIdentifier] { + section.removeExpanded(item: parentItemRef) + } + } + } + collectionViewDataSource.sectionSnapshotHandlers.snapshotForExpandingParent = { [weak self] (parentItemRef, sectionSnapshot) in + if let (_, sectionIdentifier) = self?.unwrap(parentItemRef) { + if let sectionIdentifier = sectionIdentifier, + let collectionView = self?.collectionView, + let section = self?.sectionsByID[sectionIdentifier] { + return section.provideHierarchicContent(for: collectionView, parentItemRef: parentItemRef, existingSectionSnapshot: sectionSnapshot) + } + } + + return sectionSnapshot + } + } + + collectionViewDataSource.supplementaryViewProvider = { [weak self] (collectionView, elementKind, indexPath) in + // Fetch by section ID (may fail if section is process of being hidden) + if let sectionIdentifier = self?.collectionViewDataSource.sectionIdentifier(for: indexPath.section), + let section = self?.sectionsByID[sectionIdentifier], !section.hidden, + let supplementaryItemProvider = CollectionViewSupplementaryCellProvider.providerFor(elementKind), + let supplementaryItem = section.boundarySupplementaryItems?.first(where: { item in + return item.elementKind == elementKind + }) { + return supplementaryItemProvider.provideCell(for: collectionView, section: section, supplementaryItem: supplementaryItem, indexPath: indexPath) + } + + // Fetch by section offset + if let section = self?.section(at: indexPath.section), + let supplementaryItemProvider = CollectionViewSupplementaryCellProvider.providerFor(elementKind), + let supplementaryItem = section.boundarySupplementaryItems?.first(where: { item in + return item.elementKind == elementKind + }) { + return supplementaryItemProvider.provideCell(for: collectionView, section: section, supplementaryItem: supplementaryItem, indexPath: indexPath) + } + + return nil } // initial data updateSource(animatingDifferences: false) } - var sections : [CollectionViewSection] = [] - var sectionsByID : [CollectionViewSection.SectionIdentifier : CollectionViewSection] = [:] + private var dataSourceWorkQueue = OCAsyncSequentialQueue() + + func performDataSourceUpdate(with block: @escaping (_ updateDone: @escaping () -> Void) -> Void) { + // Usage of a queue that performs immediately (unless busy) is needed as requesting an update during an update + // will raise an exception "Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue" - even though the updates have been performed from the same (main) queue always + dataSourceWorkQueue.async({ updateDone in + block(updateDone) + }) + } // MARK: - Sections - public func add(sections sectionsToAdd: [CollectionViewSection]) { - for section in sectionsToAdd { - section.collectionViewController = self + var sections: [CollectionViewSection] = [] + var sectionsByID: [CollectionViewSection.SectionIdentifier : CollectionViewSection] = [:] + + public var allSections: [CollectionViewSection] { + return sections + } + + private func associate(section: CollectionViewSection) { + section.collectionViewController = self + sectionsByID[section.identifier] = section + } + + private func disassociate(section: CollectionViewSection) { + section.collectionViewController = nil + sectionsByID[section.identifier] = nil + } + open func add(sections sectionsToAdd: [CollectionViewSection]) { + for section in sectionsToAdd { + associate(section: section) sections.append(section) - sectionsByID[section.identifier] = section } updateSource() } - public func remove(sections sectionsToRemove: [CollectionViewSection]) { + open func insert(sections sectionsToAdd: [CollectionViewSection], at index: Int) { + for section in sectionsToAdd { + associate(section: section) + } + + sections.insert(contentsOf: sectionsToAdd, at: index) + + updateSource() + } + + open func remove(sections sectionsToRemove: [CollectionViewSection]) { for section in sectionsToRemove { - section.collectionViewController = nil + disassociate(section: section) if let sectionIdx = sections.firstIndex(of: section) { sections.remove(at: sectionIdx) - sectionsByID[section.identifier] = nil } } updateSource() } - public var animateDifferences : Bool = true + // MARK: - Sections Datasource changes + private var _needsSourceUpdate: Bool = false + private var _needsSourceUpdateWithAnimation: Bool = false + + open func setNeedsSourceUpdate(animatingDifferences: Bool = true) { + var needsUpdate: Bool = false + + OCSynchronized(self) { + if !_needsSourceUpdate { + _needsSourceUpdate = true + _needsSourceUpdateWithAnimation = animatingDifferences + + needsUpdate = true + } else { + if _needsSourceUpdateWithAnimation && !animatingDifferences { + _needsSourceUpdateWithAnimation = false + } + } + } + + if needsUpdate { + OnMainThread { + var performUpdate: Bool = false + var animatedUpdate: Bool = false + + OCSynchronized(self) { + performUpdate = self._needsSourceUpdate + animatedUpdate = self._needsSourceUpdateWithAnimation + + self._needsSourceUpdate = false + } + + if performUpdate { + self.__updateSource(animatingDifferences: animatedUpdate) + } + } + } + } + + // MARK: - Sections Datasource + private var _sectionsSubscription: OCDataSourceSubscription? + open var sectionsDataSource: OCDataSource? { + willSet { + _sectionsSubscription?.terminate() + _sectionsSubscription = nil + } + + didSet { + _sectionsSubscription = sectionsDataSource?.subscribe(updateHandler: { [weak self] (subscription) in + self?.updateSections(from: subscription.snapshotResettingChangeTracking(true)) + }, on: .main, trackDifferences: true, performInitialUpdate: true) + } + } + + private func updateSections(from snapshot: OCDataSourceSnapshot) { + var newSections: [CollectionViewSection] = [] + + if let addedItems = snapshot.addedItems { + for itemRef in addedItems { + if let itemRecord = try? sectionsDataSource?.record(forItemRef: itemRef), let section = itemRecord.item as? CollectionViewSection { + associate(section: section) + } + } + } + + if let removedItems = snapshot.removedItems { + for itemRef in removedItems { + if let itemRecord = try? sectionsDataSource?.record(forItemRef: itemRef), let section = itemRecord.item as? CollectionViewSection { + disassociate(section: section) + } + } + } + + for itemRef in snapshot.items { + if let itemRecord = try? sectionsDataSource?.record(forItemRef: itemRef), let section = itemRecord.item as? CollectionViewSection { + newSections.append(section) + } + } + + sections = newSections + + updateSource() + } + + open var animateDifferences : Bool = true func updateSource(animatingDifferences: Bool = true) { + setNeedsSourceUpdate(animatingDifferences: animatingDifferences) + } + + func __updateSource(animatingDifferences: Bool = true) { + performDataSourceUpdate { updateDone in + self._updateSource(animatingDifferences: animatingDifferences) + updateDone() + } + } + + func _updateSource(animatingDifferences: Bool = true) { guard let collectionViewDataSource = collectionViewDataSource else { return } var snapshot = NSDiffableDataSourceSnapshot() + var snapshotsBySection = [CollectionViewSection.SectionIdentifier : NSDiffableDataSourceSectionSnapshot]() + var updatedItems : [CollectionViewController.ItemRef] = [] + + // Log.debug("<=========================>") for section in sections { if !section.hidden { snapshot.appendSections([section.identifier]) - section.populate(snapshot: &snapshot) + if !useWrappedIdentifiers { + section.populate(snapshot: &snapshot) + } else { + snapshotsBySection[section.identifier] = collectionViewDataSource.snapshot(for: section.identifier) + } } } - collectionViewDataSource.apply(snapshot, animatingDifferences: animatingDifferences) + collectionViewDataSource.apply(snapshot, animatingDifferences: animatingDifferences && !useWrappedIdentifiers) + + if useWrappedIdentifiers { + for section in sections { + if !section.hidden { + let (sectionSnapshot, sectionUpdatedItems) = section.composeSectionSnapshot(from: snapshotsBySection[section.identifier]) + + collectionViewDataSource.apply(sectionSnapshot, to: section.identifier, animatingDifferences: false) + + if let sectionUpdatedItems = sectionUpdatedItems { + updatedItems.append(contentsOf: sectionUpdatedItems) + } + } + } + + if updatedItems.count > 0 { + var snapshot = collectionViewDataSource.snapshot() + snapshot.reconfigureItems(updatedItems) + collectionViewDataSource.apply(snapshot, animatingDifferences: false) + } + } + + // Notify view controller of content updates + setContentDidUpdate() } - public func section(at targetIndex: Int) -> CollectionViewSection? { + open func section(at inTargetIndex: Int) -> CollectionViewSection? { + let targetIndex = collectionView.dataSourceSectionIndex(forPresentationSectionIndex: inTargetIndex) + if (targetIndex >= 0) && (targetIndex < sections.count) { var index : Int = 0 @@ -282,7 +578,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, return nil } - public func index(of findSection: CollectionViewSection) -> Int? { + open func index(of findSection: CollectionViewSection) -> Int? { var index : Int = 0 for section in sections { @@ -307,10 +603,17 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, let reloadSectionIDs = sections.map({ section in return section.identifier }) if reloadSectionIDs.count > 0 { - var snapshot = collectionViewDataSource.snapshot() - snapshot.reloadSections(reloadSectionIDs) + performDataSourceUpdate { updateDone in + var snapshot = self.collectionViewDataSource.snapshot() + snapshot.reloadSections(reloadSectionIDs) + + self.collectionViewDataSource.apply(snapshot, animatingDifferences: animated) - collectionViewDataSource.apply(snapshot, animatingDifferences: animated) + // Notify view controller of content updates + self.setContentDidUpdate() + + updateDone() + } } } @@ -339,10 +642,14 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public override var hash: Int { return dataItemReference.hash ^ sectionIdentifier.hash } + + public override var description: String { + return "" + } } public func wrap(references: [OCDataItemReference], forSection: CollectionViewSection.SectionIdentifier) -> [ItemRef] { - if supportsHierarchicContent { + if useWrappedIdentifiers { // wrap references and section ID together into a single object var itemRefs : [ItemRef] = [] @@ -358,7 +665,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } public func unwrap(_ collectionItemRef: ItemRef) -> (OCDataItemReference, CollectionViewSection.SectionIdentifier?) { - if supportsHierarchicContent, let wrappedItem = collectionItemRef as? WrappedItem { + if useWrappedIdentifiers, let wrappedItem = collectionItemRef as? WrappedItem { // unwrap bundled item references + section ID return (wrappedItem.dataItemReference, wrappedItem.sectionIdentifier) } @@ -366,7 +673,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, return (collectionItemRef, nil) } - public func retrieveItem(at indexPath: IndexPath, synchronous: Bool = false, action: @escaping ((_ record: OCDataItemRecord, _ indexPath: IndexPath) -> Void), handleError: ((_ error: Error?) -> Void)? = nil) { + public func retrieveItem(at indexPath: IndexPath, synchronous: Bool = false, action: @escaping ((_ record: OCDataItemRecord, _ indexPath: IndexPath, _ section: CollectionViewSection) -> Void), handleError: ((_ error: Error?) -> Void)? = nil) { guard let collectionItemRef = collectionViewDataSource.itemIdentifier(for: indexPath) else { handleError?(nil) return @@ -374,13 +681,13 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, if let sectionIdentifier = collectionViewDataSource.sectionIdentifier(for: indexPath.section), let section = sectionsByID[sectionIdentifier], - let dataSource = section.dataSource { + let dataSource = section.contentDataSource { let (itemRef, _) = unwrap(collectionItemRef) if synchronous { do { let record = try dataSource.record(forItemRef: itemRef) - action(record, indexPath) + action(record, indexPath, section) } catch { handleError?(error) } @@ -391,7 +698,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, return } - action(record, indexPath) + action(record, indexPath, section) }) } } else { @@ -403,7 +710,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, var recordsByIndexPath : [IndexPath : OCDataItemRecord] = [:] for indexPath in indexPaths { - retrieveItem(at: indexPath, synchronous: true, action: { record, indexPath in + retrieveItem(at: indexPath, synchronous: true, action: { record, indexPath, _ in recordsByIndexPath[indexPath] = record }, handleError: handleError) } @@ -411,15 +718,136 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, action(recordsByIndexPath) } + public func retrieveIndexPaths(for items: [CollectionViewController.ItemRef]) -> [IndexPath] { + var indexPaths: [IndexPath] = [] + + for item in items { + if let indexPath = collectionViewDataSource.indexPath(for: item) { + indexPaths.append(indexPath) + } + } + + return indexPaths + } + + // MARK: - Expand / Collapse +// public func expandCollapse(_ collectionViewItemRef: CollectionViewController.ItemRef) { +// let (_, sectionID) = unwrap(collectionViewItemRef) +// +// if let sectionID = sectionID { +// var snap = collectionViewDataSource.snapshot(for: sectionID) +// if snap.isExpanded(collectionViewItemRef) { +// snap.collapse([collectionViewItemRef]) +// } else { +// snap.expand([collectionViewItemRef]) +// } +// collectionViewDataSource.apply(snap, to: sectionID) +// } +// } + + // MARK: - Actions + var actions: [CollectionViewAction] = [] + public func addActions(_ addedActions: [CollectionViewAction]) { + self.actions.append(contentsOf: addedActions) + + OnMainThread { + self.runActions() + } + } + + private func runActions() { + var remainingActions: [CollectionViewAction] = [] + + for action in self.actions { + if !action.apply(on: self, completion: nil) { + remainingActions.append(action) + } + } + + self.actions = remainingActions + } + + // MARK: - Content update + private var contentDidUpdate: Bool = false + public func setContentDidUpdate() { + contentDidUpdate = true + OnMainThread { + if self.contentDidUpdate { + self.contentDidUpdate = false + self.handleContentUpdate() + } + } + } + + private func handleContentUpdate() { + runActions() + } + + // MARK: - Selection +// var selectedItemReferences: [ItemRef]? +// +// enum SelectionOperation { +// case add +// case replace +// case toggle +// case remove +// case clear +// } +// +// func recordSelection(ofItemWith itemRef: ItemRef?, operation: SelectionOperation = .replace) { +// var effectiveOperation: SelectionOperation = operation +// +// if operation == .toggle, let itemRef { +// effectiveOperation = selectedItemReferences?.contains(itemRef) == true ? .remove :. add +// } +// +// switch effectiveOperation { +// case .add: +// if let itemRef { +// if selectedItemReferences != nil { +// selectedItemReferences?.append(itemRef) +// } else { +// selectedItemReferences = [ itemRef ] +// } +// } +// +// case .replace: +// if let itemRef { +// selectedItemReferences = [ itemRef ] +// } +// +// case .remove: +// if let itemRef { +// selectedItemReferences = selectedItemReferences?.filter({ checkItemRef in +// return checkItemRef != itemRef +// }) +// } +// +// case .clear: +// selectedItemReferences = nil +// +// default: break +// } +// } +// +// func recordSelection(ofItemAt indexPath: IndexPath, operation: SelectionOperation = .replace) { +// recordSelection(ofItemWith: collectionViewDataSource?.itemIdentifier(for: indexPath), operation: operation) +// } + // MARK: - Collection View Delegate public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { var shouldSelect : Bool = false let interaction : ClientItemInteraction = collectionView.isEditing ? .multiselection : .selection - retrieveItem(at: indexPath, synchronous: true, action: { [weak self] record, indexPath in - // Return early if .contextMenu is not allowed - if self?.clientContext?.validate(interaction: interaction, for: record) != false { + retrieveItem(at: indexPath, synchronous: true, action: { [weak self] record, indexPath, section in + // Return early if .selection is not allowed + let clientContext = section.clientContext ?? self?.clientContext + + if clientContext?.validate(interaction: interaction, for: record, in: self) != false { shouldSelect = true + if let clientContext, let self { + shouldSelect = self.allowSelection(of: record, at: indexPath, clientContext: clientContext) + } } }) @@ -429,13 +857,17 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let interaction : ClientItemInteraction = collectionView.isEditing ? .multiselection : .selection - retrieveItem(at: indexPath, action: { [weak self] record, indexPath in + // recordSelection(ofItemAt: indexPath, operation: .add) + + retrieveItem(at: indexPath, action: { [weak self] record, indexPath, section in // Return early if .selection is not allowed - if self?.clientContext?.validate(interaction: interaction, for: record) != false { + let clientContext = section.clientContext ?? self?.clientContext + + if clientContext?.validate(interaction: interaction, for: record, in: self) != false, let clientContext { if interaction == .multiselection { - self?.handleMultiSelection(of: record, at: indexPath, isSelected: true) + self?.handleMultiSelection(of: record, at: indexPath, isSelected: true, clientContext: clientContext) } else { - self?.handleSelection(of: record, at: indexPath) + self?.handleSelection(of: record, at: indexPath, clientContext: clientContext) } } }, handleError: { error in @@ -448,14 +880,18 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { let interaction : ClientItemInteraction = collectionView.isEditing ? .multiselection : .selection + // recordSelection(ofItemAt: indexPath, operation: .remove) + if interaction != .multiselection { return } - retrieveItem(at: indexPath, action: { [weak self] record, indexPath in + retrieveItem(at: indexPath, action: { [weak self] record, indexPath, section in // Return early if .selection is not allowed - if self?.clientContext?.validate(interaction: interaction, for: record) != false { - self?.handleMultiSelection(of: record, at: indexPath, isSelected: false) + let clientContext = section.clientContext ?? self?.clientContext + + if clientContext?.validate(interaction: interaction, for: record, in: self) != false, let clientContext { + self?.handleMultiSelection(of: record, at: indexPath, isSelected: false, clientContext: clientContext) } }) } @@ -463,10 +899,12 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { var contextMenuConfiguration : UIContextMenuConfiguration? - retrieveItem(at: indexPath, synchronous: true, action: { [weak self] record, indexPath in + retrieveItem(at: indexPath, synchronous: true, action: { [weak self] record, indexPath, section in // Return early if .contextMenu is not allowed - if self?.clientContext?.validate(interaction: .contextMenu, for: record) != false { - contextMenuConfiguration = self?.provideContextMenuConfiguration(for: record, at: indexPath, point: point) + let clientContext = section.clientContext ?? self?.clientContext + + if clientContext?.validate(interaction: .contextMenu, for: record, in: self) != false, let clientContext { + contextMenuConfiguration = self?.provideContextMenuConfiguration(for: record, at: indexPath, point: point, clientContext: clientContext) } }, handleError: { error in collectionView.deselectItem(at: indexPath, animated: true) @@ -476,19 +914,33 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } // MARK: - Cell action subclassing points - @discardableResult public func handleSelection(of record: OCDataItemRecord, at indexPath: IndexPath) -> Bool { + @discardableResult public func allowSelection(of record: OCDataItemRecord, at indexPath: IndexPath, clientContext: ClientContext) -> Bool { + if let selectionInteraction = record.item as? DataItemSelectionInteraction { + if selectionInteraction.allowSelection?(in: self, section: section(at: indexPath.section), with: clientContext) == false { + return false + } + } + + return true + } + + @discardableResult public func handleSelection(of record: OCDataItemRecord, at indexPath: IndexPath, clientContext: ClientContext) -> Bool { // Use item's DataItemSelectionInteraction if let selectionInteraction = record.item as? DataItemSelectionInteraction { // Try selection first if selectionInteraction.handleSelection?(in: self, with: clientContext, completion: { [weak self] success in - self?.collectionView.deselectItem(at: indexPath, animated: true) + if self?.shouldDeselect(record: record, at: indexPath, afterInteraction: .selection, clientContext: clientContext) == true { + self?.collectionView.deselectItem(at: indexPath, animated: true) + } }) == true { return true } // Then try opening if selectionInteraction.openItem?(from: self, with: clientContext, animated: true, pushViewController: true, completion: { [weak self] success in - self?.collectionView.deselectItem(at: indexPath, animated: true) + if self?.shouldDeselect(record: record, at: indexPath, afterInteraction: .selection, clientContext: clientContext) == true { + self?.collectionView.deselectItem(at: indexPath, animated: true) + } }) != nil { return true } @@ -497,13 +949,17 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, return false } - @discardableResult public func handleMultiSelection(of record: OCDataItemRecord, at indexPath: IndexPath, isSelected: Bool) -> Bool { + public func shouldDeselect(record: OCDataItemRecord, at indexPath: IndexPath, afterInteraction: ClientItemInteraction, clientContext: ClientContext) -> Bool { + return true + } + + @discardableResult public func handleMultiSelection(of record: OCDataItemRecord, at indexPath: IndexPath, isSelected: Bool, clientContext: ClientContext) -> Bool { return false } - @discardableResult public func provideContextMenuConfiguration(for record: OCDataItemRecord, at indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + @discardableResult public func provideContextMenuConfiguration(for record: OCDataItemRecord, at indexPath: IndexPath, point: CGPoint, clientContext: ClientContext) -> UIContextMenuConfiguration? { // Use context.contextMenuProvider - if let item = record.item, let clientContext = clientContext, let contextMenuProvider = clientContext.contextMenuProvider { + if let item = record.item, let contextMenuProvider = clientContext.contextMenuProvider { return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] _ in guard let self = self else { return nil @@ -589,8 +1045,10 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, if let destinationIndexPath = indexPath { // Retrieve item at index path if provided - retrieveItem(at: destinationIndexPath, synchronous: true, action: { record, indexPath in - if self.clientContext?.validate(interaction: interaction, for: record) != false { + retrieveItem(at: destinationIndexPath, synchronous: true, action: { record, indexPath, section in + let clientContext = section.clientContext ?? self.clientContext + + if clientContext?.validate(interaction: interaction, for: record, in: self) != false { item = record.item } }, handleError: { error in @@ -714,23 +1172,51 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } } + // MARK: - Events + private var _navigationBarWasHidden: Bool? + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let hideNavigationBar, hideNavigationBar, navigationController?.isNavigationBarHidden != hideNavigationBar { + _navigationBarWasHidden = navigationController?.isNavigationBarHidden + navigationController?.setNavigationBarHidden(hideNavigationBar, animated: true) + } + } + + open override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + if let _navigationBarWasHidden, navigationController?.isNavigationBarHidden != _navigationBarWasHidden { + navigationController?.setNavigationBarHidden(_navigationBarWasHidden, animated: true) + } + } + // MARK: - Themeing public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { if event != .initial { collectionView.setCollectionViewLayout(createCollectionViewLayout(), animated: false) } + + collectionView.backgroundColor = collection.tableBackgroundColor + coverRootView?.backgroundColor = collection.tableBackgroundColor } } public extension CollectionViewController { func relayout(cell: UICollectionViewCell) { - collectionViewDataSource.apply(collectionViewDataSource.snapshot(), animatingDifferences: true) + performDataSourceUpdate { updateDone in + self.collectionViewDataSource.apply(self.collectionViewDataSource.snapshot(), animatingDifferences: true) + + // Notify view controller of content updates + self.setContentDidUpdate() + + updateDone() + } } - func provideEmptyFallbackCell(for indexPath: IndexPath, item itemRef: CollectionViewController.ItemRef) -> UICollectionViewCell { - if let emptyCellRegistration = emptyCellRegistration { + func provideUnknownCell(for indexPath: IndexPath, item itemRef: CollectionViewController.ItemRef) -> UICollectionViewCell { + if let unknownCellRegistration = emptyCellRegistration { let reUseIdentifier : CollectionViewController.ItemRef = NSString(string: "_empty_\(String(describing: itemRef))") - return collectionView.dequeueConfiguredReusableCell(using: emptyCellRegistration, for: indexPath, item: reUseIdentifier) + return collectionView.dequeueConfiguredReusableCell(using: unknownCellRegistration, for: indexPath, item: reUseIdentifier) } return UICollectionViewCell.emptyFallbackCell diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift index 95048ca30..04950b9b6 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift @@ -19,11 +19,16 @@ import UIKit import ownCloudSDK -public class CollectionViewSection: NSObject { +public extension OCDataItemType { + static let collectionViewSection = OCDataItemType(rawValue: "collectionViewSection") +} + +public class CollectionViewSection: NSObject, OCDataItem, OCDataItemVersioning { public enum CellLayout { case list(appearance: UICollectionLayoutListConfiguration.Appearance, headerMode: UICollectionLayoutListConfiguration.HeaderMode? = nil, headerTopPadding : CGFloat? = nil, footerMode: UICollectionLayoutListConfiguration.FooterMode? = nil, contentInsets: NSDirectionalEdgeInsets? = nil) case fullWidth(itemHeightDimension: NSCollectionLayoutDimension, groupHeightDimension: NSCollectionLayoutDimension, edgeSpacing: NSCollectionLayoutEdgeSpacing? = nil, contentInsets: NSDirectionalEdgeInsets? = nil) case sideways(item: NSCollectionLayoutItem? = nil, groupSize: NSCollectionLayoutSize? = nil, innerInsets : NSDirectionalEdgeInsets? = nil, edgeSpacing: NSCollectionLayoutEdgeSpacing? = nil, contentInsets: NSDirectionalEdgeInsets? = nil, orthogonalScrollingBehaviour: UICollectionLayoutSectionOrthogonalScrollingBehavior = .continuousGroupLeadingBoundary) + case grid(itemWidthDimension: NSCollectionLayoutDimension, itemHeightDimension: NSCollectionLayoutDimension, contentInsets: NSDirectionalEdgeInsets? = nil) case custom(generator: ((_ collectionViewController: CollectionViewController?, _ layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection)) func collectionLayoutSection(for collectionViewController: CollectionViewController? = nil, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { @@ -55,16 +60,19 @@ public class CollectionViewSection: NSObject { // Leading and trailing swipe actions if let collectionViewController = collectionViewController { - let clientContext = ClientContext(with: collectionViewController.clientContext, modifier: { context in - context.originatingViewController = collectionViewController - }) - config.leadingSwipeActionsConfigurationProvider = { [weak collectionViewController] (_ indexPath: IndexPath) in var swipeConfiguration : UISwipeActionsConfiguration? - collectionViewController?.retrieveItem(at: indexPath, synchronous: true, action: { record, indexPath in + collectionViewController?.retrieveItem(at: indexPath, synchronous: true, action: { record, indexPath, section in + // Create client context with proper originatingViewController + guard var clientContext = section.clientContext ?? collectionViewController?.clientContext else { return } + + clientContext = ClientContext(with: clientContext, modifier: { context in + context.originatingViewController = collectionViewController + }) + // Return early if leadingSwipes are not allowed - if !clientContext.validate(interaction: .leadingSwipe, for: record) { + if !clientContext.validate(interaction: .leadingSwipe, for: record, in: collectionViewController) { return } @@ -90,9 +98,16 @@ public class CollectionViewSection: NSObject { config.trailingSwipeActionsConfigurationProvider = { [weak collectionViewController] (_ indexPath: IndexPath) in var swipeConfiguration : UISwipeActionsConfiguration? - collectionViewController?.retrieveItem(at: indexPath, synchronous: true, action: { record, indexPath in + collectionViewController?.retrieveItem(at: indexPath, synchronous: true, action: { record, indexPath, section in + // Create client context with proper originatingViewController + guard var clientContext = section.clientContext ?? collectionViewController?.clientContext else { return } + + clientContext = ClientContext(with: clientContext, modifier: { context in + context.originatingViewController = collectionViewController + }) + // Return early if trailingSwipes are not allowed - if !clientContext.validate(interaction: .trailingSwipe, for: record) { + if !clientContext.validate(interaction: .trailingSwipe, for: record, in: collectionViewController) { return } @@ -156,6 +171,32 @@ public class CollectionViewSection: NSObject { } return layoutSection + case .grid(let itemWidthDimension, let itemHeightDimension, let contentInsets): + let itemSize = NSCollectionLayoutSize(widthDimension: itemWidthDimension, heightDimension: itemHeightDimension) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.contentInsets = contentInsets ?? NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) + + let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeightDimension), subitems: [ item ]) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = contentInsets ?? NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) + + section.interGroupSpacing = 0 + + return section +// let itemSize = NSCollectionLayoutSize(widthDimension: itemWidthDimension, +// heightDimension: .fractionalHeight(1.0)) +// let item = NSCollectionLayoutItem(layoutSize: itemSize) +// item.contentInsets = contentInsets ?? NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) +// +// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), +// heightDimension: itemHeightDimension) +// let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, +// subitems: [item]) +// +// let section = NSCollectionLayoutSection(group: group) +// return section + // Custom case .custom(let generator): return generator(collectionViewController, layoutEnvironment) @@ -168,13 +209,24 @@ public class CollectionViewSection: NSObject { public var identifier: SectionIdentifier + public var contentDataSource: OCDataSource? { + return combinedChildrenDataSource ?? dataSource + } + public var dataSource: OCDataSource? { willSet { dataSourceSubscription?.terminate() dataSourceSubscription = nil + + if let dataSource = dataSource { + combinedChildrenDataSource?.removeSources([dataSource]) + } } didSet { + if let dataSource = dataSource { + combinedChildrenDataSource?.addSources([dataSource]) + } updateDatasourceSubscription() } } @@ -201,17 +253,43 @@ public class CollectionViewSection: NSObject { public var cellConfigurationCustomizer : CellConfigurationCustomizer? public var animateDifferences: Bool? //!< If not specified, falls back to collectionViewController.animateDifferences - public var hidden : Bool = false + public var hidden: Bool = false { + didSet { + if hidden != oldValue { + collectionViewController?.updateSource() + } + } + } + + private var _hideIfEmptyDataSourceCondition: DataSourceCondition? + public var hideIfEmptyDataSource: OCDataSource? { + willSet { + _hideIfEmptyDataSourceCondition = nil + } + didSet { + _hideIfEmptyDataSourceCondition = DataSourceCondition(.empty, with: hideIfEmptyDataSource, initial: true, action: { condition in + OnMainThread { [weak self] in + self?.hidden = (condition.fulfilled == true) + } + }) + } + } public var cellLayout: CellLayout - public init(identifier: SectionIdentifier, dataSource inDataSource: OCDataSource?, cellStyle : CollectionViewCellStyle = .init(with:.tableCell), cellLayout: CellLayout = .list(appearance: .plain), clientContext: ClientContext? = nil ) { + public var expandedItemRefs: [CollectionViewController.ItemRef] + private var initialExpandedItems: [OCDataItemReference]? + + public init(identifier: SectionIdentifier, dataSource inDataSource: OCDataSource?, cellStyle : CollectionViewCellStyle = .init(with:.tableCell), cellLayout: CellLayout = .list(appearance: .plain), clientContext: ClientContext? = nil, expandedItems: [OCDataItemReference]? = nil) { self.identifier = identifier _cellStyle = cellStyle self.cellLayout = cellLayout + expandedItemRefs = [] super.init() + initialExpandedItems = expandedItems + self.clientContext = clientContext self.dataSource = inDataSource updateDatasourceSubscription() // dataSource.didSet is not called during initialization @@ -221,17 +299,39 @@ public class CollectionViewSection: NSObject { dataSourceSubscription?.terminate() } + // MARK: - Expand/Collapse + func addExpanded(item: CollectionViewController.ItemRef) { + expandedItemRefs.append(item) + } + + func removeExpanded(item: CollectionViewController.ItemRef) { + if let idx = expandedItemRefs.firstIndex(of: item) { + expandedItemRefs.remove(at: idx) + } + +// if let (itemRef, _) = collectionViewController?.unwrap(item) { +// dataSourcesByParentItemRef[itemRef] = nil +// +// dataSourceSubscriptionsByParentItemRef[itemRef]?.terminate() +// dataSourceSubscriptionsByParentItemRef[itemRef] = nil +// } + } + // MARK: - Data source handling func updateDatasourceSubscription() { if let dataSource = dataSource { dataSourceSubscription = dataSource.subscribe(updateHandler: { [weak self] (subscription) in self?.handleListUpdates(from: subscription) - }, on: .main, trackDifferences: true, performIntialUpdate: true) + }, on: .main, trackDifferences: true, performInitialUpdate: true) } } func handleListUpdates(from subscription: OCDataSourceSubscription) { - collectionViewController?.updateSource(animatingDifferences: animateDifferences ?? (collectionViewController?.animateDifferences ?? true)) + if collectionViewController?.supportsHierarchicContent == true { + handleUpdate(for: subscription, parentItemRef: nil) + } else { + collectionViewController?.updateSource(animatingDifferences: animateDifferences ?? (collectionViewController?.animateDifferences ?? true)) + } } // MARK: - Item provider @@ -249,32 +349,77 @@ public class CollectionViewSection: NSObject { if let wrappedItems = collectionViewController?.wrap(references: datasourceSnapshot.items, forSection: identifier) { snapshot.appendItems(wrappedItems, toSection: identifier) + // Log.debug("Section[\(identifier)] contents: \(wrappedItems.debugDescription)") } if let updatedItems = datasourceSnapshot.updatedItems, updatedItems.count > 0, let wrappedUpdatedItems = collectionViewController?.wrap(references: Array(updatedItems), forSection: identifier) { - snapshot.reloadItems(wrappedUpdatedItems) + snapshot.reconfigureItems(wrappedUpdatedItems) + } + } + } + + func composeSectionSnapshot(from existingSnapshot: NSDiffableDataSourceSectionSnapshot?) -> (NSDiffableDataSourceSectionSnapshot, [CollectionViewController.ItemRef]?) { + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + var wrappedUpdatedItems : [CollectionViewController.ItemRef]? + + if let initialExpandedItems = initialExpandedItems, let collectionViewController = collectionViewController { + self.initialExpandedItems = nil + expandedItemRefs = collectionViewController.wrap(references: initialExpandedItems, forSection: identifier) + } + + if let datasourceSnapshot = dataSourceSubscription?.snapshotResettingChangeTracking(true) { + if let collectionViewController = collectionViewController, let highlightItemReference = collectionViewController.highlightItemReference, collectionViewController.didHighlightItemReference == false { + if datasourceSnapshot.items.contains(highlightItemReference) { + collectionViewController.didHighlightItemReference = true + + OnMainThread(after: 0.1) { + collectionViewController.highlight(itemRef: highlightItemReference, animated: true) + } + } + } + + if let wrappedItems = collectionViewController?.wrap(references: datasourceSnapshot.items, forSection: identifier) { + sectionSnapshot.append(wrappedItems) + + if let collectionView = collectionViewController?.collectionView { + for expandedItemRef in expandedItemRefs { + if sectionSnapshot.contains(expandedItemRef) { + sectionSnapshot.expand([expandedItemRef]) + + let childSnapshot = provideHierarchicContent(for: collectionView, parentItemRef: expandedItemRef, existingSectionSnapshot: nil) + sectionSnapshot.replace(childrenOf: expandedItemRef, using: childSnapshot) + } + } + } + } + + if let updatedItems = datasourceSnapshot.updatedItems, updatedItems.count > 0 { + wrappedUpdatedItems = collectionViewController?.wrap(references: Array(updatedItems), forSection: identifier) } } + + return (sectionSnapshot, wrappedUpdatedItems) } // MARK: - Cell provider - func provideReusableCell(for collectionView: UICollectionView, collectionItemRef: CollectionViewController.ItemRef, indexPath: IndexPath) -> UICollectionViewCell { + func provideReusableCell(for collectionView: UICollectionView, collectionItemRef: CollectionViewController.ItemRef, indexPath: IndexPath) -> UICollectionViewCell? { var cell: UICollectionViewCell? if let collectionViewController = collectionViewController { let (dataItemRef, _) = collectionViewController.unwrap(collectionItemRef) - if let itemRecord = try? dataSource?.record(forItemRef: dataItemRef) { + if let itemRecord = try? contentDataSource?.record(forItemRef: dataItemRef) { var cellProvider = CollectionViewCellProvider.providerFor(itemRecord) if cellProvider == nil { cellProvider = CollectionViewCellProvider.providerFor(.presentable) } - let doHighlight = collectionViewController.highlightItemReference == dataItemRef + let doHighlight = (collectionViewController.highlightItemReference == dataItemRef) // || + // (collectionViewController.selectedItemReferences?.contains(collectionItemRef) == true) // Consider setting cell state (selection, etc.) here - if let cellProvider = cellProvider, let dataSource = dataSource { + if let cellProvider = cellProvider, let dataSource = contentDataSource { let cellConfiguration = CollectionViewCellConfiguration(source: dataSource, core: collectionViewController.clientContext?.core, collectionItemRef: collectionItemRef, record: itemRecord, hostViewController: collectionViewController, style: cellStyle, highlight: doHighlight, clientContext: clientContext) if let cellConfigurationCustomizer = cellConfigurationCustomizer { @@ -284,17 +429,221 @@ public class CollectionViewSection: NSObject { cell = cellProvider.provideCell(for: collectionView, cellConfiguration: cellConfiguration, itemRecord: itemRecord, collectionItemRef: collectionItemRef, indexPath: indexPath) } } + } + + return cell + } + + // MARK: - Hierarchic content provider and child data source tracking + public var combinedChildrenDataSource : OCDataSourceComposition? + private var dataSourcesByParentItemRef : [OCDataItemReference : OCDataSource] = [:] + private var dataSourceSubscriptionsByParentItemRef : [OCDataItemReference : OCDataSourceSubscription] = [:] + + func provideHierarchicContent(for collectionView: UICollectionView, parentItemRef: CollectionViewController.ItemRef, existingSectionSnapshot: NSDiffableDataSourceSectionSnapshot?) -> NSDiffableDataSourceSectionSnapshot { + + if let collectionViewController = collectionViewController, let dataSource = contentDataSource { + let (parentDataItemRef, _) = collectionViewController.unwrap(parentItemRef) + + if let itemRecord = try? dataSource.record(forItemRef: parentDataItemRef) { + if itemRecord.hasChildren { + var childrenDataSource : OCDataSource? = dataSourcesByParentItemRef[parentDataItemRef] + var childrenDataSourceSubscription : OCDataSourceSubscription? + + if childrenDataSource == nil { + childrenDataSource = itemRecord.item?.dataSourceForChildren?(using: dataSource) + + if let childrenDataSource = childrenDataSource { + let subscription = childrenDataSource.subscribe(updateHandler: { [weak self] (subscription) in + // Handle updates + self?.handleUpdate(for: subscription, parentItemRef: parentItemRef) + }, on: .main, trackDifferences: true, performInitialUpdate: false) + + childrenDataSourceSubscription = subscription + + dataSourcesByParentItemRef[parentDataItemRef] = childrenDataSource + dataSourceSubscriptionsByParentItemRef[parentDataItemRef] = childrenDataSourceSubscription + if combinedChildrenDataSource == nil, let rootDataSource = self.dataSource { + combinedChildrenDataSource = OCDataSourceComposition(sources: [rootDataSource, childrenDataSource]) + } else { + combinedChildrenDataSource?.addSources([childrenDataSource]) + } + } + } else { + childrenDataSourceSubscription = dataSourceSubscriptionsByParentItemRef[parentDataItemRef] + } + + if let childrenDataSourceSubscription = childrenDataSourceSubscription { + let childrenDataSourceSnapshot = childrenDataSourceSubscription.snapshotResettingChangeTracking(true) + + var childSnapshot = NSDiffableDataSourceSectionSnapshot() - if cell == nil { - cell = collectionViewController.provideEmptyFallbackCell(for: indexPath, item: collectionItemRef) + let wrappedItems = collectionViewController.wrap(references: childrenDataSourceSnapshot.items, forSection: identifier) + childSnapshot.append(wrappedItems) + + return childSnapshot + } + } } } - return cell ?? UICollectionViewCell.emptyFallbackCell + return existingSectionSnapshot ?? NSDiffableDataSourceSectionSnapshot() + } + + func handleUpdate(for subscription: OCDataSourceSubscription, parentItemRef: CollectionViewController.ItemRef?) { + collectionViewController?.performDataSourceUpdate { updateDone in + self._handleUpdate(for: subscription, parentItemRef: parentItemRef) + updateDone() + } } + func _handleUpdate(for subscription: OCDataSourceSubscription, parentItemRef: CollectionViewController.ItemRef?) { + // Handle updates + if let collectionViewController = collectionViewController, + let collectionViewDataSource = collectionViewController.collectionViewDataSource { + // Determine updated items + let dataSourceSnapshot = subscription.snapshotResettingChangeTracking(true) + let updatedItems = dataSourceSnapshot.updatedItems + let removedItems: Set? = dataSourceSnapshot.removedItems + + // Insert added items (through direct application of changes, not by using sectionSnapshot.replace(childrenOf:using:) - which would loose states) + if let addedItems = dataSourceSnapshot.addedItems, addedItems.count > 0 { + let allItems = dataSourceSnapshot.items + var itemsToAdd = Set(addedItems) + + var sectionSnapshot = collectionViewDataSource.snapshot(for: self.identifier) + + while itemsToAdd.count > 0 { + let lastItemsToAddCount = itemsToAdd.count + + for itemToAdd in addedItems { + if itemsToAdd.contains(itemToAdd) { + if let idx = allItems.firstIndex(of: itemToAdd) { + if let wrappedItemToAdd = collectionViewController.wrap(references: [ itemToAdd ], forSection: self.identifier).first { + if idx == 0 { + // Item at position 0 + if allItems.count > 1 { + // Try to insert item before the item that comes after it + if let wrappedItemAfter = collectionViewController.wrap(references: [allItems[1]], forSection: self.identifier).first, sectionSnapshot.contains(wrappedItemAfter) { + sectionSnapshot.insert([ wrappedItemToAdd ], before: wrappedItemAfter) + itemsToAdd.remove(itemToAdd) + } + } else { + // Only one item. Append as subitem of parent item. + if let parentItemRef, sectionSnapshot.contains(parentItemRef) { // make sure parent item exists + sectionSnapshot.append([wrappedItemToAdd], to: parentItemRef) + } + itemsToAdd.remove(itemToAdd) // remove in any case, because it can't be added later either if the parent item does not exist + } + } else { + // Item not at position 0 + // Try to insert item after the item before it + if let wrappedItemBefore = collectionViewController.wrap(references: [allItems[idx-1]], forSection: self.identifier).first, + sectionSnapshot.contains(wrappedItemBefore) { + sectionSnapshot.insert([ wrappedItemToAdd ], after: wrappedItemBefore) + itemsToAdd.remove(itemToAdd) + } + } + } else { + // itemToAdd can't be wrapped + Log.error("itemToAdd \(itemToAdd) can't be wrapped. This should never happen.") + itemsToAdd.remove(itemToAdd) // avoid infinite loop + } + } else { + // itemToAdd is not part of allItems (?!) + Log.error("itemToAdd \(itemToAdd) not part of datasource, not adding. This should never happen.") + itemsToAdd.remove(itemToAdd) // avoid infinite loop + } + } + } + + if lastItemsToAddCount == itemsToAdd.count { + // Couldn't insert any item, quit loop + Log.warning("Could not insert items \(itemsToAdd). Quitting loop without inserting.") + break + } + } + + collectionViewDataSource.apply(sectionSnapshot, to: self.identifier) + } + + // Compile data source snapshot to notify collection view of updated items + if updatedItems != nil || removedItems != nil { + // Get snapshot of complete source + var snapshot = collectionViewDataSource.snapshot() + + // Tell snapshot that updatedItems were reconfigured + if let updatedItems = updatedItems { + var wrappedUpdatedItems = collectionViewController.wrap(references: Array(updatedItems), forSection: self.identifier) + + if collectionViewController.useWrappedIdentifiers { + wrappedUpdatedItems = wrappedUpdatedItems.filter({ itemRef in + return snapshot.indexOfItem(itemRef) != nil + }) + } + + snapshot.reconfigureItems(wrappedUpdatedItems) + } + + // Tell snapshot that removedItems were removed + if let removedItems = removedItems { + // Find removed items with children and remove them as well (=> crashes otherwise as then child items without parent remain) + var wrappedRemovedItems : [CollectionViewController.ItemRef] = [] + + func addRemovedItems(_ items: [OCDataItemReference]) { + let wrappedItems = collectionViewController.wrap(references: Array(items), forSection: self.identifier) + + if expandedItemRefs.count > 0 { + for wrappedItem in wrappedItems { + if expandedItemRefs.firstIndex(of: wrappedItem) != nil { + let (dataItemRef, _) = collectionViewController.unwrap(wrappedItem) + if let subscription = dataSourceSubscriptionsByParentItemRef[dataItemRef] { + let containedItems = subscription.snapshotResettingChangeTracking(false).items + addRemovedItems(containedItems) + } + } + } + } + + wrappedRemovedItems.append(contentsOf: wrappedItems) + } + + addRemovedItems(Array(removedItems)) + + // wrappedRemovedItems = collectionViewController.wrap(references: Array(removedItems), forSection: self.identifier) + snapshot.deleteItems(wrappedRemovedItems) + } + + // Apply changes and animate them + collectionViewDataSource.apply(snapshot, animatingDifferences: true) + } + + // Notify view controller of content updates + collectionViewController.setContentDidUpdate() + } + } + + // MARK: - Supplementary items + public var boundarySupplementaryItems: [CollectionViewSupplementaryItem]? + // MARK: - Section layout open func provideCollectionLayoutSection(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { - return cellLayout.collectionLayoutSection(for: self.collectionViewController, layoutEnvironment: layoutEnvironment) + let layoutSection = cellLayout.collectionLayoutSection(for: self.collectionViewController, layoutEnvironment: layoutEnvironment) + + if let supplementaryItems = boundarySupplementaryItems?.compactMap({ item in + return item.supplementaryItem as? NSCollectionLayoutBoundarySupplementaryItem + }) { + layoutSection.boundarySupplementaryItems = supplementaryItems + } + + return layoutSection } + + // MARK: - Data Item & Versioning conformance + public let dataItemType: OCDataItemType = .collectionViewSection + + open var dataItemReference: OCDataItemReference { + return identifier as NSObject + } + + public let dataItemVersion: OCDataItemVersion = NSNumber(0) } diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider+StandardImplementations.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider+StandardImplementations.swift new file mode 100644 index 000000000..08a4494de --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider+StandardImplementations.swift @@ -0,0 +1,26 @@ +// +// CollectionViewSupplementaryCellProvider+StandardImplementations.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension CollectionViewSupplementaryCellProvider { + static func registerStandardImplementations() { + TitleSupplementaryCell.registerSupplementaryCellProvider() + ViewSupplementaryCell.registerSupplementaryCellProvider() + } +} diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider.swift new file mode 100644 index 000000000..dbdad7996 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider.swift @@ -0,0 +1,57 @@ +// +// CollectionViewSupplementaryCellProvider.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +open class CollectionViewSupplementaryCellProvider: NSObject { + // MARK: - Types + public typealias CellProvider = (_ collectionView: UICollectionView, _ section: CollectionViewSection?, _ supplementaryItem: CollectionViewSupplementaryItem, _ indexPath: IndexPath) -> UICollectionReusableView + + // MARK: - Global registry + static var cellProviders : [CollectionViewSupplementaryItem.ElementKind:CollectionViewSupplementaryCellProvider] = [:] + + public static func register(_ cellProvider: CollectionViewSupplementaryCellProvider) { + cellProviders[cellProvider.elementKind] = cellProvider + } + + public static func providerFor(_ supplementaryItem: CollectionViewSupplementaryItem) -> CollectionViewSupplementaryCellProvider? { + return cellProviders[supplementaryItem.elementKind] + } + + public static func providerFor(_ itemType: CollectionViewSupplementaryItem.ElementKind) -> CollectionViewSupplementaryCellProvider? { + return cellProviders[itemType] + } + + // MARK: - Implementation + + var provider : CellProvider + var elementKind: CollectionViewSupplementaryItem.ElementKind + + public func provideCell(for collectionView: UICollectionView, section: CollectionViewSection?, supplementaryItem: CollectionViewSupplementaryItem, indexPath: IndexPath) -> UICollectionReusableView { + // Ask provider to provide cell and return it + return provider(collectionView, section, supplementaryItem, indexPath) + } + + public init(for elementKind : CollectionViewSupplementaryItem.ElementKind, with cellProvider: @escaping CellProvider) { + self.provider = cellProvider + self.elementKind = elementKind + + super.init() + } + +} diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryItem.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryItem.swift new file mode 100644 index 000000000..2bbce7c5f --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryItem.swift @@ -0,0 +1,34 @@ +// +// CollectionViewSupplementaryItem.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +open class CollectionViewSupplementaryItem: NSObject { + public typealias ElementKind = String + + open var supplementaryItem: NSCollectionLayoutSupplementaryItem + open var elementKind: ElementKind + + open var content: Any? + + init(supplementaryItem: NSCollectionLayoutSupplementaryItem, elementKind: ElementKind? = nil, content: Any? = nil) { + self.supplementaryItem = supplementaryItem + self.elementKind = elementKind ?? supplementaryItem.elementKind + self.content = content + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Supplementary Cells/TitleSupplementaryCell.swift b/ownCloudAppShared/Client/Collection Views/Supplementary Cells/TitleSupplementaryCell.swift new file mode 100644 index 000000000..d3820bee1 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Supplementary Cells/TitleSupplementaryCell.swift @@ -0,0 +1,98 @@ +// +// TitleSupplementaryCell.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension CollectionViewSupplementaryItem.ElementKind { + static let title = "title" +} + +class TitleSupplementaryCell: UICollectionReusableView, Themeable { + // MARK: - Content + var label: UILabel? + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError() + } + + func configure() { + label = UILabel() + label?.translatesAutoresizingMaskIntoConstraints = false + + label?.setContentHuggingPriority(.required, for: .vertical) + label?.setContentCompressionResistancePriority(.required, for: .vertical) + + if let label { + addSubview(label) + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 15), + label.trailingAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.trailingAnchor, constant: -15), + label.topAnchor.constraint(equalTo: topAnchor, constant: 15), + label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -3) + ]) + } + + Theme.shared.register(client: self, applyImmediately: true) + } + + // MARK: - Themeing + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + label?.applyThemeCollection(collection, itemStyle: .system(textStyle: .title3, weight: .bold)) + backgroundColor = collection.tableBackgroundColor + } + + // MARK: - Prepare for reuse + override func prepareForReuse() { + super.prepareForReuse() + label?.text = "" + } + + // MARK: - Registration + static func registerSupplementaryCellProvider() { + let supplementaryCellRegistration = UICollectionView.SupplementaryRegistration(elementKind: CollectionViewSupplementaryItem.ElementKind.title) { supplementaryView, elementKind, indexPath in + } + + CollectionViewSupplementaryCellProvider.register(CollectionViewSupplementaryCellProvider(for: .title, with: { collectionView, section, supplementaryItem, indexPath in + let cellView = collectionView.dequeueConfiguredReusableSupplementary(using: supplementaryCellRegistration, for: indexPath) + + cellView.label?.text = supplementaryItem.content as? String + + return cellView + })) + } +} + +public extension CollectionViewSupplementaryItem { + static func title(_ title: String, pinned: Bool = false) -> CollectionViewSupplementaryItem { + let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(30)) + let supplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: .title, alignment: .top) + + if pinned { + supplementaryItem.pinToVisibleBounds = true + supplementaryItem.zIndex = 2 + } + + return CollectionViewSupplementaryItem(supplementaryItem: supplementaryItem, content: title) + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Supplementary Cells/ViewSupplementaryCell.swift b/ownCloudAppShared/Client/Collection Views/Supplementary Cells/ViewSupplementaryCell.swift new file mode 100644 index 000000000..9dda54aa5 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Supplementary Cells/ViewSupplementaryCell.swift @@ -0,0 +1,72 @@ +// +// ViewSupplementaryCell.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension CollectionViewSupplementaryItem.ElementKind { + static let view = "view" +} + +class ViewSupplementaryCell: UICollectionReusableView { + // MARK: - Content + var view: UIView? { + willSet { + view?.removeFromSuperview() + } + + didSet { + if let view { + embed(toFillWith: view, insets: .zero, enclosingAnchors: safeAreaAnchorSet) + } + } + } + + // MARK: - Prepare for reuse + override func prepareForReuse() { + super.prepareForReuse() + view = nil + } + + // MARK: - Registration + static func registerSupplementaryCellProvider() { + let viewSupplementaryCellRegistration = UICollectionView.SupplementaryRegistration(elementKind: CollectionViewSupplementaryItem.ElementKind.view) { supplementaryView, elementKind, indexPath in + } + + CollectionViewSupplementaryCellProvider.register(CollectionViewSupplementaryCellProvider(for: .view, with: { collectionView, section, supplementaryItem, indexPath in + let cellView = collectionView.dequeueConfiguredReusableSupplementary(using: viewSupplementaryCellRegistration, for: indexPath) + + cellView.view = supplementaryItem.content as? UIView + + return cellView + })) + } +} + +public extension CollectionViewSupplementaryItem { + static func view(_ view: UIView, pinned: Bool = false) -> CollectionViewSupplementaryItem { + let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(view.frame.size.height)) + let supplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: .view, alignment: .top) + + if pinned { + supplementaryItem.pinToVisibleBounds = true + supplementaryItem.zIndex = 2 + } + + return CollectionViewSupplementaryItem(supplementaryItem: supplementaryItem, content: view) + } +} diff --git a/ownCloudAppShared/Client/Context/ClientContext.swift b/ownCloudAppShared/Client/Context/ClientContext.swift index 4f0d026a5..2abbbdfdc 100644 --- a/ownCloudAppShared/Client/Context/ClientContext.swift +++ b/ownCloudAppShared/Client/Context/ClientContext.swift @@ -54,17 +54,17 @@ public protocol ContextMenuProvider : AnyObject { } public protocol InlineMessageCenter : AnyObject { - func hasInlineMessage(for item: OCItem) -> Bool - func showInlineMessageFor(item: OCItem) + func hasInlineMessage(for item: OCDataItem) -> Bool + func showInlineMessage(for item: OCDataItem) } -//extension ClientContext { -// public enum DropSessionStage : CaseIterable { -// case begin -// case updated -// case end -// } -//} +public protocol ViewControllerPusher: AnyObject { + func pushViewController(context: ClientContext?, provider: (_ context: ClientContext) -> UIViewController?, push: Bool, animated: Bool) -> UIViewController? +} + +public protocol NavigationRevocationHandler: AnyObject { + func handleRevocation(event: NavigationRevocationEvent, context: ClientContext?, for viewController: UIViewController) +} @objc public protocol DropTargetsProvider : AnyObject { func canProvideDropTargets(for dropSession: UIDropSession, target view: UIView) -> Bool @@ -80,31 +80,60 @@ public enum ClientItemInteraction { case trailingSwipe case drag case acceptDrop + + case moreOptions + case search + case addContent +} + +public enum ClientItemAppearance { + case regular + case disabled } public class ClientContext: NSObject { - public typealias PermissionHandler = (_ context: ClientContext?, _ dataItemRecord: OCDataItemRecord?, _ checkInteraction: ClientItemInteraction) -> Bool + public typealias PermissionHandler = (_ context: ClientContext?, _ dataItemRecord: OCDataItemRecord?, _ checkInteraction: ClientItemInteraction, _ inViewController: UIViewController?) -> Bool + + public typealias ItemStyler = (_ context: ClientContext?, _ dataItemRecord: OCDataItemRecord?, _ item: OCDataItem?) -> ClientItemAppearance public weak var parent: ClientContext? + // MARK: - Account Connection + public weak var accountConnection: AccountConnection? + // MARK: - Core - public weak var core: OCCore? + private weak var _core: OCCore? + public weak var core: OCCore? { + get { + return _core ?? parent?.core ?? accountConnection?.core + } + + set { + _core = newValue + } + } // MARK: - Drive public var drive: OCDrive? + public weak var query: OCQuery? + public weak var queryDatasource: OCDataSource? // Data source with the contents of a .query // MARK: - Items public var rootItem : OCDataItem? // MARK: - UI objects + public weak var scene: UIScene? public weak var rootViewController: UIViewController? + public weak var browserController: BrowserNavigationViewController? // Browser navigation controller to push to public weak var navigationController: UINavigationController? // Navigation controller to push to public weak var originatingViewController: UIViewController? // Originating view controller for f.ex. actions public weak var progressSummarizer: ProgressSummarizer? public weak var actionProgressHandlerProvider: ActionProgressHandlerProvider? + public weak var alertQueue: OCAsyncSequentialQueue? + // MARK: - UI item handling public weak var openItemHandler: OpenItemAction? public weak var viewItemHandler: ViewItemAction? @@ -115,12 +144,18 @@ public class ClientContext: NSObject { public weak var inlineMessageCenter: InlineMessageCenter? public weak var dropTargetsProvider: DropTargetsProvider? + // MARK: - UI Handling + public weak var viewControllerPusher: ViewControllerPusher? + public weak var navigationRevocationHandler: NavigationRevocationHandler? + public weak var bookmarkEditingHandler: AccountAuthenticationHandlerBookmarkEditingHandler? + // MARK: - Permissions public var permissionHandlers : [PermissionHandler]? public var permissions : [ClientItemInteraction]? // MARK: - Display options @objc public dynamic var sortDescriptor: SortDescriptor? + public var itemStyler: ItemStyler? /* public var sortMethod : SortMethod? { didSet { @@ -139,23 +174,29 @@ public class ClientContext: NSObject { public typealias PostInitializationModifier = (_ owner: Any?, _ context: ClientContext) -> Void public var postInitializationModifier: PostInitializationModifier? - public init(with inParent: ClientContext? = nil, core inCore: OCCore? = nil, drive inDrive: OCDrive? = nil, rootViewController inRootViewController : UIViewController? = nil, originatingViewController inOriginatingViewController: UIViewController? = nil, navigationController inNavigationController: UINavigationController? = nil, progressSummarizer inProgressSummarizer: ProgressSummarizer? = nil, modifier: ((_ context: ClientContext) -> Void)? = nil) { + public init(with inParent: ClientContext? = nil, accountConnection inAccountConnection: AccountConnection? = nil, core inCore: OCCore? = nil, drive inDrive: OCDrive? = nil, scene inScene: UIScene? = nil, rootViewController inRootViewController : UIViewController? = nil, originatingViewController inOriginatingViewController: UIViewController? = nil, navigationController inNavigationController: UINavigationController? = nil, progressSummarizer inProgressSummarizer: ProgressSummarizer? = nil, alertQueue inAlertQueue: OCAsyncSequentialQueue? = nil, modifier: ((_ context: ClientContext) -> Void)? = nil) { super.init() parent = inParent + accountConnection = inAccountConnection ?? inParent?.accountConnection core = inCore ?? inParent?.core drive = inDrive ?? inParent?.drive query = inParent?.query + queryDatasource = inParent?.queryDatasource + scene = inScene ?? inParent?.scene rootViewController = inRootViewController ?? inParent?.rootViewController + browserController = inParent?.browserController navigationController = inNavigationController ?? inParent?.navigationController originatingViewController = inOriginatingViewController ?? inParent?.originatingViewController progressSummarizer = inProgressSummarizer ?? inParent?.progressSummarizer actionProgressHandlerProvider = inParent?.actionProgressHandlerProvider + alertQueue = inAlertQueue ?? inParent?.alertQueue + openItemHandler = inParent?.openItemHandler viewItemHandler = inParent?.viewItemHandler moreItemHandler = inParent?.moreItemHandler @@ -164,8 +205,11 @@ public class ClientContext: NSObject { swipeActionsProvider = inParent?.swipeActionsProvider inlineMessageCenter = inParent?.inlineMessageCenter dropTargetsProvider = inParent?.dropTargetsProvider + viewControllerPusher = inParent?.viewControllerPusher + navigationRevocationHandler = inParent?.navigationRevocationHandler sortDescriptor = inParent?.sortDescriptor + itemStyler = inParent?.itemStyler permissions = inParent?.permissions permissionHandlers = inParent?.permissionHandlers @@ -237,7 +281,7 @@ public class ClientContext: NSObject { permissionHandlers?.append(permissionHandler) } - public func validate(interaction: ClientItemInteraction, for record: OCDataItemRecord) -> Bool { + public func validate(interaction: ClientItemInteraction, for record: OCDataItemRecord, in viewController: UIViewController? = nil) -> Bool { if let permissions = permissions { if !permissions.contains(interaction) { return false @@ -248,7 +292,7 @@ public class ClientContext: NSObject { var allowed = true for permissionHandler in permissionHandlers { - if !permissionHandler(self, record, interaction) { + if !permissionHandler(self, record, interaction, viewController) { allowed = false break } @@ -260,3 +304,50 @@ public class ClientContext: NSObject { return true } } + +extension ClientContext { + public var canPushViewControllerToNavigation: Bool { + return viewControllerPusher != nil || navigationController != nil + } + + public func pushViewControllerToNavigation(context: ClientContext?, provider: (_ context: ClientContext) -> UIViewController?, push: Bool, animated: Bool) -> UIViewController? { + var viewController: UIViewController? + + if let browserController { + viewController = provider(context ?? self) + + if push, let viewController { + browserController.push(viewController: viewController) + } + + return viewController + } + + if let viewControllerPusher = viewControllerPusher { + viewController = viewControllerPusher.pushViewController(context: context, provider: provider, push: push, animated: animated) + } else if let navigationController = navigationController { + viewController = provider(context ?? self) + + if push, let viewController { + navigationController.pushViewController(viewController, animated: animated) + } + } + + return viewController + } +} + +extension ClientContext { + public var presentationViewController: UIViewController? { + return originatingViewController ?? rootViewController + } + + @discardableResult public func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)? = nil) -> Bool { + if let fromViewController = presentationViewController { + fromViewController.present(viewControllerToPresent, animated: animated, completion: completion) + return true + } + + return false + } +} diff --git a/ownCloudAppShared/Client/Context/SortedItemDataSource.swift b/ownCloudAppShared/Client/Context/SortedItemDataSource.swift new file mode 100644 index 000000000..fd1327dc9 --- /dev/null +++ b/ownCloudAppShared/Client/Context/SortedItemDataSource.swift @@ -0,0 +1,52 @@ +// +// SortedItemDataSource.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 15.12.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +open class SortedItemDataSource: OCDataSourceComposition { + var sortComparatorObserver: NSKeyValueObservation? + + open weak var sortingFollowsContext: ClientContext? { + willSet { + sortComparatorObserver?.invalidate() + sortComparatorObserver = nil + } + + didSet { + sortComparatorObserver = sortingFollowsContext?.observe(\.sortDescriptor, options: .initial, changeHandler: { [weak self] context, change in + if let comparator = context.sortDescriptor?.comparator { + self?.sortComparator = { (source1, ref1, source2, ref2) in + if let record1 = try? source1.record(forItemRef: ref1), + let record2 = try? source2.record(forItemRef: ref2), + let item1 = record1.item as? OCItem, + let item2 = record2.item as? OCItem { + return comparator(item1, item2) + } + + return .orderedDescending + } + } + }) + } + } + + public init(itemDataSource: OCDataSource) { + super.init(sources: [itemDataSource]) + } +} diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCAction+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCAction+Interactions.swift index ccce0928f..0afef2ba9 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCAction+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCAction+Interactions.swift @@ -21,16 +21,35 @@ import ownCloudSDK extension OCAction : DataItemSelectionInteraction { public func handleSelection(in viewController: UIViewController?, with context: ClientContext?, completion: ((Bool) -> Void)?) -> Bool { - run(options: nil, completionHandler: { error in + guard (self as? CollectionSidebarAction) == nil else { + // Use openItem() for CollectionSidebarAction + return false + } + + var options: [OCActionRunOptionKey:Any] = [:] + + if let context { + options[.clientContext] = context + } + + run(options: options, completionHandler: { error in completion?(error == nil) }) return true } + + public func allowSelection(in viewController: UIViewController?, section: CollectionViewSection?, with context: ClientContext?) -> Bool { + return selectable + } } extension OCAction : DataItemDropInteraction { public func allowDropOperation(for session: UIDropSession, with context: ClientContext?) -> UICollectionViewDropProposal? { + if supportsDrop == false { + return nil + } + if session.localDragSession == nil { return nil } @@ -39,7 +58,13 @@ extension OCAction : DataItemDropInteraction { } public func performDropOperation(of items: [UIDragItem], with context: ClientContext?, handlingCompletion: @escaping (Bool) -> Void) { - run(options: nil, completionHandler: { error in + var options: [OCActionRunOptionKey:Any] = [:] + + if let context { + options[.clientContext] = context + } + + run(options: options, completionHandler: { error in handlingCompletion(error == nil) }) } diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCDataItem+InteractionProtocols.swift b/ownCloudAppShared/Client/Data Item Interactions/OCDataItem+InteractionProtocols.swift index 8f6f04e48..6a48dcaa2 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCDataItem+InteractionProtocols.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCDataItem+InteractionProtocols.swift @@ -21,6 +21,9 @@ import ownCloudSDK // MARK: - Selection @objc public protocol DataItemSelectionInteraction: OCDataItem { + // Allow selection + @objc optional func allowSelection(in viewController: UIViewController?, section: CollectionViewSection?, with context: ClientContext?) -> Bool + // Handle selection: suitable for f.ex. actions @objc optional func handleSelection(in viewController: UIViewController?, with context: ClientContext?, completion: ((_ success: Bool) -> Void)?) -> Bool @@ -56,3 +59,9 @@ public struct LocalDataItem { @objc optional func allowDropOperation(for session: UIDropSession, with context: ClientContext?) -> UICollectionViewDropProposal? func performDropOperation(of items: [UIDragItem], with context: ClientContext?, handlingCompletion: @escaping (_ didSucceed: Bool) -> Void) } + +// MARK: - BrowserNavigationBookmark restoration +@objc public protocol DataItemBrowserNavigationBookmarkReStore: OCDataItem { + func store(in bookmarkUUID: UUID?, context: ClientContext?, restoreAction: BrowserNavigationBookmark.BookmarkRestoreAction) -> BrowserNavigationBookmark? + static func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context:ClientContext?, completion: @escaping ((_ error: Error?, _ viewController: UIViewController?) -> Void)) +} diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCDrive+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCDrive+Interactions.swift index e92261aac..3be583a0e 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCDrive+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCDrive+Interactions.swift @@ -20,23 +20,22 @@ import UIKit import ownCloudSDK import ownCloudApp -// MARK: - Selection > Open -extension OCDrive : DataItemSelectionInteraction { - public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { - let driveContext = ClientContext(with: context, modifier: { context in - context.drive = self - }) - let query = OCQuery(for: self.rootLocation) - DisplaySettings.shared.updateQuery(withDisplaySettings: query) +extension OCDrive { + func rootLocation(with context: ClientContext?) -> OCLocation { + let location = self.rootLocation - let rootFolderViewController = ClientItemViewController(context: driveContext, query: query) - - if pushViewController { - viewController?.navigationController?.pushViewController(rootFolderViewController, animated: animated) + if location.bookmarkUUID == nil { + location.bookmarkUUID = context?.core?.bookmark.uuid } - completion?(true) + return location + } +} - return rootFolderViewController +// MARK: - Selection > Open +extension OCDrive: DataItemSelectionInteraction { + public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + let rootLocation = self.rootLocation(with: context) + return rootLocation.openItem(from: viewController, with: context, animated: animated, pushViewController: pushViewController, completion: completion) } } diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift index 31cfe6226..09a192fe0 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift @@ -24,36 +24,40 @@ import UniformTypeIdentifiers // MARK: - Selection > Open extension OCItem : DataItemSelectionInteraction { public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { - if let context = context, let core = context.core { + if let context = context, context.core != nil { let item = self - let activity = OpenItemUserActivity(detailItem: item, detailBookmark: core.bookmark) - viewController?.view.window?.windowScene?.userActivity = activity.openItemUserActivity - switch item.type { case .collection: if let location = item.location { let query = OCQuery(for: location) DisplaySettings.shared.updateQuery(withDisplaySettings: query) - let queryViewController = ClientItemViewController(context: context, query: query) - if pushViewController { - context.navigationController?.pushViewController(queryViewController, animated: animated) - } + if let queryViewController = context.pushViewControllerToNavigation(context: context, provider: { context in + let location = item.location - completion?(true) + if location?.bookmarkUUID == nil { + location?.bookmarkUUID = context.core?.bookmark.uuid + } - return queryViewController + let viewController = ClientItemViewController(context: context, query: query, location: location) + viewController.navigationBookmark = BrowserNavigationBookmark.from(dataItem: self, clientContext: context, restoreAction: .open) + viewController.revoke(in: context, when: [.connectionClosed, .driveRemoved]) + return viewController + }, push: pushViewController, animated: animated) { + completion?(true) + return queryViewController + } } case .file: - if let viewController = context.viewItemHandler?.provideViewer(for: self, context: context) { - if pushViewController { - context.navigationController?.pushViewController(viewController, animated: animated) - } - + if let viewController = context.pushViewControllerToNavigation(context: context, provider: { context in + let viewController = context.viewItemHandler?.provideViewer(for: self, context: context) + viewController?.navigationBookmark = BrowserNavigationBookmark.from(dataItem: self, clientContext: context, restoreAction: .open) + viewController?.revoke(in: context, when: [.connectionClosed, .driveRemoved]) + return viewController + }, push: pushViewController, animated: animated) { completion?(true) - return viewController } } @@ -65,23 +69,20 @@ extension OCItem : DataItemSelectionInteraction { } public func revealItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((_ success: Bool) -> Void)?) -> UIViewController? { - if let context = context, let core = context.core { - let activity = OpenItemUserActivity(detailItem: self, detailBookmark: core.bookmark) - viewController?.view.window?.windowScene?.userActivity = activity.openItemUserActivity - + if let context = context, context.core != nil { if let parentLocation = location?.parent { let query = OCQuery(for: parentLocation) DisplaySettings.shared.updateQuery(withDisplaySettings: query) - let queryViewController = ClientItemViewController(context: context, query: query, highlightItemReference: self.dataItemReference) - - if pushViewController { - context.navigationController?.pushViewController(queryViewController, animated: animated) + if let queryViewController = context.pushViewControllerToNavigation(context: context, provider: { context in + let viewController = ClientItemViewController(context: context, query: query, location: parentLocation, highlightItemReference: self.dataItemReference) + viewController.navigationBookmark = BrowserNavigationBookmark.from(dataItem: parentLocation, clientContext: context, restoreAction: .open) + viewController.revoke(in: context, when: [.connectionClosed, .driveRemoved]) + return viewController + }, push: pushViewController, animated: animated) { + completion?(true) + return queryViewController } - - completion?(true) - - return queryViewController } } @@ -233,7 +234,7 @@ extension OCItem : DataItemDropInteraction { for dragItem in dragItems { if let localDataItem = dragItem.localObject as? LocalDataItem { if let item = localDataItem.dataItem as? OCItem, localDataItem.bookmarkUUID == bookmarkUUID, item.driveID == driveID, let itemLocation = item.location { - if (item.path == path) || (itemLocation.parent.path == path) { + if (item.path == path) || (itemLocation.parent?.path == path) { return UICollectionViewDropProposal(operation: .cancel, intent: .unspecified) } } @@ -332,18 +333,19 @@ extension OCItem : DataItemDropInteraction { useUTI = UTType.data.identifier } + let finalUTI = useUTI ?? UTType.data.identifier var fileName: String? - droppedItem.itemProvider.loadFileRepresentation(forTypeIdentifier: useUTI!) { (itemURL, _ error) in + droppedItem.itemProvider.loadFileRepresentation(forTypeIdentifier: finalUTI) { (itemURL, _ error) in guard let url = itemURL else { return } let fileNameMaxLength = 16 - if useUTI == UTType.utf8PlainText.identifier { + if finalUTI == UTType.utf8PlainText.identifier { fileName = try? String(String(contentsOf: url, encoding: .utf8).prefix(fileNameMaxLength) + ".txt") } - if useUTI == UTType.rtf.identifier { + if finalUTI == UTType.rtf.identifier { let options = [NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.rtf] fileName = try? String(NSAttributedString(url: url, options: options, documentAttributes: nil).string.prefix(fileNameMaxLength) + ".rtf") } @@ -376,3 +378,51 @@ extension OCItem : DataItemDropInteraction { handlingCompletion(allSuccessful) } } + +// MARK: - BrowserNavigationBookmark (re)store +extension OCItem: DataItemBrowserNavigationBookmarkReStore { + public func store(in bookmarkUUID: UUID?, context: ClientContext?, restoreAction: BrowserNavigationBookmark.BookmarkRestoreAction) -> BrowserNavigationBookmark? { + let navigationBookmark = BrowserNavigationBookmark(for: self, in: bookmarkUUID, restoreAction: restoreAction) + var storeLocation = self.location + + // Make sure OCLocation.bookmarkUUID is set + if storeLocation?.bookmarkUUID == nil, let bookmarkUUID, let locationCopy = storeLocation?.copy() as? OCLocation { + locationCopy.bookmarkUUID = bookmarkUUID + storeLocation = locationCopy + } + + navigationBookmark?.location = storeLocation + navigationBookmark?.itemLocalID = localID + + return navigationBookmark + } + + public static func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context: ClientContext?, completion: @escaping ((Error?, UIViewController?) -> Void)) { + if let location = navigationBookmark.location, let context, let core = context.core { + let refKeeper: NSMutableArray = NSMutableArray() + + if let trackItemToken = core.trackItem(at: location, trackingHandler: { (error, item, isInitial) in + if let error { + // An error occured + completion(error, nil) + + // End tracking + refKeeper.removeAllObjects() + } else if let item { + // Item found + OnMainThread { + let viewController = item.openItem(from: viewController, with: context, animated: false, pushViewController: false, completion: nil) + completion(nil, viewController) + } + + // End tracking + refKeeper.removeAllObjects() + } + }) { + refKeeper.add(trackItemToken) + } + } else { + completion(NSError(ocError: .insufficientParameters), nil) + } + } +} diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCItemPolicy+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCItemPolicy+Interactions.swift new file mode 100644 index 000000000..d784139c9 --- /dev/null +++ b/ownCloudAppShared/Client/Data Item Interactions/OCItemPolicy+Interactions.swift @@ -0,0 +1,77 @@ +// +// OCItemPolicy+Interactions.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 15.12.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 OCItemPolicy: DataItemSelectionInteraction { + public func allowSelection(in viewController: UIViewController?, section: CollectionViewSection?, with context: ClientContext?) -> Bool { + return false + } + + public func revealItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + return location?.revealItem(from: viewController, with: context, animated: animated, pushViewController: pushViewController, completion: completion) + } +} + +extension OCItemPolicy: DataItemSwipeInteraction { + func canDelete(in clientContext: ClientContext?) -> Bool { + if clientContext != nil, clientContext?.core != nil { + return true + } + + return false + } + + func delete(in clientContext: ClientContext?) { + if let clientContext, let core = clientContext.core { + core.removeAvailableOfflinePolicy(self, completionHandler: nil) + } + } + + public func provideTrailingSwipeActions(with context: ClientContext?) -> UISwipeActionsConfiguration? { + guard canDelete(in: context) else { + return nil + } + + let deleteAction = UIContextualAction(style: .destructive, title: "Make unavailable offline".localized, handler: { [weak self] (_ action, _ view, _ uiCompletionHandler) in + uiCompletionHandler(false) + self?.delete(in: context) + }) + deleteAction.image = UIImage(named: "cloud-unavailable-offline") + + return UISwipeActionsConfiguration(actions: [ deleteAction ]) + } +} + +extension OCItemPolicy: DataItemContextMenuInteraction { + public func composeContextMenuItems(in viewController: UIViewController?, location: OCExtensionLocationIdentifier, with context: ClientContext?) -> [UIMenuElement]? { + guard canDelete(in: context) else { + return nil + } + + let deleteAction = UIAction(handler: { [weak self] action in + self?.delete(in: context) + }) + deleteAction.title = "Make unavailable offline".localized + deleteAction.image = UIImage(named: "cloud-unavailable-offline") + deleteAction.attributes = .destructive + + return [ deleteAction ] + } +} diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift new file mode 100644 index 000000000..409f231b6 --- /dev/null +++ b/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift @@ -0,0 +1,91 @@ +// +// OCLocation+Interactions.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +// MARK: - Selection > Open +extension OCLocation : DataItemSelectionInteraction { + public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + let driveContext = ClientContext(with: context, modifier: { context in + if let driveID = self.driveID, let core = context.core { + context.drive = core.drive(withIdentifier: driveID) + } + }) + let query = OCQuery(for: self) + DisplaySettings.shared.updateQuery(withDisplaySettings: query) + + let locationViewController = context?.pushViewControllerToNavigation(context: driveContext, provider: { context in + let location = OCLocation(bookmarkUUID: self.bookmarkUUID, driveID: self.driveID, path: self.path) + + if location.bookmarkUUID == nil { + location.bookmarkUUID = driveContext.core?.bookmark.uuid + } + + let viewController = ClientItemViewController(context: context, query: query, location: location) + viewController.navigationBookmark = BrowserNavigationBookmark.from(dataItem: location, clientContext: context, restoreAction: .open) + viewController.revoke(in: context, when: [ .connectionClosed, .driveRemoved ]) + + return viewController + }, push: pushViewController, animated: animated) + + completion?(true) + + return locationViewController + } + + public func revealItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + if let core = context?.core { + if let item = try? core.cachedItem(at: self) { + return item.revealItem(from: viewController, with: context, animated: animated, pushViewController: pushViewController, completion: completion) + } + } + + completion?(true) + + return nil + } +} + +// MARK: - BrowserNavigationBookmark (re)store +extension OCLocation: DataItemBrowserNavigationBookmarkReStore { + public func store(in bookmarkUUID: UUID?, context: ClientContext?, restoreAction: BrowserNavigationBookmark.BookmarkRestoreAction) -> BrowserNavigationBookmark? { + let navigationBookmark = BrowserNavigationBookmark(for: self, in: bookmarkUUID, restoreAction: restoreAction) + var storeLocation = self + + // Make sure OCLocation.bookmarkUUID is set + if storeLocation.bookmarkUUID == nil, let bookmarkUUID, let locationCopy = copy() as? OCLocation { + locationCopy.bookmarkUUID = bookmarkUUID + storeLocation = locationCopy + } + + navigationBookmark?.location = storeLocation + + return navigationBookmark + } + + public static func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context: ClientContext?, completion: ((Error?, UIViewController?) -> Void)) { + if let location = navigationBookmark.location { + let viewController = location.openItem(from: viewController, with: context, animated: false, pushViewController: false, completion: nil) + completion(nil, viewController) + } else { + completion(NSError(ocError: .insufficientParameters), nil) + } + } +} diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift index d474447e6..78e182c4d 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift @@ -20,6 +20,11 @@ import UIKit import ownCloudSDK import ownCloudApp +extension OCSavedSearchUserInfoKey { + static let customIconName = OCSavedSearchUserInfoKey(rawValue: "customIconName") + static let useNameAsTitle = OCSavedSearchUserInfoKey(rawValue: "useNameAsTitle") +} + extension OCSavedSearch { func canDelete(in context: ClientContext?) -> Bool { guard let context = context, let core = context.core, let savedSearches = core.vault.savedSearches else { @@ -69,9 +74,70 @@ extension OCSavedSearch { return composedCondition } + + var customIconName: String? { + set { + if userInfo == nil, let newValue { + userInfo = [.customIconName : newValue] + } else { + userInfo?[.customIconName] = newValue + } + } + + get { + return userInfo?[.customIconName] as? String + } + } + + var useNameAsTitle: Bool? { + set { + if userInfo == nil, let newValue { + userInfo = [.useNameAsTitle : newValue] + } else { + userInfo?[.useNameAsTitle] = newValue + } + } + + get { + return userInfo?[.useNameAsTitle] as? Bool + } + } + + func withCustomIcon(name: String) -> OCSavedSearch { + customIconName = name + return self + } + + func useNameAsTitle(_ useIt: Bool) -> OCSavedSearch { + useNameAsTitle = useIt + return self + } } extension OCSavedSearch: DataItemSelectionInteraction { + func buildViewController(with context: ClientContext) -> ClientItemViewController? { + if let condition = condition() { + let query = OCQuery(condition: condition, inputFilter: nil) + DisplaySettings.shared.updateQuery(withDisplaySettings: query) + + let resultsContext = ClientContext(with: context, modifier: { context in + context.query = query + }) + + let viewController = ClientItemViewController(context: resultsContext, query: query, showRevealButtonForItems: true, emptyItemListIcon: OCSymbol.icon(forSymbolName: "magnifyingglass"), emptyItemListTitleLocalized: "No matches".localized, emptyItemListMessageLocalized: "No items found matching the search criteria.".localized) + if self.useNameAsTitle == true { + viewController.navigationTitle = sideBarDisplayName + } else { + viewController.navigationTitle = sideBarDisplayName + " (" + (isTemplate ? "Search template".localized : "Saved search".localized) + ")" + } + viewController.revoke(in: context, when: .connectionClosed) + viewController.navigationBookmark = BrowserNavigationBookmark.from(dataItem: self, clientContext: context, restoreAction: .handleSelection) + return viewController + } + + return nil + } + public func handleSelection(in viewController: UIViewController?, with context: ClientContext?, completion: ((Bool) -> Void)?) -> Bool { if isTemplate { if let host = viewController as? SearchViewControllerHost { @@ -80,21 +146,15 @@ extension OCSavedSearch: DataItemSelectionInteraction { return true } } else { - if let condition = condition(), let context = context { - let query = OCQuery(condition: condition, inputFilter: nil) - DisplaySettings.shared.updateQuery(withDisplaySettings: query) - - let resultsContext = ClientContext(with: context, modifier: { context in - context.query = query - }) - - let queryViewController = ClientItemViewController(context: resultsContext, query: query) - queryViewController.navigationItem.title = name - - context.navigationController?.pushViewController(queryViewController, animated: true) - - completion?(true) - return true + if let context = context, let viewController = buildViewController(with: context) { + let resultsContext = viewController.clientContext + + if context.pushViewControllerToNavigation(context: resultsContext, provider: { context in + return viewController + }, push: true, animated: true) != nil { + completion?(true) + return true + } } } @@ -113,7 +173,7 @@ extension OCSavedSearch: DataItemSwipeInteraction { uiCompletionHandler(false) self?.delete(in: context) }) - deleteAction.image = UIImage(systemName: "trash")?.withRenderingMode(.alwaysTemplate) + deleteAction.image = OCSymbol.icon(forSymbolName: "trash") return UISwipeActionsConfiguration(actions: [ deleteAction ]) } @@ -129,9 +189,29 @@ extension OCSavedSearch: DataItemContextMenuInteraction { self?.delete(in: context) }) deleteAction.title = "Delete".localized - deleteAction.image = UIImage(systemName: "trash")?.withRenderingMode(.alwaysTemplate) + deleteAction.image = OCSymbol.icon(forSymbolName: "trash") deleteAction.attributes = .destructive return [ deleteAction ] } } + +// MARK: - BrowserNavigationBookmark (re)store +extension OCSavedSearch: DataItemBrowserNavigationBookmarkReStore { + public func store(in bookmarkUUID: UUID?, context: ClientContext?, restoreAction: BrowserNavigationBookmark.BookmarkRestoreAction) -> BrowserNavigationBookmark? { + let navigationBookmark = BrowserNavigationBookmark(for: self, in: bookmarkUUID, restoreAction: restoreAction) + + navigationBookmark?.savedSearch = self + + return navigationBookmark + } + + public static func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context: ClientContext?, completion: ((Error?, UIViewController?) -> Void)) { + if let savedSearch = navigationBookmark.savedSearch, let context { + let viewController = savedSearch.buildViewController(with: context) + completion(nil, viewController) + } else { + completion(NSError(ocError: .insufficientParameters), nil) + } + } +} diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCShare+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCShare+Interactions.swift new file mode 100644 index 000000000..f612ff35b --- /dev/null +++ b/ownCloudAppShared/Client/Data Item Interactions/OCShare+Interactions.swift @@ -0,0 +1,132 @@ +// +// OCShare+Interactions.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 04.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +extension OCShare { + func makeDecision(accept: Bool, context: ClientContext) { + if let core = context.core { + let completionHandler: (Error?) -> Void = { error in + if let error { + OnMainThread { + let alertController = ThemedAlertController(with: (accept ? "Accept Share failed".localized : "Decline Share failed".localized), message: error.localizedDescription, okLabel: "OK".localized, action: nil) + context.present(alertController, animated: true) + } + } + } + + if category == .byMe { + core.delete(self, completionHandler: completionHandler) + } else { + core.makeDecision(on: self, accept: accept, completionHandler: completionHandler) + } + } + } + + func accept(in context: ClientContext) { + makeDecision(accept: true, context: context) + } + + func decline(in context: ClientContext) { + makeDecision(accept: false, context: context) + } +} + +extension OCShare: DataItemSwipeInteraction { + public func provideTrailingSwipeActions(with context: ClientContext?) -> UISwipeActionsConfiguration? { + guard let context else { + return nil + } + + var actions: [UIContextualAction] = [] + + if self.state == .pending || self.state == .accepted || self.category == .byMe { + // Decline / Unshare + let title = (self.category == .byMe) ? "Unshare".localized : "Decline".localized + let action = UIContextualAction(style: .destructive, title: title, handler: { [weak self] (_ action, _ view, _ uiCompletionHandler) in + uiCompletionHandler(false) + self?.decline(in: context) + }) + action.image = OCSymbol.icon(forSymbolName: "minus.circle") + + actions.append(action) + } + + if self.state == .pending || self.state == .declined { + // Accept + let action = UIContextualAction(style: .normal, title: "Accept".localized, handler: { [weak self] (_ action, _ view, _ uiCompletionHandler) in + uiCompletionHandler(false) + self?.accept(in: context) + }) + action.image = OCSymbol.icon(forSymbolName: "checkmark") + + actions.append(action) + } + + return UISwipeActionsConfiguration(actions: actions) + } +} + +extension OCShare: DataItemContextMenuInteraction { + public func composeContextMenuItems(in viewController: UIViewController?, location: OCExtensionLocationIdentifier, with context: ClientContext?) -> [UIMenuElement]? { + guard let context else { + return nil + } + + var elements: [UIMenuElement] = [] + + if self.state == .pending || self.state == .declined { + // Accept + let action = UIAction(handler: { [weak self] action in + self?.accept(in: context) + }) + action.title = "Accept".localized + action.image = OCSymbol.icon(forSymbolName: "checkmark") + + elements.append(action) + } + + if self.state == .pending || self.state == .accepted || self.category == .byMe { + // Decline / Unshare + let action = UIAction(handler: { [weak self] action in + self?.decline(in: context) + }) + let title = (self.category == .byMe) ? "Unshare".localized : "Decline".localized + action.title = title + action.image = OCSymbol.icon(forSymbolName: "minus.circle") + action.attributes = .destructive + + elements.append(action) + } + + return elements + } +} + +extension OCShare: DataItemSelectionInteraction { + public func revealItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + if let item = try? context?.core?.cachedItem(at: itemLocation) { + return item.revealItem(from: viewController, with: context, animated: animated, pushViewController: pushViewController, completion: completion) + } + + completion?(false) + return nil + } +} diff --git a/ownCloudAppShared/Client/Data Source Conditions/DataSourceCondition.swift b/ownCloudAppShared/Client/Data Source Conditions/DataSourceCondition.swift new file mode 100644 index 000000000..d89bcfc02 --- /dev/null +++ b/ownCloudAppShared/Client/Data Source Conditions/DataSourceCondition.swift @@ -0,0 +1,155 @@ +// +// DataSourceCondition.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +open class DataSourceCondition: NSObject { + public enum Condition { + // Item count + case empty + case countEqual(_ itemCount: Int) + case countMinimum(_ itemCount: Int) + case countMaximum(_ itemCount: Int) + + // Logic combination of conditions + case allOf(_ conditions: [DataSourceCondition]) + case anyOf(_ conditions: [DataSourceCondition]) + } + + public typealias Action = (_ condition: DataSourceCondition) -> Void + open weak var parent: DataSourceCondition? + + open var datasource: OCDataSource? + var subscription: OCDataSourceSubscription? + open var condition: Condition { + willSet { + setParentOf(condition: condition, to: nil) + } + + didSet { + setParentOf(condition: condition, to: self) + } + } + + private func setParentOf(condition: Condition, to newParent: DataSourceCondition?) { + switch condition { + case .anyOf(let conditions): + for condition in conditions { + condition.parent = newParent + } + + case .allOf(let conditions): + for condition in conditions { + condition.parent = newParent + } + + default: break + } + } + + var action: Action? + + open var fulfilled: Bool? { + didSet { + if fulfilled != oldValue { + if let parent { + parent.updateResult(fromChild: self) + } + + if let action { + action(self) + } + } + } + } + + public init(_ inCondition: Condition, with inDatasource: OCDataSource? = nil, initial: Bool = false, action inAction: Action? = nil) { + condition = inCondition + + super.init() + + datasource = inDatasource + subscription = datasource?.subscribe(updateHandler: { [weak self] subscription in + self?.updateResult(fromSubscription: subscription) + }, on: .main, trackDifferences: false, performInitialUpdate: true) + + updateResult() + + setParentOf(condition: condition, to: self) + action = inAction + + if initial, let action { + action(self) + } + } + + deinit { + subscription?.terminate() + } + + func updateResult(fromSubscription subscription: OCDataSourceSubscription? = nil, fromChild childCondition: DataSourceCondition? = nil) { + if let subscription { + let snapshot = subscription.snapshotResettingChangeTracking(true) + + switch condition { + case .empty: + fulfilled = snapshot.numberOfItems == 0 + + case .countEqual(let numberOfItems): + fulfilled = snapshot.numberOfItems == numberOfItems + + case .countMinimum(let numberOfItems): + fulfilled = snapshot.numberOfItems >= numberOfItems + + case .countMaximum(let numberOfItems): + fulfilled = snapshot.numberOfItems <= numberOfItems + + default: break + } + } + + switch condition { + case .allOf(let conditions): + var newFulfilled: Bool = true + + for childCondition in conditions { + if childCondition.fulfilled != true { + newFulfilled = false + break + } + } + + fulfilled = newFulfilled + + case .anyOf(let conditions): + var newFulfilled: Bool = false + + for childCondition in conditions { + if childCondition.fulfilled == true { + newFulfilled = true + break + } + } + + fulfilled = newFulfilled + + default: break + } + } +} diff --git a/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift b/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift deleted file mode 100644 index 39ca3835c..000000000 --- a/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift +++ /dev/null @@ -1,383 +0,0 @@ -// -// ClientDirectoryPickerViewController.swift -// ownCloud -// -// Created by Pablo Carrascal on 22/08/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 . -* -*/ - -import UIKit -import ownCloudSDK -import ownCloudApp -import CoreServices - -public typealias ClientDirectoryPickerLocationFilter = (_ location: OCLocation) -> Bool -public typealias ClientDirectoryPickerChoiceHandler = (_ chosenItem: OCItem?, _ needsToDismissViewController: Bool) -> Void - -extension NSErrorDomain { - static let ClientDirectoryPickerErrorDomain = "ClientDirectoryPickerErrorDomain" -} - -open class ClientDirectoryPickerViewController: ClientQueryViewController { - - private let SELECT_BUTTON_HEIGHT: CGFloat = 44.0 - - // MARK: - Instance Properties - open var selectButton: UIBarButtonItem? - private var selectButtonTitle: String? - private var cancelBarButton: UIBarButtonItem? - open var directoryLocation : OCLocation? - - open var choiceHandler: ClientDirectoryPickerChoiceHandler? - open var allowedLocationFilter : ClientDirectoryPickerLocationFilter? - open var navigationLocationFilter : ClientDirectoryPickerLocationFilter? - private var hasFavorites: Bool = false - private var showFavorites: Bool { - if let directoryLocationPath = directoryLocation?.path, directoryLocationPath == "/", hasFavorites == true { - return true - } - return false - } - - let favoriteQuery = OCQuery(condition: .require([ - .where(.isFavorite, isEqualTo: true), - .where(.type, isEqualTo: OCItemType.collection.rawValue) - ]), inputFilter:nil) - - // MARK: - Init & deinit - convenience public init(core inCore: OCCore, location: OCLocation, selectButtonTitle: String, avoidConflictsWith items: [OCItem], choiceHandler: @escaping ClientDirectoryPickerChoiceHandler) { - let folderItemLocations = items.filter({ (item) -> Bool in - return item.type == .collection && item.path != nil && !item.isRoot - }).map { (item) -> OCLocation in - return item.location! - } - let itemParentLocations = items.filter({ (item) -> Bool in - return item.location?.parent != nil - }).map { (item) -> OCLocation in - return item.location!.parent - } - - var navigationPathFilter : ClientDirectoryPickerLocationFilter? - - if folderItemLocations.count > 0 { - navigationPathFilter = { (targetLocation) in - return !folderItemLocations.contains(targetLocation) - } - } - - self.init(core: inCore, location: location, selectButtonTitle: selectButtonTitle, allowedLocationFilter: { (targetLocation) in - // Disallow all paths as target that are parent of any of the items - return !itemParentLocations.contains(targetLocation) - }, navigationLocationFilter: navigationPathFilter, choiceHandler: choiceHandler) - } - - public init(core inCore: OCCore, location: OCLocation, selectButtonTitle: String, allowedLocationFilter: ClientDirectoryPickerLocationFilter? = nil, navigationLocationFilter: ClientDirectoryPickerLocationFilter? = nil, choiceHandler: @escaping ClientDirectoryPickerChoiceHandler) { - let targetDirectoryQuery = OCQuery(for: location) - let drive = (location.driveID != nil) ? inCore.drive(withIdentifier: location.driveID!) : nil - - // Sort folders first - targetDirectoryQuery.sortComparator = { (leftVal, rightVal) in - guard let leftItem = leftVal as? OCItem, let rightItem = rightVal as? OCItem else { - return .orderedSame - } - if leftItem.type == OCItemType.collection && rightItem.type != OCItemType.collection { - return .orderedAscending - } else if leftItem.type != OCItemType.collection && rightItem.type == OCItemType.collection { - return .orderedDescending - } else if leftItem.name != nil && rightItem.name != nil { - return leftItem.name!.caseInsensitiveCompare(rightItem.name!) - } - return .orderedSame - } - - super.init(core: inCore, drive: drive, query: targetDirectoryQuery, rootViewController: nil) - - self.directoryLocation = location - - self.choiceHandler = choiceHandler - - self.selectButtonTitle = selectButtonTitle - self.allowedLocationFilter = allowedLocationFilter - self.navigationLocationFilter = navigationLocationFilter - - // Force disable sorting options - self.shallShowSortBar = true - - // Disable pull to refresh - allowPullToRefresh = false - - isMoreButtonPermanentlyHidden = true - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - ViewController lifecycle - override open func viewDidLoad() { - super.viewDidLoad() - - favoriteQuery.delegate = self - self.core?.start(favoriteQuery) - - // Adapt to disabled pull-to-refresh - self.tableView.alwaysBounceVertical = false - - // Select button creation - selectButton = UIBarButtonItem(title: selectButtonTitle, style: .plain, target: self, action: #selector(selectButtonPressed)) - selectButton?.title = selectButtonTitle - - if let allowedLocationFilter = allowedLocationFilter, let directoryLocation = directoryLocation { - selectButton?.isEnabled = allowedLocationFilter(directoryLocation) - } - - // Cancel button creation - cancelBarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelBarButtonPressed)) - - sortBar?.allowMultiSelect = false - tableView.dragInteractionEnabled = false - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(true) - - if let cancelBarButton = cancelBarButton { - navigationItem.rightBarButtonItems = [cancelBarButton] - } - - if let navController = self.navigationController, let selectButton = selectButton { - navController.isToolbarHidden = false - navController.toolbar.isTranslucent = false - let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - - if let leftButtonImage = Theme.shared.image(for: "folder-create", size: CGSize(width: 30.0, height: 30.0))?.withRenderingMode(.alwaysTemplate) { - let createFolderBarButton = UIBarButtonItem(image: leftButtonImage, style: .plain, target: self, action: #selector(createFolderButtonPressed)) - createFolderBarButton.accessibilityIdentifier = "client.folder-create" - - self.setToolbarItems([createFolderBarButton, flexibleSpaceBarButton, selectButton, flexibleSpaceBarButton], animated: false) - } else { - self.setToolbarItems([flexibleSpaceBarButton, selectButton, flexibleSpaceBarButton], animated: false) - } - } - } - - private func allowNavigationFor(item: OCItem?) -> Bool { - guard let item = item else { return false } - - var allowNavigation = item.type == .collection - - if allowNavigation, let navigationLocationFilter = navigationLocationFilter, let itemLocation = item.location { - allowNavigation = navigationLocationFilter(itemLocation) - } - - return allowNavigation - } - - // MARK: - Table view data source - - override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if showFavorites, indexPath.section == 0 { - return estimatedTableRowHeight - } - - return UITableView.automaticDimension - } - - override open func numberOfSections(in tableView: UITableView) -> Int { - if showFavorites { - return 2 - } - return 1 - } - - override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if showFavorites, section == 0 { - return 1 - } - - return self.items.count - } - - override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if showFavorites, indexPath.section == 0 { - let cellStyle = UITableViewCell.CellStyle.default - let cell = ThemeTableViewCell(withLabelColorUpdates: true, style: cellStyle, reuseIdentifier: nil) - cell.textLabel?.text = "Favorites".localized - cell.imageView?.image = UIImage(named: "star")!.paddedTo(width: 40) - cell.separatorInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0) - - return cell - } - - let cell = super.tableView(tableView, cellForRowAt: indexPath) - - if let clientItemCell = cell as? ClientItemCell { - clientItemCell.isMoreButtonPermanentlyHidden = true - clientItemCell.isActive = self.allowNavigationFor(item: clientItemCell.item) - } - - return cell - } - - override open func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - if showFavorites, indexPath.section == 0 { - return true - } else if let item : OCItem = itemAt(indexPath: indexPath), allowNavigationFor(item: item) { - return true - } - - return false - } - - override open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - if showFavorites, indexPath.section == 0 { - return indexPath - } else if let item : OCItem = itemAt(indexPath: indexPath), allowNavigationFor(item: item) { - return indexPath - } - - return nil - } - - override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if showFavorites, indexPath.section == 0 { - selectFavoriteItem() - } else { - guard let item : OCItem = itemAt(indexPath: indexPath), item.type == OCItemType.collection, let core = self.core, let location = item.location, let selectButtonTitle = selectButtonTitle, let choiceHandler = choiceHandler else { - return - } - - let pickerController = ClientDirectoryPickerViewController(core: core, location: location, selectButtonTitle: selectButtonTitle, allowedLocationFilter: allowedLocationFilter, navigationLocationFilter: navigationLocationFilter, choiceHandler: choiceHandler) - pickerController.cancelAction = cancelAction - pickerController.breadCrumbsPush = self.breadCrumbsPush - - self.navigationController?.pushViewController(pickerController, animated: true) - } - } - - override open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - return nil - } - - override open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - return .none - } - - @available(iOS 13.0, *) - open override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - return nil - } - - // MARK: - Actions - open func userChose(item: OCItem?, needsToDismissViewController: Bool) { - self.choiceHandler?(item, needsToDismissViewController) - } - - private func dismissWithChoice(item: OCItem?) { - if self.presentingViewController != nil { - dismiss(animated: true, completion: { - self.userChose(item: item, needsToDismissViewController: false) - }) - } else { - self.userChose(item: item, needsToDismissViewController: true) - } - } - - open var cancelAction : (() -> Void)? - - @objc private func cancelBarButtonPressed() { - if cancelAction != nil { - cancelAction?() - } else { - dismissWithChoice(item: nil) - } - } - - @objc private func selectButtonPressed() { - dismissWithChoice(item: self.query.rootItem) - } - - @objc open func createFolderButtonPressed(_ sender: UIBarButtonItem) { - // 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, sender: sender) - - let actions = Action.sortedApplicableActions(for: actionContext).filter { (action) -> Bool in - if action.actionExtension.identifier == OCExtensionIdentifier("com.owncloud.action.createFolder") { - return true - } - - return false - } - - let createFolderAction = actions.first - createFolderAction?.progressHandler = makeActionProgressHandler() - createFolderAction?.run() - } - } - - func selectFavoriteItem() { - guard let core = self.core else { - return - } - - let customFileListController = QueryFileListTableViewController(core: core, query: favoriteQuery) - customFileListController.title = "Favorites".localized - customFileListController.isMoreButtonPermanentlyHidden = true - customFileListController.showSelectButton = false - customFileListController.pullToRefreshAction = { [weak self] (completion) in - self?.core?.refreshFavorites(completionHandler: { (_, _) in - completion() - }) - } - if let cancelBarButton = cancelBarButton { - customFileListController.navigationItem.rightBarButtonItems = [cancelBarButton] - } - - customFileListController.didSelectCellAction = { [weak self, customFileListController] (completion) in - guard let favoriteIndexPath = customFileListController.tableView?.indexPathForSelectedRow, let item : OCItem = customFileListController.itemAt(indexPath: favoriteIndexPath), item.type == OCItemType.collection, let core = self?.core, let location = item.location, let selectButtonTitle = self?.selectButtonTitle, let choiceHandler = self?.choiceHandler else { - return - } - - let pickerController = ClientDirectoryPickerViewController(core: core, location: location, selectButtonTitle: selectButtonTitle, allowedLocationFilter: self?.allowedLocationFilter, navigationLocationFilter: self?.navigationLocationFilter, choiceHandler: choiceHandler) - pickerController.cancelAction = self?.cancelAction - - self?.navigationController?.pushViewController(pickerController, animated: true) - } - - self.navigationController?.pushViewController(customFileListController, animated: true) - } - - open override func queryHasChangesAvailable(_ query: OCQuery) { - if query == favoriteQuery { - hasFavorites = (query.queryResults?.count ?? 0 > 0) ? true : false - } else { - super.queryHasChangesAvailable(query) - } - } - - public override func revealViewController(core: OCCore, location: OCLocation, item: OCItem, rootViewController: UIViewController?) -> UIViewController? { - guard let selectButtonTitle = selectButtonTitle, let choiceHandler = choiceHandler else { - return nil - } - - let pickerController = ClientDirectoryPickerViewController(core: core, location: location, selectButtonTitle: selectButtonTitle, allowedLocationFilter: allowedLocationFilter, navigationLocationFilter: navigationLocationFilter, choiceHandler: choiceHandler) - - pickerController.revealItemLocalID = item.localID - pickerController.cancelAction = cancelAction - pickerController.breadCrumbsPush = true - - return pickerController - } -} diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift deleted file mode 100644 index 7e210e1c4..000000000 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ /dev/null @@ -1,965 +0,0 @@ -// -// ClientQueryViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 05.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 -import ownCloudApp -import CoreServices -import UniformTypeIdentifiers - -public typealias ClientActionVieDidAppearHandler = () -> Void -public typealias ClientActionCompletionHandler = (_ actionPerformed: Bool) -> Void - -public struct OCItemDraggingValue { - var item : OCItem - var bookmarkUUID : String -} - -open class ClientQueryViewController: QueryFileListTableViewController, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate { - public var folderActionBarButton: UIBarButtonItem? - public var plusBarButton: UIBarButtonItem? - - public var quotaLabel = UILabel() - public var quotaObservation : NSKeyValueObservation? - public var titleButtonThemeApplierToken : ThemeApplierToken? - - public var breadCrumbsPush : Bool = false - - weak public var clientRootViewController : UIViewController? - - private var _actionProgressHandler : ActionProgressHandler? - public var revealItemLocalID : String? - private var revealItemFound : Bool = false - - private let ItemDataUTI = "com.owncloud.ios-app.item-data" - private let moreCellIdentifier = "moreCell" - private let moreCellAccessibilityIdentifier = "more-results" - public var drive : OCDrive? - - open override var activeQuery : OCQuery { - if let customSearchQuery = customSearchQuery { - return customSearchQuery - } else { - return query - } - } - - var customSearchQuery : OCQuery? { - willSet { - if customSearchQuery != newValue, let customQuery = customSearchQuery { - core?.stop(customQuery) - customQuery.delegate = nil - } - } - - didSet { - if customSearchQuery != nil, let customQuery = customSearchQuery { - customQuery.delegate = self - customQuery.sortComparator = sortMethod.comparator(direction: sortDirection) - core?.start(customQuery) - } - } - } - - public var hasSearchResults : Bool { - return customSearchQuery?.queryResults?.count ?? 0 > 0 - } - - open override var searchScope: SortBarSearchScope { - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "search-scope") - } - - get { - let scope = SortBarSearchScope(rawValue: UserDefaults.standard.integer(forKey: "search-scope")) ?? SortBarSearchScope.local - return scope - } - } - - // MARK: - Init & Deinit - public override convenience init(core inCore: OCCore, query inQuery: OCQuery) { - self.init(core: inCore, drive: nil, query: inQuery, rootViewController: nil) - } - - public init(core inCore: OCCore, drive inDrive: OCDrive?, query inQuery: OCQuery, reveal inItem: OCItem? = nil, rootViewController: UIViewController?) { - clientRootViewController = rootViewController - revealItemLocalID = inItem?.localID - breadCrumbsPush = revealItemLocalID != nil - drive = inDrive - - super.init(core: inCore, query: inQuery) - updateTitleView() - - let lastPathComponent = (query.queryLocation?.path as NSString?)!.lastPathComponent - if lastPathComponent.isRootPath { - quotaObservation = core?.observe(\OCCore.rootQuotaBytesUsed, options: [.initial], changeHandler: { [weak self, weak core] (_, _) in - let quotaUsed = core?.rootQuotaBytesUsed?.int64Value ?? 0 - - OnMainThread { [weak self, weak core] in - var footerText: String? - - if quotaUsed > 0 { - - let byteCounterFormatter = ByteCountFormatter() - byteCounterFormatter.allowsNonnumericFormatting = false - - let quotaUsedFormatted = byteCounterFormatter.string(fromByteCount: quotaUsed) - - // A rootQuotaBytesRemaining value of nil indicates that no quota has been set - if core?.rootQuotaBytesRemaining != nil, let quotaTotal = core?.rootQuotaBytesTotal?.int64Value { - let quotaTotalFormatted = byteCounterFormatter.string(fromByteCount: quotaTotal ) - footerText = String(format: "%@ of %@ used".localized, quotaUsedFormatted, quotaTotalFormatted) - } else { - footerText = String(format: "Total: %@".localized, quotaUsedFormatted) - } - - if let self = self { - if self.items.count == 1 { - footerText = String(format: "%@ item | ".localized, "\(self.items.count)") + (footerText ?? "") - } else if self.items.count > 1 { - footerText = String(format: "%@ items | ".localized, "\(self.items.count)") + (footerText ?? "") - } - } - } - - self?.updateFooter(text: footerText) - } - }) - } - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - customSearchQuery = nil - - queryStateObservation = nil - quotaObservation = nil - - if titleButtonThemeApplierToken != nil { - Theme.shared.remove(applierForToken: titleButtonThemeApplierToken) - titleButtonThemeApplierToken = nil - } - } - - open override func registerCellClasses() { - super.registerCellClasses() - - self.tableView.register(ThemeTableViewCell.self, forCellReuseIdentifier: moreCellIdentifier) - } - - // MARK: - Search events - open override func willPresentSearchController(_ searchController: UISearchController) { - self.sortBar?.showSearchScope = true - self.tableView.setContentOffset(.zero, animated: false) - } - - open override func willDismissSearchController(_ searchController: UISearchController) { - self.sortBar?.showSearchScope = false - } - - // MARK: - Search scope support - private var searchText: String? - private let maxResultCountDefault = 100 // Maximum number of results to return from database (default) - private var maxResultCount = 100 // Maximum number of results to return from database (flexible) - - open override func applySearchFilter(for searchText: String?, to query: OCQuery) { - self.searchText = searchText - - updateCustomSearchQuery() - } - - open override func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SortBarSearchScope) { - updateCustomSearchQuery() - } - - open override func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { - sortMethod = didUpdateSortMethod - - let comparator = sortMethod.comparator(direction: sortDirection) - - query.sortComparator = comparator - customSearchQuery?.sortComparator = comparator - - if (customSearchQuery?.queryResults?.count ?? 0) >= maxResultCount { - updateCustomSearchQuery() - } - } - - private var lastSearchText : String? - private var scrollToTopWithNextRefresh : Bool = false - - public func updateCustomSearchQuery() { - if lastSearchText != searchText { - // Reset max result count when search text changes - maxResultCount = maxResultCountDefault - lastSearchText = searchText - - // Scroll to top when search text changes - scrollToTopWithNextRefresh = true - } - - if let searchText = searchText, - let searchScope = sortBar?.searchScope, - searchScope == .global, - let condition = OCQueryCondition.fromSearchTerm(searchText) { - if let sortPropertyName = sortBar?.sortMethod.sortPropertyName { - condition.sortBy = sortPropertyName - condition.sortAscending = (sortDirection != .ascendant) - } - - condition.maxResultCount = NSNumber(value: maxResultCount) - - self.customSearchQuery = OCQuery(condition:condition, inputFilter: nil) - } else { - self.customSearchQuery = nil - } - - super.applySearchFilter(for: searchText, to: query) - - self.queryHasChangesAvailable(activeQuery) - } - - // MARK: - View controller events - open override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.dragDelegate = self - self.tableView.dropDelegate = self - self.tableView.dragInteractionEnabled = true - - var rightInset : CGFloat = 2 - var leftInset : CGFloat = 0 - if self.view.effectiveUserInterfaceLayoutDirection == .rightToLeft { - rightInset = 0 - leftInset = 2 - } - - folderActionBarButton = UIBarButtonItem(image: UIImage(named: "more-dots")?.withInset(UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)), style: .plain, target: self, action: #selector(moreBarButtonPressed)) - folderActionBarButton?.accessibilityIdentifier = "client.folder-action" - folderActionBarButton?.accessibilityLabel = "Actions".localized - plusBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(plusBarButtonPressed)) - plusBarButton?.accessibilityIdentifier = "client.file-add" - - self.navigationItem.rightBarButtonItems = [folderActionBarButton!, plusBarButton!] - - quotaLabel.textAlignment = .center - quotaLabel.font = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize) - quotaLabel.numberOfLines = 0 - } - - private var viewControllerVisible : Bool = false - - open override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - searchController?.delegate = self - } - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if let multiSelectionSupport = self as? MultiSelectSupport { - multiSelectionSupport.exitMultiselection() - } - } - - private func updateFooter(text:String?) { - let labelText = text ?? "" - - // Resize quota label - self.quotaLabel.text = labelText - self.quotaLabel.sizeToFit() - var frame = self.quotaLabel.frame - // Width is ignored and set by the UITableView when assigning to tableFooterView property - frame.size.height = floor(self.quotaLabel.frame.size.height * 2.0) - quotaLabel.frame = frame - self.tableView.tableFooterView = quotaLabel - } - - // MARK: - Theme support - open override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - super.applyThemeCollection(theme: theme, collection: collection, event: event) - - self.quotaLabel.textColor = collection.tableRowColors.secondaryLabelColor - } - - // MARK: - Table view datasource - open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - var numberOfRows = super.tableView(tableView, numberOfRowsInSection: section) - - if customSearchQuery != nil, numberOfRows >= maxResultCount { - numberOfRows += 1 - } - - return numberOfRows - } - - open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let numberOfRows = super.tableView(tableView, numberOfRowsInSection: 0) - var cell : UITableViewCell? - - if indexPath.row < numberOfRows { - cell = super.tableView(tableView, cellForRowAt: indexPath) - - if revealItemLocalID != nil, let itemCell = cell as? ClientItemCell, let itemLocalID = itemCell.item?.localID { - itemCell.revealHighlight = (itemLocalID == revealItemLocalID) - } - } else { - let moreCell = tableView.dequeueReusableCell(withIdentifier: moreCellIdentifier, for: indexPath) as? ThemeTableViewCell - - moreCell?.accessibilityIdentifier = moreCellAccessibilityIdentifier - moreCell?.textLabel?.text = "Show more results".localized - - cell = moreCell - } - - return cell! - } - - public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let cell = tableView.cellForRow(at: indexPath), cell.accessibilityIdentifier == moreCellAccessibilityIdentifier { - maxResultCount += maxResultCountDefault - updateCustomSearchQuery() - } else { - super.tableView(tableView, didSelectRowAt: indexPath) - } - } - - public override func showReveal(at path: IndexPath) -> Bool { - return (customSearchQuery != nil) - } - - // MARK: - Table view delegate - - open override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - return true - } - - open func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { - 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 - } - - open func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { - - if session.localDragSession != nil { - if let indexPath = destinationIndexPath, items.count - 1 < indexPath.row { - return UITableViewDropProposal(operation: .forbidden) - } - - if let indexPath = destinationIndexPath, items[indexPath.row].type == .file { - return UITableViewDropProposal(operation: .move) - } else { - return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) - } - } else { - return UITableViewDropProposal(operation: .copy) - } - } - - open func updateToolbarItemsForDropping(_ draggingValues: [OCItemDraggingValue]) { - guard let tabBarController = self.tabBarController as? ToolAndTabBarToggling 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 - let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .multiSelection) - let actionContext = ActionContext(viewController: self, core: core, query: query, items: uniqueItems, location: actionsLocation) - self.actions = Action.sortedApplicableActions(for: actionContext) - - // Enable / disable tool-bar items depending on action availability - for item in toolbarItems { - if self.actions?.contains(where: {type(of:$0).identifier == item.actionIdentifier}) ?? false { - item.isEnabled = true - } else { - item.isEnabled = false - } - } - } - } - - // MARK: - UIBarButtonItem Drop Delegate - open func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { - if customSearchQuery != nil { - // No dropping on a smart search toolbar - return false - } - return true - } - - open func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { - return UIDropProposal(operation: .copy) - } - - open func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { - guard let button = interaction.view as? UIButton, let identifier = button.actionIdentifier else { return } - - if let action = self.actions?.first(where: {type(of:$0).identifier == identifier}) { - // Configure progress handler - action.progressHandler = makeActionProgressHandler() - - action.completionHandler = { (_, _) in - } - - // Execute the action - action.perform() - } - } - - open func dragInteraction(_ interaction: UIDragInteraction, - session: UIDragSession, - didEndWith operation: UIDropOperation) { - removeToolbar() - } - - // MARK: - Upload - open func upload(itemURL: URL, name: String, completionHandler: ClientActionCompletionHandler? = nil) { - if let rootItem = query.rootItem, - let progress = core?.importItemNamed(name, at: rootItem, from: itemURL, isSecurityScoped: false, options: nil, placeholderCompletionHandler: nil, resultHandler: { (error, _ core, _ item, _) in - if error != nil { - Log.debug("Error uploading \(Log.mask(name)) file to \(Log.mask(rootItem.path))") - completionHandler?(false) - } else { - Log.debug("Success uploading \(Log.mask(name)) file to \(Log.mask(rootItem.path))") - completionHandler?(true) - } - }) { - self.progressSummarizer?.startTracking(progress: progress) - } - } - - // MARK: - Navigation Bar Actions - - @objc open func plusBarButtonPressed(_ sender: UIBarButtonItem) { - let controller = ThemedAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - // 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, sender: sender) - - let actions = Action.sortedApplicableActions(for: actionContext) - - if actions.count == 0 { - // Handle case of no actions - let alert = ThemedAlertController(title: "No actions available".localized, message: "No actions are available for this folder, possibly because of missing permissions.".localized, preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: "OK".localized, style: .default)) - - self.present(alert, animated: true) - - return - } - - for action in actions { - action.progressHandler = makeActionProgressHandler() - - if let controllerAction = action.provideAlertAction() { - controller.addAction(controllerAction) - } - } - } - - // Cancel button - let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) - controller.addAction(cancelAction) - - if let popoverController = controller.popoverPresentationController { - popoverController.barButtonItem = sender - } - - self.present(controller, animated: true) - } - - @objc open func moreBarButtonPressed(_ sender: UIBarButtonItem) { - guard let core = core, let rootItem = self.query.rootItem else { - return - } - - if let moreItemHandling = self as? MoreItemHandling { - moreItemHandling.moreOptions(for: rootItem, at: .moreFolder, core: core, query: query, sender: sender) - } - } - - // MARK: - Path Bread Crumb Action - @objc open func showPathBreadCrumb(_ sender: UIButton) { - let tableViewController = BreadCrumbTableViewController() - tableViewController.modalPresentationStyle = UIModalPresentationStyle.popover - tableViewController.parentNavigationController = self.navigationController - tableViewController.queryPath = (query.queryLocation?.path as NSString?)! - if let shortName = core?.bookmark.shortName { - tableViewController.bookmarkShortName = shortName - } - if breadCrumbsPush { - tableViewController.navigationHandler = { [weak self] (location) in - if let self = self, let core = self.core { - let queryViewController = ClientQueryViewController(core: core, query: OCQuery(for: location)) - queryViewController.breadCrumbsPush = true - - self.navigationController?.pushViewController(queryViewController, animated: true) - } - } - } - - // On iOS 13.0/13.1, the table view's content needs to be inset by the height of the arrow - // (this can hopefully be removed again in the future, if/when Apple addresses the issue) - let popoverArrowHeight : CGFloat = 13 - - tableViewController.tableView.contentInsetAdjustmentBehavior = .never - tableViewController.tableView.contentInset = UIEdgeInsets(top: popoverArrowHeight, left: 0, bottom: 0, right: 0) - tableViewController.tableView.separatorInset = UIEdgeInsets() - - let popoverPresentationController = tableViewController.popoverPresentationController - popoverPresentationController?.sourceView = sender - popoverPresentationController?.delegate = self - popoverPresentationController?.sourceRect = CGRect(x: 0, y: 0, width: sender.frame.size.width, height: sender.frame.size.height) - - present(tableViewController, animated: true, completion: nil) - } - - // MARK: - ClientItemCell item resolution - open override func item(for cell: ClientItemCell) -> OCItem? { - guard let indexPath = self.tableView.indexPath(for: cell) else { - return nil - } - - return self.itemAt(indexPath: indexPath) - } - - // MARK: - Updates - open override func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { - guard query == activeQuery else { - return - } - - super.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) - - if let revealItemLocalID = revealItemLocalID, !revealItemFound { - var rowIdx : Int = 0 - - for item in items { - if item.localID == revealItemLocalID { - OnMainThread { - self.tableView.scrollToRow(at: IndexPath(row: rowIdx, section: 0), at: .middle, animated: true) - } - revealItemFound = true - break - } - rowIdx += 1 - } - } - - if let rootItem = self.query.rootItem, searchText == nil { - if let queryPath = query.queryLocation?.path, queryPath != "/" { - var totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) - if self.items.count == 1 { - totalSize = String(format: "%@ item | ".localized, "\(self.items.count)") + totalSize - } else if self.items.count > 1 { - totalSize = String(format: "%@ items | ".localized, "\(self.items.count)") + totalSize - } - self.updateFooter(text: totalSize) - } - - if let bookmarkContainer = self.tabBarController as? BookmarkContainer { - // Use parent folder for UI state restoration - let activity = OpenItemUserActivity(detailItem: rootItem, detailBookmark: bookmarkContainer.bookmark) - view.window?.windowScene?.userActivity = activity.openItemUserActivity - } - } else { - self.updateFooter(text: nil) - } - } - - open override func delegatedTableViewDataReload() { - super.delegatedTableViewDataReload() - - if scrollToTopWithNextRefresh { - scrollToTopWithNextRefresh = false - - OnMainThread { - self.tableView.setContentOffset(.zero, animated: false) - } - } - } - - // MARK: - Reloads - open override func restoreSelectionAfterTableReload() { - // Restore previously selected items - guard tableView.isEditing else { return } - - guard selectedItemIds.count > 0 else { return } - - for row in 0.. UIModalPresentationStyle { - return .none - } - - @objc open func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) { - popoverPresentationController.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor - } -} - -// MARK: - Drag & Drop delegates -extension ClientQueryViewController: UITableViewDropDelegate { - public func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { - guard let core = self.core else { return } - - for item in coordinator.items { - if item.dragItem.localObject != nil { - - var destinationItem: OCItem - - guard let itemValues = item.dragItem.localObject as? OCItemDraggingValue, let itemName = itemValues.item.name, let sourceBookmark = OCBookmarkManager.shared.bookmark(forUUIDString: itemValues.bookmarkUUID) else { - return - } - let item = itemValues.item - - if coordinator.proposal.intent == .insertIntoDestinationIndexPath { - - guard let destinationIndexPath = coordinator.destinationIndexPath else { - return - } - - guard items.count >= destinationIndexPath.row else { - return - } - - let rootItem = items[destinationIndexPath.row] - - guard rootItem.type == .collection else { - return - } - - destinationItem = rootItem - - } else { - - guard let rootItem = self.query.rootItem, item.parentFileID != rootItem.fileID else { - return - } - - destinationItem = rootItem - } - - // 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) { (_, _, _, _) in - } - } - }) - } - } - } - } else { - // Import Items from outside - let typeIdentifiers = item.dragItem.itemProvider.registeredTypeIdentifiers - let preferredUTIs = [ - UTType.image, - UTType.movie, - UTType.pdf, - UTType.text, - UTType.rtf, - UTType.html, - UTType.plainText - ] - var useUTI : String? - var useIndex : Int = Int.max - - for typeIdentifier in typeIdentifiers { - if typeIdentifier != ItemDataUTI, !typeIdentifier.hasPrefix("dyn."), let typeIdentifierUTI = UTType(typeIdentifier) { - for preferredUTI in preferredUTIs { - let conforms = typeIdentifierUTI.conforms(to: preferredUTI) - - // Log.log("\(preferredUTI) vs \(typeIdentifier) -> \(conforms)") - - if conforms { - if let utiIndex = preferredUTIs.firstIndex(of: preferredUTI), utiIndex < useIndex { - useUTI = typeIdentifier - useIndex = utiIndex - } - } - } - } - } - - if useUTI == nil, typeIdentifiers.count == 1 { - useUTI = typeIdentifiers.first - } - - if useUTI == nil { - useUTI = UTType.data.identifier - } - - var fileName: String? - - item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: useUTI!) { (url, _ error) in - guard let url = url else { return } - - let fileNameMaxLength = 16 - - if useUTI == UTType.utf8PlainText.identifier { - fileName = try? String(String(contentsOf: url, encoding: .utf8).prefix(fileNameMaxLength) + ".txt") - } - - if useUTI == UTType.rtf.identifier { - let options = [NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.rtf] - fileName = try? String(NSAttributedString(url: url, options: options, documentAttributes: nil).string.prefix(fileNameMaxLength) + ".rtf") - } - - fileName = fileName? - .trimmingCharacters(in: .illegalCharacters) - .trimmingCharacters(in: .whitespaces) - .trimmingCharacters(in: .newlines) - .filter({ $0.isASCII }) - - if fileName == nil { - fileName = url.lastPathComponent - } - - guard let name = fileName else { return } - - self.upload(itemURL: url, name: name) - } - } - } - } -} - -extension ClientQueryViewController: UITableViewDragDelegate { - public func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - - if DisplaySettings.shared.preventDraggingFiles { - return [UIDragItem]() - } - - if !self.tableView.isEditing { - if let multiSelectSupport = self as? MultiSelectSupport { - multiSelectSupport.populateToolbar() - } - } - - 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), 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, 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), let uuid = core?.bookmark.uuid.uuidString { - let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) - selectedItems.append(draggingValue) - - updateToolbarItemsForDropping(selectedItems) - - guard let dragItem = itemForDragging(draggingValue: draggingValue) else { return [] } - return [dragItem] - } - - return [] - } - - public func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { - var selectedItems = [OCItemDraggingValue]() - for dragItem in session.items { - 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), let uuid = core?.bookmark.uuid.uuidString { - let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) - selectedItems.append(draggingValue) - - updateToolbarItemsForDropping(selectedItems) - - guard let dragItem = itemForDragging(draggingValue: draggingValue) else { return [] } - return [dragItem] - } - - return [] - } - - public func tableView(_: UITableView, dragSessionDidEnd: UIDragSession) { - if !self.tableView.isEditing { - removeToolbar() - } - } - - public func itemForDragging(draggingValue : OCItemDraggingValue) -> UIDragItem? { - let item = draggingValue.item - - guard let core = self.core else { - return nil - } - - switch item.type { - case .collection: - guard let data = item.serializedData() else { return nil } - - let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: ItemDataUTI) - let dragItem = UIDragItem(itemProvider: itemProvider) - - dragItem.localObject = draggingValue - - return dragItem - - case .file: - guard let itemMimeType = item.mimeType else { return nil } - guard let itemUTI = UTType(mimeType: itemMimeType)?.identifier else { return nil } - - let itemProvider = NSItemProvider() - - itemProvider.suggestedName = item.name - - itemProvider.registerFileRepresentation(forTypeIdentifier: itemUTI, fileOptions: [], visibility: .all, loadHandler: { [weak core] (completionHandler) -> Progress? in - var progress : Progress? - - guard let core = core else { - completionHandler(nil, false, NSError(domain: OCErrorDomain, code: Int(OCError.internal.rawValue), userInfo: nil)) - return nil - } - - if let localFileURL = core.localCopy(of: item) { - // Provide local copies directly - completionHandler(localFileURL, true, nil) - } else { - // Otherwise download the file and provide it when done - progress = core.downloadItem(item, options: [ - .returnImmediatelyIfOfflineOrUnavailable : true, - .addTemporaryClaimForPurpose : OCCoreClaimPurpose.view.rawValue - ], resultHandler: { [weak self] (error, core, item, file) in - guard error == nil, let fileURL = file?.url else { - completionHandler(nil, false, error) - return - } - - completionHandler(fileURL, true, nil) - - if let claim = file?.claim, let item = item, let self = self { - self.core?.remove(claim, on: item, afterDeallocationOf: [fileURL]) - } - }) - } - - return progress - }) - - itemProvider.registerDataRepresentation(forTypeIdentifier: ItemDataUTI, visibility: .ownProcess) { (completionHandler) -> Progress? in - guard let data = item.serializedData() else { return nil } - completionHandler(data, nil) - - return nil - } - - let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = draggingValue - - return dragItem - } - } -} - -extension ClientQueryViewController { - - @objc public func exitedMultiselection() { - updateTitleView() - } - - open func updateTitleView() { - let lastPathComponent = (query.queryLocation?.path as NSString?)!.lastPathComponent - - if lastPathComponent.isRootPath, let shortName = core?.bookmark.shortName { - if let drive = drive, let driveName = drive.name { - self.navigationItem.title = driveName - } else { - self.navigationItem.title = shortName - } - } else { - if #available(iOS 14.0, *) { - self.navigationItem.backButtonDisplayMode = .generic - let lastPathComponent = (query.queryLocation?.path as NSString?)!.lastPathComponent - self.title = lastPathComponent - } - - let titleButton = UIButton() - titleButton.setTitle(lastPathComponent, for: .normal) - titleButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold) - titleButton.addTarget(self, action: #selector(showPathBreadCrumb(_:)), for: .touchUpInside) - titleButton.sizeToFit() - titleButton.accessibilityLabel = "Show parent paths".localized - titleButton.accessibilityIdentifier = "show-paths-button" - titleButton.semanticContentAttribute = (titleButton.effectiveUserInterfaceLayoutDirection == .leftToRight) ? .forceRightToLeft : .forceLeftToRight - titleButton.setImage(UIImage(named: "chevron-small-light"), for: .normal) - titleButtonThemeApplierToken = Theme.shared.add(applier: { (_, collection, _) in - titleButton.setTitleColor(collection.navigationBarColors.labelColor, for: .normal) - titleButton.tintColor = collection.navigationBarColors.labelColor - }) - self.navigationItem.titleView = titleButton - } - } -} - -// MARK: - UINavigationControllerDelegate -extension ClientQueryViewController: UINavigationControllerDelegate {} diff --git a/ownCloudAppShared/Client/File Lists/ClientSpacesTableViewController.swift b/ownCloudAppShared/Client/File Lists/ClientSpacesTableViewController.swift deleted file mode 100644 index 6ee89b47c..000000000 --- a/ownCloudAppShared/Client/File Lists/ClientSpacesTableViewController.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ClientSpacesTableViewController.swift -// ownCloudAppShared -// -// Created by Felix Schwarz on 03.03.22. -// Copyright © 2022 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2022, 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 - -public class ClientSpacesTableViewController: StaticTableViewController { - public weak var core : OCCore? - public weak var rootViewController: UIViewController? - - public override func viewDidLoad() { - super.viewDidLoad() - - addSection(driveRowsSection) - - updateFromDrives() - } - - var driveListObserver : NSKeyValueObservation? - var driveRowsSection : StaticTableViewSection - - public init(core inCore: OCCore, rootViewController inRootViewController: UIViewController) { - driveRowsSection = StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "drive-rows", rows: []) - - super.init(style: .plain) - - core = inCore - rootViewController = inRootViewController - - self.navigationItem.title = inCore.bookmark.shortName - - driveListObserver = core?.observe(\OCCore.drives, changeHandler: { [weak self] core, change in - OnMainThread { - self?.updateFromDrives() - } - }) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateFromDrives() { - if let drives = core?.drives { - var driveRows : [StaticTableViewRow] = [] - - let sortedDrives = drives.sorted { drive1, drive2 in - let name1 = drive1.name ?? drive1.identifier - let name2 = drive2.name ?? drive2.identifier - - return name1.caseInsensitiveCompare(name2) == .orderedAscending - } - - for drive in sortedDrives { - driveRows.append(StaticTableViewRow(rowWithAction: { [weak self] (staticRow, sender) in - if let core = self?.core, let rootViewController = self?.rootViewController { - let query = OCQuery(for: drive.rootLocation) - let rootFolderViewController = ClientQueryViewController(core: core, drive: drive, query: query, rootViewController: rootViewController) - - self?.navigationController?.pushViewController(rootFolderViewController, animated: true) - } - }, title: drive.name ?? drive.identifier, subtitle: drive.type.rawValue, accessoryType: .disclosureIndicator)) - } - - removeSection(driveRowsSection) - driveRowsSection.rows = driveRows - addSection(driveRowsSection) - } - } -} diff --git a/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift deleted file mode 100644 index 8a611a9db..000000000 --- a/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift +++ /dev/null @@ -1,344 +0,0 @@ -// -// FileListTableViewController.swift -// ownCloud -// -// Created by Matthias Hühne on 21.05.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 - -public protocol OpenItemHandling : AnyObject { - @discardableResult func open(item: OCItem, animated: Bool, pushViewController: Bool) -> UIViewController? -} - -public protocol MoreItemHandling : AnyObject { - @discardableResult func moreOptions(for item: OCItem, at location: OCExtensionLocationIdentifier, core: OCCore, query: OCQuery?, sender: AnyObject?) -> Bool -} - -public protocol RevealItemHandling : AnyObject { - @discardableResult func reveal(item: OCItem, core: OCCore, sender: AnyObject?) -> Bool - func showReveal(at path: IndexPath) -> Bool -} - -open class FileListTableViewController: UITableViewController, ClientItemCellDelegate, Themeable { - open weak var core : OCCore? - - public let estimatedTableRowHeight : CGFloat = 62 - - open var progressSummarizer : ProgressSummarizer? - private var _actionProgressHandler : ActionProgressHandler? - - public init(core inCore: OCCore, style: UITableView.Style = .plain) { - core = inCore - super.init(style: style) - - progressSummarizer = ProgressSummarizer.shared(forCore: inCore) - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - Theme.shared.unregister(client: self) - } - - open func makeActionProgressHandler() -> ActionProgressHandler { - if _actionProgressHandler == nil { - _actionProgressHandler = { [weak self] (progress, publish) in - if publish { - self?.progressSummarizer?.startTracking(progress: progress) - } else { - self?.progressSummarizer?.stopTracking(progress: progress) - } - } - } - - return _actionProgressHandler! - } - - // MARK: - Item retrieval - open func item(for cell: ClientItemCell) -> OCItem? { - return cell.item - } - - open func itemAt(indexPath : IndexPath) -> OCItem? { - return (self.tableView.cellForRow(at: indexPath) as? ClientItemCell)?.item - } - - // MARK: - ClientItemCellDelegate - open func moreButtonTapped(cell: ClientItemCell) { - guard let item = self.item(for: cell), let core = core, let query = query(forItem: item) else { - return - } - - if let moreItemHandling = self as? MoreItemHandling { - moreItemHandling.moreOptions(for: item, at: .moreItem, core: core, query: query, sender: cell) - } - } - - open func revealButtonTapped(cell: ClientItemCell) { - guard let item = cell.item, let core = core else { - return - } - - if let revealItemHandling = self as? RevealItemHandling { - revealItemHandling.reveal(item: item, core: core, sender: cell) - } - } - - // MARK: - Inline message support - open func hasMessage(for item: OCItem) -> Bool { - if let inlineMessageSupport = self as? InlineMessageCenter { - return inlineMessageSupport.hasInlineMessage(for: item) - } - - return false - } - - open func messageButtonTapped(cell: ClientItemCell) { - if let item = cell.item { - if let inlineMessageSupport = self as? InlineMessageCenter { - inlineMessageSupport.showInlineMessageFor(item: item) - } - } - } - - // MARK: - Visibility handling - private var viewControllerVisible : Bool = false - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - viewControllerVisible = false - } - - open override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - viewControllerVisible = true - self.reloadTableData(ifNeeded: true) - } - - // MARK: - View setup - open override func viewDidLoad() { - super.viewDidLoad() - - self.navigationController?.navigationBar.prefersLargeTitles = false - Theme.shared.register(client: self, applyImmediately: true) - self.tableView.estimatedRowHeight = estimatedTableRowHeight - - self.registerCellClasses() - - if allowPullToRefresh { - pullToRefreshControl = UIRefreshControl() - pullToRefreshControl?.tintColor = Theme.shared.activeCollection.navigationBarColors.labelColor - pullToRefreshControl?.backgroundColor = Theme.shared.activeCollection.navigationBarColors.backgroundColor - pullToRefreshControl?.addTarget(self, action: #selector(self.pullToRefreshTriggered), for: .valueChanged) - self.tableView.insertSubview(pullToRefreshControl!, at: 0) - tableView.contentOffset = CGPoint(x: 0, y: self.pullToRefreshVerticalOffset) - tableView.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) - } - - self.addThemableBackgroundView() - } - - open func registerCellClasses() { - self.tableView.register(ClientItemCell.self, forCellReuseIdentifier: "itemCell") - } - - // MARK: - Pull-to-refresh handling - open var allowPullToRefresh : Bool = false - - open var pullToRefreshControl: UIRefreshControl? - open var pullToRefreshAction: ((_ completion: @escaping () -> Void) -> Void)? - - open var pullToRefreshVerticalOffset : CGFloat { - return 0 - } - - @objc open func pullToRefreshTriggered() { - if core?.connectionStatus == OCCoreConnectionStatus.online { - UIImpactFeedbackGenerator().impactOccurred() - performPullToRefreshAction() - } else { - pullToRefreshEnded() - } - } - - open func performPullToRefreshAction() { - if pullToRefreshAction != nil { - pullToRefreshBegan() - - pullToRefreshAction?({ [weak self] in - self?.pullToRefreshEnded() - }) - } - } - - open func pullToRefreshBegan() { - if let refreshControl = pullToRefreshControl { - OnMainThread { - if refreshControl.isRefreshing { - refreshControl.beginRefreshing() - } - } - } - } - - open func pullToRefreshEnded() { - if let refreshControl = pullToRefreshControl { - OnMainThread { - if refreshControl.isRefreshing == true { - refreshControl.endRefreshing() - } - } - } - } - - // MARK: - Reload Data - private var tableReloadNeeded = false - - open func reloadTableData(ifNeeded: Bool = false) { - /* - This is a workaround to cope with the fact that: - - UITableView.reloadData() does nothing if the view controller is not currently visible (via viewWillDisappear/viewWillAppear), so cells may hold references to outdated OCItems - - OCQuery may signal updates at any time, including when the view controller is not currently visible - - This workaround effectively makes sure reloadData() is called in viewWillAppear if a reload has been signalled to the tableView while it wasn't visible. - */ - if !viewControllerVisible { - tableReloadNeeded = true - } - - if !ifNeeded || (ifNeeded && tableReloadNeeded) { - self.delegatedTableViewDataReload() - - if viewControllerVisible { - tableReloadNeeded = false - } - - self.restoreSelectionAfterTableReload() - } - } - - open func delegatedTableViewDataReload() { - self.tableView.reloadData() - } - - open func restoreSelectionAfterTableReload() { - } - - // MARK: - Single item query creation - open func query(forItem: OCItem) -> OCQuery? { - if let location = forItem.location { - return OCQuery(for: location) - } - - return nil - } - - // MARK: - Table view data source - open override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - // MARK: - Table view delegate - open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - if !self.tableView.isEditing { - guard let rowItem : OCItem = itemAt(indexPath: indexPath) else { - return - } - - if let openItemHandler = self as? OpenItemHandling { - openItemHandler.open(item: rowItem, animated: true, pushViewController: true) - } - } - } - - open override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - 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, sender: cell) - let actions = Action.sortedApplicableActions(for: actionContext) - actions.forEach({ - $0.progressHandler = makeActionProgressHandler() - }) - - let contextualActions = actions.compactMap({$0.provideContextualAction()}) - let configuration = UISwipeActionsConfiguration(actions: contextualActions) - return configuration - } - - @available(iOS 13.0, *) - open override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - - guard let core = self.core, let item : OCItem = itemAt(indexPath: indexPath), let cell = tableView.cellForRow(at: indexPath) else { - return nil - } - - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in - self.removeToolbar() - return self.makeContextMenu(for: indexPath, core: core, item: item, with: cell) - }) - } - - @available(iOS 13.0, *) - open func makeContextMenu(for indexPath: IndexPath, core: OCCore, item: OCItem, with cell: UITableViewCell) -> UIMenu { - - let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .contextMenuItem) - let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation, sender: cell) - let actions = Action.sortedApplicableActions(for: actionContext) - actions.forEach({ - $0.progressHandler = makeActionProgressHandler() - }) - - let menuItems = actions.compactMap({$0.provideUIMenuAction()}) - let mainMenu = UIMenu(title: "", identifier: UIMenu.Identifier("context"), options: .displayInline, children: menuItems) - - if core.connectionStatus == .online, core.connection.capabilities?.sharingAPIEnabled == 1 { - // Share Items - let sharingActionsLocation = OCExtensionLocation(ofType: .action, identifier: .contextMenuSharingItem) - let sharingActionContext = ActionContext(viewController: self, core: core, items: [item], location: sharingActionsLocation, sender: cell) - let sharingActions = Action.sortedApplicableActions(for: sharingActionContext) - sharingActions.forEach({ - $0.progressHandler = makeActionProgressHandler() - }) - - let sharingItems = sharingActions.compactMap({$0.provideUIMenuAction()}) - let shareMenu = UIMenu(title: "", identifier: UIMenu.Identifier("sharing"), options: .displayInline, children: sharingItems) - - return UIMenu(title: "", children: [shareMenu, mainMenu]) - } - - return UIMenu(title: "", children: [mainMenu]) - } - - // MARK: - Themable - open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.tableView.applyThemeCollection(collection) - pullToRefreshControl?.tintColor = collection.navigationBarColors.labelColor - pullToRefreshControl?.backgroundColor = Theme.shared.activeCollection.navigationBarColors.backgroundColor - - if event == .update { - self.reloadTableData() - } - } -} diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift deleted file mode 100644 index 1a4787e10..000000000 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ /dev/null @@ -1,592 +0,0 @@ -// -// QueryFileListTableViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 23.05.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 ownCloudApp - -public extension OCQueryState { - var isFinal: Bool { - switch self { - case .idle, .targetRemoved, .contentsFromCache, .stopped: - return true - default: - return false - } - } -} - -public protocol MultiSelectSupport { - func setupMultiselection() - func enterMultiselection() - func exitMultiselection() - func updateMultiselection() - func populateToolbar() -} - -open class QueryFileListTableViewController: FileListTableViewController, SortBarDelegate, RevealItemHandling, OCQueryDelegate, UISearchResultsUpdating, UISearchControllerDelegate { - - public var query : OCQuery - open var activeQuery : OCQuery { - return query - } - - public var queryRefreshRateLimiter : OCRateLimiter = OCRateLimiter(minimumTime: 0.2) - - public var messageView : MessageView? - - public var items : [OCItem] = [] - - public var selectedItemIds = [OCLocalID]() - - public var actionContext: ActionContext? - public var actions : [Action]? - - public var selectDeselectAllButtonItem: UIBarButtonItem? - public var exitMultipleSelectionBarButtonItem: UIBarButtonItem? - - public var regularLeftBarButtons : [UIBarButtonItem]? - public var regularRightBarButtons : [UIBarButtonItem]? - - public let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - public var deleteMultipleBarButtonItem: UIBarButtonItem? - public var moveMultipleBarButtonItem: UIBarButtonItem? - public var duplicateMultipleBarButtonItem: UIBarButtonItem? - public var copyMultipleBarButtonItem: UIBarButtonItem? - public var cutMultipleBarButtonItem: UIBarButtonItem? - public var openMultipleBarButtonItem: UIBarButtonItem? - public var isMoreButtonPermanentlyHidden: Bool = false - public var didSelectCellAction: ((_ completion: @escaping () -> Void) -> Void)? - public var showSelectButton: Bool = true - - public init(core inCore: OCCore, query inQuery: OCQuery) { - query = inQuery - - super.init(core: inCore) - - allowPullToRefresh = true - - NotificationCenter.default.addObserver(self, selector: #selector(QueryFileListTableViewController.displaySettingsChanged), name: .DisplaySettingsChanged, object: nil) - self.displaySettingsChanged() - - query.delegate = self - - if query.sortComparator == nil { - query.sortComparator = self.sortMethod.comparator(direction: sortDirection) - } - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - NotificationCenter.default.removeObserver(self, name: .DisplaySettingsChanged, object: nil) - } - - // MARK: - Display settings - @objc func displaySettingsChanged() { - query.sortComparator = sortMethod.comparator(direction: sortDirection) - DisplaySettings.shared.updateQuery(withDisplaySettings: query) - } - - // MARK: - Sorting - open var sortBar: SortBar? - open var sortMethod: SortMethod { - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-method") - } - - get { - let sort = SortMethod(rawValue: UserDefaults.standard.integer(forKey: "sort-method")) ?? SortMethod.alphabetically - return sort - } - } - open var searchScope: SortBarSearchScope = .local { - didSet { - updateSearchPlaceholder() - } - } - open var sortDirection: SortDirection { - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-direction") - } - - get { - let direction = SortDirection(rawValue: UserDefaults.standard.integer(forKey: "sort-direction")) ?? SortDirection.ascendant - return direction - } - } - - // MARK: - Search - open var searchController: UISearchController? - - // MARK: - Search: UISearchResultsUpdating Delegate - open func updateSearchResults(for searchController: UISearchController) { - let searchText = searchController.searchBar.text ?? "" - - applySearchFilter(for: (searchText == "") ? nil : searchText, to: query) - } - - open func willPresentSearchController(_ searchController: UISearchController) { - self.sortBar?.showSelectButton = false - } - - open func willDismissSearchController(_ searchController: UISearchController) { - self.sortBar?.showSelectButton = true - } - - open func applySearchFilter(for searchText: String?, to query: OCQuery) { - if let searchText = searchText { - let queryCondition = OCQueryCondition.fromSearchTerm(searchText) - let filterHandler: OCQueryFilterHandler = { (_, _, item) -> Bool in - if let item = item, let queryCondition = queryCondition { - return queryCondition.fulfilled(by: item) - } - return false - } - - if let filter = query.filter(withIdentifier: "text-search") { - query.updateFilter(filter, applyChanges: { filterToChange in - (filterToChange as? OCQueryFilter)?.filterHandler = filterHandler - }) - } else { - query.addFilter(OCQueryFilter.init(handler: filterHandler), withIdentifier: "text-search") - } - } else { - if let filter = query.filter(withIdentifier: "text-search") { - query.removeFilter(filter) - } - } - } - - // MARK: - Query progress reporting - open var showQueryProgress : Bool = true - - open var queryProgressSummary : ProgressSummary? { - willSet { - if newValue != nil, showQueryProgress { - progressSummarizer?.pushFallbackSummary(summary: newValue!) - } - } - - didSet { - if oldValue != nil, showQueryProgress { - progressSummarizer?.popFallbackSummary(summary: oldValue!) - } - } - } - - open var queryStateObservation : NSKeyValueObservation? - - // MARK: - Pull-to-refresh handling - override open var pullToRefreshVerticalOffset: CGFloat { - return searchController?.searchBar.frame.height ?? 0 - } - - override open func performPullToRefreshAction() { - super.performPullToRefreshAction() - core?.reload(activeQuery) - } - - open func updateQueryProgressSummary() { - let summary : ProgressSummary = ProgressSummary(indeterminate: true, progress: 1.0, message: nil, progressCount: 1) - - switch query.state { - case .stopped: - summary.message = "Stopped".localized - - case .started: - summary.message = "Started…".localized - - case .contentsFromCache: - summary.message = "Contents from cache.".localized - - case .waitingForServerReply: - summary.message = "Waiting for server response…".localized - - case .targetRemoved: - summary.message = "This folder no longer exists.".localized - - case .idle: - summary.message = "Everything up-to-date.".localized - summary.progressCount = 0 - - default: - summary.message = "Please wait…".localized - } - - Log.debug("Query(p=\(unsafeBitCast(query, to: Int.self)), custom=\(query.isCustom)) status=\(summary.message ?? "?")") - - if pullToRefreshControl != nil { - if query.state == .idle { - self.pullToRefreshBegan() - } else if query.state.isFinal { - self.pullToRefreshEnded() - } - } - - self.queryProgressSummary = summary - } - - // MARK: - SortBarDelegate - open var shallShowSortBar = true - - open func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { - sortMethod = didUpdateSortMethod - query.sortComparator = sortMethod.comparator(direction: sortDirection) - } - - open func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { - self.present(presentViewController, animated: animated, completion: completionHandler) - } - - // MARK: - Query Delegate - open func query(_ query: OCQuery, failedWithError error: Error) { - // Not applicable atm - } - - open func queryHasChangesAvailable(_ query: OCQuery) { - if query == activeQuery { - queryRefreshRateLimiter.runRateLimitedBlock { - query.requestChangeSet(withFlags: .onlyResults) { (query, changeSet) in - OnMainThread { - self.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) - } - } - } - } - } - - open func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { - guard query == activeQuery else { - return - } - - if query.state.isFinal { - OnMainThread { - if self.pullToRefreshControl?.isRefreshing == true { - self.pullToRefreshControl?.endRefreshing() - } - } - } - - let previousItemCount = self.items.count - - self.items = changeSet?.queryResult ?? [] - - // Setup new action context - if let core = self.core { - let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .multiSelection) - self.actionContext = ActionContext(viewController: self, core: core, query: query, items: [OCItem](), location: actionsLocation) - } - - switch query.state { - case .contentsFromCache, .idle, .waitingForServerReply: - if previousItemCount == 0, self.items.count == 0, query.state == .waitingForServerReply { - break - } - - if self.items.count == 0 { - if self.searchController?.searchBar.text != "" { - self.messageView?.message(show: true, with: UIEdgeInsets(top: sortBar?.frame.size.height ?? 0, left: 0, bottom: 0, right: 0), imageName: "icon-search", title: "No matches".localized, message: "There is no results for this search".localized) - } else { - self.messageView?.message(show: true, imageName: "folder", title: "Empty folder".localized, message: "This folder contains no files or folders.".localized) - } - } else { - self.messageView?.message(show: false) - } - - self.reloadTableData() - - case .targetRemoved: - self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) - self.reloadTableData() - - default: - self.messageView?.message(show: false) - } - } - - // MARK: - Themeable - open override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - super.applyThemeCollection(theme: theme, collection: collection, event: event) - - self.searchController?.searchBar.applyThemeCollection(collection) - tableView.sectionIndexColor = collection.tintColor - } - - // MARK: - Events - open override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.allowsMultipleSelectionDuringEditing = true - - searchController = UISearchController(searchResultsController: nil) - searchController?.searchResultsUpdater = self - searchController?.obscuresBackgroundDuringPresentation = false - searchController?.hidesNavigationBarDuringPresentation = true - searchController?.searchBar.applyThemeCollection(Theme.shared.activeCollection) - searchController?.delegate = self - - navigationItem.searchController = searchController - navigationItem.hidesSearchBarWhenScrolling = false - - self.definesPresentationContext = true - - if shallShowSortBar { - sortBar = SortBar(frame: CGRect(x: 0, y: 0, width: self.tableView.frame.width, height: 40), sortMethod: sortMethod) - sortBar?.delegate = self - sortBar?.sortMethod = self.sortMethod - sortBar?.searchScope = self.searchScope - sortBar?.showSelectButton = showSelectButton - - tableView.tableHeaderView = sortBar - } - - messageView = MessageView(add: self.view) - - if let multiSelectSupport = self as? MultiSelectSupport { - multiSelectSupport.setupMultiselection() - } - } - - open override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - updateSearchPlaceholder() - } - - func updateSearchPlaceholder() { - // Needs to be done here, because of an iOS 13 bug. Do not move to viewDidLoad! - let placeholderString = (searchScope == .global) ? "Search account".localized : "Search folder".localized - - let attributedStringColor = [NSAttributedString.Key.foregroundColor : Theme.shared.activeCollection.searchBarColors.secondaryLabelColor] - let attributedString = NSAttributedString(string: placeholderString, attributes: attributedStringColor) - searchController?.searchBar.searchTextField.attributedPlaceholder = attributedString - searchController?.searchBar.searchTextField.textColor = Theme.shared.activeCollection.searchBarColors.labelColor - } - - open override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - Log.debug("Query(p=\(unsafeBitCast(query, to: Int.self)), start/viewWillAppear") - - core?.start(query) - - queryStateObservation = query.observe(\OCQuery.state, options: .initial, changeHandler: { [weak self] (_, _) in - self?.updateQueryProgressSummary() - }) - - updateQueryProgressSummary() - } - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - Log.debug("Query(p=\(unsafeBitCast(query, to: Int.self)), stop/viewWillDisappear") - - queryStateObservation?.invalidate() - queryStateObservation = nil - - core?.stop(query) - - queryProgressSummary = nil - } - - // MARK: - Item retrieval - open override func itemAt(indexPath : IndexPath) -> OCItem? { - return items[indexPath.row] - } - - // MARK: - Single item query creation - open override func query(forItem: OCItem) -> OCQuery? { - return query - } - - // MARK: - Table view data source - open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.items.count - } - - open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as? ClientItemCell - if let newItem = itemAt(indexPath: indexPath) { - - cell?.accessibilityIdentifier = newItem.name - cell?.core = self.core - - if cell?.delegate == nil { - cell?.delegate = self - } - - cell?.showRevealButton = self.showReveal(at: indexPath) - - // UITableView can call this method several times for the same cell, and .dequeueReusableCell will then return the same cell again. - // Make sure we don't request the thumbnail multiple times in that case. - if newItem.displaysDifferent(than: cell?.item, in: core) { - cell?.item = newItem - } - - if let localID = newItem.localID as OCLocalID?, self.selectedItemIds.contains(localID) { - cell?.setSelected(true, animated: false) - } - - if isMoreButtonPermanentlyHidden { - cell?.isMoreButtonPermanentlyHidden = true - } - } - - return cell! - } - - public func revealViewController(core: OCCore, location: OCLocation, item: OCItem, rootViewController: UIViewController?) -> UIViewController? { - let drive = (location.driveID != nil) ? core.drive(withIdentifier: location.driveID!) : nil - return ClientQueryViewController(core: core, drive: drive, query: OCQuery(for: location), reveal: item, rootViewController: nil) - } - - public func reveal(item: OCItem, core: OCCore, sender: AnyObject?) -> Bool { - if let parentLocation = item.location?.parent, - let revealQueryViewController = revealViewController(core: core, location: parentLocation, item: item, rootViewController: nil) { - - self.navigationController?.pushViewController(revealQueryViewController, animated: true) - - return true - } - return false - } - - public func showReveal(at path: IndexPath) -> Bool { - return showRevealButtons - } - - public var showRevealButtons : Bool = false { - didSet { - if oldValue != showRevealButtons { - self.reloadTableData(ifNeeded: true) - } - } - } - - // MARK: - Table view delegate - - open override func sectionIndexTitles(for tableView: UITableView) -> [String]? { - if sortMethod == .alphabetically { - var indexTitles = Array( Set( self.items.map { String(( $0.name?.first!.uppercased())!) })).sorted() - if sortDirection == .descendant { - indexTitles.reverse() - } - if Int(tableView.estimatedRowHeight) * self.items.count > Int(tableView.visibleSize.height), indexTitles.count > 1 { - return indexTitles - } - } - - return [] - } - - override open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { - let firstItem = self.items.filter { (( $0.name?.uppercased().hasPrefix(title) ?? nil)! ) }.first - - if let firstItem = firstItem, let itemIndex = self.items.firstIndex(of: firstItem) { - OnMainThread { - let section = tableView.numberOfSections - 1 // in directory picker there could be more than one section, if favorites exists - tableView.scrollToRow(at: IndexPath(row: itemIndex, section: section), at: UITableView.ScrollPosition.top, animated: false) - } - } - - return 0 - } - - open func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SortBarSearchScope) { - } - - open func toggleSelectMode() { - if let multiSelectionSupport = self as? MultiSelectSupport { - if !tableView.isEditing { - multiSelectionSupport.enterMultiselection() - } else { - multiSelectionSupport.exitMultiselection() - } - } - } - - open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // If not in multiple-selection mode, just navigate to the file or folder (collection) - if !self.tableView.isEditing { - if let item = itemAt(indexPath: indexPath), item.type != .collection, isMoreButtonPermanentlyHidden { - return - } - if didSelectCellAction != nil { - didSelectCellAction?({ }) - } else { - super.tableView(tableView, didSelectRowAt: indexPath) - } - } else { - if let multiSelectionSupport = self as? MultiSelectSupport { - if let item = itemAt(indexPath: indexPath), let itemLocalID = item.localID { - if !selectedItemIds.contains(itemLocalID as OCLocalID) { - selectedItemIds.append(itemLocalID as OCLocalID) - } - self.actionContext?.add(item: item) - } - multiSelectionSupport.updateMultiselection() - } - } - } - - open override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { - if tableView.isEditing { - if let multiSelectionSupport = self as? MultiSelectSupport { - if let item = itemAt(indexPath: indexPath), let itemLocalID = item.localID { - selectedItemIds.removeAll(where: {$0 as String == itemLocalID}) - self.actionContext?.remove(item: item) - } - multiSelectionSupport.updateMultiselection() - } - } - } - - @available(iOS 13.0, *) - open override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - if isMoreButtonPermanentlyHidden { - return nil - } - - return super.tableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) - } - - open override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - if isMoreButtonPermanentlyHidden { - return nil - } - - return super.tableView(tableView, trailingSwipeActionsConfigurationForRowAt: indexPath) - } -} - -@available(iOS 13, *) public extension QueryFileListTableViewController { - override func tableView(_ tableView: UITableView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool { - return !DisplaySettings.shared.preventDraggingFiles - } - - override func tableView(_ tableView: UITableView, didBeginMultipleSelectionInteractionAt indexPath: IndexPath) { - if let multiSelectionSupport = self as? MultiSelectSupport { - multiSelectionSupport.enterMultiselection() - } - } -} diff --git a/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationAction.swift b/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationAction.swift new file mode 100644 index 000000000..8eaa6572f --- /dev/null +++ b/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationAction.swift @@ -0,0 +1,136 @@ +// +// NavigationRevocationAction.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public enum NavigationRevocationEvent: Equatable { + case connectionClosed(bookmarkUUID: UUID) + case driveRemoved(driveID: String, bookmarkUUID: UUID) + case itemRemoved(itemReference: OCDataItemReference, dataSource: String, bookmarkUUID: UUID?) + + public func send() { + NavigationRevocationManager.shared.handle(event: self) + } +} + +open class NavigationRevocationAction: NSObject { + private var objcAssociationHandle = 1 + + public typealias EventMatcher = (NavigationRevocationEvent) -> Bool + public typealias EventAction = (NavigationRevocationEvent?, NavigationRevocationAction) -> Void + + open var eventMatcher: EventMatcher + open var action: EventAction? + + open var triggers: [NavigationRevocationTrigger]? { + willSet { + setAction(nil, on: triggers) + } + + didSet { + setAction(self, on: triggers) + } + } + + private func setAction(_ action: NavigationRevocationAction?, on triggers: [NavigationRevocationTrigger]?) { + if let triggers { + for trigger in triggers { + trigger.action = action + } + } + } + + init(eventMatcher: @escaping EventMatcher, action: @escaping EventAction) { + self.eventMatcher = eventMatcher + self.action = action + super.init() + } + + convenience init(triggeredBy events: [NavigationRevocationEvent]? = nil, for triggers: [NavigationRevocationTrigger]? = nil, action: @escaping EventAction) { + self.init(eventMatcher: { (event) in + return events?.contains(event) ?? false + }, action: action) + + self.triggers = triggers + setAction(self, on: triggers) + } + + open func register(for obj: NSObject? = nil, globally: Bool = false) { + if globally { + NavigationRevocationManager.shared.register(action: self) + } + + if let obj = obj { + objc_setAssociatedObject(obj, &self.objcAssociationHandle, self, .OBJC_ASSOCIATION_RETAIN) + } + } + + open func unregister(for obj: NSObject? = nil, globally: Bool = false) { + if globally { + NavigationRevocationManager.shared.unregister(action: self) + } + + if let obj = obj { + objc_setAssociatedObject(obj, &self.objcAssociationHandle, nil, .OBJC_ASSOCIATION_RETAIN) + } + } + + open func handle(event: NavigationRevocationEvent) -> Bool { + if eventMatcher(event) { + return performAction(with: event) + } + return false + } + + @discardableResult open func performAction(with event: NavigationRevocationEvent?) -> Bool { + var action: EventAction? + + OCSynchronized(self) { + action = self.action + self.action = nil + } + + if let action = action { + action(event, self) + return true + } + + return false + } +} + +public extension NavigationRevocationAction { + static func forEvent(_ matchEvent: NavigationRevocationEvent, action: @escaping NavigationRevocationAction.EventAction) -> NavigationRevocationAction { + return NavigationRevocationAction(eventMatcher: { event in + return event == matchEvent + }, action: action) + } + + static func forClosing(connection: AccountConnection, action: @escaping NavigationRevocationAction.EventAction) -> NavigationRevocationAction { + return forEvent(.connectionClosed(bookmarkUUID: connection.bookmark.uuid), action: action) + } + + static func forRemoval(connection: AccountConnection, driveID: String, action: @escaping NavigationRevocationAction.EventAction) -> NavigationRevocationAction { + return forEvent(.driveRemoved(driveID: driveID, bookmarkUUID: connection.bookmark.uuid), action: action) + } + + static func forRemoval(itemReference: OCDataItemReference, dataSource: OCDataSource, bookmarkUUID: UUID? = nil, action: @escaping NavigationRevocationAction.EventAction) -> NavigationRevocationAction { + return forEvent(.itemRemoved(itemReference: itemReference, dataSource: dataSource.uuid, bookmarkUUID: bookmarkUUID), action: action) + } +} diff --git a/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationManager.swift b/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationManager.swift new file mode 100644 index 000000000..1abe497d5 --- /dev/null +++ b/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationManager.swift @@ -0,0 +1,53 @@ +// +// NavigationRevocationManager.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +open class NavigationRevocationManager: NSObject { + var actions: NSHashTable + + public static var shared : NavigationRevocationManager = { + return NavigationRevocationManager() + }() + + override init() { + self.actions = NSHashTable.weakObjects() + } + + open func register(action: NavigationRevocationAction) { + OCSynchronized(self) { + actions.add(action) + } + } + + open func unregister(action: NavigationRevocationAction) { + OCSynchronized(self) { + actions.remove(action) + } + } + + open func handle(event: NavigationRevocationEvent) { + OCSynchronized(self) { + for action in self.actions.allObjects { + if action.handle(event: event) { + self.actions.remove(action) + } + } + } + } +} diff --git a/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationTrigger.swift b/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationTrigger.swift new file mode 100644 index 000000000..cb58402e6 --- /dev/null +++ b/ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationTrigger.swift @@ -0,0 +1,133 @@ +// +// NavigationRevocationTrigger.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +open class NavigationRevocationTrigger: NSObject { + var invalidated: Bool = false + + // MARK: - Predefined event + var event: NavigationRevocationEvent? + + public init(event: NavigationRevocationEvent? = nil) { + self.event = event + super.init() + } + + // MARK: - Trigger action + weak public var action: NavigationRevocationAction? + + // MARK: - Data source event + var dataSourceSubscription: OCDataSourceSubscription? + private var objcAssociationHandle = 2 + + public init(itemRemovalTriggerFor dataSource: OCDataSource, attach: Bool = false, itemRefs: [OCDataItemReference]? = nil, bookmarkUUID: UUID? = nil, on dispatchQueue: DispatchQueue = .main) { + super.init() + + var isInitial = true + + dataSourceSubscription = dataSource.subscribe(updateHandler: { [weak self] subscription in + let snapshot = subscription.snapshotResettingChangeTracking(true) + + if let dataSource = subscription.source, let self = self { + if let removedItems = snapshot.removedItems { + if let itemRefs = itemRefs { + for removedItemRef in removedItems { + if itemRefs.firstIndex(of: removedItemRef) != nil { + self.send(event: NavigationRevocationEvent.itemRemoved(itemReference: removedItemRef, dataSource: dataSource.uuid, bookmarkUUID: bookmarkUUID)) + } + } + } else { + for removedItemRef in removedItems { + self.send(event: NavigationRevocationEvent.itemRemoved(itemReference: removedItemRef, dataSource: dataSource.uuid, bookmarkUUID: bookmarkUUID)) + } + } + } + + if isInitial { + isInitial = false + + if let itemRefs = itemRefs { + let allItems = snapshot.items + + for itemRef in itemRefs { + if allItems.firstIndex(of: itemRef) == nil { + self.send(event: NavigationRevocationEvent.itemRemoved(itemReference: itemRef, dataSource: dataSource.uuid, bookmarkUUID: bookmarkUUID)) + } + } + } + } + } + }, on: dispatchQueue, trackDifferences: true, performInitialUpdate: true) + + if attach { + objc_setAssociatedObject(dataSource, &self.objcAssociationHandle, self, .OBJC_ASSOCIATION_RETAIN) + } + } + + deinit { + dataSourceSubscription?.terminate() + } + + // MARK: - Methods + func trigger() { + if invalidated { + return + } + + if let event = event { + self.event = nil + send(event: event) + } + } + + func send(event: NavigationRevocationEvent?) { + if let event = event { + event.send() + } + + if let action = action { + action.performAction(with: event) + self.action = nil + } + } + + func invalidate() { + invalidated = true + event = nil + + if let dataSourceSubscription = dataSourceSubscription { + if let dataSource = dataSourceSubscription.source { + objc_setAssociatedObject(dataSource, &self.objcAssociationHandle, nil, .OBJC_ASSOCIATION_RETAIN) + } + dataSourceSubscription.terminate() + } + } + + // MARK: - On Deallocation + static func onDeallocation(of obj: Any, event: NavigationRevocationEvent) -> NavigationRevocationTrigger { + let trigger = NavigationRevocationTrigger(event: event) + + OCDeallocAction.add({ + trigger.trigger() + }, forDeallocationOf: obj) + + return trigger + } +} diff --git a/ownCloudAppShared/Client/Navigation Revocation/README.md b/ownCloudAppShared/Client/Navigation Revocation/README.md new file mode 100644 index 000000000..6163d432e --- /dev/null +++ b/ownCloudAppShared/Client/Navigation Revocation/README.md @@ -0,0 +1,35 @@ +# Navigation Revocation + +The Navigation Revocation set of classes address a problem occuring in split views, where: +- the selection in the (left) sidebar changes the content in the (right) main view +- the selected item goes away while its contents is still shown on the right + + +## Mechanics + +When pushing content to the content part a `NavigationRevocationAction` is +- created +- registered with the `NavigationRevocationManager` (which holds only a weak reference) and +- strongly referenced by the view controller presenting the content. + +Now, if content disappears from the sidebar, a matching `NavigationRevocationEvent` is sent to the `NavigationRevocationManager`, which subsequently shares it with all `NavigationRevocationAction`s. + +`NavigationRevocationAction`s listening for that event can then apply their action to ensure the user is presented appropriate content. + +If subsequently the view controller that held the only strong reference to the `NavigationRevocationAction` is deallocated, its registration also automatically disappears from `NavigationRevocationManager`, so that the action will not respond to subsequent events. + +This pattern ensures that only relevant `NavigationRevocationAction`s are around at any given time. + + +## Trigger + +Events are triggered by either manually being sent to the `NavigationRevocationManager` - or by a `NavigationRevocationTrigger` that can be triggered by +- deallocation of another object +- disappearance of a reference from a data source + +It's also possible to set `NavigationRevocationTrigger`s for an `NavigationRevocationAction`, so that the action then holds a strong reference to the triggers - and is run once once the first trigger is triggered. The triggers will be removed together with the action when the action is deallocated. + + +## Test Cases +- disconnect from a server whose content is currently shown +- deactivation of a space whose content is currently shown diff --git a/ownCloudAppShared/Client/Navigation Revocation/UIViewController+NavigationRevocation.swift b/ownCloudAppShared/Client/Navigation Revocation/UIViewController+NavigationRevocation.swift new file mode 100644 index 000000000..d4a518b0a --- /dev/null +++ b/ownCloudAppShared/Client/Navigation Revocation/UIViewController+NavigationRevocation.swift @@ -0,0 +1,61 @@ +// +// UIViewController+NavigationRevocation.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public struct RevocationTriggers: OptionSet { + public let rawValue: Int + public init(rawValue: Int) { + self.rawValue = rawValue + } + + static public let connectionClosed = RevocationTriggers(rawValue: 1) + static public let driveRemoved = RevocationTriggers(rawValue: 2) +} + +public extension UIViewController { + @discardableResult func revoke(in context: ClientContext?, when revocationTriggers: RevocationTriggers = [ .connectionClosed ]) -> UIViewController { + guard let context = context else { return self } + + var triggers: [NavigationRevocationTrigger] = [] + var events: [NavigationRevocationEvent] = [] + + if let bookmarkUUID = context.accountConnection?.bookmark.uuid { + events.append(.connectionClosed(bookmarkUUID: bookmarkUUID)) + + if let drivesDataSource = context.core?.subscribedDrivesDataSource, + let driveID = context.drive?.identifier as? OCDataItemReference { + let driveTrigger = NavigationRevocationTrigger(itemRemovalTriggerFor: drivesDataSource, itemRefs: [ driveID ], bookmarkUUID: bookmarkUUID) + triggers.append(driveTrigger) + } + } + + if triggers.count > 0 || events.count > 0 { + let navigationRevocationHandler = context.navigationRevocationHandler + + NavigationRevocationAction(triggeredBy: events, for: triggers, action: { [weak self, weak context, weak navigationRevocationHandler] event, action in + if let self = self, let event = event { + navigationRevocationHandler?.handleRevocation(event: event, context: context, for: self) + } + }).register(for: self, globally: true) + } + + return self + } +} diff --git a/ownCloudAppShared/Client/Resource Sources/ResourceItemIcon.swift b/ownCloudAppShared/Client/Resource Sources/ResourceItemIcon.swift index bc1e309ae..2fd66c0c0 100644 --- a/ownCloudAppShared/Client/Resource Sources/ResourceItemIcon.swift +++ b/ownCloudAppShared/Client/Resource Sources/ResourceItemIcon.swift @@ -25,6 +25,10 @@ public class ResourceItemIcon: OCResource, OCViewProvider { public static let folder : ResourceItemIcon = ResourceItemIcon(iconName: "folder") public static let file : ResourceItemIcon = ResourceItemIcon(iconName: "file") + public static func iconFor(mimeType: String) -> ResourceItemIcon { + return ResourceItemIcon(iconName: OCItem.iconName(for: mimeType) ?? "file") + } + public convenience init(iconName: String, identifier: String? = nil) { self.init() diff --git a/ownCloudAppShared/Client/Search/Item Search/ItemSearchSuggestionsViewController.swift b/ownCloudAppShared/Client/Search/Item Search/ItemSearchSuggestionsViewController.swift index ca55661e1..745cf22b5 100644 --- a/ownCloudAppShared/Client/Search/Item Search/ItemSearchSuggestionsViewController.swift +++ b/ownCloudAppShared/Client/Search/Item Search/ItemSearchSuggestionsViewController.swift @@ -134,7 +134,7 @@ class ItemSearchSuggestionsViewController: UIViewController, SearchElementUpdati case "save-search": if let savedSearch = scope.savedSearch as? OCSavedSearch, let vault = scope.clientContext.core?.vault { OnMainThread { - self?.requestName(title: "Name of search", placeholder: savedSearch.name, completionHandler: { save, name in + self?.requestName(title: "Name of saved search".localized, placeholder: "Saved search".localized, completionHandler: { save, name in if save { if let name = name { savedSearch.name = name @@ -147,7 +147,7 @@ class ItemSearchSuggestionsViewController: UIViewController, SearchElementUpdati case "save-template": if let savedSearch = scope.savedTemplate as? OCSavedSearch, let vault = scope.clientContext.core?.vault { OnMainThread { - self?.requestName(title: "Name of template", placeholder: savedSearch.name, completionHandler: { save, name in + self?.requestName(title: "Name of template".localized, placeholder: "Search template".localized, completionHandler: { save, name in if save { if let name = name { savedSearch.name = name @@ -168,28 +168,20 @@ class ItemSearchSuggestionsViewController: UIViewController, SearchElementUpdati var choices: [PopupButtonChoice] = [] if (self?.scope as? ItemSearchScope)?.canSaveSearch == true { - let saveSearchChoice = PopupButtonChoice(with: "Save as smart folder".localized, image: UIImage(systemName: "folder.badge.gearshape")?.withRenderingMode(.alwaysTemplate), representedObject: NSString("save-search")) + let saveSearchChoice = PopupButtonChoice(with: "Save search".localized, image: OCSymbol.icon(forSymbolName: "folder.badge.gearshape"), representedObject: NSString("save-search")) choices.append(saveSearchChoice) } if (self?.scope as? ItemSearchScope)?.canSaveTemplate == true { - let saveTemplateChoice = PopupButtonChoice(with: "Save template".localized, image: UIImage(systemName: "plus.square.dashed")?.withRenderingMode(.alwaysTemplate), representedObject: NSString("save-template")) + let saveTemplateChoice = PopupButtonChoice(with: "Save as search template".localized, image: OCSymbol.icon(forSymbolName: "plus.square.dashed"), representedObject: NSString("save-template")) choices.append(saveTemplateChoice) } - if let vault = self?.scope?.clientContext.core?.vault, let savedSearches = vault.savedSearches { - for savedSearch in savedSearches { - if savedSearch.isTemplate { - choices.append(PopupButtonChoice(with: savedSearch.name, image: UIImage(systemName: "square.dashed.inset.filled")?.withRenderingMode(.alwaysTemplate), representedObject: savedSearch)) - } - } - } - return choices } var buttonConfiguration = UIButton.Configuration.plain().updated(for: savedSearchPopup!.button) - buttonConfiguration.image = UIImage(systemName: "ellipsis.circle")?.withRenderingMode(.alwaysTemplate) + buttonConfiguration.image = OCSymbol.icon(forSymbolName: "ellipsis.circle") buttonConfiguration.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 5, bottom: 10, trailing: 5) buttonConfiguration.attributedTitle = nil savedSearchPopup?.adaptButton = false @@ -245,8 +237,7 @@ class ItemSearchSuggestionsViewController: UIViewController, SearchElementUpdati for queryCondition in category.options { if let localizedDescription = queryCondition.localizedDescription { - let image : UIImage? = (queryCondition.symbolName != nil) ? UIImage(systemName: queryCondition.symbolName!)?.withRenderingMode(.alwaysTemplate) : nil - let choice = PopupButtonChoice(with: localizedDescription, image: image, representedObject: queryCondition) + let choice = PopupButtonChoice(with: localizedDescription, image: OCSymbol.icon(forSymbolName: queryCondition.symbolName), representedObject: queryCondition) choices.append(choice) } } @@ -308,6 +299,13 @@ class ItemSearchSuggestionsViewController: UIViewController, SearchElementUpdati func updateFor(_ searchElements: [SearchElement]) { self.searchElements = searchElements + // Hide saved search popup button + var showSavedSearchButton : Bool = false + if let searchScope = scope as? ItemSearchScope, searchScope.canSaveSearch || searchScope.canSaveTemplate { + showSavedSearchButton = true + } + savedSearchPopup?.button.isHidden = !showSavedSearchButton + for category in categories { var categoryHasMatch: Bool = false diff --git a/ownCloudAppShared/Client/Search/Item Search/Scopes/AccountSearchScope.swift b/ownCloudAppShared/Client/Search/Item Search/Scopes/AccountSearchScope.swift index 37362ce34..feb0739a0 100644 --- a/ownCloudAppShared/Client/Search/Item Search/Scopes/AccountSearchScope.swift +++ b/ownCloudAppShared/Client/Search/Item Search/Scopes/AccountSearchScope.swift @@ -63,7 +63,7 @@ open class CustomQuerySearchScope : ItemSearchScope { composedResults?.setInclude((snapshot.numberOfItems >= maxResultCount), for: resultActionSource) } } - }, on: .main, trackDifferences: false, performIntialUpdate: true) + }, on: .main, trackDifferences: false, performInitialUpdate: true) results = composedResults } else { @@ -160,6 +160,10 @@ open class AccountSearchScope : CustomQuerySearchScope { } super.init(with: context, cellStyle: revealCellStyle, localizedName: name, localizedPlaceholder: placeholder, icon: icon) + + if let displaySettingsCondition = DisplaySettings.shared.queryConditionForDisplaySettings { + additionalRequirementCondition = displaySettingsCondition + } } open override var savedSearchScope: OCSavedSearchScope? { @@ -175,7 +179,13 @@ open class DriveSearchScope : AccountSearchScope { if context.core?.useDrives == true, let driveID = context.drive?.identifier { self.driveID = driveID - additionalRequirementCondition = .where(.driveID, isEqualTo: driveID) + let driveCondition = OCQueryCondition.where(.driveID, isEqualTo: driveID) + + if let displaySettingsCondition = DisplaySettings.shared.queryConditionForDisplaySettings { + additionalRequirementCondition = .require([displaySettingsCondition, driveCondition]) + } else { + additionalRequirementCondition = driveCondition + } } } @@ -200,17 +210,26 @@ open class ContainerSearchScope: AccountSearchScope { if context.core?.useDrives == true, let queryLocation = context.query?.queryLocation, let path = queryLocation.path { self.location = queryLocation + var containerCondition: OCQueryCondition if context.core?.useDrives == true, let driveID = queryLocation.driveID { - additionalRequirementCondition = .require([ + containerCondition = .require([ .where(.driveID, isEqualTo: driveID), - .where(.path, startsWith: path) + .where(.path, startsWith: path), + .where(.path, isNotEqualTo: path) ]) } else { - additionalRequirementCondition = .require([ - .where(.path, startsWith: path) + containerCondition = .require([ + .where(.path, startsWith: path), + .where(.path, isNotEqualTo: path) ]) } + + if let displaySettingsCondition = DisplaySettings.shared.queryConditionForDisplaySettings { + additionalRequirementCondition = .require([displaySettingsCondition, containerCondition]) + } else { + additionalRequirementCondition = containerCondition + } } } diff --git a/ownCloudAppShared/Client/Search/Item Search/Tokenizer/OCQueryCondition+SearchToken.swift b/ownCloudAppShared/Client/Search/Item Search/Tokenizer/OCQueryCondition+SearchToken.swift index 2298c9970..d639c976a 100644 --- a/ownCloudAppShared/Client/Search/Item Search/Tokenizer/OCQueryCondition+SearchToken.swift +++ b/ownCloudAppShared/Client/Search/Item Search/Tokenizer/OCQueryCondition+SearchToken.swift @@ -69,13 +69,7 @@ extension OCQueryCondition { func generateSearchToken(fallbackText: String, inputComplete: Bool) -> SearchToken? { // Use existing description and symbol if let firstDescriptiveCondition = firstDescriptiveCondition, let localizedDescription = firstDescriptiveCondition.localizedDescription { - var icon : UIImage? - - if let symbolName = firstDescriptiveCondition.symbolName { - icon = UIImage(systemName: symbolName) - } - - return SearchToken(text: localizedDescription, icon: icon, representedObject: self, inputComplete: inputComplete) + return SearchToken(text: localizedDescription, icon: OCSymbol.icon(forSymbolName: firstDescriptiveCondition.symbolName), representedObject: self, inputComplete: inputComplete) } // Try to determine a useful icon and description @@ -89,25 +83,25 @@ extension OCQueryCondition { switch effectiveProperty { case .name: if effectiveOperator == .propertyHasSuffix { - icon = UIImage(systemName: "smallcircle.filled.circle") + icon = OCSymbol.icon(forSymbolName: "smallcircle.filled.circle") } case .driveID: - icon = UIImage(systemName: "square.grid.2x2") + icon = OCSymbol.icon(forSymbolName: "square.grid.2x2") case .mimeType: - icon = UIImage(systemName: "photo") + icon = OCSymbol.icon(forSymbolName: "photo") case .size: switch effectiveOperator { case .propertyGreaterThanValue: - icon = UIImage(systemName: "greaterthan") + icon = OCSymbol.icon(forSymbolName: "greaterthan") case .propertyLessThanValue: - icon = UIImage(systemName: "lessthan") + icon = OCSymbol.icon(forSymbolName: "lessthan") case .propertyEqualToValue: - icon = UIImage(systemName: "equal") + icon = OCSymbol.icon(forSymbolName: "equal") default: break } @@ -115,7 +109,7 @@ extension OCQueryCondition { case .ownerUserName: break case .lastModified: - icon = UIImage(systemName: "calendar") + icon = OCSymbol.icon(forSymbolName: "calendar") default: break } diff --git a/ownCloudAppShared/Client/Search/Scopes/SearchScope.swift b/ownCloudAppShared/Client/Search/Scopes/SearchScope.swift index 6999e1cbf..99d518a37 100644 --- a/ownCloudAppShared/Client/Search/Scopes/SearchScope.swift +++ b/ownCloudAppShared/Client/Search/Scopes/SearchScope.swift @@ -36,7 +36,7 @@ open class SearchScope: NSObject, SearchElementUpdating { public var scopeViewController: (UIViewController & SearchElementUpdating)? static public func modifyingQuery(with context: ClientContext, localizedName: String) -> SearchScope { - return SingleFolderSearchScope(with: context, cellStyle: nil, localizedName: localizedName, localizedPlaceholder: "Search folder".localized, icon: UIImage(systemName: "folder")) + return SingleFolderSearchScope(with: context, cellStyle: nil, localizedName: localizedName, localizedPlaceholder: "Search folder".localized, icon: OCSymbol.icon(forSymbolName: "folder")) } static public func driveSearch(with context: ClientContext, cellStyle: CollectionViewCellStyle, localizedName: String) -> SearchScope { @@ -44,7 +44,7 @@ open class SearchScope: NSObject, SearchElementUpdating { if let driveName = context.drive?.name, driveName.count > 0 { placeholder = "Search {{space.name}}".localized(["space.name" : driveName]) } - return DriveSearchScope(with: context, cellStyle: cellStyle, localizedName: localizedName, localizedPlaceholder: placeholder, icon: UIImage(systemName: "square.grid.2x2")) + return DriveSearchScope(with: context, cellStyle: cellStyle, localizedName: localizedName, localizedPlaceholder: placeholder, icon: OCSymbol.icon(forSymbolName: "square.grid.2x2")) } static public func containerSearch(with context: ClientContext, cellStyle: CollectionViewCellStyle, localizedName: String) -> SearchScope { @@ -52,11 +52,11 @@ open class SearchScope: NSObject, SearchElementUpdating { if let path = context.query?.queryLocation?.lastPathComponent, path.count > 0 { placeholder = "Search from {{folder.name}}".localized(["folder.name" : path]) } - return ContainerSearchScope(with: context, cellStyle: cellStyle, localizedName: localizedName, localizedPlaceholder: placeholder, icon: UIImage(systemName: "square.stack.3d.up")) + return ContainerSearchScope(with: context, cellStyle: cellStyle, localizedName: localizedName, localizedPlaceholder: placeholder, icon: OCSymbol.icon(forSymbolName: "square.stack.3d.up")) } static public func accountSearch(with context: ClientContext, cellStyle: CollectionViewCellStyle, localizedName: String) -> SearchScope { - return AccountSearchScope(with: context, cellStyle: cellStyle, localizedName: localizedName, localizedPlaceholder: "Search account".localized, icon: UIImage(systemName: "person")) + return AccountSearchScope(with: context, cellStyle: cellStyle, localizedName: localizedName, localizedPlaceholder: "Search account".localized, icon: OCSymbol.icon(forSymbolName: "person")) } public init(with context: ClientContext, cellStyle: CollectionViewCellStyle?, localizedName name: String, localizedPlaceholder placeholder: String? = nil, icon: UIImage? = nil) { diff --git a/ownCloudAppShared/Client/Search/SearchViewController.swift b/ownCloudAppShared/Client/Search/SearchViewController.swift index 9edce4430..da8c35029 100644 --- a/ownCloudAppShared/Client/Search/SearchViewController.swift +++ b/ownCloudAppShared/Client/Search/SearchViewController.swift @@ -161,28 +161,12 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl if let scopeViewController = scopeViewController, let scopeViewControllerView = scopeViewController.view { addChild(scopeViewController) view.addSubview(scopeViewControllerView) - scopeViewControllerConstraints = [ - scopeViewControllerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 5), - scopeViewControllerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10), - scopeViewControllerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10), - scopeViewControllerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10) - ] + scopeViewControllerConstraints = view.embed(toFillWith: scopeViewControllerView, insets: NSDirectionalEdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 10), enclosingAnchors: view.safeAreaAnchorSet) scopeViewController.didMove(toParent: self) } } } - private var scopeViewControllerConstraints : [NSLayoutConstraint]? { - willSet { - if let scopeViewControllerConstraints = scopeViewControllerConstraints { - NSLayoutConstraint.deactivate(scopeViewControllerConstraints) - } - } - didSet { - if let scopeViewControllerConstraints = scopeViewControllerConstraints { - NSLayoutConstraint.activate(scopeViewControllerConstraints) - } - } - } + private var scopeViewControllerConstraints : [NSLayoutConstraint]? open override func loadView() { let rootView = UIView() @@ -190,7 +174,6 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl scopePopup = PopupButtonController(with: [], selectedChoice: nil, choiceHandler: { [weak self] (choice, _) in self?.activeScope = choice.representedObject as? SearchScope }) - // scopePopup?.showTitleInButton = false var scopePopupButtonConfiguration = UIButton.Configuration.borderless() scopePopupButtonConfiguration.contentInsets.leading = 0 @@ -239,23 +222,22 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl var targetNavigationItem: UINavigationItem? var niInjected: Bool = false - var niTitleView: UIView? - var niRightBarButtonItems: [UIBarButtonItem]? var niHidesBackButton: Bool = false func injectIntoNavigationItem() { if !niInjected, let targetNavigationItem = targetNavigationItem { // Store content - niTitleView = targetNavigationItem.titleView - niRightBarButtonItems = targetNavigationItem.rightBarButtonItems niHidesBackButton = targetNavigationItem.hidesBackButton - // Overwrite content - targetNavigationItem.titleView = searchField - // Alternative implementation as a standard "Cancel" button, more convention compliant, but needs more space: let cancelToolbarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(endSearch)) - let cancelToolbarButton = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .done, target: self, action: #selector(endSearch)) - targetNavigationItem.rightBarButtonItems = [ cancelToolbarButton ] + let cancelToolbarButton = UIBarButtonItem(image: OCSymbol.icon(forSymbolName: "xmark"), style: .done, target: self, action: #selector(endSearch)) + + // Overwrite content + targetNavigationItem.navigationContent.add(items: [ + NavigationContentItem(identifier: "search-left", area: .left, priority: .highest, position: .trailing, items: [ ]), + NavigationContentItem(identifier: "search-field", area: .title, priority: .highest, position: .leading, titleView: searchField), + NavigationContentItem(identifier: "search-right", area: .right, priority: .highest, position: .trailing, items: [ cancelToolbarButton ]) + ]) targetNavigationItem.hidesBackButton = true niInjected = true @@ -265,8 +247,11 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl func restoreNavigationItem() { if niInjected, let targetNavigationItem = targetNavigationItem { // Restore content - targetNavigationItem.titleView = niTitleView - targetNavigationItem.rightBarButtonItems = niRightBarButtonItems + targetNavigationItem.navigationContent.remove(itemsWithIdentifiers: [ + "search-left", + "search-field", + "search-right" + ]) targetNavigationItem.hidesBackButton = niHidesBackButton niInjected = false } @@ -329,7 +314,7 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl if oldValue != scopeResults { scopeResultsSubscription = scopeResults?.subscribe(updateHandler: { [weak self] (subscription) in self?.scopeResultsItemCount = subscription.snapshotResettingChangeTracking(true).numberOfItems - }, on: .main, trackDifferences: false, performIntialUpdate: true) + }, on: .main, trackDifferences: false, performInitialUpdate: true) } } } @@ -350,7 +335,15 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl // Determine current content func updateCurrentContent() { - if searchField.tokens.count == 0, searchField.text?.count == 0 { + var searchFieldText = searchField.text ?? "" + + if searchFieldText.count > 0 { + // Strip white space and new lines (if pasted) to determine effective length of search term + let charSet = CharacterSet.whitespacesAndNewlines + searchFieldText = searchFieldText.trimmingCharacters(in: charSet) + } + + if searchField.tokens.count == 0, searchFieldText.count == 0 { currentContent = suggestionContent } else { if scopeResultsItemCount == 0 { @@ -438,6 +431,7 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl // MARK: - Theme support public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.view.backgroundColor = collection.navigationBarColors.backgroundColor + self.view.backgroundColor = collection.navigationBarAppearance(for: .content).backgroundColor + searchField.applyThemeCollection(collection) } } diff --git a/ownCloudAppShared/Client/Sharing/GroupSharingEditTableViewController.swift b/ownCloudAppShared/Client/Sharing/GroupSharingEditTableViewController.swift index 862bfcb8a..64693b3f7 100644 --- a/ownCloudAppShared/Client/Sharing/GroupSharingEditTableViewController.swift +++ b/ownCloudAppShared/Client/Sharing/GroupSharingEditTableViewController.swift @@ -127,7 +127,7 @@ open class GroupSharingEditTableViewController: StaticTableViewController { self?.changePermissions(enabled: selected, permissions: [.share], completionHandler: {(_) in }) } - }, title: "Can Share".localized, subtitle: "", selected: canShare, identifier: "permission-section-share")) + }, title: "Can Share".localized, subtitle: "", selected: canShare, identifier: "permission-section-share")) } if canEdit || canIncreasePermissions { @@ -149,7 +149,7 @@ open class GroupSharingEditTableViewController: StaticTableViewController { }) } } - }, title: item?.type == .collection ? "Can Edit".localized : "Can Edit and Change".localized, subtitle: "", selected: canEdit, identifier: "permission-section-edit")) + }, title: item?.type == .collection ? "Can Edit".localized : "Can Edit and Change".localized, subtitle: "", selected: canEdit, identifier: "permission-section-edit")) } let subtitles = [ diff --git a/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift b/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift index 3f5d28639..02102342a 100644 --- a/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift +++ b/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift @@ -188,7 +188,7 @@ open class GroupSharingTableViewController: SharingTableViewController, UISearch progressView.startAnimating() row.cell?.accessoryView = progressView - self.core?.makeDecision(on: share, accept: false, completionHandler: { [weak self] (error) in + self.core?.delete(share, completionHandler: { [weak self] (error) in guard let self = self else { return } OnMainThread { if error == nil { diff --git a/ownCloudAppShared/Client/Sharing/PublicLinkTableViewController.swift b/ownCloudAppShared/Client/Sharing/PublicLinkTableViewController.swift index 074796b1a..932b5b314 100644 --- a/ownCloudAppShared/Client/Sharing/PublicLinkTableViewController.swift +++ b/ownCloudAppShared/Client/Sharing/PublicLinkTableViewController.swift @@ -29,6 +29,11 @@ open class PublicLinkTableViewController: SharingTableViewController { return false } + var privateLinkSharingEnabled : Bool { + if let core = core, core.connectionStatus == .online, core.connection.capabilities?.sharingAPIEnabled == true, core.connection.capabilities?.supportsPrivateLinks == true, item.isShareable { return true } + return false + } + open override func viewDidLoad() { super.viewDidLoad() @@ -41,7 +46,9 @@ open class PublicLinkTableViewController: SharingTableViewController { } addHeaderView() - addPrivateLinkSection() + if privateLinkSharingEnabled { + addPrivateLinkSection() + } if publicLinkSharingEnabled { self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addPublicLink)) @@ -237,9 +244,7 @@ open class PublicLinkTableViewController: SharingTableViewController { }), UIContextualAction(style: .normal, title: "Copy".localized, handler: { (_, _, completionHandler) in - if let shareURL = share.url { - UIPasteboard.general.url = shareURL - + if share.copyToClipboard() { _ = NotificationHUDViewController(on: self, title: share.name ?? "Public Link".localized, subtitle: "URL was copied to the clipboard".localized) } @@ -271,12 +276,14 @@ open class PublicLinkTableViewController: SharingTableViewController { var acceptedCloudShares : [OCShare]? var sharedWithMeShares : [OCShare]? - dispatchGroup.enter() + if core.connection.capabilities?.federatedSharingSupported == true { + dispatchGroup.enter() - core.acceptedCloudShares(for: item, initialPopulationHandler: { (shares) in - acceptedCloudShares = shares - dispatchGroup.leave() - }, allowPartialMatch: true) + core.acceptedCloudShares(for: item, initialPopulationHandler: { (shares) in + acceptedCloudShares = shares + dispatchGroup.leave() + }, allowPartialMatch: true) + } dispatchGroup.enter() diff --git a/ownCloudAppShared/Client/Sharing/ShareClientItemCell.swift b/ownCloudAppShared/Client/Sharing/ShareClientItemCell.swift deleted file mode 100644 index c62b87cb5..000000000 --- a/ownCloudAppShared/Client/Sharing/ShareClientItemCell.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ShareClientItemCell.swift -// ownCloud -// -// Created by Matthias Hühne on 16.05.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 - -open class ShareClientItemCell: ClientItemResolvingCell { - var iconSize : CGSize = CGSize(width: 40, height: 40) - - // MARK: - Share Item - open override func titleLabelString(for item: OCItem?) -> NSAttributedString { - if let shareItemPath = share?.itemLocation.path { - return NSMutableAttributedString() - .appendBold(shareItemPath) - } - - return super.titleLabelString(for: item) - } - - public var share : OCShare? { - didSet { - if let share = share { - if share.itemType == .collection { - self.iconView.activeViewProvider = ResourceItemIcon.folder - } else { - self.iconView.activeViewProvider = ResourceItemIcon.file - } - - self.itemResolutionLocation = share.itemLocation - - self.updateLabels(with: item) - } - } - } -} diff --git a/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift b/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift deleted file mode 100644 index ac39ff964..000000000 --- a/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// BreadCrumbTableViewController.swift -// ownCloud -// -// Created by Matthias Hühne on 09.04.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 - -open class BreadCrumbTableViewController: StaticTableViewController { - - // MARK: - Constants - private let maxContentWidth : CGFloat = 500 - private let rowHeight : CGFloat = 44 - private let imageWidth : CGFloat = 30 - private let imageHeight : CGFloat = 30 - - // MARK: - Instance Variables - open var parentNavigationController : UINavigationController? - open var queryPath : NSString = "" - open var bookmarkShortName : String? - open var navigationHandler : ((_ location: OCLocation) -> Void)? - - open override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.isScrollEnabled = false - guard let stackViewControllers = parentNavigationController?.viewControllers else { return } - var pathComp = queryPath.pathComponents - - if queryPath.hasSuffix("/") { - pathComp.removeLast() - } - if pathComp.count > 1 { - pathComp.removeLast() - } - - var rows : [StaticTableViewRow] = [] - let pathCount = pathComp.count - var currentViewContollerIndex = 2 - let contentHeight : CGFloat = rowHeight * CGFloat(pathCount) + 10 - let contentWidth : CGFloat = (view.frame.size.width < maxContentWidth) ? view.frame.size.width : maxContentWidth - self.preferredContentSize = CGSize(width: contentWidth, height: contentHeight) - - for (idx, currentPath) in pathComp.enumerated().reversed() { - var stackIndex = stackViewControllers.count - currentViewContollerIndex - if stackIndex < 0 { - stackIndex = 0 - } - var pathTitle = currentPath - if currentPath.isRootPath, let shortName = self.bookmarkShortName { - pathTitle = shortName - } - var fullPath = ((pathComp as NSArray).subarray(with: NSRange(location: 1, length: idx)) as NSArray).componentsJoined(by: "/") + "/" - if !fullPath.hasPrefix("/") { - fullPath = "/" + fullPath - } - let aRow = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in - guard let self = self else { return } - if let navigationHandler = self.navigationHandler { - navigationHandler(OCLocation.legacyRootPath(fullPath)) - } else { - if stackViewControllers.indices.contains(stackIndex) { - self.parentNavigationController?.popToViewController((stackViewControllers[stackIndex]), animated: true) - } - } - self.dismiss(animated: false, completion: nil) - }, title: pathTitle, image: Theme.shared.image(for: "folder", size: CGSize(width: imageWidth, height: imageHeight))) - - rows.append(aRow) - currentViewContollerIndex += 1 - } - - let section : StaticTableViewSection = StaticTableViewSection(headerTitle: nil, footerTitle: nil, rows: rows) - self.addSection(section) - } -} diff --git a/ownCloudAppShared/Client/User Interface/ClientItemCell.swift b/ownCloudAppShared/Client/User Interface/ClientItemCell.swift deleted file mode 100644 index 8b28d7ac3..000000000 --- a/ownCloudAppShared/Client/User Interface/ClientItemCell.swift +++ /dev/null @@ -1,603 +0,0 @@ -// -// ClientItemCell.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 -import ownCloudApp - -public protocol ClientItemCellDelegate: AnyObject { - - func moreButtonTapped(cell: ClientItemCell) - func messageButtonTapped(cell: ClientItemCell) - func revealButtonTapped(cell: ClientItemCell) - - func hasMessage(for item: OCItem) -> Bool -} - -open class ClientItemCell: ThemeTableViewCell { - private let horizontalMargin : CGFloat = 15 - private let verticalLabelMargin : CGFloat = 10 - private let verticalIconMargin : CGFloat = 10 - private let horizontalSmallMargin : CGFloat = 10 - private let spacing : CGFloat = 15 - private let smallSpacing : CGFloat = 2 - private let iconViewWidth : CGFloat = 40 - private let detailIconViewHeight : CGFloat = 15 - private let moreButtonWidth : CGFloat = 60 - private let revealButtonWidth : CGFloat = 35 - private let verticalLabelMarginFromCenter : CGFloat = 2 - private let iconSize : CGSize = CGSize(width: 40, height: 40) - private let thumbnailSize : CGSize = CGSize(width: 60, height: 60) - - open weak var delegate: ClientItemCellDelegate? { - didSet { - isMoreButtonPermanentlyHidden = (delegate as? MoreItemHandling == nil) - } - } - - open var titleLabel : UILabel = UILabel() - open var detailLabel : UILabel = UILabel() - open var iconView : ResourceViewHost = ResourceViewHost() - open var cloudStatusIconView : UIImageView = UIImageView() - open var sharedStatusIconView : UIImageView = UIImageView() - open var publicLinkStatusIconView : UIImageView = UIImageView() - open var moreButton : UIButton = UIButton() - open var messageButton : UIButton = UIButton() - open var revealButton : UIButton = UIButton() - open var progressView : ProgressView? - - open var moreButtonWidthConstraint : NSLayoutConstraint? - open var revealButtonWidthConstraint : NSLayoutConstraint? - - open var sharedStatusIconViewZeroWidthConstraint : NSLayoutConstraint? - open var publicLinkStatusIconViewZeroWidthConstraint : NSLayoutConstraint? - open var cloudStatusIconViewZeroWidthConstraint : NSLayoutConstraint? - - open var sharedStatusIconViewRightMarginConstraint : NSLayoutConstraint? - open var publicLinkStatusIconViewRightMarginConstraint : NSLayoutConstraint? - open var cloudStatusIconViewRightMarginConstraint : NSLayoutConstraint? - - open var activeThumbnailRequest : OCResourceRequestItemThumbnail? - - open var hasMessageForItem : Bool = false - - open var isMoreButtonPermanentlyHidden = false { - didSet { - if isMoreButtonPermanentlyHidden { - moreButtonWidthConstraint?.constant = 0 - } else { - moreButtonWidthConstraint?.constant = showRevealButton ? revealButtonWidth : moreButtonWidth - } - } - } - - open var isActive = true { - didSet { - let alpha : CGFloat = self.isActive ? 1.0 : 0.5 - titleLabel.alpha = alpha - detailLabel.alpha = alpha - iconView.alpha = alpha - cloudStatusIconView.alpha = alpha - } - } - - open weak var core : OCCore? - - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - prepareViewAndConstraints() - self.multipleSelectionBackgroundView = { - let blankView = UIView(frame: CGRect.zero) - blankView.backgroundColor = UIColor.clear - blankView.layer.masksToBounds = true - return blankView - }() - - NotificationCenter.default.addObserver(self, selector: #selector(updateAvailableOfflineStatus(_:)), name: .OCCoreItemPoliciesChanged, object: OCItemPolicyKind.availableOffline) - - NotificationCenter.default.addObserver(self, selector: #selector(updateHasMessage(_:)), name: .ClientSyncRecordIDsWithMessagesChanged, object: nil) - - PointerEffect.install(on: self.contentView, effectStyle: .hover) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - deinit { - NotificationCenter.default.removeObserver(self, name: .OCCoreItemPoliciesChanged, object: OCItemPolicyKind.availableOffline) - - NotificationCenter.default.removeObserver(self, name: .ClientSyncRecordIDsWithMessagesChanged, object: nil) - - self.localID = nil - self.core = nil - } - - func prepareViewAndConstraints() { - titleLabel.translatesAutoresizingMaskIntoConstraints = false - - detailLabel.translatesAutoresizingMaskIntoConstraints = false - - iconView.translatesAutoresizingMaskIntoConstraints = false - iconView.contentMode = .scaleAspectFit - - moreButton.translatesAutoresizingMaskIntoConstraints = false - - revealButton.translatesAutoresizingMaskIntoConstraints = false - - messageButton.translatesAutoresizingMaskIntoConstraints = false - - cloudStatusIconView.translatesAutoresizingMaskIntoConstraints = false - cloudStatusIconView.contentMode = .center - cloudStatusIconView.contentMode = .scaleAspectFit - - sharedStatusIconView.translatesAutoresizingMaskIntoConstraints = false - sharedStatusIconView.contentMode = .center - sharedStatusIconView.contentMode = .scaleAspectFit - - publicLinkStatusIconView.translatesAutoresizingMaskIntoConstraints = false - publicLinkStatusIconView.contentMode = .center - publicLinkStatusIconView.contentMode = .scaleAspectFit - - titleLabel.font = UIFont.preferredFont(forTextStyle: .callout) - titleLabel.adjustsFontForContentSizeCategory = true - titleLabel.lineBreakMode = .byTruncatingMiddle - - detailLabel.font = UIFont.preferredFont(forTextStyle: .footnote) - detailLabel.adjustsFontForContentSizeCategory = true - - self.contentView.addSubview(titleLabel) - self.contentView.addSubview(detailLabel) - self.contentView.addSubview(iconView) - self.contentView.addSubview(sharedStatusIconView) - self.contentView.addSubview(publicLinkStatusIconView) - self.contentView.addSubview(cloudStatusIconView) - self.contentView.addSubview(moreButton) - self.contentView.addSubview(revealButton) - self.contentView.addSubview(messageButton) - - moreButton.setImage(UIImage(named: "more-dots"), for: .normal) - moreButton.contentMode = .center - moreButton.isPointerInteractionEnabled = true - - revealButton.setImage(UIImage(systemName: "arrow.right.circle.fill"), for: .normal) - revealButton.isPointerInteractionEnabled = true - revealButton.contentMode = .center - revealButton.isHidden = !showRevealButton - revealButton.accessibilityLabel = "Reveal in folder".localized - - messageButton.setTitle("⚠️", for: .normal) - messageButton.contentMode = .center - messageButton.isPointerInteractionEnabled = true - messageButton.isHidden = true - - moreButton.addTarget(self, action: #selector(moreButtonTapped), for: .primaryActionTriggered) - revealButton.addTarget(self, action: #selector(revealButtonTapped), for: .primaryActionTriggered) - messageButton.addTarget(self, action: #selector(messageButtonTapped), for: .primaryActionTriggered) - - sharedStatusIconView.setContentHuggingPriority(.required, for: .vertical) - sharedStatusIconView.setContentHuggingPriority(.required, for: .horizontal) - sharedStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) - sharedStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) - - publicLinkStatusIconView.setContentHuggingPriority(.required, for: .vertical) - publicLinkStatusIconView.setContentHuggingPriority(.required, for: .horizontal) - publicLinkStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) - publicLinkStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) - - cloudStatusIconView.setContentHuggingPriority(.required, for: .vertical) - cloudStatusIconView.setContentHuggingPriority(.required, for: .horizontal) - cloudStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) - cloudStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) - - iconView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - - moreButtonWidthConstraint = moreButton.widthAnchor.constraint(equalToConstant: showRevealButton ? revealButtonWidth : moreButtonWidth) - revealButtonWidthConstraint = revealButton.widthAnchor.constraint(equalToConstant: showRevealButton ? revealButtonWidth : 0) - - cloudStatusIconViewZeroWidthConstraint = cloudStatusIconView.widthAnchor.constraint(equalToConstant: 0) - sharedStatusIconViewZeroWidthConstraint = sharedStatusIconView.widthAnchor.constraint(equalToConstant: 0) - publicLinkStatusIconViewZeroWidthConstraint = publicLinkStatusIconView.widthAnchor.constraint(equalToConstant: 0) - - cloudStatusIconViewRightMarginConstraint = sharedStatusIconView.leadingAnchor.constraint(equalTo: cloudStatusIconView.trailingAnchor) - sharedStatusIconViewRightMarginConstraint = publicLinkStatusIconView.leadingAnchor.constraint(equalTo: sharedStatusIconView.trailingAnchor) - publicLinkStatusIconViewRightMarginConstraint = detailLabel.leadingAnchor.constraint(equalTo: publicLinkStatusIconView.trailingAnchor) - - NSLayoutConstraint.activate([ - iconView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: horizontalMargin), - iconView.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -spacing), - iconView.widthAnchor.constraint(equalToConstant: iconViewWidth), - iconView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalIconMargin), - iconView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalIconMargin), - - titleLabel.trailingAnchor.constraint(equalTo: moreButton.leadingAnchor, constant: 0), - detailLabel.trailingAnchor.constraint(equalTo: moreButton.leadingAnchor, constant: 0), - - cloudStatusIconViewZeroWidthConstraint!, - sharedStatusIconViewZeroWidthConstraint!, - publicLinkStatusIconViewZeroWidthConstraint!, - - cloudStatusIconView.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: spacing), - cloudStatusIconViewRightMarginConstraint!, - sharedStatusIconViewRightMarginConstraint!, - publicLinkStatusIconViewRightMarginConstraint!, - - titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalLabelMargin), - titleLabel.bottomAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: -verticalLabelMarginFromCenter), - detailLabel.topAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: verticalLabelMarginFromCenter), - detailLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalLabelMargin), - - cloudStatusIconView.centerYAnchor.constraint(equalTo: detailLabel.centerYAnchor), - sharedStatusIconView.centerYAnchor.constraint(equalTo: detailLabel.centerYAnchor), - publicLinkStatusIconView.centerYAnchor.constraint(equalTo: detailLabel.centerYAnchor), - - cloudStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), - sharedStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), - publicLinkStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), - - moreButton.topAnchor.constraint(equalTo: self.contentView.topAnchor), - moreButton.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), - moreButtonWidthConstraint!, - moreButton.trailingAnchor.constraint(equalTo: revealButton.leadingAnchor), - - revealButton.topAnchor.constraint(equalTo: self.contentView.topAnchor), - revealButton.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), - revealButtonWidthConstraint!, - revealButton.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor), - - messageButton.leadingAnchor.constraint(equalTo: moreButton.leadingAnchor), - messageButton.trailingAnchor.constraint(equalTo: moreButton.trailingAnchor), - messageButton.topAnchor.constraint(equalTo: moreButton.topAnchor), - messageButton.bottomAnchor.constraint(equalTo: moreButton.bottomAnchor) - ]) - - self.accessibilityElements = [titleLabel, detailLabel, moreButton, revealButton] - } - - // MARK: - Present item - open var item : OCItem? { - didSet { - localID = item?.localID as NSString? - - if let newItem = item { - updateWith(newItem) - } - } - } - - open func titleLabelString(for item: OCItem?) -> NSAttributedString { - guard let item = item else { return NSAttributedString(string: "") } - - if item.type == .file, let itemName = item.baseName, let itemExtension = item.fileExtension { - return NSMutableAttributedString() - .appendBold(itemName) - .appendNormal(".") - .appendNormal(itemExtension) - } else if item.type == .collection, let itemName = item.name { - return NSMutableAttributedString() - .appendBold(itemName) - } - - return NSAttributedString(string: "") - } - - open func detailLabelString(for item: OCItem?) -> String { - if let item = item { - var size: String = item.sizeLocalized - - if item.size < 0 { - size = "Pending".localized - } - - return size + " - " + item.lastModifiedLocalized - } - - return "" - } - - open func updateWith(_ item: OCItem) { - // Cancel any already active request - if let activeThumbnailRequest = activeThumbnailRequest { - core?.vault.resourceManager?.stop(activeThumbnailRequest) - self.activeThumbnailRequest = nil - } - - // Set has message - self.hasMessageForItem = delegate?.hasMessage(for: item) ?? false - - // Set the icon and initiate thumbnail generation - let thumbnailRequest = OCResourceRequestItemThumbnail.request(for: item, maximumSize: self.thumbnailSize, scale: 0, waitForConnectivity: true, changeHandler: nil) - - iconView.request = thumbnailRequest - - // Start new thumbnail request - core?.vault.resourceManager?.start(thumbnailRequest) - - self.accessoryType = .none - - if item.isSharedWithUser || item.sharedByUserOrGroup { - sharedStatusIconView.image = UIImage(named: "group") - sharedStatusIconViewRightMarginConstraint?.constant = smallSpacing - sharedStatusIconViewZeroWidthConstraint?.isActive = false - } else { - sharedStatusIconView.image = nil - sharedStatusIconViewRightMarginConstraint?.constant = 0 - sharedStatusIconViewZeroWidthConstraint?.isActive = true - } - sharedStatusIconView.invalidateIntrinsicContentSize() - - if item.sharedByPublicLink { - publicLinkStatusIconView.image = UIImage(named: "link") - publicLinkStatusIconViewRightMarginConstraint?.constant = smallSpacing - publicLinkStatusIconViewZeroWidthConstraint?.isActive = false - } else { - publicLinkStatusIconView.image = nil - publicLinkStatusIconViewRightMarginConstraint?.constant = 0 - publicLinkStatusIconViewZeroWidthConstraint?.isActive = true - } - publicLinkStatusIconView.invalidateIntrinsicContentSize() - - self.updateCloudStatusIcon(with: item) - - self.updateLabels(with: item) - - self.iconView.alpha = item.isPlaceholder ? 0.5 : 1.0 - self.moreButton.isHidden = (item.isPlaceholder || (progressView != nil)) ? true : false - - self.moreButton.accessibilityLabel = "Actions".localized - self.moreButton.accessibilityIdentifier = (item.name != nil) ? (item.name! + " " + "Actions".localized) : "Actions".localized - - self.updateStatus() - } - - open func updateCloudStatusIcon(with item: OCItem?) { - var cloudStatusIcon : UIImage? - var cloudStatusIconAlpha : CGFloat = 1.0 - - if let item = item { - let availableOfflineCoverage : OCCoreAvailableOfflineCoverage = core?.availableOfflinePolicyCoverage(of: item) ?? .none - - switch availableOfflineCoverage { - case .direct, .none: cloudStatusIconAlpha = 1.0 - case .indirect: cloudStatusIconAlpha = 0.5 - } - - if item.type == .file { - switch item.cloudStatus { - case .cloudOnly: - cloudStatusIcon = UIImage(named: "cloud-only") - cloudStatusIconAlpha = 1.0 - - case .localCopy: - cloudStatusIcon = (item.downloadTriggerIdentifier == OCItemDownloadTriggerID.availableOffline) ? UIImage(named: "cloud-available-offline") : nil - - case .locallyModified, .localOnly: - cloudStatusIcon = UIImage(named: "cloud-local-only") - cloudStatusIconAlpha = 1.0 - } - } else { - if availableOfflineCoverage == .none { - cloudStatusIcon = nil - } else { - cloudStatusIcon = UIImage(named: "cloud-available-offline") - } - } - } - - cloudStatusIconView.image = cloudStatusIcon - cloudStatusIconView.alpha = cloudStatusIconAlpha - - cloudStatusIconViewZeroWidthConstraint?.isActive = (cloudStatusIcon == nil) - cloudStatusIconViewRightMarginConstraint?.constant = (cloudStatusIcon == nil) ? 0 : smallSpacing - - cloudStatusIconView.invalidateIntrinsicContentSize() - } - - open func updateLabels(with item: OCItem?) { - self.titleLabel.attributedText = titleLabelString(for: item) - self.detailLabel.text = detailLabelString(for: item) - } - - // MARK: - Available offline tracking - @objc open func updateAvailableOfflineStatus(_ notification: Notification) { - OnMainThread { [weak self] in - self?.updateCloudStatusIcon(with: self?.item) - } - } - - // MARK: - Has Message tracking - @objc open func updateHasMessage(_ notification: Notification) { - if let notificationCore = notification.object as? OCCore, let core = self.core, notificationCore === core { - OnMainThread { [weak self] in - let oldMessageForItem = self?.hasMessageForItem ?? false - - if let item = self?.item, let hasMessage = self?.delegate?.hasMessage(for: item) { - self?.hasMessageForItem = hasMessage - } else { - self?.hasMessageForItem = false - } - - if oldMessageForItem != self?.hasMessageForItem { - self?.updateStatus() - } - } - } - } - - // MARK: - Progress - open var localID : OCLocalID? { - willSet { - if localID != nil { - NotificationCenter.default.removeObserver(self, name: .OCCoreItemChangedProgress, object: nil) - } - } - - didSet { - if localID != nil { - NotificationCenter.default.addObserver(self, selector: #selector(progressChangedForItem(_:)), name: .OCCoreItemChangedProgress, object: nil) - } - } - } - - @objc open func progressChangedForItem(_ notification : Notification) { - if notification.object as? NSString == localID { - OnMainThread { - self.updateStatus() - } - } - } - - open func updateStatus() { - var progress : Progress? - - if let item = item, (item.syncActivity.rawValue & (OCItemSyncActivity.downloading.rawValue | OCItemSyncActivity.uploading.rawValue) != 0), !hasMessageForItem { - progress = self.core?.progress(for: item, matching: .none)?.first - - if progress == nil { - progress = Progress.indeterminate() - } - } - - if progress != nil { - if progressView == nil { - let progressView = ProgressView() - progressView.contentMode = .center - progressView.translatesAutoresizingMaskIntoConstraints = false - - self.contentView.addSubview(progressView) - - NSLayoutConstraint.activate([ - progressView.leftAnchor.constraint(equalTo: moreButton.leftAnchor), - progressView.rightAnchor.constraint(equalTo: moreButton.rightAnchor), - progressView.topAnchor.constraint(equalTo: moreButton.topAnchor), - progressView.bottomAnchor.constraint(equalTo: moreButton.bottomAnchor) - ]) - - self.progressView = progressView - } - - self.progressView?.progress = progress - - moreButton.isHidden = true - messageButton.isHidden = true - } else { - moreButton.isHidden = hasMessageForItem - messageButton.isHidden = !hasMessageForItem - - progressView?.removeFromSuperview() - progressView = nil - } - } - - // MARK: - Themeing - open var revealHighlight : Bool = false { - didSet { - if revealHighlight { - Log.debug("Highlighted!") - } - - applyThemeCollectionToCellContents(theme: Theme.shared, collection: Theme.shared.activeCollection) - } - } - - override open func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection) { - let itemState = ThemeItemState(selected: self.isSelected) - - titleLabel.applyThemeCollection(collection, itemStyle: .title, itemState: itemState) - detailLabel.applyThemeCollection(collection, itemStyle: .message, itemState: itemState) - - sharedStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor - publicLinkStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor - cloudStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor - detailLabel.textColor = collection.tableRowColors.secondaryLabelColor - - moreButton.tintColor = collection.tableRowColors.secondaryLabelColor - - if revealHighlight { - backgroundColor = collection.tableRowHighlightColors.backgroundColor?.withAlphaComponent(0.5) - } else { - backgroundColor = collection.tableBackgroundColor - } - } - - // MARK: - Editing mode - open func setMoreButton(hidden:Bool, animated: Bool = false) { - if hidden || isMoreButtonPermanentlyHidden { - moreButtonWidthConstraint?.constant = 0 - } else { - moreButtonWidthConstraint?.constant = showRevealButton ? revealButtonWidth : moreButtonWidth - } - moreButton.isHidden = ((item?.isPlaceholder == true) || (progressView != nil)) ? true : hidden - if animated { - UIView.animate(withDuration: 0.25) { - self.contentView.layoutIfNeeded() - } - } else { - self.contentView.layoutIfNeeded() - } - } - - var showRevealButton : Bool = false { - didSet { - if showRevealButton != oldValue { - self.setRevealButton(hidden: !showRevealButton, animated: false) - } - } - } - - open func setRevealButton(hidden:Bool, animated: Bool = false) { - if hidden { - revealButtonWidthConstraint?.constant = 0 - } else { - revealButtonWidthConstraint?.constant = revealButtonWidth - } - revealButton.isHidden = hidden - if animated { - UIView.animate(withDuration: 0.25) { - self.contentView.layoutIfNeeded() - } - } else { - self.contentView.layoutIfNeeded() - } - } - - override open func setEditing(_ editing: Bool, animated: Bool) { - super.setEditing(editing, animated: animated) - - setMoreButton(hidden: editing, animated: animated) - setRevealButton(hidden: editing ? true : !showRevealButton, animated: animated) - } - - // MARK: - Actions - @objc open func moreButtonTapped() { - self.delegate?.moreButtonTapped(cell: self) - } - @objc open func messageButtonTapped() { - self.delegate?.messageButtonTapped(cell: self) - } - @objc open func revealButtonTapped() { - self.delegate?.revealButtonTapped(cell: self) - } -} - -public extension NSNotification.Name { - static let ClientSyncRecordIDsWithMessagesChanged = NSNotification.Name(rawValue: "client-sync-record-ids-with-messages-changed") -} diff --git a/ownCloudAppShared/Client/User Interface/ComposedMessageView.swift b/ownCloudAppShared/Client/User Interface/ComposedMessageView.swift index 0b77fbb66..f986f4132 100644 --- a/ownCloudAppShared/Client/User Interface/ComposedMessageView.swift +++ b/ownCloudAppShared/Client/User Interface/ComposedMessageView.swift @@ -29,6 +29,7 @@ public class ComposedMessageElement: NSObject { case progressCircle(progress: Progress? = nil) case activityIndicator(style: UIActivityIndicatorView.Style = .medium, size: CGSize) case spacing(size: CGFloat) + case button(action: UIAction) } public enum Alignment { @@ -40,7 +41,11 @@ public class ComposedMessageElement: NSObject { public var kind: Kind public var alignment: Alignment - public var text: String? + public var text: String? { + didSet { + textView?.text = text + } + } public var font: UIFont? public var style: ThemeItemStyle? public var textView: UILabel? @@ -141,6 +146,12 @@ public class ComposedMessageElement: NSObject { NSLayoutConstraint.activate(constraints) + add(applier: { [weak self] theme, collection, event in + if let self = self, let imageView = self.imageView { + imageView.tintColor = collection.tintColor + } + }) + _view = rootView case .divider: @@ -265,6 +276,20 @@ public class ComposedMessageElement: NSObject { spacingView.heightAnchor.constraint(equalToConstant: spacing).isActive = true _view = spacingView + + case .button(let action): + let button = ThemeButton() + button.translatesAutoresizingMaskIntoConstraints = false + // button.setTitle(text, for: .normal) + + var buttonConfig = UIButton.Configuration.filled() + buttonConfig.title = text + buttonConfig.cornerStyle = .large + button.configuration = buttonConfig + + button.addAction(action, for: .primaryActionTriggered) + + _view = button } } @@ -296,7 +321,7 @@ public class ComposedMessageElement: NSObject { } } - init(kind: Kind, alignment: Alignment, insets altInsets: NSDirectionalEdgeInsets? = nil) { + public init(kind: Kind, alignment: Alignment, insets altInsets: NSDirectionalEdgeInsets? = nil) { self.kind = kind self.alignment = alignment super.init() @@ -331,6 +356,13 @@ public class ComposedMessageElement: NSObject { return element } + static public func button(_ title: String, action: UIAction, alignment: Alignment = .leading, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + let element = ComposedMessageElement(kind: .button(action: action), alignment: alignment, insets: altInsets) + element.text = title + + return element + } + static public func image(_ image: UIImage, size: CGSize?, adaptSizeToRatio: Bool = false, alignment: Alignment = .centered, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { return ComposedMessageElement(kind: .image(image: image, imageSize: size, adaptSizeToRatio: adaptSizeToRatio), alignment: alignment, insets: altInsets) } @@ -384,7 +416,7 @@ public class ComposedMessageView: UIView, Themeable { } } - init(elements: [ComposedMessageElement]) { + public init(elements: [ComposedMessageElement]) { super.init(frame: .zero) self.translatesAutoresizingMaskIntoConstraints = false @@ -511,4 +543,15 @@ public extension ComposedMessageView { return infoBoxView } + + static func sectionHeader(titled title: String) -> ComposedMessageView { + let headerView = ComposedMessageView(elements: [ + .spacing(10), + .title(title, alignment: .leading, insets: .zero) + ]) + headerView.elementInsets.leading = 15 + headerView.elementInsets.bottom = 5 + + return headerView + } } diff --git a/ownCloudAppShared/Client/User Interface/GradientView.swift b/ownCloudAppShared/Client/User Interface/GradientView.swift index 4dcd6a4e4..7d461417f 100644 --- a/ownCloudAppShared/Client/User Interface/GradientView.swift +++ b/ownCloudAppShared/Client/User Interface/GradientView.swift @@ -19,6 +19,24 @@ import UIKit public class GradientView : UIView { + public enum Direction { + case vertical + case horizontal + + var startPoint: CGPoint { + switch self { + case .vertical: return CGPoint(x: 0.5, y: 0.0) + case .horizontal: return CGPoint(x: 0.0, y: 0.5) + } + } + var endPoint: CGPoint { + switch self { + case .vertical: return CGPoint(x: 0.5, y: 1.0) + case .horizontal: return CGPoint(x: 1.0, y: 0.5) + } + } + } + public var colors: [CGColor] { didSet { gradientLayer?.colors = colors @@ -32,7 +50,7 @@ public class GradientView : UIView { var gradientLayer : CAGradientLayer? - public init(with colors: [CGColor], locations: [NSNumber]) { + public init(with colors: [CGColor], locations: [NSNumber], direction: Direction = .vertical) { self.colors = colors self.locations = locations @@ -43,6 +61,8 @@ public class GradientView : UIView { gradientLayer = CAGradientLayer() gradientLayer?.colors = colors gradientLayer?.locations = locations + gradientLayer?.startPoint = direction.startPoint + gradientLayer?.endPoint = direction.endPoint } required public init?(coder: NSCoder) { diff --git a/ownCloud/Issues/IssuesCardViewController.swift b/ownCloudAppShared/Client/User Interface/IssuesCardViewController.swift similarity index 86% rename from ownCloud/Issues/IssuesCardViewController.swift rename to ownCloudAppShared/Client/User Interface/IssuesCardViewController.swift index 5dabe7554..300aa53cf 100644 --- a/ownCloud/Issues/IssuesCardViewController.swift +++ b/ownCloudAppShared/Client/User Interface/IssuesCardViewController.swift @@ -17,9 +17,8 @@ import UIKit import ownCloudSDK -import ownCloudAppShared -class CardCellBackgroundView : UIView { +public class CardCellBackgroundView : UIView { init(backgroundColor: UIColor, insets: NSDirectionalEdgeInsets, cornerRadius: CGFloat) { super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) @@ -37,10 +36,10 @@ class CardCellBackgroundView : UIView { } } -class CardHeaderView : UIView, Themeable { - var label : UILabel +public class CardHeaderView : UIView, Themeable { + public var label : UILabel - init(title: String) { + public init(title: String) { label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -71,7 +70,7 @@ class CardHeaderView : UIView, Themeable { Theme.shared.unregister(client: self) } - override func didMoveToSuperview() { + override public func didMoveToSuperview() { super.didMoveToSuperview() if self.superview != nil { @@ -79,7 +78,7 @@ class CardHeaderView : UIView, Themeable { } } - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize * 1.4, weight: .bold) label.textColor = collection.tableRowColors.labelColor @@ -87,15 +86,15 @@ class CardHeaderView : UIView, Themeable { } } -enum IssueUserResponse { +public enum IssueUserResponse { case cancel case approve case dismiss } -class IssuesCardViewController: StaticTableViewController { - typealias CompletionHandler = (IssueUserResponse) -> Void - typealias DismissHandler = () -> Void +open class IssuesCardViewController: StaticTableViewController { + public typealias CompletionHandler = (IssueUserResponse) -> Void + public typealias DismissHandler = () -> Void var issues : DisplayIssues var headerTitle : String? @@ -105,7 +104,7 @@ class IssuesCardViewController: StaticTableViewController { private var completionHandler : CompletionHandler? private var dismissHandler : DismissHandler? - required init(with issue: OCIssue, displayIssues: DisplayIssues? = nil, bookmark: OCBookmark? = nil, completion:@escaping CompletionHandler, dismissed: DismissHandler? = nil) { + required public init(with issue: OCIssue, displayIssues: DisplayIssues? = nil, bookmark: OCBookmark? = nil, completion:@escaping CompletionHandler, dismissed: DismissHandler? = nil) { issues = (displayIssues != nil) ? displayIssues! : issue.prepareForDisplay() options = [] completionHandler = completion @@ -189,9 +188,15 @@ class IssuesCardViewController: StaticTableViewController { let row = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in if issue.type == .certificate, let certificate = issue.certificate { - let certificateViewController = ThemeCertificateViewController(certificate: certificate, compare: bookmark?.certificate) + var compareCertificate: OCCertificate? - if bookmark?.certificate != nil { + if let hostname = issue.certificateURL?.host, let certificateStore = bookmark?.certificateStore { + compareCertificate = certificateStore.certificate(forHostname: hostname, lastModified: nil) + } + + let certificateViewController = ThemeCertificateViewController(certificate: certificate, compare: compareCertificate) + + if compareCertificate != nil { certificateViewController.showDifferences = true } @@ -231,11 +236,11 @@ class IssuesCardViewController: StaticTableViewController { self.tableView.separatorStyle = .none } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - static func present(on hostViewController: UIViewController, issue: OCIssue, displayIssues: DisplayIssues? = nil, bookmark: OCBookmark? = nil, completion:@escaping CompletionHandler, dismissed: DismissHandler? = nil) { + static public func present(on hostViewController: UIViewController, issue: OCIssue, displayIssues: DisplayIssues? = nil, bookmark: OCBookmark? = nil, completion:@escaping CompletionHandler, dismissed: DismissHandler? = nil) { let issuesViewController = self.init(with: issue, displayIssues: displayIssues, bookmark: bookmark, completion: completion, dismissed: dismissed) let headerView = CardHeaderView(title: issuesViewController.headerTitle ?? "") @@ -258,7 +263,7 @@ class IssuesCardViewController: StaticTableViewController { self.presentingViewController?.dismiss(animated: true) } - override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + override public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { super.applyThemeCollection(theme: theme, collection: collection, event: event) tableView.backgroundColor = collection.tableBackgroundColor diff --git a/ownCloudAppShared/Client/User Interface/PopupButtonController.swift b/ownCloudAppShared/Client/User Interface/PopupButtonController.swift index 83a2c8d12..c9587ee9a 100644 --- a/ownCloudAppShared/Client/User Interface/PopupButtonController.swift +++ b/ownCloudAppShared/Client/User Interface/PopupButtonController.swift @@ -38,7 +38,7 @@ open class PopupButtonChoice : NSObject { } } -open class PopupButtonController : NSObject { +open class PopupButtonController : NSObject, Themeable { typealias TitleCustomizer = (_ choice: PopupButtonChoice, _ isSelected: Bool) -> String typealias SelectionCustomizer = (_ choice: PopupButtonChoice, _ isSelected: Bool) -> Bool typealias ChoiceHandler = (_ choice: PopupButtonChoice, _ wasSelected: Bool) -> Void @@ -148,6 +148,8 @@ open class PopupButtonController : NSObject { ]) _updateTitleFromSelectedChoice() + + Theme.shared.register(client: self, applyImmediately: true) } private func _updateTitleFromSelectedChoice() { @@ -188,4 +190,8 @@ open class PopupButtonController : NSObject { button.sizeToFit() } } + + open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + button.applyThemeCollection(collection) + } } diff --git a/ownCloud/UI Elements/RoundedLabel.swift b/ownCloudAppShared/Client/User Interface/RoundedLabel.swift similarity index 82% rename from ownCloud/UI Elements/RoundedLabel.swift rename to ownCloudAppShared/Client/User Interface/RoundedLabel.swift index 08a68df10..0faa69fee 100644 --- a/ownCloud/UI Elements/RoundedLabel.swift +++ b/ownCloudAppShared/Client/User Interface/RoundedLabel.swift @@ -18,18 +18,18 @@ import UIKit -class RoundedLabel: UIView { +public class RoundedLabel: UIView { - struct Style { - var textColor : UIColor - var backgroundColor : UIColor + public struct Style { + public var textColor : UIColor + public var backgroundColor : UIColor - var horizontalPadding : CGFloat - var verticalPadding : CGFloat - var cornerRadius : CGFloat + public var horizontalPadding : CGFloat + public var verticalPadding : CGFloat + public var cornerRadius : CGFloat - static var token = Style(textColor: .white, backgroundColor: .red, horizontalPadding: 5, verticalPadding: 2, cornerRadius: 5) - static var round = Style(textColor: .white, backgroundColor: .red, horizontalPadding: 10, verticalPadding: 5, cornerRadius: -1) + static public var token = Style(textColor: .white, backgroundColor: .red, horizontalPadding: 5, verticalPadding: 2, cornerRadius: 5) + static public var round = Style(textColor: .white, backgroundColor: .red, horizontalPadding: 10, verticalPadding: 5, cornerRadius: -1) } // MARK: - Constants @@ -58,12 +58,12 @@ class RoundedLabel: UIView { // MARK: - Init & Deinit - init() { + public init() { super.init(frame: CGRect.zero) styleView() } - init(text: String = "", style: Style = .token) { + public init(text: String = "", style: Style = .token) { super.init(frame: CGRect.zero) labelText = text @@ -77,7 +77,7 @@ class RoundedLabel: UIView { styleView() } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index fd350ca2f..84f058a5f 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -7,60 +7,24 @@ // /* -* 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 . -* -*/ + * 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 -public class SegmentedControl: UISegmentedControl { - var oldValue : Int! - - public override func touchesBegan(_ touches: Set, with event: UIEvent? ) { - self.oldValue = self.selectedSegmentIndex - super.touchesBegan(touches, with: event) - } - - public override func touchesEnded(_ touches: Set, with event: UIEvent? ) { - super.touchesEnded(touches, with: event ) - - if self.oldValue == self.selectedSegmentIndex { - sendActions(for: UIControl.Event.valueChanged) - } - } -} - -public enum SortBarSearchScope : Int, CaseIterable { - case global - case local - - var label : String { - var name : String! - - switch self { - case .global: name = "Account".localized - case .local: name = "Folder".localized - } - - return name - } -} - public protocol SortBarDelegate: AnyObject { var sortDirection: SortDirection { get set } var sortMethod: SortMethod { get set } - var searchScope: SortBarSearchScope { get set } func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) - func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SortBarSearchScope) - func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) func toggleSelectMode() @@ -80,13 +44,11 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate let rightPadding: CGFloat = 20.0 let rightSelectButtonPadding: CGFloat = 8.0 let rightSearchScopePadding: CGFloat = 15.0 - let topPadding: CGFloat = 2.0 - let bottomPadding: CGFloat = 2.0 + let topPadding: CGFloat = 10.0 + let bottomPadding: CGFloat = 10.0 // MARK: - Instance variables. - public var sortButton: UIButton? - public var searchScopeSegmentedControl : SegmentedControl? public var selectButton: UIButton? public var allowMultiSelect: Bool = true { didSet { @@ -109,30 +71,6 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate UIAccessibility.post(notification: .layoutChanged, argument: nil) } - var showSearchScope: Bool = false { - didSet { - showSelectButton = !self.showSearchScope - self.searchScopeSegmentedControl?.isHidden = false - self.searchScopeSegmentedControl?.alpha = oldValue ? 1.0 : 0.0 - - // Woraround for Accessibility: remove all elements, when element is hidden, otherwise the elements are still available for accessibility - if oldValue == false { - for scope in SortBarSearchScope.allCases { - searchScopeSegmentedControl?.insertSegment(withTitle: scope.label, at: scope.rawValue, animated: false) - } - searchScopeSegmentedControl?.selectedSegmentIndex = searchScope.rawValue - } else { - self.searchScopeSegmentedControl?.removeAllSegments() - } - - UIView.animate(withDuration: 0.3, animations: { - self.searchScopeSegmentedControl?.alpha = self.showSearchScope ? 1.0 : 0.0 - }, completion: { (_) in - self.searchScopeSegmentedControl?.isHidden = !self.showSearchScope - }) - } - } - public var sortMethod: SortMethod { didSet { if self.superview != nil { // Only toggle direction if the view is already in the view hierarchy (i.e. not during initial setup) @@ -155,47 +93,24 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } } - public var searchScope : SortBarSearchScope { - didSet { - delegate?.searchScope = searchScope - searchScopeSegmentedControl?.selectedSegmentIndex = searchScope.rawValue - } - } - // MARK: - Init & Deinit - - public init(frame: CGRect = .zero, sortMethod: SortMethod, searchScope: SortBarSearchScope = .local) { + public init(frame: CGRect = .zero, sortMethod: SortMethod) { selectButton = UIButton() sortButton = UIButton(type: .system) - searchScopeSegmentedControl = SegmentedControl() self.sortMethod = sortMethod - self.searchScope = searchScope super.init(frame: frame) - if let sortButton = sortButton, let searchScopeSegmentedControl = searchScopeSegmentedControl, let selectButton = selectButton { + if let sortButton, let selectButton { sortButton.translatesAutoresizingMaskIntoConstraints = false selectButton.translatesAutoresizingMaskIntoConstraints = false - searchScopeSegmentedControl.translatesAutoresizingMaskIntoConstraints = false sortButton.accessibilityIdentifier = "sort-bar.sortButton" - searchScopeSegmentedControl.accessibilityIdentifier = "sort-bar.searchScopeSegmentedControl" - searchScopeSegmentedControl.accessibilityLabel = "Search scope".localized - searchScopeSegmentedControl.isHidden = !self.showSearchScope - searchScopeSegmentedControl.addTarget(self, action: #selector(searchScopeValueChanged), for: .valueChanged) self.addSubview(sortButton) - self.addSubview(searchScopeSegmentedControl) self.addSubview(selectButton) - // Sort segmented control - NSLayoutConstraint.activate([ - searchScopeSegmentedControl.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: -rightSearchScopePadding), - searchScopeSegmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: topPadding), - searchScopeSegmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -bottomPadding) - ]) - // Sort Button sortButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline) sortButton.titleLabel?.adjustsFontForContentSizeCategory = true @@ -267,11 +182,9 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } // MARK: - Theme support - public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { self.sortButton?.applyThemeCollection(collection) self.selectButton?.applyThemeCollection(collection) - self.searchScopeSegmentedControl?.applyThemeCollection(collection) self.backgroundColor = collection.tableRowColors.backgroundColor } @@ -290,13 +203,6 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } // MARK: - Actions - @objc private func searchScopeValueChanged() { - if let selectedIndex = searchScopeSegmentedControl?.selectedSegmentIndex { - self.searchScope = SortBarSearchScope(rawValue: selectedIndex)! - delegate?.sortBar(self, didUpdateSearchScope: self.searchScope) - } - } - @objc private func toggleSelectMode() { delegate?.toggleSelectMode() } diff --git a/ownCloudAppShared/Client/View Controllers/ClientDefaultViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientDefaultViewController.swift new file mode 100644 index 000000000..71aa72998 --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/ClientDefaultViewController.swift @@ -0,0 +1,43 @@ +// +// ClientDefaultViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 28.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ownCloudApp + +public class ClientDefaultViewController: UIViewController, Themeable { + public override func loadView() { + let rootView = UIView() + + let logoView = VectorImageView() + logoView.translatesAutoresizingMaskIntoConstraints = false + logoView.vectorImage = Theme.shared.tvgImage(for: "owncloud-logo") + + rootView.embed(centered: logoView, minimumInsets: NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), fixedSize: CGSize(width: 480, height: 480), minimumSize: CGSize(width: 240, height: 180), maximumSize: CGSize(width: 280, height: 280)) + + self.view = rootView + } + + public override func viewDidLoad() { + super.viewDidLoad() + Theme.shared.register(client: self, applyImmediately: true) + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.view.backgroundColor = collection.tableBackgroundColor + } +} diff --git a/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift similarity index 60% rename from ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift rename to ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift index db024d51b..27c39f141 100644 --- a/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift +++ b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift @@ -26,45 +26,69 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, case loading case empty + case removed case hasContent case searchNonItemContent } public var query: OCQuery? + private var _itemsDatasource: OCDataSource? // stores the data source passed to init (if any) - public var itemsLeadInDataSource : OCDataSourceArray = OCDataSourceArray() - public var itemsQueryDataSource : OCDataSource? - public var itemsTrailingDataSource : OCDataSourceArray = OCDataSourceArray() - public var itemSectionDataSource : OCDataSourceComposition? - public var itemSection : CollectionViewSection? + public var itemsLeadInDataSource: OCDataSourceArray = OCDataSourceArray() + public var itemsListDataSource: OCDataSource? // typically query.queryResultsDataSource or .itemsDatasource + public var itemsTrailingDataSource: OCDataSourceArray = OCDataSourceArray() + public var itemSectionDataSource: OCDataSourceComposition? + public var itemSection: CollectionViewSection? - public var driveSection : CollectionViewSection? + public var driveSection: CollectionViewSection? - public var driveSectionDataSource : OCDataSourceComposition? - public var singleDriveDatasource : OCDataSourceComposition? - private var singleDriveDatasourceSubscription : OCDataSourceSubscription? - public var driveAdditionalItemsDataSource : OCDataSourceArray = OCDataSourceArray() + public var driveSectionDataSource: OCDataSourceComposition? + public var singleDriveDatasource: OCDataSourceComposition? + private var singleDriveDatasourceSubscription: OCDataSourceSubscription? + public var driveAdditionalItemsDataSource: OCDataSourceArray = OCDataSourceArray() - public var emptyItemListDataSource : OCDataSourceArray = OCDataSourceArray() - public var emptyItemListDecisionSubscription : OCDataSourceSubscription? - public var emptyItemListItem : ComposedMessageView? + public var emptyItemListDataSource: OCDataSourceArray = OCDataSourceArray() + public var emptyItemListDecisionSubscription: OCDataSourceSubscription? + public var emptyItemListItem: ComposedMessageView? public var emptySectionDataSource: OCDataSourceComposition? public var emptySection: CollectionViewSection? - public var loadingListItem : ComposedMessageView? + public var loadingListItem: ComposedMessageView? + public var folderRemovedListItem: ComposedMessageView? + public var footerItem: UIView? + public var footerFolderStatisticsLabel: UILabel? - private var stateObservation : NSKeyValueObservation? - private var queryRootItemObservation : NSKeyValueObservation? + public var location: OCLocation? - public init(context inContext: ClientContext?, query inQuery: OCQuery, highlightItemReference: OCDataItemReference? = nil) { + private var stateObservation: NSKeyValueObservation? + private var queryStateObservation: NSKeyValueObservation? + private var queryRootItemObservation: NSKeyValueObservation? + + var navigationTitleLabel: UILabel = UILabel() + + private var viewControllerUUID: UUID + + public init(context inContext: ClientContext?, query inQuery: OCQuery?, itemsDatasource inDataSource: OCDataSource? = nil, location: OCLocation? = nil, highlightItemReference: OCDataItemReference? = nil, showRevealButtonForItems: Bool = false, emptyItemListIcon: UIImage? = nil, emptyItemListTitleLocalized: String? = nil, emptyItemListMessageLocalized: String? = nil) { + inQuery?.queryResultsDataSourceIncludesStatistics = true query = inQuery + _itemsDatasource = inDataSource + + self.location = location var sections : [ CollectionViewSection ] = [] + let vcUUID = UUID() + viewControllerUUID = vcUUID + let itemControllerContext = ClientContext(with: inContext, modifier: { context in // Add permission handler limiting interactions for specific items and scenarios - context.add(permissionHandler: { (context, record, interaction) in + context.add(permissionHandler: { (context, record, interaction, viewController) in + guard let viewController = viewController as? ClientItemViewController, viewController.viewControllerUUID == vcUUID else { + // Only apply this permission handler to this view controller, otherwise -> just pass through + return true + } + switch interaction { case .selection: if record?.type == .drive { @@ -94,6 +118,16 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, return true } }) + + // Set .drive based on location.driveID + if let driveID = location?.driveID, let core = context.core { + context.drive = core.drive(withIdentifier: driveID) + } + + // Use inDataSource as queryDatasource if no query was provided + if inQuery == nil, let inDataSource { + context.queryDatasource = inDataSource + } }) itemControllerContext.postInitializationModifier = { (owner, context) in if context.openItemHandler == nil { @@ -119,15 +153,16 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, context.originatingViewController = owner as? UIViewController } - if let queryResultsDatasource = query?.queryResultsDataSource, let core = itemControllerContext.core { - itemsQueryDataSource = queryResultsDatasource - singleDriveDatasource = OCDataSourceComposition(sources: [core.drivesDataSource]) + if let contentsDataSource = query?.queryResultsDataSource ?? _itemsDatasource, let core = itemControllerContext.core { + itemsListDataSource = contentsDataSource - if query?.queryLocation?.isRoot == true { + if query?.queryLocation?.isRoot == true, core.useDrives { // Create data source from one drive + singleDriveDatasource = OCDataSourceComposition(sources: [core.drivesDataSource]) singleDriveDatasource?.filter = OCDataSourceComposition.itemFilter(withItemRetrieval: false, fromRecordFilter: { itemRecord in if let drive = itemRecord?.item as? OCDrive { - if drive.identifier == itemControllerContext.drive?.identifier { + if drive.identifier == itemControllerContext.drive?.identifier, + drive.specialType == .space { // limit to spaces, do not show header for f.ex. the personal space or the Shares Jail space return true } } @@ -142,8 +177,14 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, driveSection = CollectionViewSection(identifier: "drive", dataSource: driveSectionDataSource, cellStyle: .init(with: .header), cellLayout: .list(appearance: .plain)) } - itemSectionDataSource = OCDataSourceComposition(sources: [itemsLeadInDataSource, queryResultsDatasource, itemsTrailingDataSource]) - itemSection = CollectionViewSection(identifier: "items", dataSource: itemSectionDataSource, cellLayout: .list(appearance: .plain, contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)), clientContext: itemControllerContext) + itemSectionDataSource = OCDataSourceComposition(sources: [itemsLeadInDataSource, contentsDataSource, itemsTrailingDataSource]) + let itemSectionCellStyle = CollectionViewCellStyle(from: .init(with: .tableCell), changing: { cellStyle in + if showRevealButtonForItems { + cellStyle.showRevealButton = true + } + }) + + itemSection = CollectionViewSection(identifier: "items", dataSource: itemSectionDataSource, cellStyle: itemSectionCellStyle, cellLayout: .list(appearance: .plain, contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)), clientContext: itemControllerContext) if let driveSection = driveSection { sections.append(driveSection) @@ -162,13 +203,23 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, super.init(context: itemControllerContext, sections: sections, useStackViewRoot: true, highlightItemReference: highlightItemReference) // Track query state and recompute content state when it changes - stateObservation = itemsQueryDataSource?.observe(\OCDataSource.state, options: [], changeHandler: { [weak self] query, change in + stateObservation = itemsListDataSource?.observe(\OCDataSource.state, options: [], changeHandler: { [weak self] query, change in + self?.recomputeContentState() + }) + + queryStateObservation = query?.observe(\OCQuery.state, options: [], changeHandler: { [weak self] query, change in self?.recomputeContentState() }) queryRootItemObservation = query?.observe(\OCQuery.rootItem, options: [], changeHandler: { [weak self] query, change in OnMainThread(inline: true) { self?.clientContext?.rootItem = query.rootItem + if self?.location != nil { + self?.location = query.rootItem?.location + self?.updateLocationBarViewController() + } + self?.updateNavigationTitleFromContext() + self?.refreshEmptyActions() } self?.recomputeContentState() }) @@ -176,14 +227,16 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // Subscribe to singleDriveDatasource for changes, to update driveSectionDataSource singleDriveDatasourceSubscription = singleDriveDatasource?.subscribe(updateHandler: { [weak self] (subscription) in self?.updateAdditionalDriveItems(from: subscription) - }, on: .main, trackDifferences: true, performIntialUpdate: true) + }, on: .main, trackDifferences: true, performInitialUpdate: true) + + if let queryDatasource = query?.queryResultsDataSource ?? inDataSource { + let emptyFolderMessage = emptyItemListMessageLocalized ?? "This folder is empty.".localized // "This folder is empty. Fill it with content:".localized - if let queryDatasource = query?.queryResultsDataSource { emptyItemListItem = ComposedMessageView(elements: [ - .image(UIImage(systemName: "folder.fill")!, size: CGSize(width: 64, height: 48), alignment: .centered), - .text("No contents".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered), - .spacing(25), - .text("This folder is empty. Fill it with content:".localized, style: .systemSecondary(textStyle: .body), alignment: .centered) + .image(emptyItemListIcon ?? OCSymbol.icon(forSymbolName: "folder.fill")!, size: CGSize(width: 64, height: 48), alignment: .centered), + .text(emptyItemListTitleLocalized ?? "No contents".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered), + .spacing(5), + .text(emptyFolderMessage, style: .systemSecondary(textStyle: .body), alignment: .centered) ]) emptyItemListItem?.elementInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 2, trailing: 0) @@ -199,17 +252,41 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, .text("Loading…".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered) ]) + folderRemovedListItem = ComposedMessageView(elements: [ + .image(OCSymbol.icon(forSymbolName: "nosign")!, size: CGSize(width: 64, height: 48), alignment: .centered), + .text("Folder removed".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered), + .spacing(5), + .text("This folder no longer exists on the server.".localized, style: .systemSecondary(textStyle: .body), alignment: .centered) + ]) + + footerItem = UIView() + footerItem?.translatesAutoresizingMaskIntoConstraints = false + + footerFolderStatisticsLabel = UILabel() + footerFolderStatisticsLabel?.translatesAutoresizingMaskIntoConstraints = false + footerFolderStatisticsLabel?.font = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize) + footerFolderStatisticsLabel?.textAlignment = .center + footerFolderStatisticsLabel?.setContentHuggingPriority(.required, for: .vertical) + footerFolderStatisticsLabel?.setContentCompressionResistancePriority(.required, for: .vertical) + footerFolderStatisticsLabel?.numberOfLines = 0 + footerFolderStatisticsLabel?.text = "-" + + footerItem?.embed(toFillWith: footerFolderStatisticsLabel!, insets: NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) + footerItem?.separatorLayoutGuideCustomizer = SeparatorLayoutGuideCustomizer(with: { viewCell, view in + return [ viewCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: viewCell.contentView.trailingAnchor) ] + }) + footerItem?.layoutIfNeeded() + emptyItemListDecisionSubscription = queryDatasource.subscribe(updateHandler: { [weak self] (subscription) in self?.updateEmptyItemList(from: subscription) - }, on: .main, trackDifferences: false, performIntialUpdate: true) + }, on: .main, trackDifferences: false, performInitialUpdate: true) } // Initialize sort method handleSortMethodChange() - if let navigationTitle = query?.queryLocation?.isRoot == true ? clientContext?.drive?.name : query?.queryLocation?.lastPathComponent { - navigationItem.title = navigationTitle - } + // Update title + updateNavigationTitleFromContext() } required public init?(coder: NSCoder) { @@ -219,80 +296,73 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, deinit { stateObservation?.invalidate() queryRootItemObservation?.invalidate() + queryStateObservation?.invalidate() singleDriveDatasourceSubscription?.terminate() } public override func viewDidLoad() { super.viewDidLoad() - var rightInset : CGFloat = 2 - var leftInset : CGFloat = 0 - if self.view.effectiveUserInterfaceLayoutDirection == .rightToLeft { - rightInset = 0 - leftInset = 2 - } - - var viewActionButtons : [UIBarButtonItem] = [] - - if query?.queryLocation != nil { - if clientContext?.moreItemHandler != nil { - let folderActionBarButton = UIBarButtonItem(image: UIImage(named: "more-dots")?.withInset(UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)), style: .plain, target: self, action: #selector(moreBarButtonPressed)) - folderActionBarButton.accessibilityIdentifier = "client.folder-action" - folderActionBarButton.accessibilityLabel = "Actions".localized - - viewActionButtons.append(folderActionBarButton) - } - - let plusBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) - plusBarButton.menu = UIMenu(title: "", children: [ - UIDeferredMenuElement.uncached({ [weak self] completion in - if let self = self, let rootItem = self.query?.rootItem, let clientContext = self.clientContext { - let contextMenuProvider = rootItem as DataItemContextMenuInteraction - - if let contextMenuElements = contextMenuProvider.composeContextMenuItems(in: self, location: .folderAction, with: clientContext) { - completion(contextMenuElements) - } - } - }) - ]) - plusBarButton.accessibilityIdentifier = "client.file-add" - - viewActionButtons.append(plusBarButton) - } - - // Add search button - let searchButton = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(startSearch)) - viewActionButtons.append(searchButton) - - self.navigationItem.rightBarButtonItems = viewActionButtons + // Add navigation bar button items + updateNavigationBarButtonItems() // Setup sort bar sortBar = SortBar(sortMethod: sortMethod) sortBar?.translatesAutoresizingMaskIntoConstraints = false sortBar?.delegate = self sortBar?.sortMethod = sortMethod - sortBar?.searchScope = searchScope sortBar?.showSelectButton = true - itemsLeadInDataSource.setVersionedItems([ sortBar! ]) + if let sortBar { + itemSection?.boundarySupplementaryItems = [ + .view(sortBar, pinned: true) + ] + } + + // itemsLeadInDataSource.setVersionedItems([ sortBar! ]) // Setup multiselect collectionView.allowsSelectionDuringEditing = true collectionView.allowsMultipleSelectionDuringEditing = true } + var locationBarViewController: ClientLocationBarController? { + willSet { + if let locationBarViewController { + removeStacked(child: locationBarViewController) + } + } + didSet { + if let locationBarViewController { + addStacked(child: locationBarViewController, position: .bottom) + } + } + } + + func updateLocationBarViewController() { + if let location, let clientContext { + self.locationBarViewController = ClientLocationBarController(clientContext: clientContext, location: location) + } else { + self.locationBarViewController = nil + } + } + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let query = query { + if let query { clientContext?.core?.start(query) } + + if locationBarViewController == nil { + updateLocationBarViewController() + } } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if let query = query { + if let query { clientContext?.core?.stop(query) } } @@ -306,6 +376,8 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, let drive = itemRecord.item as? OCDrive, let driveRepresentation = OCDataRenderer.default.renderItem(drive, asType: .presentable, error: nil) as? OCDataItemPresentable, let descriptionResourceRequest = try? driveRepresentation.provideResourceRequest(.coverDescription) { + driveQuota = drive.quota + descriptionResourceRequest.lifetime = .singleRun descriptionResourceRequest.changeHandler = { [weak self] (request, error, isOngoing, previousResource, newResource) in // Log.debug("REQ_Readme request: \(String(describing: request)) | error: \(String(describing: error)) | isOngoing: \(isOngoing) | newResource: \(String(describing: newResource))") @@ -322,7 +394,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // MARK: - Empty item list handling func emptyActions() -> [OCAction]? { - guard let context = clientContext, let core = context.core, let item = context.query?.rootItem else { + guard let context = clientContext, let core = context.core, let item = context.query?.rootItem, clientContext?.hasPermission(for: .addContent) == true else { return nil } let locationIdentifier: OCExtensionLocationIdentifier = .emptyFolder @@ -355,21 +427,23 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } } else { // Regular usage, use itemsQueryDataSource to determine state - switch self.itemsQueryDataSource?.state { + switch self.itemsListDataSource?.state { case .loading: self.contentState = .loading case .idle: - let numberOfItems = self.emptyItemListDecisionSubscription?.snapshotResettingChangeTracking(true).numberOfItems + let snapshot = self.emptyItemListDecisionSubscription?.snapshotResettingChangeTracking(true) + let numberOfItems = snapshot?.numberOfItems - if let numberOfItems = numberOfItems, numberOfItems > 0 { + if self.query?.state == .targetRemoved { + self.contentState = .removed + } else if let numberOfItems = numberOfItems, numberOfItems > 0 { self.contentState = .hasContent + self.folderStatistics = snapshot?.specialItems?[.folderStatistics] as? OCStatistic + } else if (numberOfItems == nil) || ((self.query?.rootItem == nil) && (self.query != nil) && (self.query?.isCustom != true)) { + self.contentState = .loading } else { - if (numberOfItems == nil) || (self.query?.rootItem == nil) { - self.contentState = .loading - } else { - self.contentState = .empty - } + self.contentState = .empty } default: break @@ -379,6 +453,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } private var hadRootItem: Bool = false + private var hadSearchActive: Bool? public var contentState : ContentState = .loading { didSet { let hasRootItem = (query?.rootItem != nil) @@ -386,27 +461,21 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, var itemSectionHiddenNew = false let emptySectionHidden = emptySection?.hidden var emptySectionHiddenNew = false + let changeFromOrToRemoved = ((contentState == .removed) || (oldValue == .removed)) && (oldValue != contentState) - if (contentState == oldValue) && (hadRootItem == hasRootItem) { + if (contentState == oldValue) && (hadRootItem == hasRootItem) && (hadSearchActive == searchActive) { return } hadRootItem = hasRootItem + hadSearchActive = searchActive switch contentState { case .empty: - var emptyItems : [OCDataItem] = [ ] - - if let emptyItemListItem = emptyItemListItem { - emptyItems.append(emptyItemListItem) - } - - if let emptyActions = emptyActions() { - emptyItems.append(contentsOf: emptyActions) - } - - emptyItemListDataSource.setItems(emptyItems, updated: nil) + refreshEmptyActions() + sortBar?.isHidden = true itemsLeadInDataSource.setVersionedItems([ ]) + itemsTrailingDataSource.setVersionedItems([ ]) case .loading: var loadingItems : [OCDataItem] = [ ] @@ -415,22 +484,50 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, loadingItems.append(loadingListItem) } emptyItemListDataSource.setItems(loadingItems, updated: nil) + sortBar?.isHidden = true + itemsLeadInDataSource.setVersionedItems([ ]) + itemsTrailingDataSource.setVersionedItems([ ]) + + case .removed: + var folderRemovedItems : [OCDataItem] = [ ] + + if let folderRemovedListItem = folderRemovedListItem { + folderRemovedItems.append(folderRemovedListItem) + } + emptyItemListDataSource.setItems(folderRemovedItems, updated: nil) + sortBar?.isHidden = true itemsLeadInDataSource.setVersionedItems([ ]) + itemsTrailingDataSource.setVersionedItems([ ]) case .hasContent: emptyItemListDataSource.setItems(nil, updated: nil) - if let sortBar = sortBar { - itemsLeadInDataSource.setVersionedItems([ sortBar ]) + sortBar?.isHidden = false +// if let sortBar = sortBar { +// itemsLeadInDataSource.setVersionedItems([ sortBar ]) +// } + + if searchActive == true { + itemsTrailingDataSource.setVersionedItems([ ]) + } else { + if let footerItem = footerItem { + itemsTrailingDataSource.setVersionedItems([ footerItem ]) + } } emptySectionHiddenNew = true case .searchNonItemContent: emptyItemListDataSource.setItems(nil, updated: nil) + sortBar?.isHidden = true itemsLeadInDataSource.setVersionedItems([ ]) + itemsTrailingDataSource.setVersionedItems([ ]) itemSectionHiddenNew = true } + if changeFromOrToRemoved { + updateNavigationBarButtonItems() + } + if (itemSectionHidden != itemSectionHiddenNew) || (emptySectionHidden != emptySectionHiddenNew) { updateSections(with: { sections in self.itemSection?.hidden = itemSectionHiddenNew @@ -440,7 +537,63 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } } - // MARK: - Navigation Bar Actions + // MARK: - Navigation Bar + open func updateNavigationBarButtonItems() { + var rightInset : CGFloat = 2 + var leftInset : CGFloat = 0 + if self.view.effectiveUserInterfaceLayoutDirection == .rightToLeft { + rightInset = 0 + leftInset = 2 + } + + var viewActionButtons : [UIBarButtonItem] = [] + + if contentState != .removed { + if query?.queryLocation != nil { + // More menu for folder + if clientContext?.moreItemHandler != nil, clientContext?.hasPermission(for: .moreOptions) == true { + let folderActionBarButton = UIBarButtonItem(image: UIImage(named: "more-dots")?.withInset(UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)), style: .plain, target: self, action: #selector(moreBarButtonPressed)) + folderActionBarButton.accessibilityIdentifier = "client.folder-action" + folderActionBarButton.accessibilityLabel = "Actions".localized + + viewActionButtons.append(folderActionBarButton) + } + + // Plus button for folder + if clientContext?.hasPermission(for: .addContent) == true { + let plusBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) + plusBarButton.menu = UIMenu(title: "", children: [ + UIDeferredMenuElement.uncached({ [weak self] completion in + if let self = self, let rootItem = self.query?.rootItem, let clientContext = self.clientContext { + let contextMenuProvider = rootItem as DataItemContextMenuInteraction + + if let contextMenuElements = contextMenuProvider.composeContextMenuItems(in: self, location: .folderAction, with: clientContext) { + completion(contextMenuElements) + } + } + }) + ]) + plusBarButton.accessibilityIdentifier = "client.file-add" + plusBarButton.accessibilityLabel = "Add item".localized + + viewActionButtons.append(plusBarButton) + } + + // Add search button + if clientContext?.hasPermission(for: .search) == true { + let searchButton = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(startSearch)) + searchButton.accessibilityIdentifier = "client.search" + searchButton.accessibilityLabel = "Search".localized + viewActionButtons.append(searchButton) + } + } + } + + navigationItem.navigationContent.add(items: [ + NavigationContentItem(identifier: "client-actions-right", area: .right, priority: .standard, position: .trailing, items: viewActionButtons) + ]) + } + @objc open func moreBarButtonPressed(_ sender: UIBarButtonItem) { guard let rootItem = query?.rootItem else { return @@ -451,6 +604,43 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } } + // MARK: - Navigation title + var navigationTitle: String? { + get { + return navigationItem.titleLabel?.text + } + + set { + navigationItem.titleLabelText = newValue + navigationItem.title = newValue + } + } + + func updateNavigationTitleFromContext() { + var navigationTitle: String? + + // Set navigation title from location (if provided) + if let location { + navigationTitle = location.displayName(in: clientContext) + } + + // Set navigation title from queryLocation + if navigationTitle == nil, let queryLocation = query?.queryLocation { + navigationTitle = queryLocation.displayName(in: clientContext) + } + + // Set navigation title from rootItem.name + if navigationTitle == nil { + navigationTitle = (self.clientContext?.rootItem as? OCItem)?.name + } + + if let navigationTitle { + self.navigationTitle = navigationTitle + } else { + self.navigationTitle = navigationItem.title + } + } + // MARK: - Sorting open var sortBar: SortBar? open var sortMethod: SortMethod { @@ -464,7 +654,6 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, return sort } } - open var searchScope: SortBarSearchScope = .local // only for SortBarDelegate protocol conformance open var sortDirection: SortDirection { set { UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-direction") @@ -495,10 +684,6 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // } } - public func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SortBarSearchScope) { - // only for SortBarDelegate protocol conformance - } - public func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { self.present(presentViewController, animated: animated, completion: completionHandler) } @@ -513,6 +698,18 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, var multiSelectionActionContext: ActionContext? var multiSelectionActionsDatasource: OCDataSourceArray? + var multiSelectionToggleSelectionBarButtonItem: UIBarButtonItem? { + didSet { + if let multiSelectionToggleSelectionBarButtonItem { + navigationItem.navigationContent.add(items: [ + NavigationContentItem(identifier: "multiselect-toggle", area: .left, priority: .high, position: .trailing, items: [ multiSelectionToggleSelectionBarButtonItem ]) + ]) + } else { + navigationItem.navigationContent.remove(itemsWithIdentifier: "multiselect-toggle") + } + } + } + public var isMultiSelecting : Bool = false { didSet { if oldValue != isMultiSelecting { @@ -525,12 +722,19 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, multiSelectionActionContext = ActionContext(viewController: self, clientContext: clientContext, core: core, query: query, items: [OCItem](), location: actionsLocation) } + // Setup select all / deselect all in navigation item + multiSelectionToggleSelectionBarButtonItem = UIBarButtonItem(title: "Select All".localized, primaryAction: UIAction(handler: { [weak self] action in + self?.selectDeselectAll() + })) + // Setup multi selection action datasource multiSelectionActionsDatasource = OCDataSourceArray() refreshMultiselectActions() showActionsBar(with: multiSelectionActionsDatasource!) } else { + // Restore navigation item closeActionsBar() + multiSelectionToggleSelectionBarButtonItem = nil multiSelectionActionsDatasource = nil multiSelectionActionContext = nil } @@ -557,6 +761,8 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, self.actionsBarViewControllerSection?.animateDifferences = true } } + + multiSelectionToggleSelectionBarButtonItem?.title = "Select All".localized } else { let actions = Action.sortedApplicableActions(for: multiSelectionActionContext) let actionCompletionHandler : ActionCompletionHandler = { [weak self] action, error in @@ -569,17 +775,18 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, action.completionHandler = actionCompletionHandler actionItems.append(action.provideOCAction(singleVersion: true)) } + + multiSelectionToggleSelectionBarButtonItem?.title = "Deselect All".localized } multiSelectionActionsDatasource?.setVersionedItems(actionItems) } } - public override func handleMultiSelection(of record: OCDataItemRecord, at indexPath: IndexPath, isSelected: Bool) -> Bool { - if !super.handleMultiSelection(of: record, at: indexPath, isSelected: isSelected), + public override func handleMultiSelection(of record: OCDataItemRecord, at indexPath: IndexPath, isSelected: Bool, clientContext: ClientContext) -> Bool { + if !super.handleMultiSelection(of: record, at: indexPath, isSelected: isSelected, clientContext: clientContext), let multiSelectionActionContext = multiSelectionActionContext { - - retrieveItem(at: indexPath, synchronous: true, action: { [weak self] record, indexPath in + retrieveItem(at: indexPath, synchronous: true, action: { [weak self] record, indexPath, _ in if record.type == .item, let item = record.item as? OCItem { if isSelected { multiSelectionActionContext.add(item: item) @@ -595,6 +802,40 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, return true } + func itemRefs(for items: [OCItem]) -> [ItemRef] { + return items.map { item in + return item.dataItemReference + } + } + + private var selectAllSubscription: OCDataSourceSubscription? + + open func selectDeselectAll() { + if let selectedItems = multiSelectionActionContext?.items, selectedItems.count > 0 { + // Deselect all + let selectedIndexPaths = retrieveIndexPaths(for: itemRefs(for: selectedItems)) + + for indexPath in selectedIndexPaths { + collectionView.deselectItem(at: indexPath, animated: false) + self.collectionView(collectionView, didDeselectItemAt: indexPath) + } + } else { + // Select all + selectAllSubscription = itemsListDataSource?.subscribe(updateHandler: { (subscription) in + let snapshot = subscription.snapshotResettingChangeTracking(true) + let selectIndexPaths = self.retrieveIndexPaths(for: snapshot.items) + + for indexPath in selectIndexPaths { + self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .left) + self.collectionView(self.collectionView, didSelectItemAt: indexPath) + } + + subscription.terminate() + self.selectAllSubscription = nil + }, on: .main, trackDifferences: false, performInitialUpdate: true) + } + } + // MARK: - Drag & Drop public override func targetedDataItem(for indexPath: IndexPath?, interaction: ClientItemInteraction) -> OCDataItem? { var dataItem: OCDataItem? = super.targetedDataItem(for: indexPath, interaction: interaction) @@ -727,7 +968,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // No results let noResultContent = SearchViewController.Content(type: .noResults, source: OCDataSourceArray(), style: emptySection!.cellStyle) - let noResultsView = ComposedMessageView.infoBox(image: UIImage(systemName: "magnifyingglass"), title: "No matches".localized, subtitle: "The search term you entered did not match any item in the selected scope.".localized) + let noResultsView = ComposedMessageView.infoBox(image: OCSymbol.icon(forSymbolName: "magnifyingglass"), title: "No matches".localized, subtitle: "The search term you entered did not match any item in the selected scope.".localized) (noResultContent.source as? OCDataSourceArray)?.setVersionedItems([ noResultsView @@ -765,7 +1006,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, }), savedSearches.count > 0 { let savedSearchTemplatesHeaderView = ComposedMessageView(elements: [ .spacing(10), - .text("Smart folders".localized, style: .system(textStyle: .headline), alignment: .leading, insets: .zero) + .text("Saved searches".localized, style: .system(textStyle: .headline), alignment: .leading, insets: .zero) ]) savedSearchTemplatesHeaderView.elementInsets = .zero @@ -836,7 +1077,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, var searchResultsDataSource: OCDataSource? { willSet { - if let oldDataSource = searchResultsDataSource, let itemsQueryDataSource = itemsQueryDataSource, oldDataSource != itemsQueryDataSource { + if let oldDataSource = searchResultsDataSource, let itemsQueryDataSource = itemsListDataSource, oldDataSource != itemsQueryDataSource { itemSectionDataSource?.removeSources([ oldDataSource ]) if (newValue == nil) || (newValue == itemsQueryDataSource) { @@ -846,7 +1087,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } didSet { - if let newDataSource = searchResultsDataSource, let itemsQueryDataSource = itemsQueryDataSource, newDataSource != itemsQueryDataSource { + if let newDataSource = searchResultsDataSource, let itemsQueryDataSource = itemsListDataSource, newDataSource != itemsQueryDataSource { itemSectionDataSource?.setInclude(false, for: itemsQueryDataSource) itemSectionDataSource?.insertSources([ newDataSource ], after: itemsQueryDataSource) } @@ -898,4 +1139,73 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, recomputeContentState() } + + // MARK: - Statistics + var folderStatistics: OCStatistic? { + didSet { + self.updateStatisticsFooter() + } + } + + var driveQuota: GAQuota? { + didSet { + self.updateStatisticsFooter() + } + } + + func updateStatisticsFooter() { + var folderStatisticsText: String = "" + var quotaInfoText: String = "" + + if let folderStatistics = folderStatistics { + folderStatisticsText = "{{itemCount}} items with {{totalSize}} total ({{fileCount}} files, {{folderCount}} folders)".localized([ + "itemCount" : NumberFormatter.localizedString(from: NSNumber(value: folderStatistics.itemCount?.intValue ?? 0), number: .decimal), + "fileCount" : NumberFormatter.localizedString(from: NSNumber(value: folderStatistics.fileCount?.intValue ?? 0), number: .decimal), + "folderCount" : NumberFormatter.localizedString(from: NSNumber(value: folderStatistics.folderCount?.intValue ?? 0), number: .decimal), + "totalSize" : folderStatistics.localizedSize ?? "-" + ]) + } + + if let driveQuota = driveQuota, let remainingBytes = driveQuota.remaining { + quotaInfoText = "{{remaining}} available".localized([ + "remaining" : ByteCountFormatter.string(fromByteCount: remainingBytes.int64Value, countStyle: .file) + ]) + + if folderStatisticsText.count > 0 { + folderStatisticsText += "\n" + quotaInfoText + } else { + folderStatisticsText = quotaInfoText + } + } + + OnMainThread { + if let footerFolderStatisticsLabel = self.footerFolderStatisticsLabel { + footerFolderStatisticsLabel.text = folderStatisticsText + } + } + } + + // MARK: - Empty actions + func refreshEmptyActions() { + guard contentState == .empty else { return } + + var emptyItems : [OCDataItem] = [ ] + + if let emptyItemListItem = emptyItemListItem { + emptyItems.append(emptyItemListItem) + } + + if let emptyActions = emptyActions() { + emptyItems.append(contentsOf: emptyActions) + } + + emptyItemListDataSource.setItems(emptyItems, updated: nil) + } + + // MARK: - Themeing + public override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + navigationTitleLabel.textColor = collection.navigationBarColors.labelColor + footerFolderStatisticsLabel?.textColor = collection.tableRowColors.secondaryLabelColor + } } diff --git a/ownCloudAppShared/Client/View Controllers/ClientSharedByMeViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientSharedByMeViewController.swift new file mode 100644 index 000000000..42378335d --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/ClientSharedByMeViewController.swift @@ -0,0 +1,91 @@ +// +// ClientSharedByMeViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 06.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 ClientSharedByMeViewController: CollectionViewController { + var hasByMeSection: Bool + var sharedByMeSection: CollectionViewSection? + + var hasByLinkSection: Bool + var sharedByLinkSection: CollectionViewSection? + + var noItemsCondition: DataSourceCondition? + + init(context inContext: ClientContext?, byMe: Bool = false, byLink: Bool = false) { + hasByMeSection = byMe + hasByLinkSection = byLink + let context = ClientContext(with: inContext, modifier: { context in + context.viewControllerPusher = nil + }) + super.init(context: context, sections: nil, useStackViewRoot: true) + revoke(in: inContext, when: [ .connectionClosed ]) + navigationItem.titleLabelText = (byMe && !byLink) ? "Shared by me".localized : ((!byMe && byLink) ? "Shared by link".localized : "Shared".localized) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + func buildSection(identifier: CollectionViewSection.SectionIdentifier, titled title: String, contentDataSource: OCDataSource) -> CollectionViewSection { + let section = CollectionViewSection(identifier: identifier, dataSource: contentDataSource, cellStyle: .init(with: .tableCell), cellLayout: .list(appearance: .plain, contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0)), clientContext: clientContext) + section.hideIfEmptyDataSource = contentDataSource + section.hidden = true + + section.boundarySupplementaryItems = [ + .title(title, pinned: true) + ] + + return section + } + + func addNoItemsCondition(imageName: String, title: String, datasource: OCDataSource) { + let noShareMessage = ComposedMessageView(elements: [ + .image(OCSymbol.icon(forSymbolName: imageName)!, size: CGSize(width: 64, height: 48), alignment: .centered), + .text(title, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered) + ]) + + noItemsCondition = DataSourceCondition(.empty, with: datasource, initial: true, action: { [weak self] condition in + let coverView = (condition.fulfilled == true) ? noShareMessage : nil + self?.setCoverView(coverView, layout: .top) + }) + } + + var sectionsToAdd: [CollectionViewSection] = [] + + if hasByMeSection, let byMeDataSource = clientContext?.core?.sharedByMeDataSource { + sharedByMeSection = buildSection(identifier: "byMe", titled: "Shared by me".localized, contentDataSource: byMeDataSource) + sectionsToAdd.append(sharedByMeSection!) + + addNoItemsCondition(imageName: "arrowshape.turn.up.right", title: "No items shared by you".localized, datasource: byMeDataSource) + } + + if hasByLinkSection, let byLinkDataSource = clientContext?.core?.sharedByLinkDataSource { + sharedByLinkSection = buildSection(identifier: "byLink", titled: "Shared by link".localized, contentDataSource: byLinkDataSource) + sectionsToAdd.append(sharedByLinkSection!) + + addNoItemsCondition(imageName: "link", title: "No items shared by link".localized, datasource: byLinkDataSource) + } + + add(sections: sectionsToAdd) + } +} diff --git a/ownCloudAppShared/Client/View Controllers/ClientSharedWithMeViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientSharedWithMeViewController.swift new file mode 100644 index 000000000..5079941ac --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/ClientSharedWithMeViewController.swift @@ -0,0 +1,96 @@ +// +// ClientSharedWithMeViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.12.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ClientSharedWithMeViewController: CollectionViewController { + var pendingSectionDataSource: OCDataSourceComposition = OCDataSourceComposition(sources: []) + var pendingSection: CollectionViewSection? + + var acceptedSectionDataSource: OCDataSourceComposition = OCDataSourceComposition(sources: []) + var acceptedSection: CollectionViewSection? + + var declinedSectionDataSource: OCDataSourceComposition = OCDataSourceComposition(sources: []) + var declinedSection: CollectionViewSection? + + var noItemsCondition: DataSourceCondition? + + init(context inContext: ClientContext?) { + super.init(context: inContext, sections: nil, useStackViewRoot: true) + revoke(in: inContext, when: [ .connectionClosed ]) + navigationItem.titleLabelText = "Shared with me".localized + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + func buildSection(identifier: CollectionViewSection.SectionIdentifier, titled title: String, compositionDataSource: OCDataSourceComposition, contentDataSource: OCDataSource, queryDataSource: OCDataSource? = nil) -> CollectionViewSection { + var sectionContext = clientContext + + if let queryDataSource, clientContext?.queryDatasource == nil { + sectionContext = ClientContext(with: sectionContext, modifier: { context in + context.queryDatasource = queryDataSource + context.viewControllerPusher = nil + }) + } + + let section = CollectionViewSection(identifier: identifier, dataSource: contentDataSource, cellStyle: .init(with: .tableCell), cellLayout: .list(appearance: .plain, contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0)), clientContext: sectionContext) + section.hideIfEmptyDataSource = contentDataSource + section.hidden = true + + section.boundarySupplementaryItems = [ + .title(title, pinned: true) + ] + + return section + } + + if let pendingDataSource = clientContext?.core?.sharedWithMePendingDataSource, + let acceptedDataSource = clientContext?.core?.sharedWithMeAcceptedDataSource, + let declinedDataSource = clientContext?.core?.sharedWithMeDeclinedDataSource { + pendingSection = buildSection(identifier: "pending", titled: "Pending".localized, compositionDataSource: pendingSectionDataSource, contentDataSource: pendingDataSource) + acceptedSection = buildSection(identifier: "accepted", titled: "Accepted".localized, compositionDataSource: acceptedSectionDataSource, contentDataSource: acceptedDataSource, queryDataSource: clientContext?.core?.useDrives == true ? acceptedDataSource : nil) + declinedSection = buildSection(identifier: "declined", titled: "Declined".localized, compositionDataSource: declinedSectionDataSource, contentDataSource: declinedDataSource) + + add(sections: [ + pendingSection!, + acceptedSection!, + declinedSection! + ]) + + let noShareMessage = ComposedMessageView(elements: [ + .image(OCSymbol.icon(forSymbolName: "arrowshape.turn.up.left")!, size: CGSize(width: 64, height: 48), alignment: .centered), + .text("No items shared with you".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered) + ]) + + noItemsCondition = DataSourceCondition(.allOf([ + DataSourceCondition(.empty, with: pendingDataSource), + DataSourceCondition(.empty, with: acceptedDataSource), + DataSourceCondition(.empty, with: declinedDataSource) + ]), initial: true, action: { [weak self] condition in + let coverView = (condition.fulfilled == true) ? noShareMessage : nil + self?.setCoverView(coverView, layout: .top) + }) + } + } +} diff --git a/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift new file mode 100644 index 000000000..0cadb0ddd --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift @@ -0,0 +1,182 @@ +// +// ClientSidebarViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 21.11.22. +// Copyright © 2022 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 + +public class ClientSidebarViewController: CollectionSidebarViewController, NavigationRevocationHandler { + public var accountsSectionSubscription: OCDataSourceSubscription? + public var accountsControllerSectionSource: OCDataSourceMapped? + public var controllerConfiguration: AccountController.Configuration + + public init(context inContext: ClientContext, controllerConfiguration: AccountController.Configuration) { + self.controllerConfiguration = controllerConfiguration + + super.init(context: inContext, sections: nil, navigationPusher: { sideBarViewController, viewController, animated in + // Push new view controller to detail view controller + if let contentNavigationController = inContext.navigationController { + contentNavigationController.setViewControllers([viewController], animated: false) + sideBarViewController.splitViewController?.showDetailViewController(contentNavigationController, sender: sideBarViewController) + } + }) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var selectionChangeObservation: NSKeyValueObservation? + + override public func viewDidLoad() { + super.viewDidLoad() + + // Set up AccountsControllerSource + accountsControllerSectionSource = OCDataSourceMapped(source: OCBookmarkManager.shared.bookmarksDatasource, creator: { [weak self] (_, bookmarkDataItem) in + if let bookmark = bookmarkDataItem as? OCBookmark, let self = self, let clientContext = self.clientContext { + let controller = AccountController(bookmark: bookmark, context: clientContext, configuration: self.controllerConfiguration) + + return AccountControllerSection(with: controller) + } + + return nil + }, updater: nil, destroyer: { _, bookmarkItemRef, accountController in + // Safely disconnect account controller if currently connected + if let accountController = accountController as? AccountController { + accountController.destroy() // needs to be called since AccountController keeps a reference to itself otherwise + } + }, queue: .main) + + // Set up Collection View + sectionsDataSource = accountsControllerSectionSource + navigationItem.largeTitleDisplayMode = .never + navigationItem.titleView = self.buildNavigationLogoView() + + // Add 10pt space at the top so that the first section's account doesn't "stick" to the top + collectionView.contentInset.top += 10 + } + + deinit { + accountsControllerSectionSource?.source = nil // Clear all AccountController instances from the controller and make OCDataSourceMapped call the destroyer + } + + // MARK: - NavigationRevocationHandler + public func handleRevocation(event: NavigationRevocationEvent, context: ClientContext?, for viewController: UIViewController) { + if let history = sidebarContext.browserController?.history { + // Log.debug("Revoke view controller: \(viewController) \(viewController.navigationItem.titleLabelText)") + if let historyItem = history.item(for: viewController) { + history.remove(item: historyItem, completion: nil) + } + } /* else { + _ = sidebarContext.pushViewControllerToNavigation(context: sidebarContext, provider: { [weak self] context in + return self?.provideDefaultViewController() + }, push: true, animated: false) + } */ + } + + // MARK: - Default view (shown if nothing is selected in sidebar) + public func provideDefaultViewController() -> UIViewController { + return ClientDefaultViewController() + } + + // MARK: - Selected Bookmark + private var focusedBookmarkNavigationRevocationAction: NavigationRevocationAction? + + @objc public dynamic var focusedBookmark: OCBookmark? { + didSet { + Log.debug("New focusedBookmark:: \(focusedBookmark?.displayName ?? "-")") + } + } + + public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + super.collectionView(collectionView, didSelectItemAt: indexPath) + + var newFocusedBookmark: OCBookmark? + + if let accountControllerSection = self.sectionOfCurrentSelection as? AccountControllerSection { + newFocusedBookmark = accountControllerSection.accountController.connection?.bookmark + + if let newFocusedBookmarkUUID = newFocusedBookmark?.uuid { + focusedBookmarkNavigationRevocationAction = NavigationRevocationAction(triggeredBy: [.connectionClosed(bookmarkUUID: newFocusedBookmarkUUID)], action: { [weak self] event, action in + if self?.focusedBookmark?.uuid == newFocusedBookmarkUUID { + self?.focusedBookmark = nil + } + }) + focusedBookmarkNavigationRevocationAction?.register(globally: true) + } + } + + focusedBookmark = newFocusedBookmark + } +} + +// MARK: - Branding +extension ClientSidebarViewController { + func buildNavigationLogoView() -> ThemeView { + let logoImage = UIImage(named: "branding-login-logo") + let logoImageView = UIImageView(image: logoImage) + logoImageView.contentMode = .scaleAspectFit + logoImageView.translatesAutoresizingMaskIntoConstraints = false + if let logoImage = logoImage { + // Keep aspect ratio + scale logo to 90% of available height + logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor, multiplier: (logoImage.size.width / logoImage.size.height) * 0.9).isActive = true + } + + let logoLabel = UILabel() + logoLabel.translatesAutoresizingMaskIntoConstraints = false + logoLabel.text = VendorServices.shared.appName + logoLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + logoLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + logoLabel.setContentCompressionResistancePriority(.required, for: .vertical) + + let logoContainer = UIView() + logoContainer.translatesAutoresizingMaskIntoConstraints = false + logoContainer.addSubview(logoImageView) + logoContainer.addSubview(logoLabel) + logoContainer.setContentHuggingPriority(.required, for: .horizontal) + logoContainer.setContentHuggingPriority(.required, for: .vertical) + + let logoWrapperView = ThemeView() + logoWrapperView.addSubview(logoContainer) + + NSLayoutConstraint.activate([ + logoImageView.topAnchor.constraint(greaterThanOrEqualTo: logoContainer.topAnchor), + logoImageView.bottomAnchor.constraint(lessThanOrEqualTo: logoContainer.bottomAnchor), + logoImageView.centerYAnchor.constraint(equalTo: logoContainer.centerYAnchor), + logoLabel.topAnchor.constraint(greaterThanOrEqualTo: logoContainer.topAnchor), + logoLabel.bottomAnchor.constraint(lessThanOrEqualTo: logoContainer.bottomAnchor), + logoLabel.centerYAnchor.constraint(equalTo: logoContainer.centerYAnchor), + + logoImageView.leadingAnchor.constraint(equalTo: logoContainer.leadingAnchor), + logoLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: logoImageView.trailingAnchor, multiplier: 1), + logoLabel.trailingAnchor.constraint(equalTo: logoContainer.trailingAnchor), + + logoContainer.topAnchor.constraint(equalTo: logoWrapperView.topAnchor), + logoContainer.bottomAnchor.constraint(equalTo: logoWrapperView.bottomAnchor), + logoContainer.centerXAnchor.constraint(equalTo: logoWrapperView.centerXAnchor) + ]) + + logoWrapperView.addThemeApplier({ (_, collection, _) in + logoLabel.applyThemeCollection(collection, itemStyle: .logo) + if !VendorServices.shared.isBranded { + logoImageView.image = logoImageView.image?.tinted(with: collection.navigationBarColors.labelColor) + } + }) + + return logoWrapperView + } +} diff --git a/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationBarController.swift b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationBarController.swift new file mode 100644 index 000000000..602d299ab --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationBarController.swift @@ -0,0 +1,95 @@ +// +// ClientLocationBarController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 23.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +open class ClientLocationBarController: UIViewController, Themeable { + public var location: OCLocation + public var clientContext: ClientContext + + public var seperatorView: UIView? + public var segmentView: SegmentView? + + public init(clientContext: ClientContext, location: OCLocation) { + self.location = location + self.clientContext = clientContext + + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func viewDidLoad() { + super.viewDidLoad() + + seperatorView = UIView() + seperatorView?.translatesAutoresizingMaskIntoConstraints = false + + segmentView = SegmentView(with: composeSegments(location: location, in: clientContext), truncationMode: .truncateTail, scrollable: true, limitVerticalSpaceUsage: true) + segmentView?.translatesAutoresizingMaskIntoConstraints = false + + segmentView?.itemSpacing = 0 + + if let segmentView, let seperatorView { + let seperatorThickness: CGFloat = 0.5 + + view.addSubview(segmentView) + view.addSubview(seperatorView) + + NSLayoutConstraint.activate([ + seperatorView.topAnchor.constraint(equalTo: view.topAnchor), + seperatorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + seperatorView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + seperatorView.heightAnchor.constraint(equalToConstant: seperatorThickness), + + segmentView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10), + segmentView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10), + segmentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 9 + seperatorThickness), // + 1 for the seperatorView + segmentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10) + ]) + } + + Theme.shared.register(client: self, applyImmediately: true) + } + + func composeSegments(location: OCLocation, in clientContext: ClientContext) -> [SegmentViewItem] { + return OCLocation.composeSegments(breadcrumbs: location.breadcrumbs(in: clientContext), in: clientContext, segmentConfigurator: { breadcrumb, segment in + // Make breadcrumbs tappable using the provided action's .actionBlock + if breadcrumb.actionBlock != nil { + segment.gestureRecognizers = [ + ActionTapGestureRecognizer(action: { [weak self] _ in + if let clientContext = self?.clientContext { + breadcrumb.run(options: [.clientContext : clientContext]) + } + }) + ] + } + }) + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + let backgroundFillColor = collection.toolbarColors.backgroundColor ?? collection.tableGroupBackgroundColor + + view.backgroundColor = backgroundFillColor + segmentView?.scrollViewOverlayGradientColor = backgroundFillColor.cgColor + seperatorView?.backgroundColor = collection.tableSeparatorColor + } +} diff --git a/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift new file mode 100644 index 000000000..8d8e1f278 --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift @@ -0,0 +1,167 @@ +// +// OCLocation+Breadcrumbs.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 26.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension OCActionPropertyKey { + static let location = OCActionPropertyKey(rawValue: "location") +} + +public extension OCLocation { + func displayName(in context: ClientContext?) -> String { + switch type { + case .drive: + if let core = context?.core, let driveID, let drive = core.drive(withIdentifier: driveID), let driveName = drive.name { + return driveName + } + return "Space".localized + + case .folder, .file: + if driveID == nil, isRoot { + // OC 10 root folder + return "Files".localized + } + + if let lastPathComponent { + return lastPathComponent + } + + case .account: + if let bookmarkUUID, let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { + return bookmark.displayName ?? bookmark.shortName + } + + default: break + } + + return "" + } + + func displayIcon(in context: ClientContext?, forSidebar: Bool = false) -> UIImage? { + switch type { + case .drive: + if let core = context?.core, let driveID, let drive = core.drive(withIdentifier: driveID), let specialType = drive.specialType { + switch specialType { + case .personal: + return OCSymbol.icon(forSymbolName: forSidebar ? "person" : "person.fill") + + case .shares: + return OCSymbol.icon(forSymbolName: forSidebar ? "arrowshape.turn.up.left" : "arrowshape.turn.up.left.fill") + + default: break + } + } + + return OCSymbol.icon(forSymbolName: forSidebar ? "square.grid.2x2" : "square.grid.2x2.fill") + + case .folder: + return OCSymbol.icon(forSymbolName: forSidebar ? "folder" : "folder.fill") + + case .file: + return OCSymbol.icon(forSymbolName: forSidebar ? "doc" : "doc.fill") + + default: break + } + + return nil + } + + func breadcrumbs(in clientContext: ClientContext, includeServerName: Bool = true, includeDriveName: Bool = true) -> [OCAction] { + var breadcrumbs: [OCAction] = [] + var currentLocation = self + + func addCrumb(title: String?, icon: UIImage?, location: OCLocation? = nil) { + var actionBlock: OCActionBlock? + + if let location { + actionBlock = { [weak clientContext] (action, options, completion) in + if let context = (options?[.clientContext] as? ClientContext) ?? clientContext { + _ = (location as DataItemSelectionInteraction).openItem?(from: nil, with: context, animated: true, pushViewController: true, completion: { (success) in + completion(success ? nil : NSError(ocError: .internal)) + }) + } + } + } + + let action = OCAction(title: title ?? "?", icon: icon, action: actionBlock) + action.properties[.location] = location + + breadcrumbs.insert(action, at: 0) + } + + // Location in reverse + if currentLocation.type == .folder { + while !currentLocation.isRoot, currentLocation.path != nil { + if currentLocation.type == .folder { + addCrumb(title: currentLocation.displayName(in: clientContext), icon: currentLocation.displayIcon(in: clientContext), location: currentLocation) + } + + if let parent = currentLocation.parent { + currentLocation = parent + } else { + break + } + } + } + + // Drive name + if let driveID = self.driveID, includeDriveName { + let location = OCLocation(driveID: driveID, path: "/") + addCrumb(title: location.displayName(in: clientContext), icon: location.displayIcon(in: clientContext), location: location) + } + + // Server name + if let bookmark = clientContext.core?.bookmark, includeServerName { + addCrumb(title: bookmark.displayName ?? bookmark.shortName, icon: OCSymbol.icon(forSymbolName: "server.rack"), location: (self.driveID == nil) ? OCLocation.legacyRoot : nil) + } + + return breadcrumbs + } +} + +extension OCLocation { + static func composeSegments(breadcrumbs: [OCAction], in clientContext: ClientContext, segmentConfigurator: ((_ breadcrumb: OCAction, _ segment: SegmentViewItem) -> Void)? = nil) -> [SegmentViewItem] { + var segments: [SegmentViewItem] = [] + + for breadcrumb in breadcrumbs { + if !segments.isEmpty { + let seperatorSegment = SegmentViewItem(with: OCSymbol.icon(forSymbolName: "chevron.right")) + seperatorSegment.insets.leading = 0 + seperatorSegment.insets.trailing = 0 + segments.append(seperatorSegment) + } + + let segment = SegmentViewItem(with: breadcrumb.icon, title: breadcrumb.title, style: .plain, titleTextStyle: .footnote) + + if let segmentConfigurator { + segmentConfigurator(breadcrumb, segment) + } + + segments.append(segment) + } + + segments.last?.titleTextWeight = .semibold + segments.last?.gestureRecognizers = nil + + segments.first?.insets.leading = 0 + segments.last?.insets.trailing = 0 + + return segments + } +} diff --git a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift new file mode 100644 index 000000000..b5e37f729 --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift @@ -0,0 +1,381 @@ +// +// ClientLocationPicker.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +// MARK: - OCLocation additions +extension OCLocation { + // LocationLevel property + public var clientLocationLevel: ClientLocationPicker.LocationLevel { + if path != nil, !isRoot { + return .folder + } else if driveID != nil { + return .drive + } else if bookmarkUUID != nil { + return .account + } + + return .accounts + } + + // OCLocation creation with app terminology + public static var accounts: OCLocation { + return OCLocation() + } + + public static func account(_ bookmark: OCBookmark) -> OCLocation { + return OCLocation(bookmarkUUID: bookmark.uuid, driveID: nil, path: nil) + } + + public static func drive(_ driveID: String, bookmark: OCBookmark) -> OCLocation { + return OCLocation(bookmarkUUID: bookmark.uuid, driveID: driveID, path: "/") + } + + public static func folder(_ item: OCItem, bookmark: OCBookmark) -> OCLocation { + let folderLocation = item.location! + + if folderLocation.bookmarkUUID == nil { + folderLocation.bookmarkUUID = bookmark.uuid + } + + return folderLocation + } +} + +// MARK: - Location Picker +public class ClientLocationPicker : NSObject { + // MARK: - Types + public enum LocationLevel: CaseIterable { + case accounts // Choice between accounts is possible + case account // Choice within a single account is possible + case drive // Choice within a single drive is possible + case folder // Choice within a folder (+ subfolders) is possible + } + + public typealias LocationFilter = (_ location: OCLocation, _ context: ClientContext?) -> Bool + public typealias ChoiceHandler = (_ chosenItem: OCItem?, _ location: OCLocation?, _ context: ClientContext?, _ cancelled: Bool) -> Void + + // MARK: - Options + public var showFavorites: Bool + public var recentLocations: [OCLocation]? + + public var startLocation: OCLocation + public var maximumLevel: LocationLevel + + public var conflictItems: [OCItem]? + public var choiceHandler: ChoiceHandler? + + public var allowedLocationFilter: LocationFilter? // Determines which locations can be picked by the user + public var navigationLocationFilter: LocationFilter? // Determines which locations can be selected by the user during navigation + + public var headerView: UIView? + var headerViewTitleElement: ComposedMessageElement? + var headerViewSubtitleElement: ComposedMessageElement? + + public var headerTitle: String? { + didSet { + headerViewTitleElement?.text = headerTitle + } + } + public var headerSubTitle: String? { + didSet { + headerViewSubtitleElement?.text = headerSubTitle + } + } + + public var selectButtonTitle: String + public var selectPrompt: String? + public var accountControllerConfiguration: AccountController.Configuration? + + // MARK: - Init + public init(location: OCLocation, maximumLevel: LocationLevel = .folder, showFavorites: Bool = true, showRecents: Bool = true, selectButtonTitle: String?, selectPrompt: String? = nil, headerTitle: String? = nil, headerSubTitle: String? = nil, headerView: UIView? = nil, requiredPermissions: OCItemPermissions? = [.createFile], avoidConflictsWith conflictItems: [OCItem]?, choiceHandler: @escaping ChoiceHandler) { + self.startLocation = location + self.showFavorites = showFavorites + self.selectButtonTitle = selectButtonTitle ?? "Select folder".localized + self.selectPrompt = selectPrompt + self.headerTitle = headerTitle + self.headerSubTitle = headerSubTitle + self.conflictItems = conflictItems + self.maximumLevel = maximumLevel + self.headerView = headerView + + self.accountControllerConfiguration = .pickerConfiguration + + super.init() + + if location.clientLocationLevel == .account { + // No point showing the account pill if its the only account being shown + // self.accountControllerConfiguration?.showAccountPill = false + } + + self.choiceHandler = choiceHandler + + // Create header view if title is specified, but headerView isn't + if let headerTitle, headerView == nil { + headerViewTitleElement = .text(headerTitle, style: .system(textStyle: .title2, weight: .bold), alignment: .leading) + + var elements: [ComposedMessageElement] = [ headerViewTitleElement! ] + + if let headerSubTitle { + headerViewSubtitleElement = .text(headerSubTitle, style: .systemSecondary(textStyle: .body), alignment: .leading) + elements.append(headerViewSubtitleElement!) + } + + self.headerView = ComposedMessageView(elements: elements) + } + + // Create allowedLocationFilter and navigationLocationFilter from conflictItems + var effectiveConflictItems = conflictItems + + if conflictItems == nil, requiredPermissions != nil { + // Ensure .allowedLocationFilter is also created with empty conflictItems - + // if there are permission requirements to check for + effectiveConflictItems = [] + } + + if let effectiveConflictItems { + var navigationPathFilter : LocationFilter? + + let folderItemLocations = effectiveConflictItems.filter({ (item) -> Bool in + return item.type == .collection && item.path != nil && !item.isRoot + }).map { (item) -> OCLocation in + return item.location! + } + let itemParentLocations = effectiveConflictItems.filter({ (item) -> Bool in + return item.location?.parent != nil + }).map { (item) -> OCLocation in + return item.location!.parent! + } + + if folderItemLocations.count > 0 { + navigationPathFilter = { (targetLocation, _) in + return !folderItemLocations.contains(targetLocation) + } + } + + allowedLocationFilter = { (targetLocation, context) in + // Disallow all paths as target that are parent of any of the items + if itemParentLocations.contains(targetLocation) { + return false + } + + // Check that destination meets permission requirements + if let requiredPermissions { + if let item = try? context?.core?.cachedItem(at: targetLocation) { + return item.permissions.contains(requiredPermissions) + } + } + + return true + } + + navigationLocationFilter = navigationPathFilter + } + } + + // MARK: - Permission checks + func checkPermission(context: ClientContext?, dataItemRecord: OCDataItemRecord?, interaction: ClientItemInteraction, viewController: UIViewController?) -> Bool { + switch interaction { + case .selection: + if let item = dataItemRecord?.item as? OCItem { + if item.type == .file { + return false + } + + if let itemLocation = item.location, let navigationLocationFilter { + return navigationLocationFilter(itemLocation, context) + } + } + return true + + case .multiselection, .contextMenu, .leadingSwipe, .trailingSwipe, .drag, .acceptDrop, .search, .moreOptions, .addContent: + return false + } + } + + // MARK: - Provide data source and initial view controller + func provideDataSource(for location: OCLocation, maximumLevel: LocationLevel, context: ClientContext) -> OCDataSource? { + var sectionDataSource: OCDataSource? + + let level = location.clientLocationLevel + + switch level { + case .accounts, .account: + sectionDataSource = OCDataSourceMapped(source: OCBookmarkManager.shared.bookmarksDatasource, creator: { [weak self] (_, bookmarkDataItem) in + if let bookmark = bookmarkDataItem as? OCBookmark, + let self = self, + let rootContext = self.rootContext, + let accountControllerConfiguration = self.accountControllerConfiguration { + if level == .account, bookmark.uuid != self.startLocation.bookmarkUUID { + // If level is account, only return the start location's account (if provided) + return nil + } + + let controller = AccountController(bookmark: bookmark, context: rootContext, configuration: accountControllerConfiguration) + + return AccountControllerSection(with: controller) + } + + return nil + }, updater: nil, destroyer: { _, bookmarkItemRef, accountController in + // Safely disconnect account controller if currently connected + if let accountController = accountController as? AccountController { + accountController.destroy() // needs to be called since AccountController keeps a reference to itself otherwise + } + }, queue: .main) + + case .drive, .folder: break + } + + return sectionDataSource + } + + func provideViewController(for location: OCLocation, maximumLevel: LocationLevel, context: ClientContext) -> CollectionViewController? { + let sectionDataSource = provideDataSource(for: location, maximumLevel: maximumLevel, context: context) + var viewController: CollectionViewController? + + if let sectionDataSource = sectionDataSource { + viewController = CollectionViewController(context: context, sections: nil, useStackViewRoot: true, hierarchic: true) + viewController?.sectionsDataSource = sectionDataSource + } else { + viewController = location.openItem(from: nil, with: context, animated: true, pushViewController: false, completion: nil) as? CollectionViewController + } + + if let viewController { + var title: String? + + switch location.clientLocationLevel { + case .accounts: + title = "Accounts".localized + + case .account: + title = "Account".localized + if let bookmarkUUID = location.bookmarkUUID { + title = OCBookmarkManager.shared.bookmark(for: bookmarkUUID)?.displayName + } + viewController.hideNavigationBar = true + + case .drive: + if let driveID = location.driveID { + title = context.core?.drive(withIdentifier: driveID)?.name + } + + case .folder: break + } + + if let title { + viewController.navigationItem.titleLabelText = title + } + } + + return viewController + } + + // MARK: - Presentation & Choice + var rootNavigationController: UINavigationController? + var rootViewController: CollectionViewController? + var rootContext: ClientContext? + + public func pickerViewControllerForPresentation(with baseContext: ClientContext? = nil) -> UIViewController? { + let navigationController = ThemeNavigationController() + + // Set up navigation controller and context + rootNavigationController = navigationController + rootContext = ClientContext(with: baseContext, modifier: { context in + context.add(permissionHandler: { [weak self] context, dataItemRecord, checkInteraction, viewController in + return self?.checkPermission(context: context, dataItemRecord: dataItemRecord, interaction: checkInteraction, viewController: viewController) ?? false + }) + context.viewControllerPusher = nil + context.browserController = nil + context.navigationController = navigationController + context.permissions = [ .selection ] + context.itemStyler = { [weak self] (context, _, item) in + if let item = item as? OCItem { + if item.type == .file { + return .disabled + } + + if let itemLocation = item.location, + let navigationLocationFilter = self?.navigationLocationFilter, + !navigationLocationFilter(itemLocation, context) { + return .disabled + } + } + return .regular + } + }) + + // Compose view controller + if let rootContext = rootContext { + rootViewController = provideViewController(for: startLocation, maximumLevel: maximumLevel, context: rootContext) + if let rootViewController { + navigationController.pushViewController(rootViewController, animated: false) + + let pickerViewController = ClientLocationPickerViewController(with: self) + pickerViewController.contentViewController = navigationController + pickerViewController.isModalInPresentation = true + + return pickerViewController + + } + } + + return nil + } + + public func present(in clientContext: ClientContext, baseContext: ClientContext? = nil) { + // Compose and present view controller + if let pickerViewController = pickerViewControllerForPresentation(with: baseContext) { + clientContext.present(pickerViewController, animated: true) + } + } + + func choose(item: OCItem?, location: OCLocation?, context: ClientContext?, cancelled: Bool) { + if let choiceHandler { + self.choiceHandler = nil + + if !cancelled { + // Add missing counterparts + if let location, item == nil { + // Add missing item for location + if let core = context?.core { + core.cachedItem(at: location, resultHandler: { error, item in + if item?.bookmarkUUID == nil, let bookmarkUUID = location.bookmarkUUID?.uuidString { + item?.bookmarkUUID = bookmarkUUID + } + OnMainThread { + choiceHandler(item, location, context, cancelled) + } + }) + return + } + } else if let item, location == nil { + // Add missing location for item + if item.bookmarkUUID == nil, let bookmarkUUID = context?.core?.bookmark.uuid.uuidString { + item.bookmarkUUID = bookmarkUUID + } + choiceHandler(item, item.location, context, cancelled) + return + } + } + + choiceHandler(item, location, context, cancelled) + } + } +} diff --git a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPickerViewController.swift b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPickerViewController.swift new file mode 100644 index 000000000..48e35d77b --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPickerViewController.swift @@ -0,0 +1,218 @@ +// +// ClientLocationPickerViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 ClientLocationPickerViewController: EmbeddingViewController, CustomViewControllerEmbedding, Themeable, UINavigationControllerDelegate { + var locationPicker: ClientLocationPicker + + init(with locationPicker: ClientLocationPicker) { + self.locationPicker = locationPicker + super.init(nibName: nil, bundle: nil) + self.locationPicker.rootNavigationController?.delegate = self + + currentLocation = (self.locationPicker.rootNavigationController?.topViewController as? ClientItemViewController)?.location + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var bottomBarContainer: UIView = UIView() + var selectButton: UIButton = UIButton() + var cancelButton: UIButton = UIButton() + var promptLabel: UILabel = UILabel() + var separatorLine: UIView = UIView() + + override func viewDidLoad() { + super.viewDidLoad() + + let showCancelButton = locationPicker.headerView != nil + + bottomBarContainer.translatesAutoresizingMaskIntoConstraints = false + selectButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.translatesAutoresizingMaskIntoConstraints = false + promptLabel.translatesAutoresizingMaskIntoConstraints = false + separatorLine.translatesAutoresizingMaskIntoConstraints = false + + var selectButtonConfig = UIButton.Configuration.borderedProminent() + selectButtonConfig.title = locationPicker.selectButtonTitle + selectButtonConfig.cornerStyle = .large + selectButton.configuration = selectButtonConfig + + selectButton.addTarget(self, action: #selector(chooseCurrentLocation), for: .primaryActionTriggered) + + if showCancelButton { + var cancelButtonConfig = UIButton.Configuration.bordered() + cancelButtonConfig.title = "Cancel".localized + cancelButtonConfig.cornerStyle = .large + cancelButton.configuration = cancelButtonConfig + cancelButton.addTarget(self, action: #selector(cancel), for: .primaryActionTriggered) + + bottomBarContainer.addSubview(cancelButton) + } + + promptLabel.text = locationPicker.selectPrompt + + bottomBarContainer.addSubview(selectButton) + bottomBarContainer.addSubview(promptLabel) + bottomBarContainer.addSubview(separatorLine) + view.addSubview(bottomBarContainer) + + var constraints: [NSLayoutConstraint] = [] + var leadingButtonAnchor = selectButton.leadingAnchor + + if let headerView = locationPicker.headerView { + headerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(headerView) + + leadingButtonAnchor = cancelButton.leadingAnchor + + constraints.append(contentsOf: [ + headerView.leftAnchor.constraint(equalTo: view.leftAnchor), + headerView.rightAnchor.constraint(equalTo: view.rightAnchor), + headerView.topAnchor.constraint(equalTo: view.topAnchor), + + cancelButton.trailingAnchor.constraint(equalTo: selectButton.leadingAnchor, constant: -15), + cancelButton.centerYAnchor.constraint(equalTo: selectButton.centerYAnchor) + ]) + } + + constraints.append(contentsOf: [ + bottomBarContainer.leftAnchor.constraint(equalTo: view.leftAnchor), + bottomBarContainer.rightAnchor.constraint(equalTo: view.rightAnchor), + bottomBarContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + promptLabel.leadingAnchor.constraint(equalTo: bottomBarContainer.safeAreaLayoutGuide.leadingAnchor, constant: 20), + promptLabel.trailingAnchor.constraint(lessThanOrEqualTo: leadingButtonAnchor, constant: -20), + promptLabel.centerYAnchor.constraint(equalTo: bottomBarContainer.safeAreaLayoutGuide.centerYAnchor), + + selectButton.trailingAnchor.constraint(equalTo: bottomBarContainer.safeAreaLayoutGuide.trailingAnchor, constant: -20), + selectButton.topAnchor.constraint(equalTo: bottomBarContainer.safeAreaLayoutGuide.topAnchor, constant: 20), + selectButton.bottomAnchor.constraint(equalTo: bottomBarContainer.safeAreaLayoutGuide.bottomAnchor, constant: -20), + + separatorLine.leftAnchor.constraint(equalTo: bottomBarContainer.leftAnchor), + separatorLine.rightAnchor.constraint(equalTo: bottomBarContainer.rightAnchor), + separatorLine.topAnchor.constraint(equalTo: bottomBarContainer.topAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: 1) + ]) + + NSLayoutConstraint.activate(constraints) + + Theme.shared.register(client: self, applyImmediately: true) + } + + // MARK: - UINavigationControllerDelegate + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + var rightBarButtonItems: [UIBarButtonItem] = [] + + if locationPicker.headerView == nil { + // Add cancel button to navigation bar if no headerView is used + rightBarButtonItems.append(UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction(handler: { [weak self] (_) in + self?.choose(cancelled: true) + }))) + } + + if let itemViewController = viewController as? ClientItemViewController, let location = itemViewController.location { + currentLocationContext = itemViewController.clientContext + + if let bookmark = itemViewController.clientContext?.core?.bookmark, location.bookmarkUUID == nil { + // Add bookmark UUID to location + currentLocation = OCLocation(bookmarkUUID: bookmark.uuid, driveID: location.driveID, path: location.path) + } else { + currentLocation = location + } + + // Add actions for location + if let currentLocation, let currentLocationContext { + var rootItem = currentLocationContext.rootItem as? OCItem + if rootItem == nil { + rootItem = try? currentLocationContext.core?.cachedItem(at: currentLocation) + } + + if let rootItem, let core = currentLocationContext.core { + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .locationPickerBar) + let actionContext = ActionContext(viewController: itemViewController, clientContext: currentLocationContext, core: core, query: currentLocationContext.query, items: [rootItem], location: actionsLocation, sender: self) + let actions = Action.sortedApplicableActions(for: actionContext) + + for action in actions { + rightBarButtonItems.append(action.provideBarButtonItem()) + } + } + } + } else { + currentLocation = nil + currentLocationContext = nil + } + + viewController.navigationItem.navigationContent.add(items: [ + NavigationContentItem(identifier: "location-picker-actions-right", area: .right, priority: .high, position: .trailing, items: rightBarButtonItems) + ]) + } + + // MARK: - Location tracking and choice + var currentLocationContext: ClientContext? // context in which to see currentLocation + var currentLocation: OCLocation? { + didSet { + var validTargetLocation: Bool = false + + if let currentLocation { + if let allowedLocationFilter = locationPicker.allowedLocationFilter { + validTargetLocation = allowedLocationFilter(currentLocation, currentLocationContext) + } else { + validTargetLocation = true + } + } + + selectButton.isEnabled = validTargetLocation + } + } + + @objc func chooseCurrentLocation() { + choose(location: currentLocation, cancelled: false) + } + + @objc func cancel() { + choose(cancelled: true) + } + + func choose(item: OCItem? = nil, location: OCLocation? = nil, cancelled: Bool) { + locationPicker.choose(item: item, location: location, context: currentLocationContext, cancelled: cancelled) + self.dismiss(animated: true) + } + + // MARK: - CustomViewControllerEmbedding + func constraintsForEmbedding(contentView: UIView) -> [NSLayoutConstraint] { + var defaultAnchorSet = view.defaultAnchorSet + + defaultAnchorSet.bottomAnchor = bottomBarContainer.topAnchor + + if let headerView = locationPicker.headerView { + defaultAnchorSet.topAnchor = headerView.bottomAnchor + } + + return view.embed(toFillWith: contentView, enclosingAnchors: defaultAnchorSet) + } + + // MARK: - Themeable + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + view.backgroundColor = collection.tableBackgroundColor + separatorLine.backgroundColor = collection.tableSeparatorColor + } +} diff --git a/ownCloudAppShared/Intent/OCLicenseManager+Setup.swift b/ownCloudAppShared/Intent/OCLicenseManager+Setup.swift index ba2960158..32ab62730 100644 --- a/ownCloudAppShared/Intent/OCLicenseManager+Setup.swift +++ b/ownCloudAppShared/Intent/OCLicenseManager+Setup.swift @@ -90,8 +90,11 @@ public extension OCLicenseManager { // Set up EMM Provider let emmProvider = OCLicenseEMMProvider(unlockedProductIdentifiers: [.bundlePro]) - add(emmProvider) + + // Set up QA Provider + let qaProvider = OCLicenseQAProvider(unlockedProductIdentifiers: [.bundlePro], delegate: VendorServices.shared) + add(qaProvider) } } diff --git a/ownCloudAppShared/SDK Extensions/OCBookmarkManager+Locking.swift b/ownCloudAppShared/SDK Extensions/OCBookmarkManager+Locking.swift new file mode 100644 index 000000000..bde6e434c --- /dev/null +++ b/ownCloudAppShared/SDK Extensions/OCBookmarkManager+Locking.swift @@ -0,0 +1,91 @@ +// +// OCBookmarkManager+Locking.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public extension OCBookmarkManager { + static private let lastConnectedBookmarkUUIDDefaultsKey = "last-connected-bookmark-uuid" + + // MARK: - Defaults Keys + static var lastBookmarkSelectedForConnection : OCBookmark? { + get { + if let bookmarkUUIDString = OCAppIdentity.shared.userDefaults?.string(forKey: OCBookmarkManager.lastConnectedBookmarkUUIDDefaultsKey), let bookmarkUUID = UUID(uuidString: bookmarkUUIDString) { + return OCBookmarkManager.shared.bookmark(for: bookmarkUUID) + } + + return nil + } + + set { + OCAppIdentity.shared.userDefaults?.set(newValue?.uuid.uuidString, forKey: OCBookmarkManager.lastConnectedBookmarkUUIDDefaultsKey) + } + } + + static var lockedBookmarks : [OCBookmark] = [] + + @discardableResult static func attemptLock(bookmark: OCBookmark, presentErrorOn hostViewController: UIViewController? = nil, action: (_ bookmark: OCBookmark, _ completion: @escaping () -> Void) -> Void) -> Bool { + if !isLocked(bookmark: bookmark, presentAlertOn: hostViewController) { + self.lock(bookmark: bookmark) + + action(bookmark, { + self.unlock(bookmark: bookmark) + }) + + return true + } + + return false + } + + static func lock(bookmark: OCBookmark) { + OCSynchronized(self) { + self.lockedBookmarks.append(bookmark) + } + } + + static func unlock(bookmark: OCBookmark) { + OCSynchronized(self) { + if let removeIndex = self.lockedBookmarks.firstIndex(of: bookmark) { + self.lockedBookmarks.remove(at: removeIndex) + } + } + } + + static func isLocked(bookmark: OCBookmark, presentAlertOn viewController: UIViewController? = nil, completion: ((_ isLocked: Bool) -> Void)? = nil) -> Bool { + if self.lockedBookmarks.contains(bookmark) { + if viewController != nil { + let alertController = ThemedAlertController(title: NSString(format: "'%@' is currently locked".localized as NSString, bookmark.shortName as NSString) as String, + message: NSString(format: "An operation is currently performed that prevents connecting to '%@'. Please try again later.".localized as NSString, bookmark.shortName as NSString) as String, + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: { (_) in + completion?(true) + })) + + viewController?.present(alertController, animated: true, completion: nil) + } + + return true + } + + completion?(false) + + return false + } +} diff --git a/ownCloudAppShared/SDK Extensions/OCBookmarkManager+Management.swift b/ownCloudAppShared/SDK Extensions/OCBookmarkManager+Management.swift new file mode 100644 index 000000000..e9adedc6a --- /dev/null +++ b/ownCloudAppShared/SDK Extensions/OCBookmarkManager+Management.swift @@ -0,0 +1,84 @@ +// +// OCBookmarkManager+Management.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 22.11.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public extension OCBookmarkManager { + func delete(withAlertOn hostViewController: UIViewController, bookmark: OCBookmark, completion: (() -> Void)? = nil) { + var presentationStyle: UIAlertController.Style = .actionSheet + if UIDevice.current.isIpad { + presentationStyle = .alert + } + + var alertTitle = "Really delete '%@'?".localized + var destructiveTitle = "Delete".localized + var failureTitle = "Deletion of '%@' failed".localized + if VendorServices.shared.isBranded { + alertTitle = "Do you want to log out from '%@'?".localized + destructiveTitle = "Log out".localized + failureTitle = "Log out of '%@' failed".localized + } + + let alertController = ThemedAlertController(title: NSString(format: alertTitle as NSString, bookmark.shortName) as String, + message: "This will also delete all locally stored file copies.".localized, + preferredStyle: presentationStyle) + + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: { _ in + completion?() + })) + + alertController.addAction(UIAlertAction(title: destructiveTitle, style: .destructive, handler: { (_) in + if !OCBookmarkManager.attemptLock(bookmark: bookmark, presentErrorOn: hostViewController, action: { bookmark, lockActionCompletion in + OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, offlineOperationCompletion) in + let vault : OCVault = OCVault(bookmark: bookmark) + + vault.erase(completionHandler: { (_, error) in + OnMainThread { + if error != nil { + // Inform user if vault couldn't be erased + let alertController = ThemedAlertController(title: NSString(format: failureTitle as NSString, bookmark.shortName as NSString) as String, + message: error?.localizedDescription, + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + + hostViewController.present(alertController, animated: true) + } else { + // Success! We can now remove the bookmark + OCMessageQueue.global.dequeueAllMessages(forBookmarkUUID: bookmark.uuid) + + if let bookmark = OCBookmarkManager.shared.bookmark(for: bookmark.uuid) { + OCBookmarkManager.shared.removeBookmark(bookmark) + } + } + + completion?() // delete(withAlertOn:) completion Handler + offlineOperationCompletion() // OCCoreManager.scheduleOfflineOperation completion handler + lockActionCompletion() // OCBookmarkManager.attemptLock completion handler + } + }) + }, for: bookmark) + }) { + completion?() + } + })) + + hostViewController.present(alertController, animated: true, completion: nil) + } +} diff --git a/ownCloudAppShared/SDK Extensions/OCCore+Extension.swift b/ownCloudAppShared/SDK Extensions/OCCore+Extension.swift index 3466c191b..0782040ec 100644 --- a/ownCloudAppShared/SDK Extensions/OCCore+Extension.swift +++ b/ownCloudAppShared/SDK Extensions/OCCore+Extension.swift @@ -51,15 +51,17 @@ extension OCCore { start(shareQuery) } - if let shareQuery = OCShareQuery(scope: .acceptedCloudShares, item: item) { - dispatchGroup.enter() + if connection.capabilities?.federatedSharingSupported == true { + if let shareQuery = OCShareQuery(scope: .acceptedCloudShares, item: item) { + dispatchGroup.enter() - shareQuery.initialPopulationHandler = { [weak self] query in - combinedShares.addObjects(from: query.queryResults) - dispatchGroup.leave() - self?.stop(query) + shareQuery.initialPopulationHandler = { [weak self] query in + combinedShares.addObjects(from: query.queryResults) + dispatchGroup.leave() + self?.stop(query) + } + start(shareQuery) } - start(shareQuery) } dispatchGroup.notify(queue: .main, execute: { diff --git a/ownCloud/SDK Extensions/OCIssue+Extension.swift b/ownCloudAppShared/SDK Extensions/OCIssue+DisplayIssues.swift similarity index 73% rename from ownCloud/SDK Extensions/OCIssue+Extension.swift rename to ownCloudAppShared/SDK Extensions/OCIssue+DisplayIssues.swift index e9cbc3d7e..506a48a37 100644 --- a/ownCloud/SDK Extensions/OCIssue+Extension.swift +++ b/ownCloudAppShared/SDK Extensions/OCIssue+DisplayIssues.swift @@ -19,18 +19,18 @@ import UIKit import ownCloudSDK -struct DisplayIssues { - var targetIssue : OCIssue //!< The issue to send the approve or decline message to - var displayLevel : OCIssueLevel //!< The issue level to be used for display - var displayIssues: [OCIssue] //!< The selection of issues to be used for display - var primaryCertificate : OCCertificate? //!< The first certificate found among the issues +public struct DisplayIssues { + public var targetIssue : OCIssue //!< The issue to send the approve or decline message to + public var displayLevel : OCIssueLevel //!< The issue level to be used for display + public var displayIssues: [OCIssue] //!< The selection of issues to be used for display + public var primaryCertificate : OCCertificate? //!< The first certificate found among the issues - func isAtLeast(level minLevel: OCIssueLevel) -> Bool { + public func isAtLeast(level minLevel: OCIssueLevel) -> Bool { return displayLevel.rawValue >= minLevel.rawValue } } -extension OCIssue { +public extension OCIssue { func prepareForDisplay() -> DisplayIssues { var displayIssues: [OCIssue] = [] var primaryCertificate: OCCertificate? = self.certificate diff --git a/ownCloud/SDK Extensions/OCMessage+Extension.swift b/ownCloudAppShared/SDK Extensions/OCMessage+Extension.swift similarity index 93% rename from ownCloud/SDK Extensions/OCMessage+Extension.swift rename to ownCloudAppShared/SDK Extensions/OCMessage+Extension.swift index 53ae781bb..3a8d421f2 100644 --- a/ownCloud/SDK Extensions/OCMessage+Extension.swift +++ b/ownCloudAppShared/SDK Extensions/OCMessage+Extension.swift @@ -18,9 +18,9 @@ import UIKit import ownCloudSDK -import ownCloudAppShared +import ownCloudApp -extension OCMessage { +public extension OCMessage { func showInApp() { NotificationCenter.default.post(name: .NotificationMessagePresenterShowMessage, object: self) } diff --git a/ownCloudAppShared/SDK Extensions/OCShare+Extension.swift b/ownCloudAppShared/SDK Extensions/OCShare+Extension.swift index 31792b768..c3f5d3d58 100644 --- a/ownCloudAppShared/SDK Extensions/OCShare+Extension.swift +++ b/ownCloudAppShared/SDK Extensions/OCShare+Extension.swift @@ -67,4 +67,12 @@ extension OCShare { return permissionsDescription.joined(separator:", ") } + + func copyToClipboard() -> Bool { + if let url { + UIPasteboard.general.url = url + return true + } + return false + } } diff --git a/ownCloudAppShared/Tools/VendorServices.swift b/ownCloudAppShared/Tools/VendorServices.swift index 511a27fd0..ef8e4fb40 100644 --- a/ownCloudAppShared/Tools/VendorServices.swift +++ b/ownCloudAppShared/Tools/VendorServices.swift @@ -226,3 +226,9 @@ extension VendorServices : OCClassSettingsSupport { ] } } + +extension VendorServices: OCLicenseQAProviderDelegate { + public var isQALicenseUnlockPossible: Bool { + return isBetaBuild + } +} diff --git a/ownCloudAppShared/UIKit Extension/UIBarButtonItem+Extension.swift b/ownCloudAppShared/UIKit Extension/UIBarButtonItem+Extension.swift deleted file mode 100644 index f07345fb3..000000000 --- a/ownCloudAppShared/UIKit Extension/UIBarButtonItem+Extension.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// UIBarButtonItem+Extension.swift -// ownCloud -// -// Created by Michael Neuwert on 24.01.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 UIKit -import ownCloudSDK - -public extension UIBarButtonItem { - - private struct imageFrame { - static var x = 0.0 - static var y = 0.0 - static var width = 30.0 - static var height = 30.0 - } - - private struct AssociatedKeys { - static var actionKey = "actionKey" - } - - var actionIdentifier: OCExtensionIdentifier? { - get { - return objc_getAssociatedObject(self, &AssociatedKeys.actionKey) as? OCExtensionIdentifier - } - - set { - if newValue != nil { - objc_setAssociatedObject(self, &AssociatedKeys.actionKey, newValue!, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) - } - } - } - - convenience init(image: UIImage?, target: AnyObject, action: Selector, dropTarget: AnyObject, actionIdentifier: OCExtensionIdentifier) { - let button = UIButton(type: .custom) - button.setImage(image, for: .normal) - button.frame = CGRect(x: imageFrame.x, y: imageFrame.y, width: imageFrame.width, height: imageFrame.height) - button.actionIdentifier = actionIdentifier - button.addTarget(target, action: action, for: .touchUpInside) - button.sizeToFit() - - if let dropDelegate = dropTarget as? UIDropInteractionDelegate { - let dropInteraction = UIDropInteraction(delegate: dropDelegate) - button.addInteraction(dropInteraction) - } - - self.init(customView: button) - self.actionIdentifier = actionIdentifier - } -} diff --git a/ownCloudAppShared/UIKit Extension/UICollectionViewDiffableDataSource+Tools.swift b/ownCloudAppShared/UIKit Extension/UICollectionViewDiffableDataSource+Tools.swift index 54bfc4803..1d7f4d77c 100644 --- a/ownCloudAppShared/UIKit Extension/UICollectionViewDiffableDataSource+Tools.swift +++ b/ownCloudAppShared/UIKit Extension/UICollectionViewDiffableDataSource+Tools.swift @@ -24,4 +24,10 @@ public extension UICollectionViewDiffableDataSource { snapshot.reconfigureItems(items) apply(snapshot, animatingDifferences: animated) } + + func requestReloadOfItems(_ items: [ItemIdentifierType], animated: Bool = true) { + var snapshot = snapshot() + snapshot.reloadItems(items) + apply(snapshot, animatingDifferences: animated) + } } diff --git a/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift b/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift index 53c2d4a2c..beaa876fe 100644 --- a/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift @@ -100,4 +100,10 @@ extension UIColor { return (String(format: "\(leadIn)%02x%02x%02x", Int(selfRed*255.0), Int(selfGreen*255.0), Int(selfBlue*255.0))) } + + public func isLight() -> Bool { + guard let components = cgColor.components, components.count > 2 else { return false } + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } } diff --git a/ownCloudAppShared/UIKit Extension/UIImageView+Thumbnails.swift b/ownCloudAppShared/UIKit Extension/UIImageView+Thumbnails.swift deleted file mode 100644 index fc7d2a0ff..000000000 --- a/ownCloudAppShared/UIKit Extension/UIImageView+Thumbnails.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// UIImageView+Thumbnails.swift -// ownCloud -// -// Created by Michael Neuwert on 31.08.2019. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2022, 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 - -public protocol ItemContainer { - var item : OCItem? { get } -} - -public extension UIImageView { - @discardableResult func setThumbnailImage(using core: OCCore, from requestItem: OCItem, with size: CGSize, itemContainer: ItemContainer? = nil, progressHandler: ((_ progress:Progress) -> Void)? = nil) -> Progress? { - let displayThumbnail = { (thumbnail: OCItemThumbnail?) in - _ = thumbnail?.request(for: size, scale: 0, withCompletionHandler: { (_, error, _, image) in - if error == nil, image != nil, (itemContainer == nil) || ((itemContainer != nil) && itemContainer?.item?.itemVersionIdentifier == thumbnail?.itemVersionIdentifier) { - OnMainThread { - self.image = image - } - } - }) - } - - if requestItem.thumbnailAvailability == .available { - if let thumbnail = requestItem.thumbnail { - displayThumbnail(thumbnail) - } - return nil - } - - if requestItem.thumbnailAvailability != .none { - let activeThumbnailRequestProgress = core.retrieveThumbnail(for: requestItem, maximumSize: size, scale: 0, retrieveHandler: { (_, _, _, thumbnail, _, progress) in - // Did we get valid thumbnail? - if thumbnail != nil { - requestItem.thumbnail = thumbnail - displayThumbnail(thumbnail) - } - - if progress != nil { - progressHandler?(progress!) - } - }) - return activeThumbnailRequestProgress - } - - return nil - } -} diff --git a/ownCloudAppShared/UIKit Extension/UINavigationItem+Extension.swift b/ownCloudAppShared/UIKit Extension/UINavigationItem+Extension.swift new file mode 100644 index 000000000..d350fb63d --- /dev/null +++ b/ownCloudAppShared/UIKit Extension/UINavigationItem+Extension.swift @@ -0,0 +1,50 @@ +// +// UINavigationItem+Extension.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 08.12.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public extension UINavigationItem { + var titleLabel: UILabel? { + let (items, _) = navigationContent.items(withIdentifier: "ios16-truncated-title-fix") + return items.first?.titleView as? UILabel + } + + var titleLabelText: String? { + // In iOS 16, titles can get cut off when using UINavigationItem.title - this works around this bug + get { + return titleLabel?.text + } + + set { + if titleView == nil { + let navigationTitleLabel = UILabel() + navigationTitleLabel.font = UIFont.systemFont(ofSize: UIFont.buttonFontSize, weight: .semibold) + navigationTitleLabel.lineBreakMode = .byTruncatingMiddle + navigationTitleLabel.textColor = Theme.shared.activeCollection.navigationBarColors.labelColor + navigationTitleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + navigationTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + navigationContent.add(items: [ + NavigationContentItem(identifier: "ios16-truncated-title-fix", area: .title, priority: .lowest, position: .leading, titleView: navigationTitleLabel) + ]) + } + + titleLabel?.text = newValue + } + } +} diff --git a/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift b/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift index e024d90ce..adbadb5cd 100644 --- a/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift @@ -18,26 +18,7 @@ import UIKit -public protocol ToolAndTabBarToggling : UITabBarController { - var toolbar : UIToolbar? { get set } -} - public extension UIViewController { - func populateToolbar(with items:[UIBarButtonItem]) { - if let tabBarController = self.tabBarController as? ToolAndTabBarToggling { - tabBarController.toolbar?.isHidden = false - tabBarController.tabBar.isHidden = true - tabBarController.toolbar?.setItems(items, animated: true) - } - } - - func removeToolbar() { - if let tabBarController = self.tabBarController as? ToolAndTabBarToggling { - tabBarController.toolbar?.isHidden = true - tabBarController.tabBar.isHidden = false - tabBarController.toolbar?.setItems(nil, animated: true) - } - } var topMostViewController: UIViewController { @@ -55,4 +36,15 @@ public extension UIViewController { return self } + + @objc @discardableResult func openURL(_ url: URL) -> Bool { + var responder: UIResponder? = self.navigationController + while responder != nil { + if let application = responder as? UIApplication { + return application.perform(#selector(openURL(_:)), with: url) != nil + } + responder = responder?.next + } + return true + } } diff --git a/ownCloud/Messages/AlertView.swift b/ownCloudAppShared/User Interface/Alert View/AlertView.swift similarity index 85% rename from ownCloud/Messages/AlertView.swift rename to ownCloudAppShared/User Interface/Alert View/AlertView.swift index 162072c76..d7366ac82 100644 --- a/ownCloud/Messages/AlertView.swift +++ b/ownCloudAppShared/User Interface/Alert View/AlertView.swift @@ -18,17 +18,16 @@ import UIKit import ownCloudSDK -import ownCloudAppShared -class AlertOption : NSObject { - typealias ChoiceHandler = (_: AlertView, _: AlertOption) -> Void +open class AlertOption : NSObject { + public typealias ChoiceHandler = (_: AlertView, _: AlertOption) -> Void - var label : String - var handler : ChoiceHandler - var type : OCIssueChoiceType - var accessibilityIdentifier : String? + public var label : String + public var handler : ChoiceHandler + public var type : OCIssueChoiceType + public var accessibilityIdentifier : String? - init(label: String, type: OCIssueChoiceType, accessibilityIdentifier : String? = nil, handler: @escaping ChoiceHandler) { + public init(label: String, type: OCIssueChoiceType, accessibilityIdentifier : String? = nil, handler: @escaping ChoiceHandler) { self.label = label self.type = type self.handler = handler @@ -38,26 +37,26 @@ class AlertOption : NSObject { } } -class AlertView: UIView, Themeable { - var localizedHeader : String? +open class AlertView: UIView, Themeable { + public var localizedHeader : String? - var localizedTitle : String - var localizedDescription : String + public var localizedTitle : String + public var localizedDescription : String - var options : [AlertOption] + public var options : [AlertOption] - var headerLabel : UILabel = UILabel() - var headerContainer : UIView = UIView() + public var headerLabel : UILabel = UILabel() + public var headerContainer : UIView = UIView() - var titleLabel : UILabel = UILabel() - var descriptionLabel : UILabel = UILabel() - var optionStackView : UIStackView? + public var titleLabel : UILabel = UILabel() + public var descriptionLabel : UILabel = UILabel() + public var optionStackView : UIStackView? - var optionViews : [ThemeButton] = [] + public var optionViews : [ThemeButton] = [] - var textAlignment : NSTextAlignment + public var textAlignment : NSTextAlignment - init(localizedHeader: String? = nil, localizedTitle: String, localizedDescription: String, contentPadding: CGFloat = 20, textAlignment : NSTextAlignment = .left, options: [AlertOption]) { + public init(localizedHeader: String? = nil, localizedTitle: String, localizedDescription: String, contentPadding: CGFloat = 20, textAlignment : NSTextAlignment = .left, options: [AlertOption]) { self.localizedHeader = localizedHeader self.localizedTitle = localizedTitle self.localizedDescription = localizedDescription @@ -72,7 +71,7 @@ class AlertView: UIView, Themeable { Theme.shared.register(client: self, applyImmediately: true) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -80,7 +79,7 @@ class AlertView: UIView, Themeable { Theme.shared.unregister(client: self) } - func createOptionViews() { + public func createOptionViews() { var optionIdx : Int = 0 for option in options { @@ -102,13 +101,13 @@ class AlertView: UIView, Themeable { } } - @objc func optionSelected(sender: ThemeButton) { + @objc public func optionSelected(sender: ThemeButton) { let option = options[sender.tag] self.selectOption(option: option) } - func selectOption(option: AlertOption) { + public func selectOption(option: AlertOption) { option.handler(self, option) } @@ -122,7 +121,7 @@ class AlertView: UIView, Themeable { private let titleLabelFontSize : CGFloat = 17 private let descriptionLabelFontSize : CGFloat = 14 - func prepareViewAndConstraints() { + public func prepareViewAndConstraints() { headerLabel.numberOfLines = 1 headerLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.numberOfLines = 0 @@ -226,7 +225,7 @@ class AlertView: UIView, Themeable { } } - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { self.headerLabel.applyThemeCollection(collection) self.titleLabel.applyThemeCollection(collection) self.descriptionLabel.applyThemeCollection(collection) diff --git a/ownCloud/Messages/AlertViewController.swift b/ownCloudAppShared/User Interface/Alert View/AlertViewController.swift similarity index 68% rename from ownCloud/Messages/AlertViewController.swift rename to ownCloudAppShared/User Interface/Alert View/AlertViewController.swift index 2a971e6fc..155816e22 100644 --- a/ownCloud/Messages/AlertViewController.swift +++ b/ownCloudAppShared/User Interface/Alert View/AlertViewController.swift @@ -17,21 +17,20 @@ */ import UIKit -import ownCloudAppShared -class AlertViewController: UIViewController, Themeable { - var localizedHeader : String? - var localizedTitle : String - var localizedDescription : String +open class AlertViewController: UIViewController, Themeable { + open var localizedHeader : String? + open var localizedTitle : String + open var localizedDescription : String - var options : [AlertOption] + open var options : [AlertOption] private var chosenOption : AlertOption? - var alertView : AlertView? + open var alertView : AlertView? - var dismissHandler : (() -> Void)? + open var dismissHandler : (() -> Void)? - init(localizedHeader: String? = nil, localizedTitle: String, localizedDescription: String, options: [AlertOption], dismissHandler: (() -> Void)? = nil) { + public init(localizedHeader: String? = nil, localizedTitle: String, localizedDescription: String, options: [AlertOption], dismissHandler: (() -> Void)? = nil) { self.localizedHeader = localizedHeader self.localizedTitle = localizedTitle self.localizedDescription = localizedDescription @@ -56,21 +55,22 @@ class AlertViewController: UIViewController, Themeable { Theme.shared.unregister(client: self) } - override func loadView() { + override public func loadView() { alertView = AlertView(localizedHeader: localizedHeader, localizedTitle: localizedTitle, localizedDescription: localizedDescription, options: options) self.view = alertView } - override func viewDidLoad() { + override public func viewDidLoad() { + super.viewDidLoad() Theme.shared.register(client: self, applyImmediately: true) } - override func viewDidAppear(_ animated: Bool) { + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) becomeFirstResponder() } - override func viewDidDisappear(_ animated: Bool) { + override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if chosenOption == nil, let dismissHandler = dismissHandler { @@ -80,11 +80,11 @@ class AlertViewController: UIViewController, Themeable { } } - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { self.view.backgroundColor = collection.tableBackgroundColor } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift new file mode 100644 index 000000000..a530e8add --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift @@ -0,0 +1,156 @@ +// +// BrowserNavigationBookmark.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 26.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +open class BrowserNavigationBookmark: NSObject, NSSecureCoding { + public typealias BookmarkType = String + public typealias BookmarkRestoreAction = String + + open var type: BookmarkType + + open var bookmarkUUID: UUID? + open var location: OCLocation? + + open var itemLocalID: String? + + open var specialItem: AccountController.SpecialItem? + open var savedSearch: OCSavedSearch? + + open var restoreFromClass: String? + open var restoreAction: BookmarkRestoreAction? + + public init(type: BookmarkType, bookmarkUUID: UUID? = nil, location: OCLocation? = nil, itemLocalID: String? = nil, specialItem: AccountController.SpecialItem? = nil, savedSearchUUID: String? = nil, savedSearch: OCSavedSearch? = nil, restoreFromClass: String? = nil, action: BookmarkRestoreAction? = nil) { + self.type = type + + self.bookmarkUUID = bookmarkUUID + if bookmarkUUID == nil, let locationBookmarkUUID = location?.bookmarkUUID { + self.bookmarkUUID = locationBookmarkUUID + } + + self.location = location + + self.itemLocalID = itemLocalID + + self.specialItem = specialItem + + self.savedSearch = savedSearch + + self.restoreFromClass = restoreFromClass + self.restoreAction = action + } + + static public func from(dataItem: OCDataItem, bookmarkUUID: UUID? = nil, bookmark: OCBookmark? = nil, clientContext: ClientContext? = nil, restoreAction: BookmarkRestoreAction) -> BrowserNavigationBookmark? { + guard let useBookmarkUUID = bookmarkUUID ?? bookmark?.uuid ?? (dataItem as? OCLocation)?.bookmarkUUID ?? clientContext?.core?.bookmark.uuid else { + return nil + } + + if let itemReStore = dataItem as? DataItemBrowserNavigationBookmarkReStore { + return itemReStore.store(in: useBookmarkUUID, context: clientContext, restoreAction: restoreAction) + } + + return nil + } + + convenience public init?(type: BookmarkType = .dataItem, for dataItem: OCDataItem, in bookmarkUUID: UUID? = nil, restoreAction: BookmarkRestoreAction) { + let className = NSStringFromClass(Swift.type(of: dataItem)) + + self.init(type: type, bookmarkUUID: bookmarkUUID, restoreFromClass: className, action: restoreAction) + } + + // MARK: - Restoration + public var isRestorable: Bool { + if specialItem != nil { + return true + } + + if let restoreFromClass, let restoreClass = NSClassFromString(restoreFromClass), + (restoreClass as? DataItemBrowserNavigationBookmarkReStore.Type) != nil { + return true + } + + return false + } + + public func restore(in viewController: UIViewController? = nil, with context: ClientContext? = nil, completion: @escaping ((Error?, UIViewController?) -> Void)) { + if let restoreFromClass, let restoreClass = NSClassFromString(restoreFromClass), + let reStore = restoreClass as? DataItemBrowserNavigationBookmarkReStore.Type { + reStore.restore(navigationBookmark: self, in: viewController, with: context, completion: completion) + } else { + if let restorer = context?.rootViewController as? BrowserNavigationBookmarkRestore { + restorer.restore(navigationBookmark: self, in: viewController, with: context, completion: completion) + } else { + completion(NSError(ocError: .invalidType), nil) + } + } + } + + // MARK: - Secure Coding + public static var supportsSecureCoding: Bool = true + + public func encode(with coder: NSCoder) { + coder.encode(type, forKey: "type") + coder.encode(bookmarkUUID, forKey: "bookmarkUUID") + + coder.encode(location, forKey: "location") + + coder.encode(itemLocalID, forKey: "itemLocalID") + + coder.encode(specialItem?.rawValue, forKey: "specialItem") + coder.encode(savedSearch, forKey: "savedSearch") + + coder.encode(restoreFromClass, forKey: "restoreFromClass") + coder.encode(restoreAction, forKey: "restoreAction") + } + + public required init?(coder: NSCoder) { + type = (coder.decodeObject(of: NSString.self, forKey: "type") as? String) ?? .dataItem + bookmarkUUID = coder.decodeObject(of: NSUUID.self, forKey: "bookmarkUUID") as? UUID + + location = coder.decodeObject(of: OCLocation.self, forKey: "location") + + itemLocalID = coder.decodeObject(of: NSString.self, forKey: "itemLocalID") as? String + + if let specialItemString = coder.decodeObject(of: NSString.self, forKey: "specialItem") as? String { + specialItem = AccountController.SpecialItem(rawValue: specialItemString) + } + savedSearch = coder.decodeObject(of: OCSavedSearch.self, forKey: "savedSearch") + + restoreFromClass = coder.decodeObject(of: NSString.self, forKey: "restoreFromClass") as? String + restoreAction = coder.decodeObject(of: NSString.self, forKey: "restoreAction") as? String + } +} + +public extension BrowserNavigationBookmark.BookmarkType { + static let dataItem = "dataItem" + static let specialItem = "specialItem" +} + +public extension BrowserNavigationBookmark.BookmarkRestoreAction { + static let standard = "standard" + + static let open = "open" + static let reveal = "reveal" + static let handleSelection = "handleSelection" +} + +public protocol BrowserNavigationBookmarkRestore { + func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context:ClientContext?, completion: @escaping ((_ error: Error?, _ viewController: UIViewController?) -> Void)) +} diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationHistory.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationHistory.swift new file mode 100644 index 000000000..7bcedb45c --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationHistory.swift @@ -0,0 +1,168 @@ +// +// BrowserNavigationHistory.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 23.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public protocol BrowserNavigationHistoryDelegate: AnyObject { + func present(item: BrowserNavigationItem?, with direction: BrowserNavigationHistory.Direction, completion: BrowserNavigationHistory.CompletionHandler?) +} + +open class BrowserNavigationHistory { + public typealias CompletionHandler = (_ success: Bool) -> Void + + public enum Direction { + case none + case toPrevious + case toNext + } + + weak var delegate: BrowserNavigationHistoryDelegate? + + open var items: [BrowserNavigationItem] = [] + open var currentItem: BrowserNavigationItem? { + if items.count > 0, position < items.count, position >= 0 { + return items[position] + } + + return nil + } + open var position: Int = -1 + + open var canMoveBack: Bool { + return position > 0 + } + + open var canMoveForward: Bool { + return position < items.count-1 + } + + open var isEmpty: Bool { + return items.isEmpty + } + + open func push(item: BrowserNavigationItem, completion: CompletionHandler? = nil) { + if position < items.count - 1 { + items.removeSubrange((position+1)...items.count-1) + } + + items.append(item) + position += 1 + + present(item: item, with: (position == 0) ? .none : .toNext, completion: completion) + } + + @discardableResult open func moveBack(completion: CompletionHandler? = nil) -> BrowserNavigationItem? { + if position > 0 { + position -= 1 + + present(item: items[position], with: .toPrevious, completion: completion) + + return items[position] + } else { + completion?(false) + } + + return nil + } + + @discardableResult open func moveForward(completion: CompletionHandler? = nil) -> BrowserNavigationItem? { + if position < items.count - 1 { + position += 1 + + present(item: items[position], with: .toNext, completion: completion) + + return items[position] + } else { + completion?(false) + } + + return nil + } + + open func item(for viewController: UIViewController?) -> BrowserNavigationItem? { + for item in items { + if item.viewController == viewController { + return item + } + } + + return nil + } + + @discardableResult open func remove(item: BrowserNavigationItem, completion: BrowserNavigationHistory.CompletionHandler?) -> Bool { + if let index = items.firstIndex(of: item) { + var presentNewItem = (index == position) + var direction: Direction = .none + + if index == position { + direction = .toPrevious + } + + if index <= position { + position -= 1 + } + + items.remove(at: index) + + if position < 0 { + if items.count > 0 { + position = 0 + } else { + position = -1 + presentNewItem = true + } + } + + if presentNewItem { + present(item: currentItem, with: direction, completion: completion) + } else { + completion?(true) + } + + return true + } + + completion?(false) + return false + } + + private var lastPresentItem: BrowserNavigationItem? + private var lastDirection: BrowserNavigationHistory.Direction = .none + + func present(item: BrowserNavigationItem?, with direction: BrowserNavigationHistory.Direction, completion: BrowserNavigationHistory.CompletionHandler?) { + OCSynchronized(self) { + lastPresentItem = item + lastDirection = direction + } + + OnMainThread { + var performPresentation: Bool = true + + OCSynchronized(self) { + performPresentation = (self.lastPresentItem == item) && (self.lastDirection == direction) + } + + guard performPresentation, let delegate = self.delegate else { + completion?(true) + return + } + + delegate.present(item: item, with: direction, completion: completion) + } + } +} diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationItem.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationItem.swift new file mode 100644 index 000000000..7c051f6ee --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationItem.swift @@ -0,0 +1,72 @@ +// +// BrowserNavigationItem.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 16.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public protocol BrowserNavigationTrimming { + var browserNavigationBuilder: BrowserNavigationItem.Builder? { get } +} + +open class BrowserNavigationItem: NSObject { + public typealias Builder = (_ item: BrowserNavigationItem) -> UIViewController? + + private var _viewController: UIViewController? + open var viewController: UIViewController? { + if let _viewController { + return _viewController + } + + if let builder { + _viewController = builder(self) + return _viewController + } + + return nil + } + + open var builder: Builder? + + open var canTrimViewController: Bool { + if builder != nil || navigationBookmark != nil { + return true + } + + return false + } + + open var navigationBookmark: BrowserNavigationBookmark? + + init(viewController: UIViewController? = nil, builder: Builder? = nil, bookmark: BrowserNavigationBookmark? = nil) { + super.init() + + _viewController = viewController + self.builder = builder + + self.navigationBookmark = bookmark ?? viewController?.navigationBookmark + + if self.builder == nil, let trimmingSupport = viewController as? BrowserNavigationTrimming { + self.builder = trimmingSupport.browserNavigationBuilder + } + } + + open func trim() { + if canTrimViewController { + _viewController = nil + } + } +} diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift new file mode 100644 index 000000000..1a114970d --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift @@ -0,0 +1,481 @@ +// +// BrowserNavigationViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 16.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public protocol BrowserNavigationViewControllerDelegate: AnyObject { + func browserNavigation(viewController: BrowserNavigationViewController, contentViewControllerDidChange: UIViewController?) +} + +open class BrowserNavigationViewController: EmbeddingViewController, Themeable, BrowserNavigationHistoryDelegate { + + var navigationView: UINavigationBar = UINavigationBar() + var contentContainerView: UIView = UIView() + var contentContainerLidView: UIView = UIView() + + var sideBarSeperatorView: UIView = UIView() + + lazy open var history: BrowserNavigationHistory = { + let history = BrowserNavigationHistory() + history.delegate = self + return history + }() + + weak open var delegate: BrowserNavigationViewControllerDelegate? + + open override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if let windowWidth = view.window?.bounds.width { + if preferredSideBarWidth > windowWidth { + // Adapt to widths slimmer than sidebarWidth + sideBarWidth = windowWidth - 20 + } else { + // Use preferredSideBarWidth + sideBarWidth = preferredSideBarWidth + } + + if windowWidth < sideBarWidth * 2.5 { + // Slide the sidebar over the content if the content doesn't have at least 2.5x the space of the sidebar + sideBarDisplayMode = .over + } else { + // Show sidebar and content side by side if there's enough space + sideBarDisplayMode = .sideBySide + } + } else { + // Slide the sidebar over the content + // if the window width can't be determined + sideBarDisplayMode = .over + } + } + + open override func viewDidLoad() { + super.viewDidLoad() + + contentContainerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(contentContainerView) + + navigationView.translatesAutoresizingMaskIntoConstraints = false + navigationView.delegate = self + + contentContainerView.addSubview(navigationView) + + contentContainerLidView.translatesAutoresizingMaskIntoConstraints = false + contentContainerLidView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + contentContainerLidView.isHidden = true + contentContainerLidView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showHideSideBar))) + view.addSubview(contentContainerLidView) + + sideBarSeperatorView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(sideBarSeperatorView) + + NSLayoutConstraint.activate([ + contentContainerView.topAnchor.constraint(equalTo: view.topAnchor), + contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), // Allow for flexibility without having to remove this constraint. It will be overridden by constraints with higher priority (default is .required) when necessary + contentContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + contentContainerLidView.topAnchor.constraint(equalTo: contentContainerView.topAnchor), + contentContainerLidView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), + contentContainerLidView.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor), + contentContainerLidView.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor), + + sideBarSeperatorView.topAnchor.constraint(equalTo: contentContainerView.topAnchor), + sideBarSeperatorView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), + sideBarSeperatorView.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor, constant: -1), + sideBarSeperatorView.widthAnchor.constraint(equalToConstant: 1), + + navigationView.topAnchor.constraint(equalTo: contentContainerView.safeAreaLayoutGuide.topAnchor), + navigationView.leadingAnchor.constraint(equalTo: contentContainerView.safeAreaLayoutGuide.leadingAnchor), + navigationView.trailingAnchor.constraint(equalTo: contentContainerView.safeAreaLayoutGuide.trailingAnchor) + ]) + + navigationView.items = [ + ] + + Theme.shared.register(client: self, applyImmediately: true) + } + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + navigationController?.isNavigationBarHidden = true + } + + // MARK: - Push & Navigation + open func push(viewController: UIViewController, completion: BrowserNavigationHistory.CompletionHandler? = nil) { + push(item: BrowserNavigationItem(viewController: viewController), completion: completion) + } + + open func push(item: BrowserNavigationItem, completion: BrowserNavigationHistory.CompletionHandler? = nil) { + // Push to history (+ present) + history.push(item: item) + + if hideSideBarInOverDisplayModeOnPush, sideBarDisplayMode == .over { + setSideBarVisible(false, animated: true) + } + } + + open func moveBack(completion: BrowserNavigationHistory.CompletionHandler? = nil) { + history.moveBack(completion: completion) + } + + open func moveForward(completion: BrowserNavigationHistory.CompletionHandler? = nil) { + history.moveForward(completion: completion) + } + + // MARK: - View Controller presentation + open override func addContentViewControllerSubview(_ contentViewControllerView: UIView) { + contentContainerView.insertSubview(contentViewControllerView, at: 0) + } + + open override func constraintsForEmbedding(contentViewController: UIViewController) -> [NSLayoutConstraint] { + if let contentView = contentViewController.view { + return [ + contentView.topAnchor.constraint(equalTo: navigationView.bottomAnchor), + contentView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), + contentView.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor) + ] + } + + return [] + } + + @objc func showHideSideBar() { + setSideBarVisible(!isSideBarVisible) + } + + @objc func navBack() { + moveBack() + } + + @objc func navForward() { + moveForward() + } + + func buildSideBarToggleBarButtonItem() -> UIBarButtonItem { + let buttonItem = UIBarButtonItem(image: OCSymbol.icon(forSymbolName: "sidebar.leading"), style: .plain, target: self, action: #selector(showHideSideBar)) + buttonItem.tag = BarButtonTags.showHideSideBar.rawValue + return buttonItem + } + + private enum BarButtonTags: Int { + case mask = 0xC0FFEE0 + case showHideSideBar + case backButton + case forwardButton + } + + func updateLeftBarButtonItems(for navigationItem: UINavigationItem, withToggleSideBar: Bool = false, withBackButton: Bool = false, withForwardButton: Bool = false) { + let (_, existingItems) = navigationItem.navigationContent.items(withIdentifier: "browser-navigation-left") + + func reuseOrBuild(_ tag: BarButtonTags, _ build: () -> UIBarButtonItem) -> UIBarButtonItem { + for barButtonItem in existingItems { + if barButtonItem.tag == tag.rawValue { + return barButtonItem + } + } + + return build() + } + + var leadingButtons : [UIBarButtonItem] = [] + var sidebarButtons : [UIBarButtonItem] = [] + + if withToggleSideBar { + let item = reuseOrBuild(.showHideSideBar, { + return buildSideBarToggleBarButtonItem() + }) + + sidebarButtons.append(item) + } + + if withBackButton { + let item = reuseOrBuild(.backButton, { + let backButtonItem = UIBarButtonItem(image: OCSymbol.icon(forSymbolName: "chevron.backward"), style: .plain, target: self, action: #selector(navBack)) + backButtonItem.tag = BarButtonTags.backButton.rawValue + + return backButtonItem + }) + + item.isEnabled = history.canMoveBack + + leadingButtons.append(item) + } + + if withForwardButton { + let item = reuseOrBuild(.forwardButton, { + let forwardButtonItem = UIBarButtonItem(image: OCSymbol.icon(forSymbolName: "chevron.forward"), style: .plain, target: self, action: #selector(navForward)) + forwardButtonItem.tag = BarButtonTags.forwardButton.rawValue + + return forwardButtonItem + }) + + item.isEnabled = history.canMoveForward + + leadingButtons.append(item) + } + + let sideBarItem = NavigationContentItem(identifier: "browser-navigation-left", area: .left, priority: .standard, position: .leading, items: sidebarButtons) + sideBarItem.visibleInPriorities = [ .standard, .high, .highest ] + + navigationItem.navigationContent.add(items: [ + sideBarItem, + NavigationContentItem(identifier: "browser-navigation-left", area: .left, priority: .standard, position: .leading, items: leadingButtons) + ]) + } + + func updateContentNavigationItems() { + if let contentNavigationItem = contentViewController?.navigationItem { + updateLeftBarButtonItems(for: contentNavigationItem, withToggleSideBar: (effectiveSideBarDisplayMode == .sideBySide) ? !isSideBarVisible : true, withBackButton: true, withForwardButton: true) + } + + updateSideBarNavigationItem() + } + + // MARK: - BrowserNavigationHistoryDelegate + public func present(item: BrowserNavigationItem?, with direction: BrowserNavigationHistory.Direction, completion: BrowserNavigationHistory.CompletionHandler?) { + let needsSideBarLayout = (((item != nil) && (contentViewController == nil)) || ((item == nil) && (contentViewController != nil))) && (emptyHistoryBehaviour == .expandSideBarToFullWidth) + + if let item { + // Has content + let itemViewController = item.viewController + + contentViewController = itemViewController + + if let navigationItem = itemViewController?.navigationItem { + updateContentNavigationItems() + + navigationView.items = [ navigationItem ] + } + } else { + // Has no content + contentViewController = nil + } + + self.view.layoutIfNeeded() + + if needsSideBarLayout { + UIView.animate(withDuration: 0.3) { + self.updateSideBarLayoutAndAppearance() + self.view.layoutIfNeeded() + } + } + + delegate?.browserNavigation(viewController: self, contentViewControllerDidChange: contentViewController) + + completion?(true) + } + + // MARK: - Sidebar View Controller + func updateSideBarNavigationItem() { + var sideBarNavigationItem: UINavigationItem? + + if let sidebarViewController { + // Add show/hide sidebar button to sidebar left items + if let navigationController = sidebarViewController as? UINavigationController { + sideBarNavigationItem = navigationController.topViewController?.navigationItem + } else { + sideBarNavigationItem = sidebarViewController.navigationItem + } + } + + if let sideBarNavigationItem { + updateLeftBarButtonItems(for: sideBarNavigationItem, withToggleSideBar: (effectiveSideBarDisplayMode != .fullWidth)) + } + } + + open var sidebarViewController: UIViewController? { + willSet { + sidebarViewController?.willMove(toParent: nil) + sidebarViewController?.view.removeFromSuperview() + sidebarViewController?.removeFromParent() + } + didSet { + if let sidebarViewController, let sidebarViewControllerView = sidebarViewController.view { + updateSideBarNavigationItem() + + addChild(sidebarViewController) + view.addSubview(sidebarViewControllerView) + sidebarViewControllerView.translatesAutoresizingMaskIntoConstraints = false + updateSideBarLayoutAndAppearance() + sidebarViewController.didMove(toParent: self) + } else { + updateSideBarLayoutAndAppearance() + } + } + } + + // MARK: - Constraints, state & animation + private var composedConstraints : [NSLayoutConstraint]? { + willSet { + if let composedConstraints { + NSLayoutConstraint.deactivate(composedConstraints) + } + } + didSet { + if let composedConstraints { + NSLayoutConstraint.activate(composedConstraints) + } + } + } + + public enum SideBarDisplayMode { + case fullWidth + case sideBySide + case over + } + + public enum EmptyHistoryBehaviour { + case none + case expandSideBarToFullWidth + case showEmptyHistoryViewController + } + + public var isSideBarVisible: Bool = true + public var preferredSideBarWidth: CGFloat = 320 + var sideBarWidth: CGFloat = 320 + var preferredSideBarDisplayMode: SideBarDisplayMode? + var sideBarDisplayMode: SideBarDisplayMode = .over { + didSet { + updateSideBarLayoutAndAppearance() + } + } + + var effectiveSideBarDisplayMode: SideBarDisplayMode { + if history.isEmpty, emptyHistoryBehaviour == .expandSideBarToFullWidth { + return .fullWidth + } + + return sideBarDisplayMode + } + + public var emptyHistoryBehaviour: EmptyHistoryBehaviour = .expandSideBarToFullWidth + public var hideSideBarInOverDisplayModeOnPush: Bool = true + + open func setSideBarVisible(_ sideBarVisible: Bool, animated: Bool = true) { + if isSideBarVisible == sideBarVisible { + return + } + + isSideBarVisible = sideBarVisible + + if animated { + self.updateSideBarLayoutAndAppearance() + + if sideBarVisible { + switch self.effectiveSideBarDisplayMode { + case .over: + self.contentContainerLidView.alpha = 0.0 + self.contentContainerLidView.isHidden = false + default: break + } + } + + UIView.animate(withDuration: 0.25, animations: { + if sideBarVisible { + switch self.effectiveSideBarDisplayMode { + case .over: + self.contentContainerLidView.alpha = 1.0 + default: break + } + } else { + self.contentContainerLidView.alpha = 0.0 + } + self.view.layoutIfNeeded() + }, completion: { _ in + if !sideBarVisible { + self.contentContainerLidView.isHidden = true + } + }) + } else { + updateSideBarLayoutAndAppearance() + } + } + + func updateSideBarLayoutAndAppearance() { + var newConstraints : [NSLayoutConstraint] = [] + + if let sidebarViewController, let sidebarView = sidebarViewController.view, let view { + if isSideBarVisible { + switch effectiveSideBarDisplayMode { + case .fullWidth: + // Sidebar occupies full area + newConstraints = [ + sidebarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + sidebarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + sidebarView.topAnchor.constraint(equalTo: view.topAnchor), + sidebarView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ] + + contentContainerLidView.isHidden = true + + case .sideBySide: + // Sidebar + Content side-by-side + newConstraints = [ + sidebarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + sidebarView.trailingAnchor.constraint(equalTo: contentContainerView.leadingAnchor, constant: -1), + sidebarView.topAnchor.constraint(equalTo: view.topAnchor), + sidebarView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + sidebarView.widthAnchor.constraint(equalToConstant: sideBarWidth) + ] + + contentContainerLidView.isHidden = true + + case .over: + // Sidebar over content + newConstraints = [ + sidebarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + sidebarView.topAnchor.constraint(equalTo: view.topAnchor), + sidebarView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + sidebarView.widthAnchor.constraint(equalToConstant: sideBarWidth) + ] + + contentContainerLidView.isHidden = false + } + } else { + // Position sidebar left outside of view + newConstraints = [ + sidebarView.trailingAnchor.constraint(equalTo: view.leadingAnchor), + sidebarView.topAnchor.constraint(equalTo: view.topAnchor), + sidebarView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + sidebarView.widthAnchor.constraint(equalToConstant: sideBarWidth) + ] + } + } + + updateContentNavigationItems() + + composedConstraints = newConstraints + } + + // MARK: - Themeing + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + navigationView.applyThemeCollection(collection, itemStyle: .content) + + view.backgroundColor = collection.tableBackgroundColor + sideBarSeperatorView.backgroundColor = collection.tableSeparatorColor + } +} + +extension BrowserNavigationViewController: UINavigationBarDelegate { + public func position(for bar: UIBarPositioning) -> UIBarPosition { + return .topAttached + } +} diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/UIViewController+BrowserNavigation.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/UIViewController+BrowserNavigation.swift new file mode 100644 index 000000000..cd93de187 --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/UIViewController+BrowserNavigation.swift @@ -0,0 +1,32 @@ +// +// UIViewController+BrowserNavigation.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 09.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension UIViewController { + var navigationBookmark: BrowserNavigationBookmark? { + set { + self.setValue(newValue, forAnnotatedProperty: "_navigationBookmark") + } + + get { + return self.value(forAnnotatedProperty: "_navigationBookmark") as? BrowserNavigationBookmark + } + } +} diff --git a/ownCloudAppShared/User Interface/Card Presentation Controller/CardPresentationController.swift b/ownCloudAppShared/User Interface/Card Presentation Controller/CardPresentationController.swift index 5f8521559..557791f1d 100644 --- a/ownCloudAppShared/User Interface/Card Presentation Controller/CardPresentationController.swift +++ b/ownCloudAppShared/User Interface/Card Presentation Controller/CardPresentationController.swift @@ -417,7 +417,7 @@ extension CardPresentationController: UIGestureRecognizerDelegate { // MARK: - Convenience addition to UIViewController extension UIViewController { - open func present(asCard viewController: UIViewController, animated: Bool, withHandle: Bool = true, dismissable: Bool = true, completion: (() -> Void)? = nil) { + public func present(asCard viewController: UIViewController, animated: Bool, withHandle: Bool = true, dismissable: Bool = true, completion: (() -> Void)? = nil) { let animator = CardTransitionDelegate(viewControllerToPresent: viewController, presentingViewController: self, withHandle: withHandle, dismissable: dismissable) viewController.transitioningDelegate = animator // .transitioningDelegate is only weak! diff --git a/ownCloudAppShared/User Interface/EmbeddingViewController/EmbeddingViewController.swift b/ownCloudAppShared/User Interface/EmbeddingViewController/EmbeddingViewController.swift new file mode 100644 index 000000000..b69748d9f --- /dev/null +++ b/ownCloudAppShared/User Interface/EmbeddingViewController/EmbeddingViewController.swift @@ -0,0 +1,71 @@ +// +// EmbeddingViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 06.12.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public protocol CustomViewControllerEmbedding: EmbeddingViewController { + func constraintsForEmbedding(contentView: UIView) -> [NSLayoutConstraint] +} + +open class EmbeddingViewController: UIViewController { + + // MARK: - Content View Controller handling + private var contentViewControllerConstraints : [NSLayoutConstraint]? { + willSet { + if let contentViewControllerConstraints = contentViewControllerConstraints { + NSLayoutConstraint.deactivate(contentViewControllerConstraints) + } + } + didSet { + if let contentViewControllerConstraints = contentViewControllerConstraints { + NSLayoutConstraint.activate(contentViewControllerConstraints) + } + } + } + + open func constraintsForEmbedding(contentViewController: UIViewController) -> [NSLayoutConstraint] { + if let customEmbedder = self as? CustomViewControllerEmbedding { + return customEmbedder.constraintsForEmbedding(contentView: contentViewController.view) + } else { + return view.embed(toFillWith: contentViewController.view, enclosingAnchors: view.defaultAnchorSet) + } + } + + open func addContentViewControllerSubview(_ contentViewControllerView: UIView) { + view.addSubview(contentViewControllerView) + } + + @objc open var contentViewController: UIViewController? { + willSet { + contentViewController?.willMove(toParent: nil) + contentViewController?.view.removeFromSuperview() + contentViewController?.removeFromParent() + + contentViewControllerConstraints = nil + } + didSet { + if let contentViewController = contentViewController, let contentViewControllerView = contentViewController.view { + addChild(contentViewController) + addContentViewControllerSubview(contentViewControllerView) + contentViewControllerView.translatesAutoresizingMaskIntoConstraints = false + contentViewControllerConstraints = constraintsForEmbedding(contentViewController: contentViewController) + contentViewController.didMove(toParent: self) + } + } + } +} diff --git a/ownCloudAppShared/User Interface/Gesture Recognizer/ActionTapGestureRecognizer.swift b/ownCloudAppShared/User Interface/Gesture Recognizer/ActionTapGestureRecognizer.swift new file mode 100644 index 000000000..8f18443c5 --- /dev/null +++ b/ownCloudAppShared/User Interface/Gesture Recognizer/ActionTapGestureRecognizer.swift @@ -0,0 +1,36 @@ +// +// ActionTapGestureRecognizer.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 23.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +open class ActionTapGestureRecognizer: UITapGestureRecognizer { + typealias Action = (UITapGestureRecognizer) -> Void + + var action: Action + + init(action: @escaping Action) { + self.action = action + + super.init(target: nil, action:nil) // can't pass self as target in super.init() + self.addTarget(self, action: #selector(runAction)) + } + + @objc private func runAction(_ gestureRecognizer: UITapGestureRecognizer) { + action(gestureRecognizer) + } +} diff --git a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift index 2b9ea0a5a..fd61d81dc 100644 --- a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift +++ b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift @@ -41,7 +41,7 @@ open class MoreViewHeader: UIView { public init(for item: OCItem, with core: OCCore, favorite: Bool = true, adaptBackgroundColor: Bool = false, showActivityIndicator: Bool = false) { self.item = item self.core = core - self.showFavoriteButton = favorite + self.showFavoriteButton = favorite && core.bookmark.hasCapability(.favorites) self.showActivityIndicator = showActivityIndicator iconView = ResourceViewHost() diff --git a/ownCloudAppShared/User Interface/Navigation Content/NavigationContent.swift b/ownCloudAppShared/User Interface/Navigation Content/NavigationContent.swift new file mode 100644 index 000000000..6baadf0bc --- /dev/null +++ b/ownCloudAppShared/User Interface/Navigation Content/NavigationContent.swift @@ -0,0 +1,183 @@ +// +// NavigationContent.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 24.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +open class NavigationContent: NSObject { + public static let existingContentIdentifier: String = "existing-content" + + weak var navigationItem: UINavigationItem? + + // Area to fill with content + public enum Area { + case left // Left bar buttons + case right // Right bar buttons + case title // Title + } + + // Position within area + public enum Position: Int { + case leading = 0 + case middle = 10 + case trailing = 20 + } + + // Priority of content - content is only shown from the respective highest priority + public enum Priority: Int { + case lowest = 0 + case low + case standard + case high + case highest + } + + public typealias Snapshot = [NavigationContentItem] + + public var initialExistingItemsSnapshot: Snapshot + + var items: [NavigationContentItem] = [] + + init(for navigationItem: UINavigationItem, existingWithPriority priority: Priority = .standard, position: Position = .trailing) { + initialExistingItemsSnapshot = navigationItem.takeNavigationContentSnapshot(withIdentifier: NavigationContent.existingContentIdentifier, priority: priority, position: position) + + super.init() + self.navigationItem = navigationItem + + items.append(contentsOf: initialExistingItemsSnapshot) + } + + public func add(items content: [NavigationContentItem]) { + // Remove existing items for ALL identifiers used in content + items.removeAll(where: { item in + return content.contains(where: { contentItem in + return contentItem.identifier == item.identifier + }) + }) + + // Add content + items.append(contentsOf: content) + + setNeedsRecomputation() + } + + public func remove(items content: [NavigationContentItem]) { + // Remove content + items.removeAll(where: { item in + content.contains(item) + }) + + setNeedsRecomputation() + } + + public func remove(itemsWithIdentifier identifier: String) { + // Remove all items with identifier + items.removeAll(where: { item in + return item.identifier == identifier + }) + + setNeedsRecomputation() + } + + public func remove(itemsWithIdentifiers identifiers: [String]) { + // Remove all items with identifiers + items.removeAll(where: { item in + return identifiers.contains(item.identifier) + }) + + setNeedsRecomputation() + } + + public func items(withIdentifier identifier: String) -> ([NavigationContentItem], [UIBarButtonItem]) { + let contentItems = items.filter({ item in + return (item.identifier == identifier) + }) + + var barButtonItems: [UIBarButtonItem] = [] + for contentItem in contentItems { + if let content = contentItem.items { + barButtonItems.append(contentsOf: content) + } + } + + return (contentItems, barButtonItems) + } + + private var _needsRecomputation: Bool = false + func setNeedsRecomputation(applyImmediately: Bool = true) { + if !applyImmediately { + _needsRecomputation = true + + OnMainThread { + if self._needsRecomputation { + self._needsRecomputation = false + self.recompute() + } + } + } else { + self.recompute() + } + } + + func pickAndComposeItems(for area: Area) -> ([NavigationContentItem], [UIBarButtonItem]) { + var highestPriority: Priority = .lowest + var areaItems = items.compactMap({ item in + if item.area == area { + if item.priority.rawValue > highestPriority.rawValue { + highestPriority = item.priority + } + + return item + } + return nil + }) + areaItems = areaItems.compactMap({ item in + return item.visibleInPriorities.contains(highestPriority) ? item : nil + }) + areaItems.sort(by: { item1, item2 in + return item1.position.rawValue < item2.position.rawValue + }) + + var barButtonItems: [UIBarButtonItem] = [] + + for item in areaItems { + if let items = item.items { + barButtonItems.append(contentsOf: items) + } + } + + return (areaItems, barButtonItems) + } + + func recompute() { + let (_, leftBarButtonItems) = pickAndComposeItems(for: .left) + navigationItem?.leftBarButtonItems = leftBarButtonItems + + let (_, rightBarButtonItems) = pickAndComposeItems(for: .right) + navigationItem?.rightBarButtonItems = rightBarButtonItems + + let (titleItems, _) = pickAndComposeItems(for: .title) + if let titleItem = titleItems.first { + if let title = titleItem.title { + navigationItem?.titleLabelText = title + } + if let titleView = titleItem.titleView { + navigationItem?.titleView = titleView + } + } + } +} diff --git a/ownCloudAppShared/User Interface/Navigation Content/NavigationContentItem.swift b/ownCloudAppShared/User Interface/Navigation Content/NavigationContentItem.swift new file mode 100644 index 000000000..dffcef1cd --- /dev/null +++ b/ownCloudAppShared/User Interface/Navigation Content/NavigationContentItem.swift @@ -0,0 +1,51 @@ +// +// NavigationContentItem.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 24.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public class NavigationContentItem: NSObject { + public var identifier: String + + public var area: NavigationContent.Area + public var priority: NavigationContent.Priority + public var visibleInPriorities: [NavigationContent.Priority] + public var position: NavigationContent.Position + + public var items: [UIBarButtonItem]? { + didSet { + + } + } + + public var titleView: UIView? + public var title: String? + + init(identifier: String, area: NavigationContent.Area, priority: NavigationContent.Priority, position: NavigationContent.Position, items: [UIBarButtonItem]? = nil, titleView: UIView? = nil, title: String? = nil) { + self.identifier = identifier + self.area = area + self.priority = priority + self.visibleInPriorities = [ priority ] + self.position = position + + super.init() + + self.titleView = titleView + self.title = title + self.items = items + } +} diff --git a/ownCloudAppShared/User Interface/Navigation Content/README.md b/ownCloudAppShared/User Interface/Navigation Content/README.md new file mode 100644 index 000000000..cd3c0cf8a --- /dev/null +++ b/ownCloudAppShared/User Interface/Navigation Content/README.md @@ -0,0 +1,9 @@ +# Navigation Content + +`NavigationContent` aims to solve the problem that different parts of the app want access to and modify the contents of the `UINavigationItem` depending on context, which can easily create a complex network of possibilities and combination that are hard/impossible to cover - let alone in a clean way. + +`NavigationContent` therefore acts as an independant broker of interests, by allowing different parts of the app to add and remove their content, providing an `Area` where the content should appear, a `Priority` with which the content should appear - and a `Position` of the content within its `Area`. + +`NavigationContent` will first determine the currently highest `Priority` in an `Area`, then pick all `NavigationContentItem`s whose `.visibleInPriorities` property contains that `Priority`, then sorts them by `Position` - and finally applies them to the `UINavigationItem`. + +By default `.visibleInPriorities` only contains the priority provided during `NavigationContentItem`s initialization, but by adding additional priorities, its visibility can be extended. F.ex. a "toggle toolbar" item can be added with `standard` priority (to not elevate the minimum priority, which would drive out other items), but also appear in higher priorities by adding them to its `.visibleInPriorities`. diff --git a/ownCloudAppShared/User Interface/Navigation Content/UINavigationItem+NavigationContent.swift b/ownCloudAppShared/User Interface/Navigation Content/UINavigationItem+NavigationContent.swift new file mode 100644 index 000000000..34de20712 --- /dev/null +++ b/ownCloudAppShared/User Interface/Navigation Content/UINavigationItem+NavigationContent.swift @@ -0,0 +1,53 @@ +// +// UINavigationItem+NavigationContent.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 24.01.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public extension UINavigationItem { + var navigationContent: NavigationContent { + let navigationContent: NavigationContent! = value(forAnnotatedProperty: "_navigationContent", withGenerator: { + return NavigationContent(for: self) + }) as? NavigationContent + + return navigationContent + } + + func takeNavigationContentSnapshot(withIdentifier identifier: String, priority: NavigationContent.Priority, position: NavigationContent.Position) -> NavigationContent.Snapshot { + let snapshot: NavigationContent.Snapshot = [ + NavigationContentItem(identifier: identifier, area: .left, priority: priority, position: position, items: leftBarButtonItems), + NavigationContentItem(identifier: identifier, area: .right, priority: priority, position: position, items: rightBarButtonItems) + ] + + return snapshot + } + + func applyNavigationContentSnapshot(_ snapshot: NavigationContent.Snapshot) { + for item in snapshot { + switch item.area { + case .left: + leftBarButtonItems = item.items + + case .right: + rightBarButtonItems = item.items + + default: break + } + } + } +} diff --git a/ownCloudAppShared/User Interface/Push Presentation Controller/PushPresentationController.swift b/ownCloudAppShared/User Interface/Push Presentation Controller/PushPresentationController.swift deleted file mode 100644 index 5c6f23b52..000000000 --- a/ownCloudAppShared/User Interface/Push Presentation Controller/PushPresentationController.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// PushPresentationController.swift -// ownCloud -// -// Created by Felix Schwarz on 22.04.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 PushPresentationController: UIPresentationController { - override func presentationTransitionWillBegin() { - if let transitionCoordinator = self.presentingViewController.transitionCoordinator { - self.presentingViewController.view.alpha = 1.0 - self.presentedViewController.view.alpha = 1.0 - - transitionCoordinator.animate(alongsideTransition: { (_) in - self.presentingViewController.view.alpha = 0.5 - self.presentedViewController.view.alpha = 1.0 - }) - } - } - - override func dismissalTransitionWillBegin() { - if let transitionCoordinator = self.presentingViewController.transitionCoordinator { - self.presentingViewController.view.alpha = 0.5 - self.presentedViewController.view.alpha = 1.0 - - transitionCoordinator.animate(alongsideTransition: { (_) in - self.presentingViewController.view.alpha = 1.0 - self.presentedViewController.view.alpha = 1.0 - }) - } - } -} diff --git a/ownCloudAppShared/User Interface/Push Presentation Controller/PushTransition.swift b/ownCloudAppShared/User Interface/Push Presentation Controller/PushTransition.swift deleted file mode 100644 index 972464c76..000000000 --- a/ownCloudAppShared/User Interface/Push Presentation Controller/PushTransition.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// PushTransition.swift -// ownCloud -// -// Created by Felix Schwarz on 22.04.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 - -public typealias PushTransitionRecovery = (_ previousController: UIViewController, _ window: UIWindow) -> Void - -public class PushTransition: NSObject, UIViewControllerAnimatedTransitioning { - public var dismissTransition : Bool = false - public var transitionRecovery : PushTransitionRecovery? - - public init(dismiss: Bool, transitionRecovery recoveryBlock: PushTransitionRecovery? = nil) { - dismissTransition = dismiss - transitionRecovery = recoveryBlock - } - - public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { - return 0.35 - } - - public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { - if let fromViewController = transitionContext.viewController(forKey: .from), - let toViewController = transitionContext.viewController(forKey: .to) { - - if dismissTransition { - var toViewControllerTranslationXMultiplier : CGFloat = -0.5 - var fromViewControllerTranslationXMultiplier : CGFloat = 1 - if fromViewController.view.effectiveUserInterfaceLayoutDirection == .rightToLeft { - toViewControllerTranslationXMultiplier = 1 - fromViewControllerTranslationXMultiplier = -0.5 - } - - fromViewController.view.frame = transitionContext.initialFrame(for: fromViewController) - toViewController.view.frame = transitionContext.finalFrame(for: toViewController) - - transitionContext.containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view) - - fromViewController.view.transform = .identity - - toViewController.view.transform = CGAffineTransform(translationX: toViewControllerTranslationXMultiplier * toViewController.view.frame.size.width, y: 0) - - UIView.animate(withDuration: self.transitionDuration(using: transitionContext), delay: 0, options: [ .curveEaseInOut ], animations: { - fromViewController.view.transform = CGAffineTransform(translationX: fromViewControllerTranslationXMultiplier * fromViewController.view.frame.size.width, y: 0) - toViewController.view.transform = .identity - }, completion: { _ in - let window = fromViewController.view.window - - fromViewController.view.transform = .identity - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - - // Work around an iOS bug where using a custom dismissal animation removes both fromViewController and toViewController at the end of the animation, leaving behind a black device screen with no views - if !transitionContext.transitionWasCancelled, let window = window { - if toViewController.view.window == nil { - if let transitionRecovery = self.transitionRecovery { - transitionRecovery(toViewController, window) - } else { - window.addSubview(toViewController.view) - } - } - } - }) - } else { - var toViewControllerTranslationXMultiplier : CGFloat = 1 - var fromViewControllerTranslationXMultiplier : CGFloat = -0.5 - if fromViewController.view.effectiveUserInterfaceLayoutDirection == .rightToLeft { - toViewControllerTranslationXMultiplier = -0.5 - fromViewControllerTranslationXMultiplier = 1 - } - - fromViewController.view.frame = transitionContext.finalFrame(for: fromViewController) - toViewController.view.frame = transitionContext.finalFrame(for: toViewController) - - transitionContext.containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view) - - toViewController.view.transform = CGAffineTransform(translationX: toViewControllerTranslationXMultiplier * toViewController.view.frame.size.width, y: 0) - - UIView.animate(withDuration: self.transitionDuration(using: transitionContext), delay: 0, options: [ .curveEaseInOut ], animations: { - fromViewController.view.transform = CGAffineTransform(translationX: fromViewControllerTranslationXMultiplier * fromViewController.view.frame.size.width, y: 0) - toViewController.view.transform = .identity - }, completion: { _ in - fromViewController.view.transform = .identity - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - }) - } - } - } -} diff --git a/ownCloudAppShared/User Interface/Push Presentation Controller/PushTransitionDelegate.swift b/ownCloudAppShared/User Interface/Push Presentation Controller/PushTransitionDelegate.swift deleted file mode 100644 index 38afaaa82..000000000 --- a/ownCloudAppShared/User Interface/Push Presentation Controller/PushTransitionDelegate.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// PushTransitionDelegate.swift -// ownCloud -// -// Created by Felix Schwarz on 22.04.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 - -final public class PushTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { - var transitionRecovery : PushTransitionRecovery? - - public init(with transitionRecovery: PushTransitionRecovery? = nil) { - self.transitionRecovery = transitionRecovery - super.init() - } - - public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - return PushPresentationController(presentedViewController: presented, presenting: presenting) - } - - public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - return PushTransition(dismiss: false) - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - return PushTransition(dismiss: true, transitionRecovery: transitionRecovery) - } -} diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentView.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentView.swift new file mode 100644 index 000000000..f1fdc3731 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentView.swift @@ -0,0 +1,277 @@ +// +// SegmentView.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class SegmentView: ThemeView { + public enum TruncationMode { + case none + case clipTail + case truncateHead + case truncateTail + } + + open var items: [SegmentViewItem] { + willSet { + for item in items { + item.segmentView = nil + } + } + + didSet { + if superview != nil { + recreateAndLayoutItemViews() + } + } + } + open var itemSpacing: CGFloat = 5 + open var truncationMode: TruncationMode = .none + open var insets: NSDirectionalEdgeInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + open var limitVerticalSpaceUsage: Bool = false + + private var isScrollable: Bool + + public init(with items: [SegmentViewItem], truncationMode: TruncationMode, scrollable: Bool = false, limitVerticalSpaceUsage: Bool = false) { + isScrollable = scrollable + self.limitVerticalSpaceUsage = limitVerticalSpaceUsage + + self.items = items + + super.init() + + self.truncationMode = truncationMode + isOpaque = false + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func gradientColors(fadeInFromLeft: Bool, baseColor: CGColor? = nil) -> [CGColor] { + var gradientColors: [CGColor] = [ + CGColor(red: 0, green: 0, blue: 0, alpha: fadeInFromLeft ? 0.0 : 1.0), + CGColor(red: 0, green: 0, blue: 0, alpha: fadeInFromLeft ? 1.0 : 0.0) + ] + + if let startColor = baseColor?.copy(alpha: fadeInFromLeft ? 0.0 : 1.0), + let endColor = baseColor?.copy(alpha: fadeInFromLeft ? 1.0 : 0.0) { + gradientColors = [ startColor, endColor ] + } + + return gradientColors + } + + func gradientView(fadeInFromLeft: Bool, baseColor: CGColor? = nil) -> GradientView { + let gradientColors = gradientColors(fadeInFromLeft: fadeInFromLeft, baseColor: baseColor) + let gradientWidth : CGFloat = 20 + let gradientView = GradientView(with: gradientColors, locations: [0, 1], direction: .horizontal) + + gradientView.translatesAutoresizingMaskIntoConstraints = false + gradientView.widthAnchor.constraint(equalToConstant: gradientWidth).isActive = true + + return gradientView + } + + func composeMaskView(leading: Bool) -> UIView { + let fadeInFromLeft: Bool = (effectiveUserInterfaceLayoutDirection == .leftToRight) ? leading : !leading + let rootView = UIView(frame: bounds) + let fillView = UIView() + let gradientView = gradientView(fadeInFromLeft: fadeInFromLeft) + + fillView.backgroundColor = .black + + fillView.translatesAutoresizingMaskIntoConstraints = false + + var constraints: [NSLayoutConstraint] = [ + fillView.topAnchor.constraint(equalTo: rootView.topAnchor), + fillView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor), + gradientView.topAnchor.constraint(equalTo: rootView.topAnchor), + gradientView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor) + ] + + rootView.addSubview(fillView) + rootView.addSubview(gradientView) + + if fadeInFromLeft { + constraints.append(contentsOf: [ + gradientView.leftAnchor.constraint(equalTo: rootView.leftAnchor), + gradientView.rightAnchor.constraint(equalTo: fillView.leftAnchor), + fillView.rightAnchor.constraint(equalTo: rootView.rightAnchor) + ]) + } else { + constraints.append(contentsOf: [ + fillView.leftAnchor.constraint(equalTo: rootView.leftAnchor), + fillView.rightAnchor.constraint(equalTo: gradientView.leftAnchor), + gradientView.rightAnchor.constraint(equalTo: rootView.rightAnchor) + ]) + } + + NSLayoutConstraint.activate(constraints) + + return rootView + } + + private var itemViews: [UIView] = [] + private var scrollView: UIScrollView? + private var scrollGradientLeft: GradientView? + private var scrollGradientRight: GradientView? + private var scrollViewContentOffset: NSKeyValueObservation? + public var scrollViewOverlayGradientColor: CGColor? { + didSet { + scrollGradientLeft?.colors = gradientColors(fadeInFromLeft: false, baseColor: scrollViewOverlayGradientColor) + scrollGradientRight?.colors = gradientColors(fadeInFromLeft: true, baseColor: scrollViewOverlayGradientColor) + } + } + + override open func setupSubviews() { + super.setupSubviews() + recreateAndLayoutItemViews() + } + + func recreateAndLayoutItemViews() { + // Remove existing views + for itemView in itemViews { + itemView.removeFromSuperview() + } + + itemViews.removeAll() + + // Create new views + for item in items { + item.segmentView = self + + if let view = item.view { + itemViews.append(view) + } + } + + // Scroll View + var hostView: UIView = self + + if isScrollable, scrollView == nil { + scrollView = UIScrollView(frame: .zero) + scrollView?.showsVerticalScrollIndicator = false + scrollView?.showsHorizontalScrollIndicator = false + scrollView?.translatesAutoresizingMaskIntoConstraints = false + + scrollGradientLeft = gradientView(fadeInFromLeft: false, baseColor: scrollViewOverlayGradientColor) + scrollGradientRight = gradientView(fadeInFromLeft: true, baseColor: scrollViewOverlayGradientColor) + + if let scrollView { + hostView = scrollView + + embed(toFillWith: scrollView) + } + + if let scrollGradientLeft, let scrollGradientRight { + addSubview(scrollGradientLeft) + addSubview(scrollGradientRight) + + NSLayoutConstraint.activate([ + scrollGradientLeft.leftAnchor.constraint(equalTo: self.leftAnchor), + scrollGradientLeft.topAnchor.constraint(equalTo: self.topAnchor), + scrollGradientLeft.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + scrollGradientRight.rightAnchor.constraint(equalTo: self.rightAnchor), + scrollGradientRight.topAnchor.constraint(equalTo: self.topAnchor), + scrollGradientRight.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + } + + scrollViewContentOffset = scrollView?.observe(\.contentOffset, options: .initial, changeHandler: { [weak self] scrollView, _ in + let bounds = scrollView.bounds + let contentSize = scrollView.contentSize + let contentOffset = scrollView.contentOffset + + if let scrollGradientLeft = self?.scrollGradientLeft { + scrollGradientLeft.isHidden = !(contentOffset.x > 0) + } + + if let scrollGradientRight = self?.scrollGradientRight { + scrollGradientRight.isHidden = !(contentSize.width - contentOffset.x > bounds.width) + } + }) + } + + // Embed + hostView.embedHorizontally(views: itemViews, insets: insets, limitHeight: limitVerticalSpaceUsage, spacingProvider: { _, _ in + return self.itemSpacing + }, constraintsModifier: { constraintSet in + // Implement truncation + masking + var maskView: UIView? + + switch self.truncationMode { + case .none: break + + case .clipTail: + constraintSet.lastTrailingOrBottomConstraint?.priority = .defaultHigh + + case .truncateHead: + if !self.isScrollable { + constraintSet.firstLeadingOrTopConstraint?.priority = .defaultHigh + maskView = self.composeMaskView(leading: true) + } + + case .truncateTail: + if !self.isScrollable { + constraintSet.lastTrailingOrBottomConstraint?.priority = .defaultHigh + maskView = self.composeMaskView(leading: false) + } + } + + if let maskView = maskView { + maskView.translatesAutoresizingMaskIntoConstraints = false + self.embed(toFillWith: maskView) + self.mask = maskView + } + + return constraintSet + }) + + // Layout without animation + UIView.performWithoutAnimation { + layoutIfNeeded() + + if isScrollable { + scrollToTruncationTarget() + } + } + } + + func scrollToTruncationTarget() { + switch truncationMode { + case .truncateTail: + if let contentWidth = scrollView?.contentSize.width { + scrollView?.scrollRectToVisible(CGRect(x: contentWidth-1, y: 0, width: 1, height: 1), animated: false) + } + + case .truncateHead: + scrollView?.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: false) + + default: break + } + } + + public override var bounds: CGRect { + didSet { + OnMainThread { + self.scrollToTruncationTarget() + } + } + } +} diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift new file mode 100644 index 000000000..c8b67bc46 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift @@ -0,0 +1,81 @@ +// +// SegmentViewItem.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class SegmentViewItem: NSObject { + public enum CornerStyle { + case sharp + case round(points: CGFloat) + } + + public enum Style { + case plain + case label + case token + } + + open weak var segmentView: SegmentView? + + open var style: Style + open var icon: UIImage? + open var title: String? { + didSet { + _view = nil + } + } + open var titleTextStyle: UIFont.TextStyle? + open var titleTextWeight: UIFont.Weight? + + open var representedObject: AnyObject? + open weak var weakRepresentedObject: AnyObject? + + open var iconTitleSpacing: CGFloat = 2 + open var insets: NSDirectionalEdgeInsets = NSDirectionalEdgeInsets(top: 3, leading: 5, bottom: 3, trailing: 5) + open var cornerStyle: CornerStyle? + open var alpha: CGFloat = 1.0 + + open var gestureRecognizers: [UIGestureRecognizer]? + + var _view: UIView? + open var view: UIView? { + if _view == nil { + _view = SegmentViewItemView(with: self) + _view?.translatesAutoresizingMaskIntoConstraints = false + + if let gestureRecognizers { + _view?.gestureRecognizers = gestureRecognizers + } + } + return _view + } + + public init(with icon: UIImage? = nil, title: String? = nil, style: Style = .plain, titleTextStyle: UIFont.TextStyle? = nil, titleTextWeight: UIFont.Weight? = nil, representedObject: AnyObject? = nil, weakRepresentedObject: AnyObject? = nil, gestureRecognizers: [UIGestureRecognizer]? = nil) { + self.style = style + + super.init() + + self.icon = icon + self.title = title + self.titleTextStyle = titleTextStyle + self.titleTextWeight = titleTextWeight + self.representedObject = representedObject + self.weakRepresentedObject = weakRepresentedObject + self.gestureRecognizers = gestureRecognizers + } +} diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift new file mode 100644 index 000000000..8cb1a7dd1 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift @@ -0,0 +1,120 @@ +// +// SegmentViewItemView.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public class SegmentViewItemView: ThemeView { + weak var item: SegmentViewItem? + + var iconView: UIImageView? + var titleView: UILabel? + + public init(with item: SegmentViewItem) { + self.item = item + + super.init() + + isOpaque = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func setupSubviews() { + super.setupSubviews() + compose() + } + + open func compose() { + guard let item else { return } + + let rootView = self + var views : [UIView] = [] + + rootView.setContentHuggingPriority(.required, for: .horizontal) + rootView.setContentHuggingPriority(.required, for: .vertical) + + if let icon = item.icon { + iconView = UIImageView() + iconView?.image = icon.withRenderingMode(.alwaysTemplate) + iconView?.contentMode = .scaleAspectFit + iconView?.translatesAutoresizingMaskIntoConstraints = false + iconView?.setContentHuggingPriority(.required, for: .horizontal) + iconView?.setContentHuggingPriority(.required, for: .vertical) + iconView?.setContentCompressionResistancePriority(.required, for: .horizontal) + iconView?.setContentCompressionResistancePriority(.required, for: .vertical) + views.append(iconView!) + } + + if let title = item.title { + titleView = UILabel() + titleView?.translatesAutoresizingMaskIntoConstraints = false + titleView?.text = title + if let titleTextStyle = item.titleTextStyle { + if let titleTextWeight = item.titleTextWeight { + titleView?.font = .preferredFont(forTextStyle: titleTextStyle, with: titleTextWeight) + } else { + titleView?.font = .preferredFont(forTextStyle: titleTextStyle) + } + } + titleView?.setContentHuggingPriority(.required, for: .horizontal) + titleView?.setContentHuggingPriority(.required, for: .vertical) + titleView?.setContentCompressionResistancePriority(.required, for: .vertical) + titleView?.setContentCompressionResistancePriority(.required, for: .horizontal) + + views.append(titleView!) + } + + embedHorizontally(views: views, insets: item.insets, limitHeight: item.segmentView?.limitVerticalSpaceUsage ?? false, spacingProvider: { leadingView, trailingView in + if trailingView == self.titleView, leadingView == self.iconView { + return item.iconTitleSpacing + } + + return nil + }) + + switch item.cornerStyle { + case .none, .sharp: + layer.cornerRadius = 0 + + case .round(let points): + layer.cornerRadius = points + } + + alpha = item.alpha + } + + public override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + + if let item { + switch item.style { + case .plain, .label: + iconView?.tintColor = collection.tableRowColors.symbolColor + titleView?.textColor = collection.tableRowColors.secondaryLabelColor + backgroundColor = .clear + + case .token: + iconView?.tintColor = collection.tokenColors.normal.foreground + titleView?.textColor = collection.tokenColors.normal.foreground + backgroundColor = collection.tokenColors.normal.background + } + } + } +} diff --git a/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift b/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift new file mode 100644 index 000000000..36b64c171 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift @@ -0,0 +1,235 @@ +// +// UIView+EmbedAndLayout.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public extension UIView { + typealias SpacingProvider = (_ leadingView: UIView, _ trailingView: UIView) -> CGFloat? + typealias ConstraintsModifier = (_ constraintSet: ConstraintSet) -> ConstraintSet + + struct ConstraintSet { + var firstLeadingOrTopConstraint: NSLayoutConstraint? + var lastTrailingOrBottomConstraint: NSLayoutConstraint? + } + + struct AnchorSet { + var leadingAnchor: NSLayoutXAxisAnchor + var trailingAnchor: NSLayoutXAxisAnchor + + var topAnchor: NSLayoutYAxisAnchor + var bottomAnchor: NSLayoutYAxisAnchor + + var centerXAnchor: NSLayoutXAxisAnchor + var centerYAnchor: NSLayoutYAxisAnchor + } + + var defaultAnchorSet : AnchorSet { + return AnchorSet(leadingAnchor: leadingAnchor, trailingAnchor: trailingAnchor, topAnchor: topAnchor, bottomAnchor: bottomAnchor, centerXAnchor: centerXAnchor, centerYAnchor: centerYAnchor) + } + + var safeAreaAnchorSet : AnchorSet { + return AnchorSet(leadingAnchor: safeAreaLayoutGuide.leadingAnchor, trailingAnchor: safeAreaLayoutGuide.trailingAnchor, topAnchor: safeAreaLayoutGuide.topAnchor, bottomAnchor: safeAreaLayoutGuide.bottomAnchor, centerXAnchor: safeAreaLayoutGuide.centerXAnchor, centerYAnchor: safeAreaLayoutGuide.centerYAnchor) + } + + @discardableResult func embedHorizontally(views: [UIView], insets: NSDirectionalEdgeInsets, enclosingAnchors: AnchorSet? = nil, limitHeight: Bool = false, spacingProvider: SpacingProvider? = nil, constraintsModifier: ConstraintsModifier? = nil) -> ConstraintSet { + var viewIdx : Int = 0 + var previousView: UIView? + var embedConstraints: [NSLayoutConstraint] = [] + + var constraintSet: ConstraintSet = ConstraintSet() + let anchorSet = enclosingAnchors ?? defaultAnchorSet + + for view in views { + var leadingConstraint: NSLayoutConstraint? + + // Create leading constraint + if viewIdx == 0 { + leadingConstraint = view.leadingAnchor.constraint(equalTo: anchorSet.leadingAnchor, constant: insets.leading) + constraintSet.firstLeadingOrTopConstraint = leadingConstraint + } else if let previousView = previousView { + let spacing : CGFloat = spacingProvider?(previousView, view) ?? 0 + leadingConstraint = view.leadingAnchor.constraint(equalTo: previousView.trailingAnchor, constant: spacing) + } + + // Add constraints + // - leading + if let leadingConstraint = leadingConstraint { + embedConstraints.append(leadingConstraint) + } + + // - vertical position + insets + embedConstraints.append(contentsOf: [ + view.centerYAnchor.constraint(equalTo: anchorSet.centerYAnchor), + view.topAnchor.constraint(greaterThanOrEqualTo: anchorSet.topAnchor, constant: insets.top), + view.bottomAnchor.constraint(lessThanOrEqualTo: anchorSet.bottomAnchor, constant: -insets.bottom) + ]) + + if limitHeight { + // Add top/bottom constraints with stricter requirements, but with lower priority, nudging the layout engine to a more compact layout + embedConstraints.append(contentsOf: [ + view.topAnchor.constraint(equalTo: anchorSet.topAnchor, constant: insets.top).with(priority: .defaultHigh), + view.bottomAnchor.constraint(equalTo: anchorSet.bottomAnchor, constant: -insets.bottom).with(priority: .defaultHigh) + ]) + } + + // - trailing + if viewIdx == (views.count-1) { + let trailingConstraint = view.trailingAnchor.constraint(equalTo: anchorSet.trailingAnchor, constant: -insets.trailing) + constraintSet.lastTrailingOrBottomConstraint = trailingConstraint + embedConstraints.append(trailingConstraint) + } + + // Add subview + addSubview(view) + + previousView = view + viewIdx += 1 + } + + // Modify constraints + if let constraintsModifier = constraintsModifier { + constraintSet = constraintsModifier(constraintSet) + } + + // Activate constraints + NSLayoutConstraint.activate(embedConstraints) + + return constraintSet + } + + @discardableResult func embedVertically(views: [UIView], insets: NSDirectionalEdgeInsets, enclosingAnchors: AnchorSet? = nil, spacingProvider: SpacingProvider? = nil, constraintsModifier: ConstraintsModifier? = nil) -> ConstraintSet { + var viewIdx : Int = 0 + var previousView: UIView? + var embedConstraints: [NSLayoutConstraint] = [] + + var constraintSet: ConstraintSet = ConstraintSet() + let anchorSet = enclosingAnchors ?? defaultAnchorSet + + for view in views { + var topConstraint: NSLayoutConstraint? + + // Create top constraint + if viewIdx == 0 { + topConstraint = view.topAnchor.constraint(equalTo: anchorSet.topAnchor, constant: insets.top) + constraintSet.firstLeadingOrTopConstraint = topConstraint + } else if let previousView = previousView { + let spacing : CGFloat = spacingProvider?(previousView, view) ?? 0 + topConstraint = view.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: spacing) + } + + // Add constraints + // - top + if let topConstraint = topConstraint { + embedConstraints.append(topConstraint) + } + + // - horizontal position + insets + embedConstraints.append(contentsOf: [ + view.centerXAnchor.constraint(equalTo: anchorSet.centerXAnchor), + view.leadingAnchor.constraint(greaterThanOrEqualTo: anchorSet.leadingAnchor, constant: insets.leading), + view.trailingAnchor.constraint(lessThanOrEqualTo: anchorSet.trailingAnchor, constant: -insets.trailing) + ]) + + // - bottom + if viewIdx == (views.count-1) { + let bottomConstraint = view.bottomAnchor.constraint(equalTo: anchorSet.bottomAnchor, constant: -insets.bottom) + constraintSet.lastTrailingOrBottomConstraint = bottomConstraint + embedConstraints.append(bottomConstraint) + } + + // Add subview + addSubview(view) + + previousView = view + viewIdx += 1 + } + + // Modify constraints + if let constraintsModifier = constraintsModifier { + constraintSet = constraintsModifier(constraintSet) + } + + // Activate constraints + NSLayoutConstraint.activate(embedConstraints) + + return constraintSet + } + + @discardableResult func embed(toFillWith view: UIView, insets: NSDirectionalEdgeInsets = .zero, enclosingAnchors: AnchorSet? = nil) -> [NSLayoutConstraint] { + view.translatesAutoresizingMaskIntoConstraints = false + + addSubview(view) + + var constraints : [NSLayoutConstraint] + let anchorSet = enclosingAnchors ?? defaultAnchorSet + + constraints = [ + view.leadingAnchor.constraint(equalTo: anchorSet.leadingAnchor, constant: insets.leading), + view.trailingAnchor.constraint(equalTo: anchorSet.trailingAnchor, constant: -insets.trailing), + view.topAnchor.constraint(equalTo: anchorSet.topAnchor, constant: insets.top), + view.bottomAnchor.constraint(equalTo: anchorSet.bottomAnchor, constant: -insets.bottom) + ] + + NSLayoutConstraint.activate(constraints) + + return constraints + } + + @discardableResult func embed(centered view: UIView, minimumInsets insets: NSDirectionalEdgeInsets = .zero, fixedSize: CGSize? = nil, minimumSize: CGSize? = nil, maximumSize: CGSize? = nil, enclosingAnchors: AnchorSet? = nil) -> [NSLayoutConstraint] { + view.translatesAutoresizingMaskIntoConstraints = false + + addSubview(view) + + var constraints: [NSLayoutConstraint] + let anchorSet = enclosingAnchors ?? defaultAnchorSet + + constraints = [ + view.leadingAnchor.constraint(greaterThanOrEqualTo: anchorSet.leadingAnchor, constant: insets.leading), + view.trailingAnchor.constraint(lessThanOrEqualTo: anchorSet.trailingAnchor, constant: -insets.trailing), + view.topAnchor.constraint(greaterThanOrEqualTo: anchorSet.topAnchor, constant: insets.top), + view.bottomAnchor.constraint(lessThanOrEqualTo: anchorSet.bottomAnchor, constant: -insets.bottom), + view.centerXAnchor.constraint(equalTo: anchorSet.centerXAnchor), + view.centerYAnchor.constraint(equalTo: anchorSet.centerYAnchor) + ] + + if let fixedSize { + constraints += [ + view.widthAnchor.constraint(equalToConstant: fixedSize.width).with(priority: .defaultHigh), + view.heightAnchor.constraint(equalToConstant: fixedSize.height).with(priority: .defaultHigh) + ] + } + + if let minimumSize { + constraints += [ + view.widthAnchor.constraint(greaterThanOrEqualToConstant: minimumSize.width), + view.heightAnchor.constraint(greaterThanOrEqualToConstant: minimumSize.height) + ] + } + + if let maximumSize { + constraints += [ + view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumSize.width), + view.heightAnchor.constraint(lessThanOrEqualToConstant: maximumSize.height) + ] + } + + NSLayoutConstraint.activate(constraints) + + return constraints + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionConnect.swift b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionConnect.swift new file mode 100644 index 000000000..029946116 --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionConnect.swift @@ -0,0 +1,70 @@ +// +// AppStateActionConnect.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 08.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public class AppStateActionConnect: AppStateAction { + var bookmarkUUID: String? + + public init(bookmarkUUID: String, children: [AppStateAction]? = nil) { + super.init(with: children) + self.bookmarkUUID = bookmarkUUID + } + + override open class var supportsSecureCoding: Bool { + return true + } + + public required init?(coder: NSCoder) { + bookmarkUUID = coder.decodeObject(of: NSString.self, forKey: "bookmarkUUID") as? String + super.init(coder: coder) + } + + override public func encode(with coder: NSCoder) { + super.encode(with: coder) + coder.encode(bookmarkUUID, forKey: "bookmarkUUID") + } + + override public func perform(in clientContext: ClientContext, completion: @escaping AppStateAction.Completion) { + if let bookmarkUUIDString = bookmarkUUID, + let bookmark = OCBookmarkManager.shared.bookmark(forUUIDString: bookmarkUUIDString), + let connection = AccountConnectionPool.shared.connection(for: bookmark) { + connection.connect(completion: { error in + if error == nil, let contextProvider = clientContext as? ClientContextProvider, let bookmarkUUID = UUID(uuidString: bookmarkUUIDString) { + OnMainThread { + // Fetch ClientContext for the bookmark, then pass it to the children + contextProvider.provideClientContext(for: bookmarkUUID, completion: { (providerError, providedClientContext) in + completion(providerError, providedClientContext ?? clientContext) + }) + } + } else { + completion(error, clientContext) + } + }) + } else { + completion(NSError.init(ocError: .unknown), clientContext) + } + } +} + +public extension AppStateAction { + static func connection(with bookmark: OCBookmark, children: [AppStateAction]? = nil) -> AppStateActionConnect { + return AppStateActionConnect(bookmarkUUID: bookmark.uuid.uuidString, children: children) + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionGoToPersonalFolder.swift b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionGoToPersonalFolder.swift new file mode 100644 index 000000000..27d3665b2 --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionGoToPersonalFolder.swift @@ -0,0 +1,70 @@ +// +// AppStateActionGoToPersonalFolder.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 14.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public class AppStateActionGoToPersonalFolder: AppStateAction { + public init(children: [AppStateAction]? = nil) { + super.init(with: children) + } + + override open class var supportsSecureCoding: Bool { + return true + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override public func encode(with coder: NSCoder) { + super.encode(with: coder) + } + + var drivesSubscription: OCDataSourceSubscription? + + override public func perform(in clientContext: ClientContext, completion: @escaping AppStateAction.Completion) { + if let core = clientContext.core { + if core.useDrives { + if let personalDrive = core.drives.first(where: { drive in + drive.specialType == .personal + }) { + open(location: personalDrive.rootLocation, in: clientContext, completion: completion) + } else { + completion(NSError(ocError: .itemNotFound), clientContext) + } + } else { + open(location: OCLocation.legacyRoot, in: clientContext, completion: completion) + } + } + } + + func open(location: OCLocation, in clientContext: ClientContext, completion: @escaping AppStateAction.Completion) { + OnMainThread { + _ = location.openItem(from: nil, with: clientContext, animated: false, pushViewController: true, completion: { _ in + completion(nil, clientContext) + }) + } + } +} + +public extension AppStateAction { + static func goToPersonalFolder(children: [AppStateAction]? = nil) -> AppStateActionGoToPersonalFolder { + return AppStateActionGoToPersonalFolder(children: children) + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRestoreNavigationBookmark.swift b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRestoreNavigationBookmark.swift new file mode 100644 index 000000000..27e45d922 --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRestoreNavigationBookmark.swift @@ -0,0 +1,68 @@ +// +// AppStateActionRestoreNavigationBookmark.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 09.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public class AppStateActionRestoreNavigationBookmark: AppStateAction { + var navigationBookmark: BrowserNavigationBookmark? + + public init(navigationBookmark: BrowserNavigationBookmark, children: [AppStateAction]? = nil) { + super.init(with: children) + self.navigationBookmark = navigationBookmark + } + + override open class var supportsSecureCoding: Bool { + return true + } + + public required init?(coder: NSCoder) { + navigationBookmark = coder.decodeObject(of: BrowserNavigationBookmark.self, forKey: "navigationBookmark") + super.init(coder: coder) + } + + override public func encode(with coder: NSCoder) { + super.encode(with: coder) + coder.encode(navigationBookmark, forKey: "navigationBookmark") + } + + override public func perform(in clientContext: ClientContext, completion: @escaping AppStateAction.Completion) { + if let navigationBookmark { + navigationBookmark.restore(in: nil, with: clientContext, completion: { (error, viewController) in + defer { + completion(error, clientContext) + } + + guard error == nil else { + return + } + + _ = clientContext.pushViewControllerToNavigation(context: clientContext, provider: { context in + return viewController + }, push: true, animated: false) + }) + } else { + completion(NSError.init(ocError: .unknown), clientContext) + } + } +} + +public extension AppStateAction { + static func navigate(to navigationBookmark: BrowserNavigationBookmark, children: [AppStateAction]? = nil) -> AppStateActionRestoreNavigationBookmark { + return AppStateActionRestoreNavigationBookmark(navigationBookmark: navigationBookmark, children: children) + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRevealItem.swift b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRevealItem.swift new file mode 100644 index 000000000..7332980c1 --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRevealItem.swift @@ -0,0 +1,59 @@ +// +// AppStateActionRevealItem.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 14.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public class AppStateActionRevealItem: AppStateAction { + var item: OCItem? + + public init(item: OCItem, children: [AppStateAction]? = nil) { + super.init(with: children) + self.item = item + } + + override open class var supportsSecureCoding: Bool { + return true + } + + public required init?(coder: NSCoder) { + item = coder.decodeObject(of: OCItem.self, forKey: "item") + super.init(coder: coder) + } + + override public func encode(with coder: NSCoder) { + super.encode(with: coder) + coder.encode(item, forKey: "item") + } + + override public func perform(in clientContext: ClientContext, completion: @escaping AppStateAction.Completion) { + if let item { + _ = item.revealItem(from: nil, with: clientContext, animated: false, pushViewController: true, completion: { success in + completion(nil, clientContext) + }) + } else { + completion(NSError.init(ocError: .unknown), clientContext) + } + } +} + +public extension AppStateAction { + static func reveal(item: OCItem, children: [AppStateAction]? = nil) -> AppStateActionRevealItem { + return AppStateActionRevealItem(item: item, children: children) + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/AppStateAction.swift b/ownCloudAppShared/User Interface/State Restoration/AppStateAction.swift new file mode 100644 index 000000000..9a1fa9740 --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/AppStateAction.swift @@ -0,0 +1,148 @@ +// +// AppStateAction.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 07.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 UIKit +import ownCloudSDK + +public protocol ClientContextProvider { + func provideClientContext(for bookmarkUUID: UUID, completion: (Error?, ClientContext?) -> Void) +} + +open class AppStateAction: NSObject, NSSecureCoding, UserActivityCapture, UserActivityRestoration { + weak open var parent: AppStateAction? + open var children: [AppStateAction]? { + didSet { + rewireChildren() + } + } + + public typealias Completion = (_ error: Error?, _ clientContext: ClientContext) -> Void + + public init(with childActions: [AppStateAction]? = nil) { + super.init() + children = childActions + rewireChildren() + } + + public func run(in clientContext: ClientContext, completion: @escaping Completion) { + perform(in: clientContext, completion: { (error, clientContext) in + if error == nil, let children = self.children, children.count > 0 { + let dispatchGroup = DispatchGroup() + var lastChildError: Error? + + for child in children { + dispatchGroup.enter() + child.run(in: clientContext, completion: { error, clientContext in + if let error { + Log.debug("Execution of AppStateAction \(self) child \(child) failed with error \(error)") + lastChildError = error + } + + dispatchGroup.leave() + }) + } + + dispatchGroup.notify(queue: .main, execute: { + completion(lastChildError, clientContext) + }) + } else { + if let error { + Log.debug("Execution of AppStateAction \(self) failed with error \(error)") + } + completion(error, clientContext) + } + }) + } + + func rewireChildren() { + guard let children else { return } + + for child in children { + child.parent = self + } + } + + // MARK: - Subclassing points + open class var supportsSecureCoding: Bool { // Needs to be subclassed for every subclass implementing NSSecureCoding - or otherwise NSKeyedArchiver will raise an exception + return true + } + + open func encode(with coder: NSCoder) { + if let children = children as? NSArray { + coder.encode(children, forKey: "children") + } + // In subclasses, call super and encode action contents + } + + public required init?(coder: NSCoder) { + super.init() + children = coder.decodeArrayOfObjects(ofClass: AppStateAction.self, forKey: "children") + rewireChildren() + // In subclasses, call super and decode action contents + } + + open func perform(in clientContext: ClientContext, completion: @escaping Completion) { + // Perform your action here, call completion when done + + // Default implementation just calls the completion handler, allowing it to be used as a container + completion(nil, clientContext) + } + + // MARK: - UserActivityCapture + public func captureUserActivityData(with options: [UserActivityOption : NSObject]?) -> Data? { + if let data = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) { + return data + } + + return nil + } + + // MARK: - UserActivityRestoration + public static func restoreFromUserActivity(with data: Data?, options: [UserActivityOption.RawValue : NSObject]?, completion: NSUserActivity.RestoreCompletionHandler?) { + if let context = options?[UserActivityOption.clientContext.rawValue] as? ClientContext, + let actionData = data { + do { + if let action = try NSKeyedUnarchiver.unarchivedObject(ofClass: AppStateAction.self, from: actionData) { + action.run(in: context, completion: { error, clientContext in + completion?(error) + }) + } else { + completion?(NSError(ocError: .invalidType)) + } + } catch { + completion?(error) + } + + } else { + completion?(NSError(ocError: .invalidParameter)) + } + } + + // MARK: - User activities + open func userActivity(with clientContext: ClientContext?) -> NSUserActivity? { + if let clientContext { + return NSUserActivity.capture(from: self, with: [ + .clientContext : clientContext + ]) + } + + return NSUserActivity.capture(from: self) + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/NSUserActivity+SaveRestore.swift b/ownCloudAppShared/User Interface/State Restoration/NSUserActivity+SaveRestore.swift new file mode 100644 index 000000000..d0fc5aee8 --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/NSUserActivity+SaveRestore.swift @@ -0,0 +1,79 @@ +// +// NSUserActivity+SaveRestore.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 08.02.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, 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 + +public enum UserActivityOption: String { + case clientContext //!< ClientContext instance +} + +public protocol UserActivityCapture: NSObject { + func captureUserActivityData(with options: [UserActivityOption : NSObject]?) -> Data? +} + +public protocol UserActivityRestoration: NSObject { + static func restoreFromUserActivity(with data: Data?, options: [UserActivityOption.RawValue : NSObject]?, completion: NSUserActivity.RestoreCompletionHandler?) +} + +public extension NSUserActivity { + private static let CaptureRestoreActivityType = "com.owncloud.captureRestoreActivityType" + private static let UserInfoKeyClassName = "className" + private static let UserInfoKeyActivityData = "activityData" + + typealias RestoreCompletionHandler = (Error?) -> Void + + var isRestorableActivity: Bool { + return (activityType == NSUserActivity.CaptureRestoreActivityType) && (userInfo?[NSUserActivity.UserInfoKeyClassName] != nil) + } + + static func capture(from object: UserActivityCapture, with options: [UserActivityOption : NSObject]? = nil) -> NSUserActivity? { + let activity = NSUserActivity(activityType: NSUserActivity.CaptureRestoreActivityType) + let className = NSStringFromClass(type(of: object)) + + activity.addUserInfoEntries(from: [ + UserInfoKeyClassName : className + ]) + + if let activityData = object.captureUserActivityData(with: options) { + activity.addUserInfoEntries(from: [ + UserInfoKeyActivityData : activityData + ]) + } + + return activity + } + + func restore(with options: [UserActivityOption.RawValue : NSObject]? = nil, completion: RestoreCompletionHandler? = nil) { + guard let className = userInfo?[NSUserActivity.UserInfoKeyClassName] as? String, isRestorableActivity else { + completion?(NSError(ocError: .internal)) + return + } + + if let restoreClass = NSClassFromString(className) { + let activityData = userInfo?[NSUserActivity.UserInfoKeyActivityData] as? Data + + if let restoration = restoreClass as? UserActivityRestoration.Type { + restoration.restoreFromUserActivity(with: activityData, options: options, completion: completion) + } else { + completion?(NSError(ocError: .featureNotImplemented)) + } + } else { + completion?(NSError(ocError: .featureNotImplemented)) + } + } +} diff --git a/ownCloudAppShared/User Interface/State Restoration/README.md b/ownCloudAppShared/User Interface/State Restoration/README.md new file mode 100644 index 000000000..8b042b82c --- /dev/null +++ b/ownCloudAppShared/User Interface/State Restoration/README.md @@ -0,0 +1,25 @@ +# State Restoration + +App State is captured as a set of actions that can be serialized into an `NSUserActivity`, be deserialized later - and then applied. + +The smallest unit is the `AppStateAction`. + +The actual implementation resides in `perform(in:completion:)`, whereas `run(in:completion:)` is invoked when running an action. Both take a `ClientContext` and a completion handler, which takes both an `Error` and a `ClientContext`. + +## Children + +An `AppStateAction` can have children, allowing to establish dependencies and time actions. + +An `AppStateAction` with children first invokes `perform(in:completion:)` and - if it returned without error - runs the children. + +When running the children, it uses the returned `ClientContext` and waits until all children have called their completion handler before calling its own completion handler. + + +## Composition + +To allow simpler composition, subclasses provide an extension for `AppStateAction` that allows their easy creation. + + +# NSUserActivity Save & Restore + +`NSUserActivity`s can be created from `AppStateAction`s - and be restored later. This allows state restoration as well as easy, flexible and extensible construction of new `UIScene`s. diff --git a/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift b/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift index b3adf4ec3..20a72772e 100644 --- a/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift +++ b/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift @@ -17,6 +17,7 @@ */ import UIKit +import ownCloudSDK public enum ThemeItemStyle { case defaultForItem @@ -46,6 +47,9 @@ public enum ThemeItemStyle { case purchase case welcome + case welcomeInformal + + case content } public enum ThemeItemState { @@ -93,6 +97,11 @@ public extension NSObject { case .welcome: themeButton.themeColorCollection = collection.loginColors.filledColorPairCollection + case .welcomeInformal: + let fromPair = collection.loginColors.filledColorPairCollection + let normal = ThemeColorPair(foreground: fromPair.normal.foreground.lighter(0.25), background: fromPair.normal.background.lighter(0.25)) + themeButton.themeColorCollection = ThemeColorPairCollection(fromPair: normal) + case .informal: themeButton.themeColorCollection = collection.informalColors.filledColorPairCollection @@ -112,21 +121,36 @@ public extension NSObject { } if let navigationBar = self as? UINavigationBar { - navigationBar.barTintColor = collection.navigationBarColors.backgroundColor - navigationBar.backgroundColor = collection.navigationBarColors.backgroundColor - navigationBar.tintColor = collection.navigationBarColors.tintColor - navigationBar.titleTextAttributes = [ .foregroundColor : collection.navigationBarColors.labelColor ] - navigationBar.largeTitleTextAttributes = [ .foregroundColor : collection.navigationBarColors.labelColor ] - navigationBar.isTranslucent = false + let navigationBarAppearance = collection.navigationBarAppearance(for: itemStyle) + let navigationBarScrollEdgeAppearance = collection.navigationBarAppearance(for: itemStyle, scrollEdge: true) - let navigationBarAppearance = collection.navigationBarAppearance + switch itemStyle { + case .content: break + + default: break + // navigationBar.barTintColor = collection.navigationBarColors.backgroundColor + // navigationBar.backgroundColor = collection.navigationBarColors.backgroundColor + // navigationBar.tintColor = collection.navigationBarColors.tintColor + // navigationBar.titleTextAttributes = [ .foregroundColor : collection.navigationBarColors.labelColor ] + // navigationBar.largeTitleTextAttributes = [ .foregroundColor : collection.navigationBarColors.labelColor ] + // navigationBar.isTranslucent = false + } navigationBar.standardAppearance = navigationBarAppearance navigationBar.compactAppearance = navigationBarAppearance - navigationBar.scrollEdgeAppearance = navigationBarAppearance + navigationBar.scrollEdgeAppearance = navigationBarScrollEdgeAppearance } if let toolbar = self as? UIToolbar { + let standardAppearance = UIToolbarAppearance() + standardAppearance.backgroundColor = collection.toolbarColors.backgroundColor + toolbar.standardAppearance = standardAppearance + + let edgeAppearance = UIToolbarAppearance() + edgeAppearance.backgroundColor = collection.toolbarColors.backgroundColor + edgeAppearance.shadowColor = .clear + toolbar.scrollEdgeAppearance = edgeAppearance + toolbar.barTintColor = collection.toolbarColors.backgroundColor toolbar.tintColor = collection.toolbarColors.tintColor } @@ -174,7 +198,7 @@ public extension NSObject { glassIconView.tintColor = collection.searchBarColors.secondaryLabelColor } if let clearButton = searchBar.searchTextField.value(forKey: "clearButton") as? UIButton { - clearButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) + clearButton.setImage(OCSymbol.icon(forSymbolName: "xmark.circle.fill"), for: .normal) clearButton.tintColor = collection.searchBarColors.secondaryLabelColor } } @@ -247,7 +271,20 @@ public extension NSObject { } } - if let textField = self as? UITextField { + if let searchTextField = self as? UISearchTextField { + searchTextField.tintColor = collection.searchBarColors.tintColor + searchTextField.textColor = collection.searchBarColors.labelColor + searchTextField.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle + + if let glassIconView = searchTextField.leftView as? UIImageView { + glassIconView.image = glassIconView.image?.withRenderingMode(.alwaysTemplate) + glassIconView.tintColor = collection.searchBarColors.secondaryLabelColor + } + if let clearButton = searchTextField.value(forKey: "clearButton") as? UIButton { + clearButton.setImage(OCSymbol.icon(forSymbolName: "xmark.circle.fill"), for: .normal) + clearButton.tintColor = collection.searchBarColors.secondaryLabelColor + } + } else if let textField = self as? UITextField { textField.textColor = collection.tableRowColors.labelColor } diff --git a/ownCloudAppShared/User Interface/Theme/Theme.swift b/ownCloudAppShared/User Interface/Theme/Theme.swift index 272c58f25..c2e38cbbd 100644 --- a/ownCloudAppShared/User Interface/Theme/Theme.swift +++ b/ownCloudAppShared/User Interface/Theme/Theme.swift @@ -129,7 +129,7 @@ public class Theme: NSObject { return image } - public func tvgImage(for identifier: String) -> TVGImage? { + public func tvgImage(for identifier: String, autoregisterIfMissing: Bool = true) -> TVGImage? { var image : TVGImage? OCSynchronized(self) { @@ -142,6 +142,11 @@ public class Theme: NSObject { } } + if image == nil, autoregisterIfMissing { + Theme.shared.add(tvgResourceFor: identifier) + return tvgImage(for: identifier, autoregisterIfMissing: false) + } + return image } diff --git a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift index 693d4d829..7f477c2ee 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift @@ -113,9 +113,12 @@ public class ThemeCollection : NSObject { @objc public var approvalColors : ThemeColorPairCollection @objc public var neutralColors : ThemeColorPairCollection @objc public var destructiveColors : ThemeColorPairCollection + @objc public var warningColors : ThemeColorPairCollection @objc public var purchaseColors : ThemeColorPairCollection + @objc public var tokenColors: ThemeColorPairCollection + // MARK: - Label colors @objc public var informativeColor: UIColor @objc public var successColor: UIColor @@ -132,6 +135,7 @@ public class ThemeCollection : NSObject { @objc public var tableSeparatorColor : UIColor? @objc public var tableRowColors : ThemeColorCollection @objc public var tableRowHighlightColors : ThemeColorCollection + @objc public var tableRowButtonColors : ThemeColorCollection @objc public var tableRowBorderColor : UIColor? // MARK: - Bars @@ -222,6 +226,9 @@ public class ThemeCollection : NSObject { self.purchaseColors = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: lightBrandColors.labelColor, background: lightBrandColor)) self.purchaseColors.disabled.background = self.purchaseColors.disabled.background.greyscale self.destructiveColors = colors.resolveThemeColorPairCollection("Fill.destructiveColors", ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: UIColor.red))) + self.warningColors = colors.resolveThemeColorPairCollection("Fill.warningColors", ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.black, background: UIColor.systemYellow))) + + self.tokenColors = colors.resolveThemeColorPairCollection("Fill.tokenColors", ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: lightBrandColor, background: UIColor(white: 0, alpha: 0.1)))) self.tintColor = colors.resolveColor("tintColor", self.lightBrandColor) @@ -260,6 +267,15 @@ public class ThemeCollection : NSObject { filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) )) + self.tableRowButtonColors = colors.resolveThemeColorCollection("Table.tableRowButtonColors", ThemeColorCollection( + backgroundColor: tableGroupBackgroundColor, + tintColor: nil, + labelColor: defaultTableRowLabelColor, + secondaryLabelColor: UIColor(hex: 0x475770), + symbolColor: UIColor(hex: 0x475770), + filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: defaultTableRowLabelColor, background: tableGroupBackgroundColor)) + )) + self.favoriteEnabledColor = UIColor(hex: 0xFFCC00) self.favoriteDisabledColor = UIColor(hex: 0x7C7C7C) @@ -277,6 +293,8 @@ public class ThemeCollection : NSObject { self.searchBarColors = colors.resolveThemeColorCollection("Searchbar", self.darkBrandColors) self.loginColors = colors.resolveThemeColorCollection("Login", self.darkBrandColors) + self.tokenColors = colors.resolveThemeColorPairCollection("Fill.tokenColors", ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: lightBrandColor, background: UIColor(white: 1, alpha: 0.1)))) + // Table view self.tableBackgroundColor = colors.resolveColor("Table.tableBackgroundColor", navigationBarColors.backgroundColor!.darker(0.1)) self.tableGroupBackgroundColor = colors.resolveColor("Table.tableGroupBackgroundColor", navigationBarColors.backgroundColor!.darker(0.3)) @@ -302,6 +320,15 @@ public class ThemeCollection : NSObject { filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) )) + self.tableRowButtonColors = colors.resolveThemeColorCollection("Table.tableRowButtonColors", ThemeColorCollection( + backgroundColor: tableGroupBackgroundColor, + tintColor: navigationBarColors.tintColor, + labelColor: navigationBarColors.labelColor, + secondaryLabelColor: navigationBarColors.secondaryLabelColor, + symbolColor: lightColor, + filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: lightColor, background: tableGroupBackgroundColor)) + )) + // Bar styles self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: .lightContent) self.loginStatusBarStyle = styleResolver.resolveStatusBarStyle(for: "loginStatusBarStyle", fallback: self.statusBarStyle) @@ -363,10 +390,10 @@ public class ThemeCollection : NSObject { // Bars self.navigationBarColors = colors.resolveThemeColorCollection("NavigationBar", self.darkBrandColors) let tmpDarkBrandColors = self.darkBrandColors - - if VendorServices.shared.isBranded { - tmpDarkBrandColors.secondaryLabelColor = UIColor(hex: 0xF7F7F7) - } + + if VendorServices.shared.isBranded { + tmpDarkBrandColors.secondaryLabelColor = UIColor(hex: 0xF7F7F7) + } if self.tintColor == UIColor(hex: 0xFFFFFF) { tmpDarkBrandColors.secondaryLabelColor = .lightGray } @@ -386,7 +413,16 @@ public class ThemeCollection : NSObject { self.loginColors = colors.resolveThemeColorCollection("Login", self.darkBrandColors) // Bar styles - self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: .lightContent) + var defaultStatusBarStyle : UIStatusBarStyle = .lightContent + if let backgroundColor = self.navigationBarColors.backgroundColor, backgroundColor.isLight() { + if #available(iOSApplicationExtension 13.0, *) { + defaultStatusBarStyle = .darkContent + } else { + defaultStatusBarStyle = .default + } + } + + self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: defaultStatusBarStyle) self.loginStatusBarStyle = styleResolver.resolveStatusBarStyle(for: "loginStatusBarStyle", fallback: self.statusBarStyle) self.barStyle = styleResolver.resolveBarStyle(fallback: .black) @@ -400,10 +436,10 @@ public class ThemeCollection : NSObject { // Logo fill color logoFillColor = UIColor.lightGray - if lightBrandColor.isEqual(UIColor(hex: 0xFFFFFF)) { + if lightBrandColor.isLight() { self.neutralColors.normal.background = self.darkBrandColor self.lightBrandColors.filledColorPairCollection.normal.background = self.darkBrandColor - } + } } self.informalColors = colors.resolveThemeColorCollection("Informal", self.lightBrandColors) @@ -601,14 +637,27 @@ class ThemeColorValueResolver : NSObject { @available(iOS 13.0, *) extension ThemeCollection { - var navigationBarAppearance : UINavigationBarAppearance { + func navigationBarAppearance(for style: ThemeItemStyle, scrollEdge: Bool = false) -> UINavigationBarAppearance { let appearance = UINavigationBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.backgroundColor = navigationBarColors.backgroundColor - appearance.titleTextAttributes = [ .foregroundColor : navigationBarColors.labelColor ] - appearance.largeTitleTextAttributes = [ .foregroundColor : navigationBarColors.labelColor ] - appearance.shadowColor = .clear + switch style { + case .content: + appearance.configureWithOpaqueBackground() + // appearance.backgroundColor = tableBackgroundColor + appearance.titleTextAttributes = [ .foregroundColor : tableRowColors.labelColor ] + appearance.largeTitleTextAttributes = [ .foregroundColor : tableRowColors.labelColor ] + // appearance.shadowColor = .clear + + default: + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = navigationBarColors.backgroundColor + appearance.titleTextAttributes = [ .foregroundColor : navigationBarColors.labelColor ] + appearance.largeTitleTextAttributes = [ .foregroundColor : navigationBarColors.labelColor ] + } + + if scrollEdge { + appearance.shadowColor = .clear + } return appearance } diff --git a/ownCloudAppShared/User Interface/Theme/ThemeStyle+DefaultStyles.swift b/ownCloudAppShared/User Interface/Theme/ThemeStyle+DefaultStyles.swift index 5fa9ea5ce..d66f1cb24 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeStyle+DefaultStyles.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeStyle+DefaultStyles.swift @@ -23,6 +23,9 @@ import ownCloudSDK extension UIColor { static var ownCloudLightColor : UIColor { return UIColor(hex: 0x4E85C8) } static var ownCloudDarkColor : UIColor { return UIColor(hex: 0x041E42) } + static var ownCloudWebDarkColor : UIColor { return UIColor(hex: 0x292929) } + static var ownCloudWebDarkLabelColor : UIColor { return UIColor(hex: 0xDADCDF) } + static var ownCloudWebDarkFolderColor : UIColor { return UIColor(red: 44, green: 101, blue: 255) } } extension ThemeStyle { @@ -31,7 +34,15 @@ extension ThemeStyle { } static public var ownCloudDark : ThemeStyle { - return (ThemeStyle(styleIdentifier: "com.owncloud.dark", localizedName: "Dark".localized, lightColor: .ownCloudLightColor, darkColor: .ownCloudDarkColor, themeStyle: .dark)) + return (ThemeStyle(styleIdentifier: "com.owncloud.dark", localizedName: "Dark Blue".localized, lightColor: .ownCloudLightColor, darkColor: .ownCloudDarkColor, themeStyle: .dark)) + } + + static public var ownCloudWebDark : ThemeStyle { + return (ThemeStyle(styleIdentifier: "com.owncloud.web.dark", darkStyleIdentifier: "com.owncloud.web.dark", localizedName: "Dark Web".localized, lightColor: .ownCloudWebDarkLabelColor, darkColor: .ownCloudWebDarkColor, themeStyle: .dark, customColors: ["Icon.folderFillColor" : UIColor.ownCloudWebDarkFolderColor.hexString(), "Fill.neutralColors.normal.foreground" : UIColor.ownCloudWebDarkColor.hexString()])) + } + + static public var ownCloudDarkBlack : ThemeStyle { + return (ThemeStyle(styleIdentifier: "com.owncloud.dark.black", darkStyleIdentifier: "com.owncloud.dark.black", localizedName: "Dark Black".localized, lightColor: .lightGray, darkColor: .black, themeStyle: .dark, customColors: ["Icon.folderFillColor" : UIColor.ownCloudWebDarkFolderColor.hexString(), "Toolbar.tintColor" : UIColor.white.hexString(), "NavigationBar.tintColor" : UIColor(white: 0.888, alpha: 1.0) .hexString(), "Fill.neutralColors.normal.foreground" : UIColor.ownCloudWebDarkColor.hexString()])) } static public var ownCloudClassic : ThemeStyle { diff --git a/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift b/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift index 7d4fb7513..cd1e00d7a 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift @@ -196,9 +196,11 @@ extension ThemeStyle { static public func registerDefaultStyles() { if !Branding.shared.setupThemeStyles() { - OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudLight.themeStyleExtension()) - OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudDark.themeStyleExtension(isDefault: true)) - OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudClassic.themeStyleExtension()) + OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudLight.themeStyleExtension(isDefault: true)) +// OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudWebDark.themeStyleExtension(isDefault: true)) +// OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudDark.themeStyleExtension()) +// OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudDarkBlack.themeStyleExtension()) +// OCExtensionManager.shared.addExtension(ThemeStyle.ownCloudClassic.themeStyleExtension()) } } diff --git a/ownCloud/Theming/ThemeCertificateViewController.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift similarity index 78% rename from ownCloud/Theming/ThemeCertificateViewController.swift rename to ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift index a6b162e8e..218977aec 100644 --- a/ownCloud/Theming/ThemeCertificateViewController.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift @@ -18,10 +18,9 @@ import UIKit import ownCloudUI -import ownCloudAppShared -class ThemeCertificateViewController: OCCertificateViewController, Themeable { - override func viewDidLoad() { +public class ThemeCertificateViewController: OCCertificateViewController, Themeable { + override public func viewDidLoad() { super.viewDidLoad() Theme.shared.register(client: self, applyImmediately: true) @@ -31,7 +30,7 @@ class ThemeCertificateViewController: OCCertificateViewController, Themeable { Theme.shared.unregister(client: self) } - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { self.tableView.backgroundColor = collection.tableGroupBackgroundColor self.tableView.separatorColor = collection.tableSeparatorColor @@ -41,7 +40,7 @@ class ThemeCertificateViewController: OCCertificateViewController, Themeable { self.lineValueColor = collection.tableRowColors.labelColor } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell : UITableViewCell = super.tableView(tableView, cellForRowAt: indexPath) cell.backgroundColor = Theme.shared.activeCollection.tableRowColors.backgroundColor diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift index 2c5841fa2..552739348 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift @@ -22,8 +22,17 @@ public protocol CustomStatusBarViewControllerProtocol : AnyObject { func statusBarStyle() -> UIStatusBarStyle } -open class ThemeNavigationController: UINavigationController { - private var themeToken : ThemeApplierToken? +open class ThemeNavigationController: UINavigationController, Themeable { + public enum ThemeNavigationControllerStyle { + case regular + case splitViewContent + } + + public var style: ThemeNavigationControllerStyle = .regular { + didSet { + applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) + } + } override open var preferredStatusBarStyle : UIStatusBarStyle { if let object = self.viewControllers.last { @@ -44,19 +53,7 @@ open class ThemeNavigationController: UINavigationController { override open func viewDidLoad() { super.viewDidLoad() - themeToken = Theme.shared.add(applier: {[weak self] (_, themeCollection, event) in - self?.applyThemeCollection(themeCollection) - self?.toolbar.applyThemeCollection(themeCollection) - self?.view.backgroundColor = .clear - - if event == .update { - self?.setNeedsStatusBarAppearanceUpdate() - } - }, applyImmediately: true) - } - - deinit { - Theme.shared.remove(applierForToken: themeToken) + Theme.shared.register(client: self, applyImmediately: true) } open var popLastHandler : ((UIViewController?) -> Bool)? @@ -76,4 +73,16 @@ open class ThemeNavigationController: UINavigationController { return super.popViewController(animated: animated) } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + let style : ThemeItemStyle = style == .splitViewContent ? .content : .defaultForItem + + self.applyThemeCollection(collection, itemStyle: style) + self.toolbar.applyThemeCollection(collection, itemStyle: style) + self.view.backgroundColor = .clear + + if event == .update { + self.setNeedsStatusBarAppearanceUpdate() + } + } } diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift index cb07a330b..f620e7833 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift @@ -36,8 +36,12 @@ open class ThemeView: UIView, Themeable { } override open func didMoveToSuperview() { + super.didMoveToSuperview() + if self.superview != nil { if !hasRegistered { + setupSubviews() + hasRegistered = true Theme.shared.register(client: self, applyImmediately: true) } @@ -46,6 +50,10 @@ open class ThemeView: UIView, Themeable { private var themeAppliers : [ThemeApplier] = [] + open func setupSubviews() { + // Override point for subclasses + } + open func addThemeApplier(_ applier: @escaping ThemeApplier) { themeAppliers.append(applier) } diff --git a/ownCloudAppShared/User Interface/User Activities/OpenItemUserActivity.swift b/ownCloudAppShared/User Interface/User Activities/OpenItemUserActivity.swift deleted file mode 100644 index 23eb2c804..000000000 --- a/ownCloudAppShared/User Interface/User Activities/OpenItemUserActivity.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// OpenItemUserActivity.swift -// ownCloud -// -// 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 -import ownCloudSDK - -public extension OCBookmark { - static let ownCloudOpenAccountActivityType = "com.owncloud.ios-app.openAccount" - static let ownCloudOpenAccountPath = "openAccount" - static let ownCloudOpenAccountAccountUuidKey = "accountUuid" - - var openAccountUserActivity: NSUserActivity { - let userActivity = NSUserActivity(activityType: OCBookmark.ownCloudOpenAccountActivityType) - userActivity.title = OCBookmark.ownCloudOpenAccountPath - userActivity.userInfo = [OCBookmark.ownCloudOpenAccountAccountUuidKey: uuid.uuidString] - return userActivity - } -} - -public class OpenItemUserActivity : NSObject { - static public let ownCloudOpenItemActivityType = "com.owncloud.ios-app.openItem" - static public let ownCloudOpenItemPath = "openItem" - static public let ownCloudOpenItemUuidKey = "itemUuid" - - public var item : OCItem - public var bookmark : OCBookmark - - public var openItemUserActivity: NSUserActivity { - let userActivity = NSUserActivity(activityType: OpenItemUserActivity.ownCloudOpenItemActivityType) - userActivity.title = OpenItemUserActivity.ownCloudOpenItemPath - userActivity.userInfo = [OpenItemUserActivity.ownCloudOpenItemUuidKey: item.localID as Any, OCBookmark.ownCloudOpenAccountAccountUuidKey : bookmark.uuid.uuidString] - return userActivity - } - - public init(detailItem: OCItem, detailBookmark: OCBookmark) { - item = detailItem - bookmark = detailBookmark - } -} diff --git a/ownCloud/View Providers/OCResourceText+ViewProvider.swift b/ownCloudAppShared/User Interface/View Providers/OCResourceText+ViewProvider.swift similarity index 99% rename from ownCloud/View Providers/OCResourceText+ViewProvider.swift rename to ownCloudAppShared/User Interface/View Providers/OCResourceText+ViewProvider.swift index 31bb14f43..a69520a63 100644 --- a/ownCloud/View Providers/OCResourceText+ViewProvider.swift +++ b/ownCloudAppShared/User Interface/View Providers/OCResourceText+ViewProvider.swift @@ -18,7 +18,6 @@ import UIKit import ownCloudSDK -import ownCloudAppShared import Down class ThemeableTextView : UITextView, Themeable { diff --git a/ownCloudScreenshotsTests/Info.plist b/ownCloudScreenshotsTests/Info.plist deleted file mode 100644 index 6c2f3ffcd..000000000 --- a/ownCloudScreenshotsTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(APP_VERSION) - - diff --git a/ownCloudScreenshotsTests/SnapshotHelper.swift b/ownCloudScreenshotsTests/SnapshotHelper.swift deleted file mode 100644 index 09d10c0f2..000000000 --- a/ownCloudScreenshotsTests/SnapshotHelper.swift +++ /dev/null @@ -1,309 +0,0 @@ -// -// SnapshotHelper.swift -// Example -// -// Created by Felix Krause on 10/8/15. -// - -// ----------------------------------------------------- -// IMPORTANT: When modifying this file, make sure to -// increment the version number at the very -// bottom of the file to notify users about -// the new SnapshotHelper.swift -// ----------------------------------------------------- - -import Foundation -import XCTest - -var deviceLanguage = "" -var locale = "" - -func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { - Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) -} - -func snapshot(_ name: String, waitForLoadingIndicator: Bool) { - if waitForLoadingIndicator { - Snapshot.snapshot(name) - } else { - Snapshot.snapshot(name, timeWaitingForIdle: 0) - } -} - -/// - Parameters: -/// - name: The name of the snapshot -/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. -func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { - Snapshot.snapshot(name, timeWaitingForIdle: timeout) -} - -enum SnapshotError: Error, CustomDebugStringConvertible { - case cannotFindSimulatorHomeDirectory - case cannotRunOnPhysicalDevice - - var debugDescription: String { - switch self { - case .cannotFindSimulatorHomeDirectory: - return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." - case .cannotRunOnPhysicalDevice: - return "Can't use Snapshot on a physical device." - } - } -} - -@objcMembers -open class Snapshot: NSObject { - static var app: XCUIApplication? - static var waitForAnimations = true - static var cacheDirectory: URL? - static var screenshotsDirectory: URL? { - return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) - } - - open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { - - Snapshot.app = app - Snapshot.waitForAnimations = waitForAnimations - - do { - let cacheDir = try getCacheDirectory() - Snapshot.cacheDirectory = cacheDir - setLanguage(app) - setLocale(app) - setLaunchArguments(app) - } catch let error { - NSLog(error.localizedDescription) - } - } - - class func setLanguage(_ app: XCUIApplication) { - guard let cacheDirectory = self.cacheDirectory else { - NSLog("CacheDirectory is not set - probably running on a physical device?") - return - } - - let path = cacheDirectory.appendingPathComponent("language.txt") - - do { - let trimCharacterSet = CharacterSet.whitespacesAndNewlines - deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) - app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] - } catch { - NSLog("Couldn't detect/set language...") - } - } - - class func setLocale(_ app: XCUIApplication) { - guard let cacheDirectory = self.cacheDirectory else { - NSLog("CacheDirectory is not set - probably running on a physical device?") - return - } - - let path = cacheDirectory.appendingPathComponent("locale.txt") - - do { - let trimCharacterSet = CharacterSet.whitespacesAndNewlines - locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) - } catch { - NSLog("Couldn't detect/set locale...") - } - - if locale.isEmpty && !deviceLanguage.isEmpty { - locale = Locale(identifier: deviceLanguage).identifier - } - - if !locale.isEmpty { - app.launchArguments += ["-AppleLocale", "\"\(locale)\""] - } - } - - class func setLaunchArguments(_ app: XCUIApplication) { - guard let cacheDirectory = self.cacheDirectory else { - NSLog("CacheDirectory is not set - probably running on a physical device?") - return - } - - let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") - app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] - - do { - let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) - let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) - let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) - let results = matches.map { result -> String in - (launchArguments as NSString).substring(with: result.range) - } - app.launchArguments += results - } catch { - NSLog("Couldn't detect/set launch_arguments...") - } - } - - open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { - if timeout > 0 { - waitForLoadingIndicatorToDisappear(within: timeout) - } - - NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work - - if Snapshot.waitForAnimations { - sleep(1) // Waiting for the animation to be finished (kind of) - } - - #if os(OSX) - guard let app = self.app else { - NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - return - } - - app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) - #else - - guard self.app != nil else { - NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - return - } - - let screenshot = XCUIScreen.main.screenshot() - #if os(iOS) - let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image - #else - let image = screenshot.image - #endif - - guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } - - do { - // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices - let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") - let range = NSRange(location: 0, length: simulator.count) - simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") - - let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") - // #if swift(<5.0) - // UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) - // #else - try image.pngData()?.write(to: path, options: .atomic) - // #endif - } catch let error { - NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") - NSLog(error.localizedDescription) - } - #endif - } - - class func fixLandscapeOrientation(image: UIImage) -> UIImage { - #if os(watchOS) - return image - #else - if #available(iOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = image.scale - let renderer = UIGraphicsImageRenderer(size: image.size, format: format) - return renderer.image { context in - image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) - } - } else { - return image - } - #endif - } - - class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { - #if os(tvOS) - return - #endif - - guard let app = self.app else { - NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - return - } - - let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element - let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) - _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) - } - - class func getCacheDirectory() throws -> URL { - let cachePath = "Library/Caches/tools.fastlane" - // on OSX config is stored in /Users//Library - // and on iOS/tvOS/WatchOS it's in simulator's home dir - #if os(OSX) - let homeDir = URL(fileURLWithPath: NSHomeDirectory()) - return homeDir.appendingPathComponent(cachePath) - #elseif arch(i386) || arch(x86_64) || arch(arm64) - guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { - throw SnapshotError.cannotFindSimulatorHomeDirectory - } - let homeDir = URL(fileURLWithPath: simulatorHostHome) - return homeDir.appendingPathComponent(cachePath) - #else - throw SnapshotError.cannotRunOnPhysicalDevice - #endif - } -} - -private extension XCUIElementAttributes { - var isNetworkLoadingIndicator: Bool { - if hasAllowListedIdentifier { return false } - - let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) - let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) - - return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize - } - - var hasAllowListedIdentifier: Bool { - let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] - - return allowListedIdentifiers.contains(identifier) - } - - func isStatusBar(_ deviceWidth: CGFloat) -> Bool { - if elementType == .statusBar { return true } - guard frame.origin == .zero else { return false } - - let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) - let newStatusBarSize = CGSize(width: deviceWidth, height: 44) - - return [oldStatusBarSize, newStatusBarSize].contains(frame.size) - } -} - -private extension XCUIElementQuery { - var networkLoadingIndicators: XCUIElementQuery { - let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in - guard let element = evaluatedObject as? XCUIElementAttributes else { return false } - - return element.isNetworkLoadingIndicator - } - - return self.containing(isNetworkLoadingIndicator) - } - - var deviceStatusBars: XCUIElementQuery { - guard let app = Snapshot.app else { - fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - } - - let deviceWidth = app.windows.firstMatch.frame.width - - let isStatusBar = NSPredicate { (evaluatedObject, _) in - guard let element = evaluatedObject as? XCUIElementAttributes else { return false } - - return element.isStatusBar(deviceWidth) - } - - return self.containing(isStatusBar) - } -} - -private extension CGFloat { - func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { - return numberA...numberB ~= self - } -} - -// Please don't remove the lines below -// They are used to detect outdated configuration files -// SnapshotHelperVersion [1.27] diff --git a/ownCloudScreenshotsTests/ownCloudScreenshotsTests.swift b/ownCloudScreenshotsTests/ownCloudScreenshotsTests.swift deleted file mode 100644 index 248b9b680..000000000 --- a/ownCloudScreenshotsTests/ownCloudScreenshotsTests.swift +++ /dev/null @@ -1,355 +0,0 @@ -// -// ownCloudScreenshotsTests.swift -// ownCloudScreenshotsTests -// -// Created by Javier Gonzalez on 19/03/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 XCTest -import EarlGrey -import LocalAuthentication -import ownCloudSDK - -extension XCUIElement { - func forceTapElement() { - if self.isHittable { - self.tap() - } - else { - let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx:0.0, dy:0.0)) - coordinate.tap() - } - } -} - -class ScreenshotsTests: XCTestCase { - - var accountName = "ownCloud" - let takeBrandedScreenshots = false - - let url = "demo.owncloud.com" - let user = "admin" - let password = "admin" - - override func setUp() { - super.setUp() - continueAfterFailure = false - } - - override func tearDown() { - super.tearDown() - } - - func testTakeScreenshotStep() { - - - let app = XCUIApplication() - app.launchEnvironment = ["oc:app.show-beta-warning": "false", "oc:app.enable-ui-animations": "false"] - app.launchArguments.append(contentsOf: ["-preferred-theme-style", "com.owncloud.classic"]) - app.launchArguments += ["UI-Testing"] - setupSnapshot(app) - app.launch() - - if !takeBrandedScreenshots { - regularAppSetup(app: app) - - // Workaround: Open the File List to dismiss keyboard - prepareFileList(app: app) - app.navigationBars[accountName].buttons["Accounts"].tap() - - snapshot("11_ios_accounts_list_demo") - prepareFileList(app: app) - - if waitForDocumentsCell(app: app) != .completed { - XCTFail("Error: File list not loaded") - } - } else { - accountName = "ownCloud Online" - brandedAppSetup(app: app) - - let tablesQuery = app.tables - app.navigationBars[accountName].buttons["Manage"].tap() - - snapshot("11_ios_accounts_list_demo") - - tablesQuery/*@START_MENU_TOKEN@*/.staticTexts["Access Files"]/*[[".cells.staticTexts[\"Access Files\"]",".staticTexts[\"Access Files\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - } - - preparePDFFile(app: app) - preparePhotos(app: app) - prepareQuickAccess(app: app) - - if UIDevice.current.userInterfaceIdiom == .pad { - prepareMultipleWindows(app: app) - } - - XCTAssert(true, "Screenshots taken") - } - - func regularAppSetup(app: XCUIApplication) { - addUIInterruptionMonitor(withDescription: "System Dialog") { - (alert) -> Bool in - alert.buttons["Allow"].tap() - return true - } - app.tap() - - snapshot("10_ios_accounts_welcome_demo") - - //Settings - app.toolbars["Toolbar"].buttons["settingsBarButtonItem"].tap() - snapshot("60_ios_settings_demo") - app.navigationBars.element(boundBy: 0).buttons.element(boundBy: 0).tap() - - if waitForAddAccountButton(app: app) != .completed { - XCTFail("Error: Account Button not available") - } - //Add account - let credentials : [String : String] = ["url" : url, "user" : user, "password" : password, "serverDescription" : accountName] - addAccount(app: app, credentials: credentials) - - if waitForAddAccountButton(app: app) != .completed { - XCTFail("Error: Account Button not available") - } - - //Add account - let credentialsDemo : [String : String] = ["url" : url, "user" : user, "password" : password, "serverDescription" : "demo@demo.owncloud.com"] - addAccount(app: app, credentials: credentialsDemo) - - if waitForAddAccountButton(app: app) != .completed { - XCTFail("Error: Account Button not available") - } - - //Add account - let credentialsDemo2 : [String : String] = ["url" : url, "user" : user, "password" : password, "serverDescription" : "admin@demo.owncloud.com"] - addAccount(app: app, credentials: credentialsDemo2) - } - - func brandedAppSetup(app: XCUIApplication) { - snapshot("10_ios_accounts_welcome_demo") - - //Settings - app.toolbars["Toolbar"].buttons["settingsBarButtonItem"].tap() - snapshot("60_ios_settings_demo") - app.navigationBars.element(boundBy: 0).buttons.element(boundBy: 0).tap() - - let tablesQuery = app.tables - tablesQuery/*@START_MENU_TOKEN@*/.textFields["url"]/*[[".cells",".textFields[\"https:\/\/\"]",".textFields[\"url\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.tap() - tablesQuery/*@START_MENU_TOKEN@*/.textFields["url"]/*[[".cells",".textFields[\"https:\/\/\"]",".textFields[\"url\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.typeText(url) - tablesQuery.buttons["Continue"].tap() - tablesQuery/*@START_MENU_TOKEN@*/.textFields["username"]/*[[".cells",".textFields[\"Username\"]",".textFields[\"username\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.tap() - tablesQuery/*@START_MENU_TOKEN@*/.textFields["username"]/*[[".cells",".textFields[\"Username\"]",".textFields[\"username\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.typeText(user) - - let passwordSecureTextField = tablesQuery/*@START_MENU_TOKEN@*/.secureTextFields["password"]/*[[".cells",".secureTextFields[\"Password\"]",".secureTextFields[\"password\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/ - passwordSecureTextField.tap() - passwordSecureTextField.typeText(password) - tablesQuery.buttons["Login"].tap() - - if waitForAccessFilesCell(app: app) != .completed { - XCTFail("Error: Can not check auth method of the server") - } - - let accountTablesQuery = app.tables - accountTablesQuery.staticTexts["Access Files"].tap() - } - - func addAccount(app: XCUIApplication, credentials: [String : String]) { - if let url = credentials["url"], let user = credentials["user"], let password = credentials["password"], let serverDescription = credentials["serverDescription"] { - - app.navigationBars.element(boundBy: 0).buttons[localizedString(key: "Add account")].tap() - app.textFields["row-url-url"].typeText(url) - - app.navigationBars[localizedString(key: "Add account")]/*@START_MENU_TOKEN@*/.buttons["continue-bar-button"]/*[[".buttons[\"Continue\"]",".buttons[\"continue-bar-button\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - - if waitForUserNameTextField(app: app) != .completed { - XCTFail("Error: Can not check auth method of the server") - } - - app.textFields["row-credentials-username"].typeText(user) - app.secureTextFields["row-credentials-password"].tap() - app.secureTextFields["row-credentials-password"].typeText(password) - app.textFields["row-name-name"].tap() - app.textFields["row-name-name"].typeText(serverDescription) - - app.navigationBars[localizedString(key: "Add account")]/*@START_MENU_TOKEN@*/.buttons["continue-bar-button"]/*[[".buttons[\"Continue\"]",".buttons[\"continue-bar-button\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - } else { - XCTFail("Error: Adding Login failed") - } - } - - func prepareFileList(app: XCUIApplication) { - if waitForAccountList(app: app) != .completed { - XCTFail("Error: Account list not loaded") - } - let tablesQuery = app.tables - tablesQuery.staticTexts[accountName].firstMatch.tap() - } - - func preparePDFFile(app: XCUIApplication) { - if waitForPDFCell(app: app) != .completed { - XCTFail("Error: Could not open PDF") - } - let tablesQuery = app.tables - tablesQuery.staticTexts["ownCloud Manual.pdf"].tap() - - if waitForPDFViewer(app: app) != .completed { - XCTFail("Error: Loading PDF failed") - } - - sleep(5) - - let scrollViewsQuery = app.scrollViews.firstMatch - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - scrollViewsQuery.children(matching: .other).element.children(matching: .other).element.swipeLeft() - snapshot("22_ios_files_preview_pdf_demo") - - app.navigationBars["ownCloud Manual.pdf"].buttons[accountName].tap() - } - - func preparePhotos(app: XCUIApplication) { - let tablesQuery = XCUIApplication().tables - - tablesQuery.buttons[String(format: "ownCloud Manual.pdf %@", localizedString(key: "Actions"))].tap() - snapshot("21_ios_files_actions_demo") - - XCUIApplication().buttons[localizedString(key: "Close actions menu")].forceTapElement() - - tablesQuery.staticTexts["Photos"].tap() - - sleep(5) - - snapshot("20_ios_files_list_demo") - app.navigationBars["Photos"].buttons[accountName].tap() - } - - func prepareQuickAccess(app: XCUIApplication) { - app.tabBars.buttons[localizedString(key: "Quick Access")].tap() - snapshot("40_ios_quick_access_demo") - } - - func prepareMultipleWindows(app: XCUIApplication) { - XCUIDevice.shared.orientation = .landscapeLeft - sleep(2) - app.tabBars.buttons[localizedString(key: "Files")].tap() - - let tablesQuery = XCUIApplication().tables - tablesQuery/*@START_MENU_TOKEN@*/.buttons["Photos Actions"]/*[[".cells[\"Photos\"].buttons[\"Photos Actions\"]",".buttons[\"Photos Actions\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - sleep(2) - tablesQuery/*@START_MENU_TOKEN@*/.staticTexts["Open in a new Window"]/*[[".cells[\"com.owncloud.action.openscene\"].staticTexts[\"Open in a new Window\"]",".staticTexts[\"Open in a new Window\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - - sleep(2) - - let tablesQuery2 = XCUIApplication().tables - tablesQuery2/*@START_MENU_TOKEN@*/.buttons["Photos Actions"]/*[[".cells[\"Photos\"].buttons[\"Photos Actions\"]",".buttons[\"Photos Actions\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - - - snapshot("23_ios_files_list_multiple_window_landscape") - } - - // MARK: - Waiters - - func waitForAddAccountButton(app: XCUIApplication) -> XCTWaiter.Result { - let element = app.navigationBars.element(boundBy: 0).buttons[localizedString(key: "Add account")] - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: element, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func waitForAccountList(app: XCUIApplication) -> XCTWaiter.Result { - let tablesQuery = app.tables - let tableCell = tablesQuery.staticTexts[accountName].firstMatch - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: tableCell, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func waitForUserNameTextField(app: XCUIApplication) -> XCTWaiter.Result { - let textField = app.textFields["row-credentials-username"] - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: textField, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func waitForDocumentsCell(app: XCUIApplication) -> XCTWaiter.Result { - let element = app.tables.cells.staticTexts["Documents"] - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: element, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func waitForPDFCell(app: XCUIApplication) -> XCTWaiter.Result { - let element = app.tables.cells.staticTexts["ownCloud Manual.pdf"] - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: element, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func waitForAccessFilesCell(app: XCUIApplication) -> XCTWaiter.Result { - let element = app.tables.cells.staticTexts["Access Files"] - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: element, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func waitForPDFViewer(app: XCUIApplication) -> XCTWaiter.Result { - let element = app.navigationBars["ownCloud Manual.pdf"].buttons[accountName] - let predicate = NSPredicate(format: "exists == 1") - let ocExpectation = expectation(for: predicate, evaluatedWith: element, handler: nil) - - let result = XCTWaiter().wait(for: [ocExpectation], timeout: 15) - return result - } - - func localizedString(key:String) -> String { - if deviceLanguage == "en-US" { - deviceLanguage = "en" - } - - let localizationBundle = Bundle(path: Bundle(for: type(of: self)).path(forResource: deviceLanguage, ofType: "lproj")!) - let result = NSLocalizedString(key, bundle:localizationBundle!, comment: "") - - return result - } -} - -extension XCUIElement { - // The following is a workaround for inputting text in the simulator and prevent errors - func setText(text: String, application: XCUIApplication) { - UIPasteboard.general.string = text - doubleTap() - application.menuItems.element(boundBy: 0).tap() - } -} diff --git a/ownCloudTests/EarlGrey.swift b/ownCloudTests/EarlGrey.swift deleted file mode 100644 index b1395c102..000000000 --- a/ownCloudTests/EarlGrey.swift +++ /dev/null @@ -1,188 +0,0 @@ -// -// Copyright 2018 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import EarlGrey -import Foundation - -public func GREYAssert(_ expression: @autoclosure () -> Bool, reason: String) { - GREYAssert(expression(), reason, details: "Expected expression to be true") -} - -public func GREYAssertTrue(_ expression: @autoclosure () -> Bool, reason: String) { - GREYAssert(expression(), reason, details: "Expected the boolean expression to be true") -} - -public func GREYAssertFalse(_ expression: @autoclosure () -> Bool, reason: String) { - GREYAssert(!expression(), reason, details: "Expected the boolean expression to be false") -} - -public func GREYAssertNotNil(_ expression: @autoclosure ()-> Any?, reason: String) { - GREYAssert(expression() != nil, reason, details: "Expected expression to be not nil") -} - -public func GREYAssertNil(_ expression: @autoclosure () -> Any?, reason: String) { - GREYAssert(expression() == nil, reason, details: "Expected expression to be nil") -} - -public func GREYAssertEqual(_ left: @autoclosure () -> AnyObject?, - _ right: @autoclosure () -> AnyObject?, reason: String) { - GREYAssert(left() === right(), reason, details: "Expected left term to be equal to right term") -} - -public func GREYAssertNotEqual(_ left: @autoclosure () -> AnyObject?, - _ right: @autoclosure () -> AnyObject?, reason: String) { - GREYAssert(left() !== right(), reason, details: "Expected left term to not equal the right term") -} - -public func GREYAssertEqualObjects( _ left: @autoclosure () -> T?, - _ right: @autoclosure () -> T?, - reason: String) { - GREYAssert(left() == right(), reason, details: "Expected object of the left term to be equal " + - "to the object of the right term") -} - -public func GREYAssertNotEqualObjects( _ left: @autoclosure () -> T?, - _ right: @autoclosure () -> T?, - reason: String) { - GREYAssert(left() != right(), reason, details: "Expected object of the left term to not " + - "equal the object of the right term") -} - -public func GREYFail(_ reason: String) { - EarlGrey.handle(exception: GREYFrameworkException(name: kGREYAssertionFailedException, - reason: reason), - details: "") -} - -public func GREYFailWithDetails(_ reason: String, details: String) { - EarlGrey.handle(exception: GREYFrameworkException(name: kGREYAssertionFailedException, - reason: reason), - details: details) -} - -private func GREYAssert(_ expression: @autoclosure () -> Bool, - _ reason: String, details: String) { - GREYSetCurrentAsFailable() - GREYWaitUntilIdle() - if !expression() { - EarlGrey.handle(exception: GREYFrameworkException(name: kGREYAssertionFailedException, - reason: reason), - details: details) - } -} - -private func GREYSetCurrentAsFailable() { - let greyFailureHandlerSelector = - #selector(GREYFailureHandler.setInvocationFile(_:andInvocationLine:)) - let greyFailureHandler = - Thread.current.threadDictionary.value(forKey: kGREYFailureHandlerKey) as! GREYFailureHandler - if greyFailureHandler.responds(to: greyFailureHandlerSelector) { - greyFailureHandler.setInvocationFile!(#file, andInvocationLine:#line) - } -} - -private func GREYWaitUntilIdle() { - GREYUIThreadExecutor.sharedInstance().drainUntilIdle() -} - -open class EarlGrey: NSObject { - public static func selectElement(with matcher: GREYMatcher, - file: StaticString = #file, - line: UInt = #line) -> GREYInteraction { - return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) - .selectElement(with: matcher) - } - - @available(*, deprecated, renamed: "selectElement(with:)") - open class func select(elementWithMatcher matcher:GREYMatcher, - file: StaticString = #file, - line: UInt = #line) -> GREYElementInteraction { - return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) - .selectElement(with: matcher) - } - - open class func setFailureHandler(handler: GREYFailureHandler, - file: StaticString = #file, - line: UInt = #line) { - return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) - .setFailureHandler(handler) - } - - open class func handle(exception: GREYFrameworkException, - details: String, - file: StaticString = #file, - line: UInt = #line) { - return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) - .handle(exception, details: details) - } - - @discardableResult open class func rotateDeviceTo(orientation: UIDeviceOrientation, - errorOrNil: UnsafeMutablePointer!, - file: StaticString = #file, - line: UInt = #line) - -> Bool { - return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) - .rotateDevice(to: orientation, - errorOrNil: errorOrNil) - } -} - -extension GREYInteraction { - @discardableResult public func assert(_ matcher: @autoclosure () -> GREYMatcher) -> Self { - return self.__assert(with: matcher()) - } - - @discardableResult public func assert(_ matcher: @autoclosure () -> GREYMatcher, - error:UnsafeMutablePointer!) -> Self { - return self.__assert(with: matcher(), error: error) - } - - @available(*, deprecated, renamed: "assert(_:)") - @discardableResult public func assert(with matcher: GREYMatcher!) -> Self { - return self.__assert(with: matcher) - } - - @available(*, deprecated, renamed: "assert(_:error:)") - @discardableResult public func assert(with matcher: GREYMatcher!, - error:UnsafeMutablePointer!) -> Self { - return self.__assert(with: matcher, error: error) - } - - @discardableResult public func perform(_ action: GREYAction!) -> Self { - return self.__perform(action) - } - - @discardableResult public func perform(_ action: GREYAction!, - error:UnsafeMutablePointer!) -> Self { - return self.__perform(action, error: error) - } - - @discardableResult public func using(searchAction: GREYAction, - onElementWithMatcher matcher: GREYMatcher) -> Self { - return self.usingSearch(searchAction, onElementWith: matcher) - } -} - -extension GREYCondition { - open func waitWithTimeout(seconds: CFTimeInterval) -> Bool { - return self.wait(withTimeout: seconds) - } - - open func waitWithTimeout(seconds: CFTimeInterval, pollInterval: CFTimeInterval) - -> Bool { - return self.wait(withTimeout: seconds, pollInterval: pollInterval) - } -} diff --git a/ownCloudTests/File List/CreateFolderTests.swift b/ownCloudTests/File List/CreateFolderTests.swift deleted file mode 100644 index 761684b63..000000000 --- a/ownCloudTests/File List/CreateFolderTests.swift +++ /dev/null @@ -1,256 +0,0 @@ -// -// CreateFolderTests.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 08/01/2019. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking - -@testable import ownCloud - -class CreateFolderTests: FileTests { - - let hostSimulator: OCHostSimulator = OCHostSimulator() - - /* - * PASSED if: Create Folder view is shown - */ - func testShowCreateFolder() { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponse", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - self.showFileList(bookmark: bookmark) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Create folder".localized)).perform(grey_tap()) - - //Asserts - EarlGrey.selectElement(with: grey_accessibilityID("name-text-field")).assert(grey_sufficientlyVisible()) - - //Remove Mocks - OCMockManager.shared.removeAllMockingBlocks() - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - dismissFileList() - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - - /* - * PASSED if: A folder is created - */ - func testCreateFolder() { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - let folderName = "New Folder" - - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponse", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - self.showFileList(bookmark: bookmark) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Create folder".localized)).perform(grey_tap()) - - //Remove Mocks - OCMockManager.shared.removeMockingBlock(atLocation: OCMockLocation.ocQueryRequestChangeSetWithFlags) - - //Mock again - self.mockQueryPropfindResults(resourceName: "PropfindResponseNewFolder", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - - EarlGrey.selectElement(with: grey_accessibilityID("name-text-field")).perform(grey_replaceText(folderName)) - EarlGrey.selectElement(with: grey_accessibilityID("done-button")).perform(grey_tap()) - - //Assert - let isFolderCreated = GREYCondition(name: "Wait for folder is created", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_accessibilityID(folderName)).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(isFolderCreated, reason: "Failed to create the folder") - - //Remove Mocks - OCMockManager.shared.removeAllMockingBlocks() - - //Reset status - dismissFileList() - - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - - /* - * PASSED if: Done button is disabled with empty name - */ - func testDisableButtonCreateFolderWithEmptyName() { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - let folderName = "" - - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponse", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - self.showFileList(bookmark: bookmark) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Create folder".localized)).perform(grey_tap()) - - //Remove Mocks - OCMockManager.shared.removeMockingBlock(atLocation: OCMockLocation.ocQueryRequestChangeSetWithFlags) - - //Mock again - self.mockQueryPropfindResults(resourceName: "PropfindResponseNewFolder", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - - EarlGrey.selectElement(with: grey_accessibilityID("name-text-field")).perform(grey_replaceText(folderName)) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("done-button")).assert(grey_not(grey_enabled())) - - //Remove Mocks - OCMockManager.shared.removeAllMockingBlocks() - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - dismissFileList() - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - - /* - * PASSED if: Done button is enabled with a valid name - */ - func testEnableButtonCreateFolderWithValidName() { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - let folderName = "Valid Name" - - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponse", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - self.showFileList(bookmark: bookmark) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Create folder".localized)).perform(grey_tap()) - - //Remove Mocks - OCMockManager.shared.removeMockingBlock(atLocation: OCMockLocation.ocQueryRequestChangeSetWithFlags) - - //Mock again - self.mockQueryPropfindResults(resourceName: "PropfindResponseNewFolder", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - - EarlGrey.selectElement(with: grey_accessibilityID("name-text-field")).perform(grey_replaceText(folderName)) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("done-button")).assert(grey_enabled()) - - //Remove Mocks - OCMockManager.shared.removeAllMockingBlocks() - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - dismissFileList() - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - - /* - * PASSED if: Done if Forbidden Characters Alert appears - */ - func testCreateFolderWithInvalidCharacters() { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - let folderName = "New/Folder" - - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponse", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - self.showFileList(bookmark: bookmark) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Create folder".localized)).perform(grey_tap()) - - //Remove Mocks - OCMockManager.shared.removeMockingBlock(atLocation: OCMockLocation.ocQueryRequestChangeSetWithFlags) - - //Mock again - self.mockQueryPropfindResults(resourceName: "PropfindResponseNewFolder", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - - EarlGrey.selectElement(with: grey_accessibilityID("name-text-field")).perform(grey_replaceText(folderName)) - EarlGrey.selectElement(with: grey_accessibilityID("done-button")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("forbidden-characters-alert")).assert(grey_sufficientlyVisible()) - - //Remove Mocks - OCMockManager.shared.removeAllMockingBlocks() - - //Reset status - EarlGrey.selectElement(with: grey_text("OK".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - dismissFileList() - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - - /* - * PASSED if: Error is shown on the view - */ - func testCreateFolderWithExistingName() { - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - let folderName = "New Folder" - let errorTitle = "Error title" - let errorMessage = "Error message" - - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponseNewFolder", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - - let issue: OCIssue = OCIssue(forMultipleChoicesWithLocalizedTitle: errorTitle, localizedDescription: errorMessage, choices: [OCIssueChoice(type: .cancel, identifier: nil, label: "Cancel".localized, userInfo: nil, handler: nil)]) { (issue, decission) in - } - - self.showFileList(bookmark: bookmark, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Create folder".localized)).perform(grey_tap()) - - EarlGrey.selectElement(with: grey_accessibilityID("name-text-field")).perform(grey_replaceText(folderName)) - EarlGrey.selectElement(with: grey_accessibilityID("done-button")).perform(grey_tap()) - - //Assert - EarlGrey.waitForElement(withMatcher: grey_text(errorTitle), label: errorTitle) - EarlGrey.selectElement(with: grey_text(errorTitle)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text(errorMessage)).assert(grey_sufficientlyVisible()) - - //Remove Mocks - OCMockManager.shared.removeAllMockingBlocks() - - //Reset status - EarlGrey.selectElement(with: grey_text("Cancel".localized)).atIndex(0).perform(grey_tap()) - dismissFileList() - - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } -} diff --git a/ownCloudTests/File List/FileListTests.swift b/ownCloudTests/File List/FileListTests.swift deleted file mode 100644 index 12bdb54d2..000000000 --- a/ownCloudTests/File List/FileListTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// CreateBookmarkTests.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 23/10/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking -import ownCloudAppShared - -@testable import ownCloud - -class FileListTests: FileTests { - - override func setUp() { - super.setUp() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - OCMockManager.shared.removeAllMockingBlocks() - } - - /* - * PASSED if: Disconnect button appears in the view - */ - func testShowFileList() { - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - //Mocks - self.showFileList(bookmark: bookmark) - - EarlGrey.selectElement(with: grey_accessibilityID("client.file-add")).assert(grey_sufficientlyVisible()) - - self.dismissFileList() - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - - /* - * PASSED if: The expected files/folders appear in the list - */ - func testShowFileListWithItems() { - let expectedCells: Int = 3 - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - //Mocks - self.mockQueryPropfindResults(resourceName: "PropfindResponse", basePath: "/remote.php/dav/files/admin", state: .contentsFromCache) - self.showFileList(bookmark: bookmark) - - var error:NSError? - var index: UInt = 0 - while true { - EarlGrey.selectElement(with: grey_kindOfClass(ClientItemCell.self)).atIndex(index).assert( grey_notNil(), error: &error) - if error != nil { - break - } else { - index += 1 - } - } - GREYAssertEqual(index as AnyObject, expectedCells as AnyObject, reason: "Founded \(index) cells when expected \(expectedCells)") - - self.dismissFileList() - } else { - assertionFailure("File list not loaded because Bookmark is nil") - } - } - -} diff --git a/ownCloudTests/File List/FileTests.swift b/ownCloudTests/File List/FileTests.swift deleted file mode 100644 index 490bd34d0..000000000 --- a/ownCloudTests/File List/FileTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// FileTests.swift -// ownCloudTests -// Base class for tests related to file list view -// -// Created by Javier Gonzalez on 23/10/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking -import ownCloudAppShared - -@testable import ownCloud - -class FileTests: XCTestCase { - - public typealias OCMRequestCoreForBookmarkCompletionHandler = @convention(block) - (_ core: OCCore, _ error: NSError?) -> Void - - public typealias OCMRequestCoreForBookmarkSetupHandler = @convention(block) - (_ core: OCCore, _ error: NSError?) -> Void - - public typealias OCMRequestCoreForBookmark = @convention(block) - (_ bookmark: OCBookmark, _ setup: OCMRequestCoreForBookmarkSetupHandler, _ completionHandler: OCMRequestCoreForBookmarkCompletionHandler) -> Void - - public typealias OCMRequestChangeSetWithFlags = @convention(block) - (_ flags: OCQueryChangeSetRequestFlag, _ completionHandler: OCQueryChangeSetRequestCompletionHandler) -> Void - - // MARK: - XCTestCase overrides - - override func tearDown() { - super.tearDown() - OCMockManager.shared.removeAllMockingBlocks() - } - - // MARK: - UI Helpers - - func showFileList(bookmark: OCBookmark, issue: OCIssue? = nil) { - let query = MockOCQuery(path: "/") - let core = MockOCCore(query: query, bookmark: bookmark, issue: issue) - - self.mockOCoreForBookmark(mockBookmark: bookmark, mockCore: core) - - let rootViewController: MockClientRootViewController = MockClientRootViewController(core: core, query: query, bookmark: bookmark) - - rootViewController.afterCoreStart(nil, completionHandler: { (_) in - let navigationController = UserInterfaceContext.shared.currentWindow?.rootViewController - let transitionDelegate = PushTransitionDelegate() - - rootViewController.pushTransition = transitionDelegate // Keep a reference, so it's still around on dismissal - rootViewController.transitioningDelegate = transitionDelegate - rootViewController.modalPresentationStyle = .custom - - navigationController?.present(rootViewController, animated: true) - }) - } - - func dismissFileList() { - UserInterfaceContext.shared.currentWindow?.rootViewController?.dismiss(animated: false, completion: nil) - } - - // MARK: - Mocks - - func mockOCoreForBookmark(mockBookmark: OCBookmark, mockCore: OCCore? = nil) { - let requestCoreBlock : OCMRequestCoreForBookmark = { (bookmark, setupHandler, completionHandler) in - let core = mockCore ?? OCCore(bookmark: mockBookmark) - setupHandler(core, nil) - completionHandler(core, nil) - } - - OCMockManager.shared.addMocking(blocks: [OCMockLocation.ocCoreManagerRequestCoreForBookmark : requestCoreBlock]) - } - - func mockQueryPropfindResults(resourceName: String, basePath: String, state: OCQueryState) { - - let completionHandlerBlock : OCMRequestChangeSetWithFlags = { (flags, mockedBlock) in - - var items: [OCItem]? - - let bundle = Bundle.main - if let path: String = bundle.path(forResource: resourceName, ofType: "xml") { - - if let data = NSData(contentsOf: URL(fileURLWithPath: path)) { - if let parser = OCXMLParser(data: data as Data) { - parser.options = ["basePath": basePath] - parser.addObjectCreationClasses([OCItem.self]) - if parser.parse() { - items = parser.parsedObjects as? [OCItem] - } - } - } - } - - items?.removeFirst() - - let querySet: OCQueryChangeSet = OCQueryChangeSet(queryResult: items, relativeTo: nil) - let query: OCQuery = OCQuery() - query.state = state - - mockedBlock(query, querySet) - } - - OCMockManager.shared.addMocking(blocks: [OCMockLocation.ocQueryRequestChangeSetWithFlags: completionHandlerBlock]) - } -} diff --git a/ownCloudTests/File List/Subclasses/MockClientRootViewController.swift b/ownCloudTests/File List/Subclasses/MockClientRootViewController.swift deleted file mode 100644 index a9696a96d..000000000 --- a/ownCloudTests/File List/Subclasses/MockClientRootViewController.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// MockClientRootViewController.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 01/02/2019. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import UIKit -import ownCloudSDK -import ownCloudAppShared - -@testable import ownCloud - -class MockClientRootViewController: ClientRootViewController { - - var query:MockOCQuery - var mockedCore:MockOCCore - - init(core: MockOCCore, query: MockOCQuery, bookmark: OCBookmark) { - self.query = query - self.mockedCore = core - super.init(bookmark: bookmark) - self.mockedCore.delegate = self - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func coreReady(_ lastVisibleItemId: String?) { - OnMainThread { - let queryViewController = ClientQueryViewController(core: self.mockedCore, query: self.query) - self.filesNavigationController?.setViewControllers([self.emptyViewController, queryViewController], animated: false) - self.activityViewController?.core = self.core! - } - } -} diff --git a/ownCloudTests/File List/Subclasses/MockOCCore.swift b/ownCloudTests/File List/Subclasses/MockOCCore.swift deleted file mode 100644 index 7e4cd33d4..000000000 --- a/ownCloudTests/File List/Subclasses/MockOCCore.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// MockOCCore.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 10/01/2019. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import ownCloudSDK - -class MockOCCore: OCCore { - - var query:MockOCQuery - var issue: OCIssue? - - init(query: MockOCQuery, bookmark: OCBookmark, issue: OCIssue? = nil) { - self.query = query - self.issue = issue - super.init(bookmark: bookmark) - } - - override func createFolder(_ folderName: String, inside: OCItem, options: [OCCoreOption : Any]? = nil, resultHandler: OCCoreActionResultHandler? = nil) -> Progress? { - - if self.issue != nil { - self.delegate?.core(self, handleError: nil, issue: issue) - } else { - query.delegate?.queryHasChangesAvailable(query) - } - - return nil - } - - override func suggestUnusedNameBased(on name: String, at location: OCLocation, isDirectory: Bool, using nameStyle: OCCoreDuplicateNameStyle, filteredBy filter: OCCoreUnusedNameSuggestionFilter?, resultHandler: @escaping OCCoreUnusedNameSuggestionResultHandler) { - resultHandler(name, nil) - } - -// override var state: OCCoreState { -// return .running -// } -// -// override func classSetting(forOCClassSettingsKey key: OCClassSettingsKey) -> Any? { -// if key == .coreDisableItemPolicies { -// return true -// } -// -// return super.classSetting(forOCClassSettingsKey: key) -// } -} diff --git a/ownCloudTests/File List/Subclasses/MockOCQuery.swift b/ownCloudTests/File List/Subclasses/MockOCQuery.swift deleted file mode 100644 index 7552313da..000000000 --- a/ownCloudTests/File List/Subclasses/MockOCQuery.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MockOCQuery.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 08/01/2019. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import UIKit -import ownCloudSDK - -class MockOCQuery: OCQuery { - - convenience init(path: String) { - self.init(for: OCLocation.legacyRootPath(path)) - let rootItem = OCItem() - rootItem.permissions = [.createFile, .createFolder, .delete, .move, .rename, .writable] - rootItem.path = "/" - rootItem.type = OCItemType.collection - self.rootItem = rootItem - } -} diff --git a/ownCloudTests/Login/CreateBookmarkTests.swift b/ownCloudTests/Login/CreateBookmarkTests.swift deleted file mode 100644 index 3c4c46763..000000000 --- a/ownCloudTests/Login/CreateBookmarkTests.swift +++ /dev/null @@ -1,640 +0,0 @@ -// -// CreateBookmarkTests.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 23/10/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking - -@testable import ownCloud - -class CreateBookmarkTests: XCTestCase { - - override func setUp() { - super.setUp() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - override func tearDown() { - super.tearDown() - OCMockManager.shared.removeAllMockingBlocks() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - /* - * PASSED if: Initial view correct: URL field and continue button are displayed - */ - func testCheckInitialViewAuth () { - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Continue".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: Alert view with missing URL displayed if Continue is clicked with empty URL - */ - func testCheckURLEmptyBasicAuth () { - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_text("Continue".localized)).assert(grey_not(grey_enabled())) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to informal issue. Credentials fields, name and Continue are displayed - */ - func testCheckURLBasicAuthInformalIssue () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Informal issue description"]), level: .informal, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - //Assert - let isServerChecked = GREYCondition(name: "Wait for server is checked", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_kindOfClass(UITableView.self)).perform(grey_scrollToContentEdge(.top)) - EarlGrey.selectElement(with: grey_accessibilityID("row-name-name")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_kindOfClass(UITableView.self)).perform(grey_scrollToContentEdge(.bottom)) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(isServerChecked, reason: "Failed check the server") - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to warning issue. Issue with Approve and Cancel buttons are displayed - */ - func testCheckURLBasicAuthWarningIssueView () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Warning issue description"]), level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("approve-button")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to warning issue. Issue approved: URL, Credentials fields, Name and Continue are displayed - */ - func testCheckURLBasicAuthWarningIssueApproval () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Warning issue description"]), level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("approve-button")).perform(grey_tap()) - - //Assert - let isServerChecked = GREYCondition(name: "Wait for server is checked", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_kindOfClass(UITableView.self)).perform(grey_scrollToContentEdge(.top)) - EarlGrey.selectElement(with: grey_accessibilityID("row-name-name")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_kindOfClass(UITableView.self)).perform(grey_scrollToContentEdge(.bottom)) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(isServerChecked, reason: "Failed check the server") - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to warning issue. Issue cancelled: URL displayed. Credentials and name not displayed - */ - func testCheckURLBasicAuthWarningIssueCancel () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Warning issue description"]), level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-name-name")).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to error issue. Error displayed. URL displayed. Credentials and name not displayed - */ - func testCheckURLBasicAuthErrorIssue () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Error issue description"]), level: .error, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("ok-button")).perform(grey_tap()) - - //Assert - let isServerChecked = GREYCondition(name: "Wait for server is checked", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-name-name")).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(!isServerChecked, reason: "Failed check the server") - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to warning issue type Certificate. Certificate issue displayed with Approve displayed - */ - func testCheckURLBasicAuthWarningIssueCertificate() { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - if let certificate: OCCertificate = UtilsTests.getCertificate(mockUrlServer: mockUrlServer) { - guard let url = URL(string: mockUrlServer) else { - assertionFailure("Creation of URL object for \(mockUrlServer) failed") - return - } - let issue: OCIssue = OCIssue.init(for: certificate, validationResult: OCCertificateValidationResult.userAccepted, url: url, level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_text("Certificate".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Approve".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("Approve".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } else { - assertionFailure("Not possible to read the test_certificate.cer") - } - } - - /* - * PASSED if: URL leads to warning issue type Certificate. Click on certificate displays the certificate info - */ - func testCheckURLBasicAuthWarningIssueCertificateDisplayInfo() { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [.basicAuth, .oAuth2] - if let certificate: OCCertificate = UtilsTests.getCertificate(mockUrlServer: mockUrlServer) { - guard let url = URL(string: mockUrlServer) else { - assertionFailure("Creation of URL object for \(mockUrlServer) failed") - return - } - let issue: OCIssue = OCIssue(for: certificate, validationResult: .userAccepted, url: url, level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("issue-row.0")).perform(grey_tap()) - - //Assert - EarlGrey.waitForElement(withMatcher: grey_text("Certificate Details".localized), label: "Certificate Details") - EarlGrey.selectElement(with: grey_text("Certificate Details".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("ok-button-certificate-details")).perform(grey_tap()) - - EarlGrey.selectElement(with: grey_text("Approve".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } else { - assertionFailure("Not possible to read the test_certificate.cer") - } - } - - /* - * PASSED if: URL leads to warning issue type Certificate. Approve certificate leads to credentials - */ - func testCheckURLBasicAuthWarningIssueCertificateApproval() { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - if let certificate: OCCertificate = UtilsTests.getCertificate(mockUrlServer: mockUrlServer) { - guard let url = URL(string: mockUrlServer) else { - assertionFailure("Creation of URL object for \(mockUrlServer) failed") - return - } - let issue: OCIssue = OCIssue.init(for: certificate, validationResult: OCCertificateValidationResult.userAccepted, url:url, level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Approve".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - - } else { - assertionFailure("Not possible to read the test_certificate.cer") - } - } - - /* - * PASSED if: URL leads to warning issue type Certificate. Cancel certificate does not display credentials - */ - func testCheckURLBasicAuthWarningIssueCertificateCancel() { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - if let certificate: OCCertificate = UtilsTests.getCertificate(mockUrlServer: mockUrlServer) { - guard let url = URL(string: mockUrlServer) else { - assertionFailure("Creation of URL object for \(mockUrlServer) failed") - return - } - let issue: OCIssue = OCIssue.init(for: certificate, validationResult: OCCertificateValidationResult.userAccepted, url: url, level: .warning, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel-button")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - - } else { - assertionFailure("Not possible to read the test_certificate.cer") - } - } - - /* - * PASSED if: URL leads to OAuth2 authentication. Credentials fields not displayed. - */ - func testCheckURLOAuth2 () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.oAuth2, - OCAuthenticationMethodIdentifier.basicAuth] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Error description"]), level: .warning, issueHandler: nil) - - let authenticationMethodIdentifier = OCAuthenticationMethodIdentifier.oAuth2 as NSString - let tokenResponse:[String : String] = ["access_token" : "RyFyDu1wH0Wvd8KlCP0Qeo9dlTqWajgvWHNqSdfl9bVD6Wp72CGikmgSkvUaAMML", - "expires_in" : "3600", - "message_url" : "https://localhost/apps/oauth2/authorization-successful", - "refresh_token" : "khA8H18TWC84g1DmB0fzqgDOWvNRNPGJkkzQ1E6AZjq8UrqZ79QTK8UgSsJB6MrW", - "token_type" : "Bearer", - "user_id" : "admin"] - let dictionary:[String : Any] = ["bearerString" : "Bearer RyFyDu1wH0Wvd8KlCP0Qeo9dlTqWajgvWHNqSdfl9bVD6Wp72CGikmgSkvUaAMML", - "expirationDate" : NSDate.distantPast, - "tokenResponse" : tokenResponse] - let error: NSError? = nil - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - UtilsTests.mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: authenticationMethodIdentifier as OCAuthenticationMethodIdentifier, dictionary: dictionary, error: error) - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("approve-button")).perform(grey_tap()) - - //Assert - let isServerChecked = GREYCondition(name: "Wait for server is checked", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(isServerChecked, reason: "Failed check the server") - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } - - /* - * PASSED if: URL leads to correct OAuth2 authentication. Warning is displayed - */ - func testLoginOAuth2Warning () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.oAuth2, - OCAuthenticationMethodIdentifier.basicAuth] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Error description"]), level: .informal, issueHandler: nil) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - let isServerChecked = GREYCondition(name: "Wait for server is checked", block: { - var error: NSError? - - //Assert - EarlGrey.selectElement(with: grey_text("If you 'Continue', you will be prompted to allow the ownCloud App to open OAuth2 login where you can enter your credentials.".localized)).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(!isServerChecked, reason: "Failed check the server") - - //Reset - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCMockManager.shared.removeAllMockingBlocks() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - /* - * PASSED if: URL leads to correct OAuth2 authentication. Bookmark cell created and displayed - */ - func testLoginOAuth2RightCredentials () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.oAuth2, - OCAuthenticationMethodIdentifier.basicAuth] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Error description"]), level: .informal, issueHandler: nil) - - let authenticationMethodIdentifier = OCAuthenticationMethodIdentifier.oAuth2 - let tokenResponse:[String : String] = ["access_token" : "RyFyDu1wH0Wvd8KlCP0Qeo9dlTqWajgvWHNqSdfl9bVD6Wp72CGikmgSkvUaAMML", - "expires_in" : "3600", - "message_url" : "https://localhost/apps/oauth2/authorization-successful", - "refresh_token" : "khA8H18TWC84g1DmB0fzqgDOWvNRNPGJkkzQ1E6AZjq8UrqZ79QTK8UgSsJB6MrW", - "token_type" : "Bearer", - "user_id" : "admin"] - let dictionary:[String : Any] = ["bearerString" : "Bearer RyFyDu1wH0Wvd8KlCP0Qeo9dlTqWajgvWHNqSdfl9bVD6Wp72CGikmgSkvUaAMML", - "expirationDate" : NSDate.distantPast, - "tokenResponse" : tokenResponse] - let error: NSError? = nil - let user: OCUser = OCUser.init() - user.displayName = "Admin" - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - UtilsTests.mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: authenticationMethodIdentifier, dictionary: dictionary, error: error) - UtilsTests.mockOCConnectionConnectWithCompletionHandler(issue: issue, user: user, error: error) - UtilsTests.mockOCConnectionDisconnectWithCompletionHandler() - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("server-bookmark-cell")).assert(grey_sufficientlyVisible()) - - //Reset - OCMockManager.shared.removeAllMockingBlocks() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - /* - * PASSED if: URL leads to correct Basic authentication. Bookmark cell created and displayed - */ - func testLoginBasicAuthRightCredentials () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let userName = "test" - let password = "test" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Error description"]), level: .warning, issueHandler: nil) - - let error: NSError? = nil - let authenticationMethodIdentifier = OCAuthenticationMethodIdentifier.basicAuth - let dictionary:Dictionary = ["BasicAuthString" : "Basic YWRtaW46YWRtaW4=", - "passphrase" : "admin", - "username" : "admin"] - let user: OCUser = OCUser.init() - user.displayName = "Admin" - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - UtilsTests.mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: authenticationMethodIdentifier, dictionary: dictionary, error: error) - UtilsTests.mockOCConnectionConnectWithCompletionHandler(issue: issue, user: user, error: error) - UtilsTests.mockOCConnectionDisconnectWithCompletionHandler() - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("approve-button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).perform(grey_replaceText(userName)) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).perform(grey_replaceText(password)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("server-bookmark-cell")).assert(grey_sufficientlyVisible()) - - //Reset - OCMockManager.shared.removeAllMockingBlocks() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - /* - * PASSED if: URL leads to Basic authentication with warning issue. Bookmark cell is not displayed. Credentials, Name and Continue displayed. - */ - func testLoginBasicAuthWarningIssue () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let userName = "test" - let password = "test" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - - let errorURL: NSError = NSError(domain: "mocked.owncloud.server.com", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Error URL"]) - let issue: OCIssue = OCIssue(forError: errorURL, level: .informal, issueHandler: nil) - - let authenticationMethodIdentifier = OCAuthenticationMethodIdentifier.basicAuth - let dictionary:Dictionary = ["BasicAuthString" : "Basic YWRtaW46YWRtaW4=", - "passphrase" : "admin", - "username" : "admin"] - let errorCredentials: NSError = NSError(domain: "OCError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error Credentials"]) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - UtilsTests.mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: authenticationMethodIdentifier, dictionary: dictionary, error: errorCredentials) - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).perform(grey_replaceText(userName)) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).perform(grey_replaceText(password)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - - //Assert - //TO-DO: catch shake - EarlGrey.selectElement(with: grey_accessibilityID("server-bookmark-cell")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("ok-button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).assert(grey_sufficientlyVisible()) - } - - /* - * PASSED if: URL leads to Basic authentication with error. Bookmark cell is not displayed. Credentials, Name and Continue displayed. - */ - func testLoginBasicAuthErrorIssue () { - - let mockUrlServer = "http://mocked.owncloud.server.com" - let userName = "test" - let password = "test" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.basicAuth, - OCAuthenticationMethodIdentifier.oAuth2] - - let errorURL: NSError = NSError(domain: "mocked.owncloud.server.com", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Error URL"]) - let issue: OCIssue = OCIssue(forError: errorURL, level: .informal, issueHandler: nil) - - let authenticationMethodIdentifier = OCAuthenticationMethodIdentifier.basicAuth - let dictionary:Dictionary = ["BasicAuthString" : "Basic YWRtaW46YWRtaW4=", - "passphrase" : "admin", - "username" : "admin"] - let errorCredentials: NSError = NSError(domain: "OCError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error Credentials"]) - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - UtilsTests.mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: authenticationMethodIdentifier, dictionary: dictionary, error: errorCredentials) - - //Actions - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).perform(grey_replaceText(mockUrlServer)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).perform(grey_replaceText(userName)) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).perform(grey_replaceText(password)) - EarlGrey.selectElement(with: grey_text("Continue".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("ok-button")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("server-bookmark-cell")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } -} diff --git a/ownCloudTests/Login/DeleteBookmarkTests.swift b/ownCloudTests/Login/DeleteBookmarkTests.swift deleted file mode 100644 index 418274281..000000000 --- a/ownCloudTests/Login/DeleteBookmarkTests.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// DeleteBookmarkTests.swift -// ownCloudTests -// -// Created by Jesus Recio (@jesmrec) on 11/12/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking - -@testable import ownCloud - -class DeleteBookmarkTests: XCTestCase { - - override func setUp() { - super.setUp() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - override func tearDown() { - super.tearDown() - OCMockManager.shared.removeAllMockingBlocks() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - /* - * PASSED if: If the bookmark is deleted, and initial View displayed - */ - func testDeleteTheOnlyBookmark () { - - let bookmarkName: String = "BookmarkA" - - if let bookmark: OCBookmark = UtilsTests.getBookmark(bookmarkName: bookmarkName) { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_text(bookmarkName)).perform(grey_swipeSlowInDirection(.left)) - - EarlGrey.selectElement(with: grey_text("Delete".localized)).perform(grey_tap()) - if !EarlGrey.waitForElementMissing(withMatcher: grey_text("Delete".localized), label: "Wait for deletion", timeout: 2.0) { - EarlGrey.selectElement(with: grey_text("Delete".localized)).perform(grey_tap()) - } - - let isBookmarkDeleted = GREYCondition(name: "Waiting for bookmark removal", block: { - var error: NSError? - - //Assert - EarlGrey.selectElement(with: grey_text(bookmarkName)).assert(grey_notVisible(), error: &error) - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 10.0, pollInterval: 0.5) - - GREYAssertTrue(isBookmarkDeleted, reason: "Failed bookmark removal") - - //Reset status - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: Deleted bookmark not displayed, other bookmark displayed - */ - func testDeleteOneBookmarkAndOtherRemains () { - - let bookmarkName1: String = "Bookmark1" - let bookmarkName2: String = "Bookmark2" - - if let bookmark1: OCBookmark = UtilsTests.getBookmark(bookmarkName: bookmarkName1) { - if let bookmark2: OCBookmark = UtilsTests.getBookmark(bookmarkName: bookmarkName2) { - - OCBookmarkManager.shared.addBookmark(bookmark1) - OCBookmarkManager.shared.addBookmark(bookmark2) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_text(bookmarkName1)).perform(grey_swipeSlowInDirection(.left)) - - EarlGrey.selectElement(with: grey_text("Delete".localized)).perform(grey_tap()) - if !EarlGrey.waitForElementMissing(withMatcher: grey_text("Delete".localized), label: "Wait for deletion", timeout: 2.0) { - EarlGrey.selectElement(with: grey_text("Delete".localized)).perform(grey_tap()) - } - - let isBookmarkDeleted = GREYCondition(name: "Waiting for bookmark removal", block: { - var error: NSError? - - //Assert - EarlGrey.selectElement(with: grey_text(bookmarkName1)).assert(grey_notVisible(), error: &error) - EarlGrey.selectElement(with: grey_text(bookmarkName2)).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 10.0, pollInterval: 0.5) - - GREYAssertTrue(isBookmarkDeleted, reason: "Failed bookmark removal") - - //Reset status - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - } -} diff --git a/ownCloudTests/Login/EditBookmarkTests.swift b/ownCloudTests/Login/EditBookmarkTests.swift deleted file mode 100644 index f2499cf93..000000000 --- a/ownCloudTests/Login/EditBookmarkTests.swift +++ /dev/null @@ -1,311 +0,0 @@ -// -// EditBookmarkTests.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 23/11/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking - -@testable import ownCloud - -class EditBookmarkTests: XCTestCase { - - override func setUp() { - super.setUp() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - override func tearDown() { - super.tearDown() - OCMockManager.shared.removeAllMockingBlocks() - } - - /* - * PASSED if: URL and Delete Auth Data displayed - */ - func testCheckInitialViewEditBasicAuth () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: URL, Delete Auth Data and Authentication message displayed if OAuth2 - */ - func testCheckInitialViewEditOAuth2 () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark(authenticationMethod: OCAuthenticationMethodIdentifier.oAuth2) { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("If you 'Continue', you will be prompted to allow the 'ownCloud' App to open OAuth2 login where you can enter your credentials.".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: View cancelled, credentials' fields are not displayed. Server Bookmark cell displayed - */ - func testCheckCancelEditView () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("server-bookmark-cell")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-auth-data-delete")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible()) - - //Reset status - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: Server name has change to "New name" - */ - func testCheckEditServerName () { - - let expectedServerName = "New name" - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - - let mockUrlServer = "http://mocked.owncloud.server.com" - let authMethods: [OCAuthenticationMethodIdentifier] = [OCAuthenticationMethodIdentifier.oAuth2, - OCAuthenticationMethodIdentifier.basicAuth] - let issue: OCIssue = OCIssue(forError: NSError(domain: "mocked.owncloud.server.com", code: 1033, userInfo: [NSLocalizedDescriptionKey: "Error description"]), level: .informal, issueHandler: nil) - - let authenticationMethodIdentifier = OCAuthenticationMethodIdentifier.oAuth2 - let tokenResponse:[String : String] = ["access_token" : "RyFyDu1wH0Wvd8KlCP0Qeo9dlTqWajgvWHNqSdfl9bVD6Wp72CGikmgSkvUaAMML", - "expires_in" : "3600", - "message_url" : "https://localhost/apps/oauth2/authorization-successful", - "refresh_token" : "khA8H18TWC84g1DmB0fzqgDOWvNRNPGJkkzQ1E6AZjq8UrqZ79QTK8UgSsJB6MrW", - "token_type" : "Bearer", - "user_id" : "admin"] - let dictionary:[String : Any] = ["bearerString" : "Bearer RyFyDu1wH0Wvd8KlCP0Qeo9dlTqWajgvWHNqSdfl9bVD6Wp72CGikmgSkvUaAMML", - "expirationDate" : NSDate.distantPast, - "tokenResponse" : tokenResponse] - let error: NSError? = nil - let user: OCUser = OCUser.init() - user.displayName = "Admin" - - //Mock - UtilsTests.mockOCConnectionPrepareForSetup(mockUrlServer: mockUrlServer, authMethods: authMethods, issue: issue) - UtilsTests.mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: authenticationMethodIdentifier, dictionary: dictionary, error: error) - UtilsTests.mockOCConnectionConnectWithCompletionHandler(issue: issue, user: user, error: error) - UtilsTests.mockOCConnectionDisconnectWithCompletionHandler() - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-name-name")).perform(grey_replaceText(expectedServerName)) - EarlGrey.selectElement(with: grey_accessibilityID("continue-bar-button")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_text(expectedServerName)).assert(grey_sufficientlyVisible()) - - //Reset status - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: After removing the password, Delete Authentication is hidden and Continue is displayed - */ - func testCheckEditRemovingPasswordBasicAuth () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).perform(grey_replaceText("")) - //EarlGrey.selectElement(with: grey_text("Save".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("server-bookmark-cell")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-url-url")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-auth-data-delete")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: Credential fields not displayed after clicking in "Delete Authentication Data" - */ - func testCheckEditDeleteAuthenticationDataBasicAuth () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: Credential fields not displayed after clicking in "Delete Authentication Data" - */ - func testCheckEditDeleteAuthenticationDataOAuth2 () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark(authenticationMethod: OCAuthenticationMethodIdentifier.oAuth2) { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-username")).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("row-credentials-password")).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: After deleting authentication data, warning is displayed - */ - func testCheckEditWarningOAuth2 () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark(authenticationMethod: OCAuthenticationMethodIdentifier.oAuth2) { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - let isServerChecked = GREYCondition(name: "Wait for server is checked", block: { - var error: NSError? - - //Assert - EarlGrey.selectElement(with: grey_text("If you 'Continue', you will be prompted to allow the ownCloud App to open OAuth2 login where you can enter your credentials.".localized)).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - GREYAssertTrue(!isServerChecked, reason: "Failed check the server") - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: After deleting authentication data, "Warning" level of certificate is displayed - */ - func testCheckEditCertificateWarning () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark(authenticationMethod: OCAuthenticationMethodIdentifier.oAuth2, certifUserApproved: false) { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - //Asserts - EarlGrey.selectElement(with: grey_text("Warning".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } - - /* - * PASSED if: After deleting authentication data, "Accepted" level of certificate is displayed - */ - func testCheckEditCertificateAccepted () { - - if let bookmark: OCBookmark = UtilsTests.getBookmark(authenticationMethod: OCAuthenticationMethodIdentifier.oAuth2, certifUserApproved: true) { - - OCBookmarkManager.shared.addBookmark(bookmark) - EarlGrey.waitForElementMissing(accessibilityID: "addServer") - - //Actions - EarlGrey.selectElement(with: grey_allOf([grey_accessibilityID("server-bookmark-cell"), grey_sufficientlyVisible()])).perform(grey_swipeFastInDirection(.left)) - EarlGrey.selectElement(with: grey_text("Edit".localized)).perform(grey_tap()) - - //Asserts - EarlGrey.selectElement(with: grey_text("Accepted".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - } -} diff --git a/ownCloudTests/Resources/PropfindResponse.xml b/ownCloudTests/Resources/PropfindResponse.xml deleted file mode 100644 index e308cc9e3..000000000 --- a/ownCloudTests/Resources/PropfindResponse.xml +++ /dev/null @@ -1 +0,0 @@ -/remote.php/dav/files/admin/Tue, 13 Nov 2018 09:09:29 GMT"5bea94c93c2b5"-35630922563092200000015oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Documents/Tue, 13 Nov 2018 09:09:29 GMT"5bea94c93c2b5"-3362273622700000087oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Photos/Tue, 13 Nov 2018 09:09:27 GMT"5bea94c7ee8f7"-367855667855600000082oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/ownCloud%20Manual.pdfTue, 13 Nov 2018 09:09:28 GMT4916139application/pdf"4e91703f8bd536f69b30e9eb3a0582a5"491613900000086oc8r7x6edpzmRDNVWHTTP/1.1 200 OKHTTP/1.1 404 Not Found diff --git a/ownCloudTests/Resources/PropfindResponseNewFolder.xml b/ownCloudTests/Resources/PropfindResponseNewFolder.xml deleted file mode 100644 index a518b7310..000000000 --- a/ownCloudTests/Resources/PropfindResponseNewFolder.xml +++ /dev/null @@ -1 +0,0 @@ -/remote.php/dav/files/admin/Tue, 13 Nov 2018 09:09:29 GMT"5bea14c93c2b5"-35630922563092200000015oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Documents/Tue, 13 Nov 2018 09:09:29 GMT"5bea94c93c2b5"-3362273622700000087oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/New Folder/Tue, 13 Nov 2018 09:09:29 GMT"5bea14a93c2b5"-3362273622700000011oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/Photos/Tue, 13 Nov 2018 09:09:27 GMT"5bea94c7ee8f7"-367855667855600000082oc8r7x6edpzmRDNVCKHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/admin/ownCloud%20Manual.pdfTue, 13 Nov 2018 09:09:28 GMT4916139application/pdf"4e91703f8bd536f69b30e9eb3a0582a5"491613900000086oc8r7x6edpzmRDNVWHTTP/1.1 200 OKHTTP/1.1 404 Not Found diff --git a/ownCloudTests/Resources/test_certificate.cer b/ownCloudTests/Resources/test_certificate.cer deleted file mode 100644 index 9d9b50b62..000000000 Binary files a/ownCloudTests/Resources/test_certificate.cer and /dev/null differ diff --git a/ownCloudTests/Security/BiometricalTests.swift b/ownCloudTests/Security/BiometricalTests.swift deleted file mode 100644 index ad7dcaf4a..000000000 --- a/ownCloudTests/Security/BiometricalTests.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// BiometricalTests.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 15/06/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import LocalAuthentication -import ownCloudApp -import ownCloudAppShared - -@testable import ownCloud - -class BiometricalTests: XCTestCase { - - override func setUp() { - super.setUp() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - override func tearDown() { - AppLockManager.shared.passcode = nil - AppLockSettings.shared.lockEnabled = false - AppLockSettings.shared.biometricalSecurityEnabled = false - super.tearDown() - } - - // MARK: - Tests - - func testBiometricalSuccessAuthentication() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "1111" - AppLockSettings.shared.lockEnabled = true - AppLockSettings.shared.biometricalSecurityEnabled = true - - AppLockManager.shared.showLockscreenIfNeeded(context: TestLAContext(success: true, error: nil)) - - let isPasscodeUnlocked = GREYCondition(name: "Wait for passcode is unlocked by biometrical", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).assert(grey_sufficientlyVisible(), error: &error) - - return error == nil - }).wait(withTimeout: 5.0, pollInterval: 0.5) - - //Assert - GREYAssertTrue(isPasscodeUnlocked, reason: "Failed to unlock the passcode with biometrical") - } - - func testBiometricalFailedAuthentication() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "1111" - AppLockSettings.shared.lockEnabled = true - AppLockSettings.shared.biometricalSecurityEnabled = true - - AppLockManager.shared.showLockscreenIfNeeded(context: TestLAContext(success: false, error: nil)) - - let isPasscodeLocked = GREYCondition(name: "Wait for passcode is unlocked by biometrical", block: { - var error: NSError? - - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).assert(grey_sufficientlyVisible(), error: &error) - - return error?.code == 3 - }).wait(withTimeout: 2.0, pollInterval: 0.5) - - //Assert - GREYAssertTrue(isPasscodeLocked, reason: "Biometrical did not remain") - } - - // MARK: - Mocks - - //Class to mock the LAContext to test the biometrical unlock - class TestLAContext: LAContext { - - var success:Bool - var error:NSError? - - init(success: Bool = true, error: NSError?) { - self.success = success - self.error = error - super.init() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool { - return true - } - - override func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void) { - reply(success, error) - } - } -} diff --git a/ownCloudTests/Security/PasscodeTests.swift b/ownCloudTests/Security/PasscodeTests.swift deleted file mode 100644 index 1e06338c8..000000000 --- a/ownCloudTests/Security/PasscodeTests.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// PasscodeTests.swift -// ownCloudTests -// -// Created by Javier Gonzalez on 31/05/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import LocalAuthentication -import ownCloudApp -import ownCloudAppShared - -@testable import ownCloud - -class PasscodeTests: XCTestCase { - - override func setUp() { - super.setUp() - OCBookmarkManager.deleteAllBookmarks(waitForServerlistRefresh: true) - } - - override func tearDown() { - AppLockManager.shared.passcode = nil - AppLockSettings.shared.lockEnabled = false - super.tearDown() - } - - // MARK: - Passcode - - /* - * PASSED if: Passcode correct. "Add Server" view displayed - */ - func testUnlockRightPasccode() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "1111" - AppLockSettings.shared.lockEnabled = true - - // Show the passcode - AppLockManager.shared.showLockscreenIfNeeded() - EarlGrey.waitForElement(accessibilityID: "number1Button") - - // Tap the number buttons - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - - EarlGrey.waitForElement(accessibilityID: "addServer") - - // Asserts - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).assert(grey_sufficientlyVisible()) - - //Reset Status - AppLockManager.shared.dismissLockscreen(animated: false) - } - - /* - * PASSED if: Passcode incorrect. Passcode view displays with error - */ - func testUnlockWrongPasscode() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "2222" - AppLockSettings.shared.lockEnabled = true - - // Show the passcode - AppLockManager.shared.showLockscreenIfNeeded() - EarlGrey.waitForElement(accessibilityID: "number1Button") - - // Tap the number buttons - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - - // Asserts - EarlGrey.selectElement(with: grey_accessibilityID("messageLabel")).assert(grey_sufficientlyVisible()) - - //Reset Status - AppLockManager.shared.dismissLockscreen(animated: false) - } - - /* - * PASSED if: Passcode Lock disabled in Settings view after cancelling - */ - func testCancelPasscode() { - - // Assure that the passcode is disabled - AppLockSettings.shared.lockEnabled = false - - EarlGrey.waitForElementMissing(accessibilityID: "settingsBarButtonItem") - - EarlGrey.selectElement(with: grey_accessibilityID("settingsBarButtonItem")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).perform(grey_turnSwitchOn(true)) - - // Tap the number buttons - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Cancel".localized)).perform(grey_tap()) - - EarlGrey.waitForElementMissing(accessibilityID: "number1Button") - - // Asserts - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).assert(grey_switchWithOnState(false)) - - // Reset status - EarlGrey.selectElement(with: grey_text("ownCloud")).perform(grey_tap()) - } - - /* - * PASSED if: Passcode Lock disabled in Settings view after cancelling in the second typing - */ - func testCancelSecondTryPasscode() { - - // Assure that the passcode is disabled - AppLockSettings.shared.lockEnabled = false - - EarlGrey.waitForElementMissing(accessibilityID: "settingsBarButtonItem") - - EarlGrey.selectElement(with: grey_accessibilityID("settingsBarButtonItem")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).perform(grey_turnSwitchOn(true)) - - // Tap the number buttons first time - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - - // Tap the number buttons second time & cancel - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Cancel".localized)).perform(grey_tap()) - - EarlGrey.waitForElementMissing(accessibilityID: "number1Button") - - // Asserts - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).assert(grey_switchWithOnState(false)) - - //Reset status - EarlGrey.selectElement(with: grey_text("ownCloud")).perform(grey_tap()) - } - - /* - * PASSED if: Correct error when second typing is different than the first one - */ - func testEnterDifferentPasscodes() { - - // Assure that the passcode is disabled - AppLockSettings.shared.lockEnabled = false - - EarlGrey.waitForElementMissing(accessibilityID: "settingsBarButtonItem") - - EarlGrey.selectElement(with: grey_accessibilityID("settingsBarButtonItem")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).perform(grey_turnSwitchOn(true)) - - // Tap the number buttons - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - - // Tap the number buttons again - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number3Button")).perform(grey_tap()) - - // Asserts - EarlGrey.selectElement(with: grey_text("The entered codes are different".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Cancel".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).assert(grey_switchWithOnState(false)) - - //Reset status - EarlGrey.selectElement(with: grey_text("ownCloud")).perform(grey_tap()) - } - - /* - * PASSED if: Passcode lock is correctly disabled in Settings view - */ - func testDisablePasscode() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "1111" - AppLockSettings.shared.lockEnabled = true - - EarlGrey.waitForElementMissing(accessibilityID: "settingsBarButtonItem") - - EarlGrey.selectElement(with: grey_accessibilityID("settingsBarButtonItem")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).perform(grey_turnSwitchOn(false)) - - // Tap the number buttons first time - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - - EarlGrey.waitForElementMissing(accessibilityID: "number1Button") - - // Asserts - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).assert(grey_switchWithOnState(false)) - EarlGrey.selectElement(with: grey_accessibilityID("lockFrequency")).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("ownCloud")).perform(grey_tap()) - } - - /* - * PASSED if: Passcode lock keeps enabled in Settings view after cancelling - */ - func testCancelDisablePasscode() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "1111" - AppLockSettings.shared.lockEnabled = true - - EarlGrey.waitForElementMissing(accessibilityID: "settingsBarButtonItem") - - EarlGrey.selectElement(with: grey_accessibilityID("settingsBarButtonItem")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).perform(grey_turnSwitchOn(false)) - - // Tap the number buttons first time - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("number1Button")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Cancel".localized)).perform(grey_tap()) - - EarlGrey.waitForElementMissing(accessibilityID: "number1Button") - - // Asserts - EarlGrey.selectElement(with: grey_accessibilityID("passcodeSwitchIdentifier")).assert(grey_switchWithOnState(true)) - EarlGrey.selectElement(with: grey_accessibilityID("lockFrequency")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("ownCloud")).perform(grey_tap()) - } - - /* - * PASSED if: 1 minute frequency covered - */ - func testChangeFrequency() { - - // Prepare the simulator show the passcode - AppLockManager.shared.passcode = "1111" - AppLockSettings.shared.lockEnabled = true - - EarlGrey.waitForElementMissing(accessibilityID: "settingsBarButtonItem") - - EarlGrey.selectElement(with: grey_accessibilityID("settingsBarButtonItem")).perform(grey_tap()) - - // Tap the number buttons first time - EarlGrey.selectElement(with: grey_accessibilityID("lockFrequency")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("After 1 minute".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - - //Asserts - EarlGrey.selectElement(with: grey_text("After 1 minute".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("ownCloud")).perform(grey_tap()) - AppLockSettings.shared.lockEnabled = false - } -} diff --git a/ownCloudTests/SettingsTests.swift b/ownCloudTests/SettingsTests.swift deleted file mode 100644 index 93734d8d2..000000000 --- a/ownCloudTests/SettingsTests.swift +++ /dev/null @@ -1,192 +0,0 @@ -// -// Settings.swift -// ownCloudTests -// -// Created by Jesús Recio on 27/02/2019. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import XCTest -import EarlGrey -import ownCloudSDK -import ownCloudMocking -import ownCloudAppShared - -@testable import ownCloud - -class SettingsTests: XCTestCase { - - override func setUp() { - EarlGrey.waitForElement(withMatcher: grey_text("Settings".localized), label: "Settings") - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - EarlGrey.waitForElement(accessibilityID: "theme") - } - - override func tearDown() { - //Reset status - EarlGrey.selectElement(with: grey_text(VendorServices.shared.appName)).perform(grey_tap()) - EarlGrey.waitForElement(accessibilityID: "addServer") - } - - /* - * PASSED if: Theme and Logging are displayed as part of the "User Interface" section of Settings - */ - func testCheckUserInterfaceItems () { - EarlGrey.waitForElement(accessibilityID: "theme") - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("theme")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("logging")).assert(grey_sufficientlyVisible()) - } - - /* - * PASSED if: Show hidden files and folders, Drag Files are displayed as part of the "Advanced Settings" section of Settings - */ - func testCheckDisplaySettings () { - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("show-hidden-files-switch")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("sort-folders-first")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("prevent-dragging-files-switch")).assert(grey_sufficientlyVisible()) - } - - /* - * PASSED if: Media upload options are displayed as part of the "Media Upload" section of Settings - */ - func testCheckMediaUploadSettings () { - // Open media upload settings section - EarlGrey.selectElement(with:grey_accessibilityID("media-upload")).using(searchAction: grey_scrollInDirection(GREYDirection.down, 350), onElementWithMatcher: grey_kindOfClass(UITableView.self)).perform(grey_tap()) - - // Scroll into view and apply assertions - EarlGrey.selectElement(with:grey_accessibilityID("convert_heic_to_jpeg")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("convert_to_mp4")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("preserve_media_file_names")).assert(grey_sufficientlyVisible()) - - // Make sure instant uploads section is not visible if no bookmark is configured - EarlGrey.selectElement(with: grey_text("Auto Upload".localized)).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("Background uploads".localized)).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - } - - func testMediaBackgroundUploadsSettings() { - if let bookmark: OCBookmark = UtilsTests.getBookmark() { - OCBookmarkManager.shared.addBookmark(bookmark) - // Open media upload settings section - EarlGrey.selectElement(with:grey_accessibilityID("media-upload")).using(searchAction: grey_scrollInDirection(GREYDirection.down, 350), onElementWithMatcher: grey_kindOfClass(UITableView.self)).perform(grey_tap()) - - EarlGrey.selectElement(with:grey_accessibilityID("auto-upload-photos")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("auto-upload-videos")).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - OCBookmarkManager.shared.removeBookmark(bookmark) - } - } - - /* - * PASSED if: "More" options "are displayed - */ - func testCheckMoreItems () { - EarlGrey.selectElement(with: grey_kindOfClass(UITableView.self)).perform(grey_scrollToContentEdge(.bottom)) - - //Assert - EarlGrey.selectElement(with:grey_accessibilityID("help")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("send-feedback")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("recommend-friend")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("privacy-policy")).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with:grey_accessibilityID("acknowledgements")).assert(grey_sufficientlyVisible()) - } - - /* - * PASSED if: All UI components in Logging view are displayed and correctly visible when option is enabled. - */ - func testCheckLoggingInterfaceLoggingEnabled () { - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("logging")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("enable-logging")).assert(grey_switchWithOnState(true)) - EarlGrey.selectElement(with: grey_text("Debug".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Info".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Warning".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Error".localized)).assert(grey_sufficientlyVisible()) - - EarlGrey.selectElement(with: grey_accessibilityID("Logging")) - .usingSearch(grey_scrollInDirection(GREYDirection.down, 100), onElementWith: grey_text("Log HTTP requests and responses")) - .assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("Logging")) - .usingSearch(grey_scrollInDirection(GREYDirection.down, 100), onElementWith: grey_text("Standard error output".localized)) - .assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("Logging")) - .usingSearch(grey_scrollInDirection(GREYDirection.down, 100), onElementWith: grey_text("Log file".localized)) - .assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_accessibilityID("Logging")) - .usingSearch(grey_scrollInDirection(GREYDirection.down, 100), onElementWith: grey_text("Browse".localized)) - .assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - } - - /* - * PASSED if: Log level is changed to "Warning". - */ - func testSwitchLogLevel () { - EarlGrey.waitForElement(accessibilityID: "logging") - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("logging")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Warning".localized)).perform(grey_tap()) - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_text("Warning".localized)).assert(grey_sufficientlyVisible()) - } - - /* - * PASSED if: All UI components in Logging view are not displayed when option is disabled. - */ - func testCheckLoggingInterfaceLoggingDisabled () { - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("logging")).perform(grey_tap()) - EarlGrey.selectElement(with: grey_accessibilityID("enable-logging")).perform(grey_turnSwitchOn(false)) - - //Assert - EarlGrey.selectElement(with: grey_accessibilityID("enable-logging")).assert(grey_switchWithOnState(false)) - EarlGrey.selectElement(with: grey_text("Debug".localized)).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("Info".localized)).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("Warning".localized)).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("Error".localized)).assert(grey_notVisible()) - - EarlGrey.selectElement(with: grey_text("Log HTTP requests and responses".localized)).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("Standard error output".localized)).assert(grey_notVisible()) - EarlGrey.selectElement(with: grey_text("Log file".localized)).assert(grey_notVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_accessibilityID("enable-logging")).perform(grey_turnSwitchOn(true)) - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - } - - /* - * PASSED if: All themes available are displayed - */ - func testCheckThemesAvailable () { - EarlGrey.waitForElement(accessibilityID: "theme") - - //Actions - EarlGrey.selectElement(with: grey_accessibilityID("theme")).perform(grey_tap()) - - //Assert - EarlGrey.selectElement(with: grey_text("Dark".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Light".localized)).assert(grey_sufficientlyVisible()) - EarlGrey.selectElement(with: grey_text("Classic".localized)).assert(grey_sufficientlyVisible()) - - //Reset status - EarlGrey.selectElement(with: grey_text("Settings".localized)).perform(grey_tap()) - } - -} diff --git a/ownCloudTests/Tools/EarlGrey+Tools.swift b/ownCloudTests/Tools/EarlGrey+Tools.swift deleted file mode 100644 index 14fba2370..000000000 --- a/ownCloudTests/Tools/EarlGrey+Tools.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// EarlGrey+Tools.swift -// ownCloudTests -// -// Created by Felix Schwarz on 24.01.19. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import UIKit -import EarlGrey - -extension EarlGrey { - @discardableResult - static func waitForElement(withMatcher: GREYMatcher, label: String, timeout: CFTimeInterval = 2) -> Bool { - let condition : GREYCondition = GREYCondition(name: "Wait for \(label)") { () -> Bool in - var error : NSError? - - EarlGrey.select(elementWithMatcher: withMatcher).assert(grey_notNil(), error: &error) - - return error == nil - } - - return condition.wait(withTimeout: timeout) - } - - @discardableResult - static func waitForElementMissing(withMatcher: GREYMatcher, label: String, timeout: CFTimeInterval = 2) -> Bool { - let condition : GREYCondition = GREYCondition(name: "Wait for \(label)") { () -> Bool in - var error : NSError? - - EarlGrey.select(elementWithMatcher: withMatcher).assert(grey_nil(), error: &error) - - return error == nil - } - - return condition.wait(withTimeout: timeout) - } - - @discardableResult - static func waitForElement(accessibilityID: String, timeout: CFTimeInterval = 2) -> Bool { - return self.waitForElement(withMatcher: grey_accessibilityID(accessibilityID), label: accessibilityID, timeout: timeout) - } - - @discardableResult - static func waitForElementMissing(accessibilityID: String, timeout: CFTimeInterval = 2) -> Bool { - return self.waitForElementMissing(withMatcher: grey_accessibilityID(accessibilityID), label: accessibilityID, timeout: timeout) - } -} diff --git a/ownCloudTests/Tools/OCBookmarkManager+Tools.swift b/ownCloudTests/Tools/OCBookmarkManager+Tools.swift deleted file mode 100644 index e00ad7143..000000000 --- a/ownCloudTests/Tools/OCBookmarkManager+Tools.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// OCBookmarkManager+Tools.swift -// ownCloudTests -// -// Created by Felix Schwarz on 24.01.19. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -import UIKit -import ownCloudSDK -import EarlGrey - -extension OCBookmarkManager { - static func deleteAllBookmarks(waitForServerlistRefresh: Bool = false) { - let bookmarks : [OCBookmark] = OCBookmarkManager.shared.bookmarks as [OCBookmark] - - if bookmarks.count > 0 { - let waitGroup = DispatchGroup() - - for bookmark:OCBookmark in bookmarks { - waitGroup.enter() - - OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in - let vault : OCVault = OCVault(bookmark: bookmark) - - vault.erase(completionHandler: { (_, error) in - if error == nil { - OCBookmarkManager.shared.removeBookmark(bookmark) - } else { - assertionFailure("Error deleting vault for bookmark") - } - - waitGroup.leave() - - completionHandler() - }) - }, for: bookmark) - } - - switch waitGroup.wait(timeout: .now() + 5.0) { - case .success: break - case .timedOut: - let remainingBookmarks : [OCBookmark] = OCBookmarkManager.shared.bookmarks as [OCBookmark] - - for bookmark in remainingBookmarks { - NSLog("timed out waiting for bookmark \(bookmark.uuid) to complete deletion") - OCBookmarkManager.shared.removeBookmark(bookmark) - } - // assertionFailure("timed out waiting for bookmarks to complete deletion") - } - } - - if waitForServerlistRefresh { - NSLog("Waiting for element addServer result: \(EarlGrey.waitForElement(accessibilityID: "addServer"))") - } - } -} diff --git a/ownCloudTests/Tools/OCMockingManager+SwiftTools.swift b/ownCloudTests/Tools/OCMockingManager+SwiftTools.swift deleted file mode 100644 index 4dacae869..000000000 --- a/ownCloudTests/Tools/OCMockingManager+SwiftTools.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// OCMockingManagerSwiftExtension.swift -// ownCloudTests -// -// Created by Felix Schwarz on 20.09.18. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -// import Cocoa -import ownCloudMocking - -extension OCMockManager { - func addMocking(blocks: [ OCMockLocation : Any ]) { - for (location, block) in blocks { - self.setMockingBlock(block, forLocation: location) - } - } -} diff --git a/ownCloudTests/Tools/OwnCloudTests.swift b/ownCloudTests/Tools/OwnCloudTests.swift deleted file mode 100644 index c5d2e1fe5..000000000 --- a/ownCloudTests/Tools/OwnCloudTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ownCloudTests.swift -// ownCloudTests -// -// Created by Pablo Carrascal on 07/03/2018. -// Copyright © 2018 ownCloud. All rights reserved. -// - -import XCTest -import EarlGrey - -@testable import ownCloud - -class OwnCloudTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - /* - * Passed if: "Add account" button is enabled - */ - func testAddServerButtonIsEnabled() { - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).assert( grey_enabled()) - } - - func testClickOnTheButtonAndNothingHappens() { - EarlGrey.waitForElement(accessibilityID: "addServer") - EarlGrey.selectElement(with: grey_accessibilityID("addServer")).perform(grey_tap()) - EarlGrey.waitForElement(accessibilityID: "cancel") - EarlGrey.selectElement(with: grey_accessibilityID("cancel")).perform(grey_tap()) - } -} diff --git a/ownCloudTests/Tools/UtilsTests.swift b/ownCloudTests/Tools/UtilsTests.swift deleted file mode 100644 index b18e0031b..000000000 --- a/ownCloudTests/Tools/UtilsTests.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// UtilsTesting.swift -// ownCloud -// -// Created by Javier Gonzalez on 06/11/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import Foundation -import ownCloudSDK -import ownCloudMocking -import ownCloudApp -import ownCloudAppShared - -@testable import ownCloud - -class UtilsTests { - - public typealias OCMPrepareForSetupCompletionHandler = @convention(block) - (_ issue: OCIssue, _ suggestedURL: NSURL, _ supportedMethods: [OCAuthenticationMethodIdentifier], _ preferredAuthenticationMethods: [OCAuthenticationMethodIdentifier]) -> Void - - public typealias OCMPrepareForSetup = @convention(block) - (_ connection: OCConnection, _ options: NSDictionary, _ completionHandler: OCMPrepareForSetupCompletionHandler) -> Void - - public typealias OCMGenerateAuthenticationDataWithMethodCompletionHandler = @convention(block) - (_ error: NSError?, _ authenticationMethodIdentifier: OCAuthenticationMethodIdentifier, _ authenticationData: NSData?) -> Void - - public typealias OCMGenerateAuthenticationDataWithMethod = @convention(block) - (_ connection: OCConnection, _ methodIdentifier: OCAuthenticationMethodIdentifier, _ options: OCAuthenticationMethodBookmarkAuthenticationDataGenerationOptions, _ completionHandler: OCMGenerateAuthenticationDataWithMethodCompletionHandler) -> Void - - public typealias OCMConnectCompletionHandler = @convention(block) - (_ error: NSError?, _ issue: OCIssue?) -> Void - - public typealias OCMDisconnectCompletionHandler = @convention(block) - () -> Void - - public typealias OCMConnect = @convention(block) - (_ connection: OCConnection, _ completionHandler: OCMConnectCompletionHandler) -> Progress - - public typealias OCMDisconnect = @convention(block) - (_ connection: OCConnection, _ completionHandler: OCMDisconnectCompletionHandler, _ invalidate: Bool) -> Void - - static func removePasscode() { - AppLockManager.shared.passcode = nil - AppLockSettings.shared.lockEnabled = false - AppLockSettings.shared.biometricalSecurityEnabled = false - AppLockSettings.shared.lockDelay = SecurityAskFrequency.always.rawValue - AppLockManager.shared.dismissLockscreen(animated: false) - } - - // MARK: - Helper - static func getBookmark(authenticationMethod: OCAuthenticationMethodIdentifier = OCAuthenticationMethodIdentifier.basicAuth, bookmarkName: String = "Server name", certifUserApproved: Bool = true) -> OCBookmark? { - - let mockUrlServer: String = "https://mock.owncloud.com/" - - let dictionary: Dictionary = ["BasicAuthString" : "Basic YWRtaW46YWRtaW4=", - "passphrase" : "admin", - "username" : "admin"] - - var data: Data? - do { - data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .binary, options: 0) - } catch { - return nil - } - - let bookmark: OCBookmark = OCBookmark() - bookmark.name = bookmarkName - bookmark.url = URL(string: mockUrlServer) - bookmark.authenticationMethodIdentifier = authenticationMethod - bookmark.authenticationData = data - bookmark.certificate = self.getCertificate(mockUrlServer: mockUrlServer) - bookmark.certificate?.userAccepted = certifUserApproved - - return bookmark - } - - static func getCertificate(mockUrlServer: String) -> OCCertificate? { - let bundle = Bundle.main - if let url: URL = bundle.url(forResource: "test_certificate", withExtension: "cer") { - do { - let certificateData = try Data(contentsOf: url as URL) - let certificate: OCCertificate = OCCertificate(certificateData: certificateData, hostName: mockUrlServer) - - return certificate - } catch { - Log.error("Failing reading data of test_certificate.cer") - } - } else { - Log.error("Not possible to read the test_certificate.cer") - } - return nil - } - - // MARK: - Mocks - static func mockOCConnectionPrepareForSetup(mockUrlServer: String, authMethods: [OCAuthenticationMethodIdentifier], issue: OCIssue) { - let completionHandlerBlock : OCMPrepareForSetup = { - (connection, dict, mockedBlock) in - let url: NSURL = NSURL(fileURLWithPath: mockUrlServer) - mockedBlock(issue, url, authMethods, authMethods) - } - - OCMockManager.shared.addMocking(blocks: - [OCMockLocation.ocConnectionPrepareForSetupWithOptions : completionHandlerBlock]) - } - - static func mockOCConnectionGenerateAuthenticationData(authenticationMethodIdentifier: OCAuthenticationMethodIdentifier, dictionary: [String: Any], error: NSError?) { - let completionHandlerBlock : OCMGenerateAuthenticationDataWithMethod = { - (connection, methodIdentifier, options, mockedBlock) in - - var data: Data? - - do { - data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .binary, options: 0) - } catch { - return - } - - mockedBlock(error, authenticationMethodIdentifier, data! as NSData) - } - - OCMockManager.shared.addMocking(blocks: - [OCMockLocation.ocConnectionGenerateAuthenticationDataWithMethod : completionHandlerBlock]) - } - - static func mockOCConnectionConnectWithCompletionHandler(issue: OCIssue, user: OCUser?, error: NSError?) { - - let completionHandlerBlock : OCMConnect = { - (connection, mockedBlock) in - - if user != nil { - connection.loggedInUser = user - } - - mockedBlock(error, nil) - - return Progress() - } - - OCMockManager.shared.addMocking(blocks: - [OCMockLocation.ocConnectionConnectWithCompletionHandler : completionHandlerBlock]) - } - - static func mockOCConnectionDisconnectWithCompletionHandler() { - - let completionHandlerBlock : OCMDisconnect = { - (connection, mockedBlock, invalidate) in - - mockedBlock() - } - - OCMockManager.shared.addMocking(blocks: - [OCMockLocation.ocConnectionDisconnectWithCompletionHandlerInvalidate : completionHandlerBlock]) - } -} diff --git a/tools/LocaleDiff/LocaleDiff.xcodeproj/project.pbxproj b/tools/LocaleDiff/LocaleDiff.xcodeproj/project.pbxproj new file mode 100644 index 000000000..a474d6c31 --- /dev/null +++ b/tools/LocaleDiff/LocaleDiff.xcodeproj/project.pbxproj @@ -0,0 +1,289 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + DC25FDAF28F064B500D62F9A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25FDAE28F064B500D62F9A /* main.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DC25FDA928F064B500D62F9A /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + DC25FDAB28F064B500D62F9A /* LocaleDiff */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = LocaleDiff; sourceTree = BUILT_PRODUCTS_DIR; }; + DC25FDAE28F064B500D62F9A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DC25FDA828F064B500D62F9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DC25FDA228F064B500D62F9A = { + isa = PBXGroup; + children = ( + DC25FDAD28F064B500D62F9A /* LocaleDiff */, + DC25FDAC28F064B500D62F9A /* Products */, + ); + sourceTree = ""; + }; + DC25FDAC28F064B500D62F9A /* Products */ = { + isa = PBXGroup; + children = ( + DC25FDAB28F064B500D62F9A /* LocaleDiff */, + ); + name = Products; + sourceTree = ""; + }; + DC25FDAD28F064B500D62F9A /* LocaleDiff */ = { + isa = PBXGroup; + children = ( + DC25FDAE28F064B500D62F9A /* main.swift */, + ); + path = LocaleDiff; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DC25FDAA28F064B500D62F9A /* LocaleDiff */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC25FDB228F064B500D62F9A /* Build configuration list for PBXNativeTarget "LocaleDiff" */; + buildPhases = ( + DC25FDA728F064B500D62F9A /* Sources */, + DC25FDA828F064B500D62F9A /* Frameworks */, + DC25FDA928F064B500D62F9A /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LocaleDiff; + productName = LocaleDiff; + productReference = DC25FDAB28F064B500D62F9A /* LocaleDiff */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DC25FDA328F064B500D62F9A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1340; + LastUpgradeCheck = 1340; + TargetAttributes = { + DC25FDAA28F064B500D62F9A = { + CreatedOnToolsVersion = 13.4.1; + }; + }; + }; + buildConfigurationList = DC25FDA628F064B500D62F9A /* Build configuration list for PBXProject "LocaleDiff" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DC25FDA228F064B500D62F9A; + productRefGroup = DC25FDAC28F064B500D62F9A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DC25FDAA28F064B500D62F9A /* LocaleDiff */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + DC25FDA728F064B500D62F9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC25FDAF28F064B500D62F9A /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + DC25FDB028F064B500D62F9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DC25FDB128F064B500D62F9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + DC25FDB328F064B500D62F9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4AP2STM4H5; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + DC25FDB428F064B500D62F9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4AP2STM4H5; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DC25FDA628F064B500D62F9A /* Build configuration list for PBXProject "LocaleDiff" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC25FDB028F064B500D62F9A /* Debug */, + DC25FDB128F064B500D62F9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC25FDB228F064B500D62F9A /* Build configuration list for PBXNativeTarget "LocaleDiff" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC25FDB328F064B500D62F9A /* Debug */, + DC25FDB428F064B500D62F9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DC25FDA328F064B500D62F9A /* Project object */; +} diff --git a/tools/LocaleDiff/LocaleDiff.xcodeproj/xcshareddata/xcschemes/LocaleDiff.xcscheme b/tools/LocaleDiff/LocaleDiff.xcodeproj/xcshareddata/xcschemes/LocaleDiff.xcscheme new file mode 100644 index 000000000..e6c249396 --- /dev/null +++ b/tools/LocaleDiff/LocaleDiff.xcodeproj/xcshareddata/xcschemes/LocaleDiff.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/LocaleDiff/LocaleDiff/main.swift b/tools/LocaleDiff/LocaleDiff/main.swift new file mode 100644 index 000000000..f736af223 --- /dev/null +++ b/tools/LocaleDiff/LocaleDiff/main.swift @@ -0,0 +1,61 @@ +// +// main.swift +// LocaleDiff +// +// Created by Felix Schwarz on 07.10.22. +// + +/* + * Copyright (C) 2022, 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 + +if CommandLine.argc < 2 { + print("LocaleDiff [base Localizable.strings] [translated Localizable.strings] …") +} else { + let arguments = CommandLine.arguments + var argIdx = 1 + + while argIdx+1 < CommandLine.argc { + let baseStringsURL = URL(fileURLWithPath: arguments[argIdx]) + let translatedStringsURL = URL(fileURLWithPath: arguments[argIdx+1]) + + if let baseStringsData = try? Data(contentsOf: baseStringsURL), + let baseStringsDict = try? PropertyListSerialization.propertyList(from: baseStringsData, format: nil) as? [String:String], + let translatedStringsData = try? Data(contentsOf: translatedStringsURL), + let translatedStringsDict = try? PropertyListSerialization.propertyList(from: translatedStringsData, format: nil) as? [String:String] { + let baseKeys = Set(baseStringsDict.keys) + let translatedKeys = Set(translatedStringsDict.keys) + + let superfluousKeys = translatedKeys.subtracting(baseKeys) + let untranslatedKeys = baseKeys.subtracting(translatedKeys) + + if !superfluousKeys.isEmpty { + print("⛔️ Superfluous keys in \(translatedStringsURL.path):") + + for superfluousKey in superfluousKeys { + print("- \(superfluousKey)") + } + } + + if !untranslatedKeys.isEmpty { + print("⚠️ Untranslated keys in \(translatedStringsURL.path):") + + for untranslatedKey in untranslatedKeys { + let translationTemplate = "\"\(untranslatedKey.replacingOccurrences(of: "\"", with: "\\\""))\"" + + print("\(translationTemplate) = \(translationTemplate);") + } + } + } + + argIdx += 2 + } +}