From 34af585ed698ce95a3d8057405ac5a62c159ac36 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Mon, 22 May 2023 12:38:00 +0200 Subject: [PATCH] [feature/new-navigation] New navigation (#1162) [feature/css] CSS Themeing #1194 [feature/gridview] Gridview implementation (v12) (#1195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Changes from [feature/new-navigation] ## Description Contains various changes and improvements to the view navigation and hierarchy of version 12: - [x] sidebar - [x] remove legacy table view code and use new UI for OC10, too - [x] support for status 425 items - [x] show folder statistics and drive quota (if one is set) at the bottom - [x] show correct UI for ocis-based servers on first login for branded clients - [x] add missing German localizations - [x] basic sharing fixes - [x] new location picker, import sheet and share extension - [x] show new [AppStore UI](https://developer.apple.com/documentation/storekit/appstore/3803198-showmanagesubscriptions) for managing AppStore subscriptions - [x] browser-like navigation - [x] tappable breadcrumb view ## Merges This PR was started on a merge from [feature/new-search] #1142 and [feature/app-provider] #1151 to leverage work from previous PRs directly. In addition, the following PRs and changes have been merged into this branch: - [feature/disable-fileprovider] Disable access to File Provider via MDM #1158 - [feature/disallow-extensions] Add option to disallow extensions, show action for unviewable filetypes #1140 - [feature/ats-control] Add DISABLE_PLAIN_HTTP build flag #1136 - [feature/postbuild-settings] Postbuild Settings #1179 - merge `11.11.0` and `fastlane/enterprise-adhoc` changes from `master` # Changes from [feature/css] Implements CSS themeing for the V12 UI. [Documentation](https://github.com/owncloud/ios-app/tree/feature/css/ownCloudAppShared/User%20Interface/Theme/CSS#readme) # Changes from [feature/gridview] ## Description - implements a grid view for items, with switching possible with a new button i the sort bar. - implements a dynamically adapting grid layout – used for item grid view and the spaces grid overview - fixes spaces grid staying on screen after disconnect - fixes more (...) buttons next to space titles being compressed sideways in spaces grid - fixes quota not being displayed for Personal space - fixes "empty folder" being shown instead of "loading" + indicator ## Related Issue #744 # Autogenerated squashed commit messages * - change "smart folders" naming to "search view" - add missing strings to Localizable.strings * - update SDK to fix issue with detached spaces' items turning up in search results * Updated to current Xcode version * - update KNOWN_ISSUES.md to reflect changes * - ItemSearchSuggestionsViewController: - hide save search/template popup button unless saving is actually available in a scope - remove saved searches from the popup as they are now also available elsewhere - SearchViewController: strip whitespace when deciding what content to show (suggestion / noItems / results) - update SDK * - add SegmentView: composing items (SegmentViewItem) as views (SegmentViewItemView), applying background, round corners, colors, etc. (styling still work-in-progress) - SavedSearchCell: make use of SegmentView for richer, token-based visualization of saved searches - OCSavedSearch: - new property to indicate if .name has been defined by the user - convenience method to quickly create an array of SegmentViewItems from a OCSavedSearch - CollectionViewController: automatically apply .tableBackgroundColor as background color - SearchViewController: strip whitespace from search queries before determining which content to show - GradientView: add support for direction - UIView+EmbedAndLayout: convenience methods to embed views horizontally and apply custom insets and spacing - ThemeView: add hook to setup subviews upon insertion into superview * - update KNOWN_ISSUES evolution part with remaining idea on what can be done with SegmentView and how it would benefit search - Theming support improvements - ThemeCollection: add tokenColors and tableRowButtonColors - SegmentViewItemView: add support for themeing - SegmentViewItem: simplify styles - SearchViewController: theme the search field - SavedSearchCell: add support for themeing * - bump build/version to 230 * SDK update - removes space type from presentable / spaces list - adds new log.replace-newline option and makes it default * Fix App Provider review findings from #1151: - close button visibility on mixed/dark themes - display error messages when errors occur - add missing translations * - update SDK for additional bugfix * - ItemListCell: add support for server-side processing state - fix bug that caused an empty spaces list when offline (via SDK) * - OCItem+FileProviderItem: add support for OCItem.state, making non-local files that are processing remotely non-readable - SDK update * - bump build number to 231 * - add new Tool "LocaleDiff" to find superfluous and missing strings in an other translation - add German translations for all missing strings in ownCloud/Resources/en.lproj/Localizable.strings * - bump build number to 232 * - make Beta warning localizable and localize it in German * - add additional strings files to LocaleDiff scheme - add missing German ownCloudAppFramework locales * - add missing string to Localizable.strings * - update SDK to get improved error messages for downloads of files with status 425 * - update localizations * - SDK update: fix duplicate/"ghost" activities visual issue - ClientActivityViewController: fix Xcode 14 warnings - PopupButtonController: add themeing support - NSObject+ThemeApplication: add proper support for UISearchTextField, fixing #118 text color issue * - ClientRootViewController: do not restore view hierarchy for drive-based accounts since that's not yet implemented and the wrong view controllers would be created - ClientItemViewController: - use a UILabel as navigationItem.titleView to address UINavigationBar layout issues (fixing reports finding 3 in [censored] issue 118) - update title as the folder's name is being changed - update underlying emptyActions when the folder they target is moved - update SDK for moved item detection fixes * - bump build number to 233 * - SDK update to fix possible crash bug - address open/public Swift compiler warnings * - bump build number to 234 * - bump build number to 235 * Fix issues with ocis sharing: - respect OCCapabilities.federatedSharingSupported - GroupSharingTableViewController: fix delete share issue by calling the correct method * - bump version to 236 * - fix Xcode 14 warnings * - upgrade SDK - adapt to OCShare.itemType type change - replace UIImage(systemName:) code with one using OCSymbol - fix more Xcode 14 warnings * - update SDK to gain OCStatistic capabilities - ClientItemViewController: - implement support for "folder removed" state - adjust message spacing - refactor navigation bar code - add footer to display statistics for the current location, feeding from OCQuery-provided OCStatistics - prepare to also include quota information in the footer - update KNOWN_ISSUES.md * - ItemListCell: align separator layout guide with icon - ViewCell: add separatorLayoutGuideCustomizer to allow customization of separator layout guide for individual views - ClientItemViewController: add support to display quota information (space remaining) - UIView+EmbedAndLayout: add code to embed multiple views vertically, modify existing ConstraintSet type * - update SDK to gain fixed "Personal" and "Shares" names * - ios-sdk: OCSyncActionCopyMove: prevent copying and moving of items into themselves - ViewCell: fix seperatorLayoutGuide constraint on reuse (could cause visual glitch of full-length separator line when there shouldn't have been one) - update known issues * - SDK update to gain Accept-Language header addition for in-app web app presentation - ClientWebAppViewController: implement logic to immediately dismiss the view controller when tapping the close button, but keep the web view alive until the "close" event arrives from the webview - or 10 seconds have passed * - Action: add new class setting for excluded activities - OpenInAction: honor excluded activities, bring up UIActivityViewController instead of UIDocumentInteractionController even for a single item if activities are excluded - ownCloud.xcscheme: add env var example for action.excludedSystemActivities to block copying to pasteboard * - update SDK to bring over UUID-based Shares Jail detection * - bump build number to 237 * - StaticLoginSetupViewController: add OCConnection.connect() / .disconnect() to enrich bookmark with metadata and user name during setup. Required to present the correct UI for space-based accounts. * - bump build number to 238 * - fix "App Provider language always English" via ios-sdk update * - bump version to 239 * - update SDK for specific error message when trying to open a file that's being processed via open-web or open * - update SDK * - ClientSidebarViewController: side bar view controller - CollectionViewSection: add hideIfEmptyDataSource feature/property - ClientContext: add ViewControllerPusher protocol and methods for unifying pushing of view controllers to navigation / into view - OCDrive+Interactions, OCItem+Interactions, OCSavedSearch+Interactions: move to new ClientContext method for pushing * - CollectionViewController: - add support for hierarchic collection views and hierarchic data sources - add shouldDeselect() method - use snapshot.reconfigureItems() instead of snapshot.reloadItems() for more efficient updates (reuses cell rather than recreating it from scratch) - CollectionViewCellConfiguration: - add new sideBar StyleType - UIView+EmbedAndLayout: add AnchorSet and extend API to utilize it - many fixes * Snapshot with sidebar basically working, before making deeper changes - App Controllers - AppRootViewController: the root view controller of the entire app, manages sidebar and more - AppSidebarViewController: side bar view controller, managing the side bar / first view - Account Connection/Consumer classes - AccountConnection: manages a connection for the UI and makes its resources and message available to one or more consumers - AccountConnectionPool: creates new and keeps track of AccountConnection instances - AccountConnectionRichStatus: helper class to provide a consistent, simple, rich status for the whole account (wip) - AccountConsumer: group everything that is linked to an OCCore, allowing simple and clean addition and removal of consumers, providing a layer of separation/abstraction from UI concerns - Account Controller classes - AccountController: links AccountConnection to the UI, provides sidebar content for an account - AccountControllerSection: makes AccountController contents available as a CollectionViewSection - AccountControllerCell: provides a cell representation for an AccountController, providing an icon to disconnect ("eject") a connection - DriveListCell: add an icon for the sidebar, representing the (special) kind of drive - CollectionView: - CollectionSidebarViewController: extend ViewControllerNavigationPusher with sidebar view controller parameter - CollectionViewCellProvider+StandardImplementations: add cell provider for .sidebar style type - CollectionViewSection: - implement OCDataItem + OCDataItemVersioning protocol - improve support for handling dynamic updates in hierarchic layouts - CollectionViewController: - add support providing sections from an OCDataSource (just set .sectionsDataSource) - properly support per-section ClientContext (use section.clientContext if set) - OCDataItem+InteractionProtocols: add new, optional method to DataItemSelectionInteraction to provide control over whether an item should be selectable - OCLocation+Interactions: implement the DataItemSelectionInteraction - general: move many classes from ownCloud.app to ownCloudApp.framework - Temporary changes: - ClientRootViewController: semi-functional, intermediate version to validate side bar design, to be removed entirely - ClientTabBarController: see ClientRootViewController - SceneDelegate: move to AppRootViewController as root view controller * - move from UIImage(systemName:) to OCSymbol.icon(forSymbolName:) in most places - split AppSidebarViewController into framework (ClientSidebarViewController in ownCloudAppShared) and app (extension ClientSidebarViewController) parts and extend the implementation with settings and account creation toolbar items - BookmarkInfoViewController: add hook that gets called when the user taps "Done" and dismisses the view controller - BookmarkViewController: add extension with convenience method to set up and display the BookmarkViewController for editing and creating new bookmarks - OCBookmarkManager: - OCBookmarkManager+Management: extensions in app and framework to set up and display the BookmarkInfoViewController or delete an account, including confirmation by the user - OCBookmarkManager+Locking: move app-local locking of bookmarks to the ownCloudAppShared framework and extend on the concept with new attemptLock() method - AccountConnection: - add new .noCore status to differentiate it from .offline - make consumer optional for connect() and disconnect() - post AccountConnection.StatusChangedNotification notifications when .status changes - RichStatus: add property for labeling the interaction - AccountController: - add saved searches to the items data source - AccountControllerCell: listen to AccountConnection.StatusChangedNotification to update colored status indicator, fix other status reporting glitches - SavedSearchCell: add specific cell for .sideBar style - Navigation Revocations - new toolkit to detect when presented content has disappeared and provide a handling path (consult the respective README.md for details) - NavigationRevocationEvent: struct for a rich expression of events like disconnects, drive removals and more - NavigationRevocationAction: listen to events and perform an action once they find a matching event, lifetime can be to other objects - NavigationRevocationManager: global management of NavigationRevocationAction with memory semantics to allow automatic removal after a NavigationRevocationAction has become obsolete - NavigationRevocationTrigger: provides triggers for events such as specific items being removed from a datasource, objects being deallocated and more, can send a global event and/or invoke an action directly - UIViewController+NavigationRevocation: provide a simple API to register a view controller for revocation to common events - ClientContext: add new NavigationRevocationHandler protocol and property to handle navigation revocations - CollectionSidebarViewController: add support for NavigationRevocationHandler - OCDrive/OCLocation/OCSavedSearch+Interactions: register view controllers for Navigation Recovation, using NavigationRevocationHandler - ClientItemViewController: - show special drive header only for .project (so not for personal or Shares Jail) spaces - fix navigation title * - bump build number to 240 - AppRootViewController: - add focusedBookmark tracking - add focusedBookmark specific notificationPresenter and cardMessagePresenter - port over message presentation - port over beta warning - port over review prompting - port over enforced passcode setup (as of yet untested) - BookmarkViewController: - ensure editing uses the latest copy from OCBookmarkManager - present modally rather than .overFullscreen on iPad - ClientActivityViewController: add initializer for AccountConnection and use AccountConnectionConsumer to hook into events - ClientSessionManager: remove unused code, ensure delegates are only weakly referenced - AccountConnection: complete MessageSelector support - AccountConnectionConsumer: switch form NSObject to AnyObject, add new method to AccountConnectionMessageUpdates to track message counts - AccountController: introduce SpecialItems type, property and data source - AccountControllerCell: add support for message counting badge - ActionCell: add sidebar support, add badgeCount accessory support - CollectionSidebarAction: - new class, subclassing OCAction to provide specific functionality around Sidebars, f.ex. dynamic creation of view controllers upon selection, keeping track of and caching previously generated view controllers - also implements extensions to OCAction to wrap properties - CollectionSidebarViewController: add new method sectionOfCurrentSelection to easily determine the section in which the currently selected item is located - ClientSidebarViewController: add .focusedBookmark property (KVO capable) - OCAction+Interactions: add support for supportsDrop * - update KNOWN_ISSUES - AppRootViewController: add support for beta warning, bookmark editing, message presentation, release notes and more - AppDelegate: launch into AppRootViewController instead of ServerListTableViewController - remove ServerListTableViewController from target - ReleaseNotesDatasource: clean up code and make class-level methods what were instance methods - rename "Search view" to "Saved search" - AccountConnection: add AuthFailure struct and status and make Status a non-String enum - AccountConnectionPool: add AccountConnectionAuthErrorConsumer to new connections by default - refactor authentication error handling into - AccountAuthentication* classes - AccountConnectionAuthErrorConsumer (as helper for AccountConnection) - AccountConnectionErrorHandler (as helper for AccountController) - AccountController: - keep references to important items - add support for saved searches - add support for OC10 root folder - clicking spaces shows a spaces grid (AccountControllerSpacesGridViewController) - AccountControllerCell: adapt to Status enum changes - ActionCell: add support for different types (coloring), accessories, badge count, button labels and more - DriveGridCell: cell subclass to support AccountControllerSpacesGridViewController - DriveListCell: add grid support - CollectionViewController: - add performDataSourceUpdate() mechanism to avoid changes being made to the data sources while other changes are already made - CollectionSidebarAction: - extend OCAction properties - implement class to show view controllers on selection - add support for children - CollectionViewAction: actions to be performed on the collection view when specific items become available - CollectionViewSection: add support for .grid() layout, fix crash bug - ClientContext: add alertQueue, bookmarkEditingHandler, .presentationViewController property and present(viewController:animated:completion:) method - ClientSidebarViewController: add support for default view controller - ClientDefaultViewController: content shown when nothing is selected in the sidebar - ClientItemViewController: add select all/deselect all support - (re)start implementation of location picker (WIP) - UIView+EmbedAndLayout: add new method for centering another view inside an existing view, with minimum/maxiumum/fixed size and minimum insets - Theme: add support for split view content themeing, temporarily remove almost all themes until support for them arrives - ThemeNavigationController: move from ThemeApplierTokens to implementing Themeable - AppUserActivity: start (re)implementing support for NSUserActivity * - bump build number to 241 * - update KNOWN_ISSUES - move OCResourceText+ViewProvider and Down framework from app to ownCloudAppShared as it is needed for a rich spaces view in the location picker - extract embedding behaviour from AppRootViewController into EmbeddingViewController - implement ClientLocationPicker and ClientLocationPickerViewController - rewrite Share Extension based on ClientLocationPicker and ComposedMessageView - upgrade AutoUploadSecttingsSection and auto photo/video uploads to use ClientLocationPicker and OCLocations - migrate or partially rewrite CopyAction, MoveAction, ImportFilesController to use ClientLocationPicker - AccountConnectionPool: add method to disconnect all connections in the pool - AccountController: add options to - change cell appearance - hide the account pill - (not) auto select the personal folder - Action: - add method to provide a UIBarButtonItem - add new location .locationPickerBar for appearance in ClientLocationPicker - WebApp: ensure apps are presented full screen on iPad, not in a popover - DriveGridCell: always show title + subtitle to align titles, truncate both at 1 line - ItemListCell: add support for appearance (ClientItemAppearance) for regular and disabled appearances - CollectionViewController: add .hideNavigationBar property that does what it says on the tin - ClientContext: expand validation system to allow passing a ClientContext to validation methods and permission handlers (allows to implement behaviour for f.ex. a specific view controller rather than all view controllers descending from it) - ClientItemViewController: add .location and .viewControllerUUID properties - Interactions for OCDrive, OCItem, OCLocation: set bookmarkUUID in locations that are used for newly created ClientItemViewControllers - ComposedMessageView: add support for .buttons - OCBookmarkManager+Management: make sure to use exact OCBookmark instance when removing a bookmark - UINavigationItem+Extension: simplify workaround for iOS 16 navigation title truncation bug - remove ClientDirectoryPickerViewController and ClientSpacesTableViewController - bump build number to 241 * - BookmarkViewController/IssuesCardViewController/OCLicenseEnvironment: adapt to OCBookmark.certificateStore change - xcscheme: add connection.associated-certificates-tracking-rule example that opts in all *.local certificates into certificate change tracking - remove ClientTabBarController - update known issues * - update SDK - update KNOWN_ISSUES.md - remove EarlGrey and everything that depends on it - AccountController: - SpecialItems -> ExtraItems to clarify features being distinct - collect "real" special items in three dictionaries rather than each in its own instance variable * - remove legacy migration * - DisplayHostViewController: update to use an OCDataSource subscription to track underlying items, not an OCQuery - AccountController: add Quick Access, including Favorites and Available Offline - OpenInWebAppAction: add keyboard shortcut (fixing finding in #1151) - OCSavedSearch+Interactions: add new .customIconName and .useNameAsTitle properties and helper methods - SavedSearchCell: add support for .customIconName and .useNameAsTitle - CollectionViewController: add .insert(sections:at:) method - ClientContext add .queryDatasource property - SortedItemDataSource: helper data source to allow sorting of items from other data sources - OCItemPolicy+Interactions: add context menu and swipe action support to remove policy - remove unused UIImageView+Thumbnails.swift, mark others for removal * - bump build number to 243 * - AccountController: - add support for showing/hiding account pill - add sharing items - add support for favorites support capability - CollectionViewController: - separate usage of Wrapped IDs from hierarchic content - trigger data source updated when sections are hidden/shown - add setNeedsSourceUpdate() for more efficient updates - add workaround for UICollectionViewDiffableDataSource requesting cells from the wrong section when moved from there to another section - CollectionViewSection: fix hideIfEmptyDataSource issue (previously did hide when not empty) - ClientSharedWithMeViewController: new view controller to present pending, accepted and declined shares - MoreViewHeader: add support for favorites support capability * - OCShare+Interactions: add swipe + popup actions - ClientSharedWithMeViewController: add title to list - update SDK * - NEW: UniversalItemListCell - provides a single cell implementation for two-line (title + detail) + thumbnail + accessories content - fine-grained encapsulation of content into UniversalItemListCell.Content objects for atomic, extensible content updates - fed through a content provider protocol (UniversalItemListCellContentProvider) that classes can adopt to asynchronously provide and update content via a completion handler - built-in accessory support for more, reveal, progress and messages - uses SegmentView for details line to allow rich/mixed content for different types - UniversalItemListCellContentProvider implementation for OCItem, replacing ItemListCell - UniversalItemListCellContentProvider implementation for OCShare - InlineMessageCenter: make method names follow a single pattern - ResourceItemIcon: add convenience method to return the best matching icon for a mime type - SegmentView: - add new alpha property for items - various fixes (preventing unwanted animations and flickering due to tinting images + text, fix a retain loop) - ThemeView: add missing call to didMoveToSuperview() * - remove ItemListCell - update KNOWN_ISSUES.md - UnshareAction: add support for unsharing accepted "local" shares - add missing localizations - AccountConnection: inject shareJailQueryCustomizer into newly retrieved OCCore - AccountController: no longer unfold Spaces by default - ClientSharedByMeViewController: view controller to present shares shared to other users and links, including reveal and copy to clipboard - fix navigation title UILabel becoming too large - extend UniversalItemListCellContentProvider for OCShare with copy to clipboard, accept/decline and reveal accessories - upgrade project to Xcode 14.2 - bump version to 245 * fastlane gym: changed workspace to project, because it no longer is using cocoapod * - refactor and modularize URL scheme open URL handling code - add handling for "owncloud://pb/" commands to allow control of OCClassSettingsFlatSourcePostBuild * - include env var launch example for post-build.allowed-settings - add error checking when clearing individual postbuild setting values * - remove duplicate "Tap to relaunch." messages * - Browser Navigation: - replaces UINavigationController and UISplitViewController - allows back/forward navigation and invoking the side bar - automatically switches between full width sidebar, side-by-side sidebar and sidebar-floating-on-top layouts depending on available width and content - AppRootViewController: switch from UISplitViewController to BrowserNavigationViewController - ClientContext: add additional property and push support for BrowserNavigationViewController - EmbeddingViewController: add additional subclassing points for customization - ClientLocationBarController: view controller displaying an OCLocation and allowing to jump to a parent location with a single tap - ClientItemViewController: add bottom location bar if .location is set, using ClientLocationBarController - SegmentView: - add support for scrolling - add support for limiting vertical usage - allow customization of "overflow" gradient color - add support for providing gesture recognizers for items - ActionTapGestureRecognizer: subclass of UITapGestureRecognizer that allows providing a closure as action to perform - UIView+EmbedAndLayout: embedHorizontally() gains limitHeight option that limits the height of the layout - CollectionViewController: fill entire height with stack view, use a helper UIView to limit UICollectionView to safe area - OCItem+Interactions: add revocations for view controllers pushed by reveal and open interactions - UniversalItemListCell: work around _UITemporaryLayoutWidths auto-layout warning for accessory views - Licensing - add OCLicenseQAProvider to allow enabling Pro Features for QA purposes - OCLicenseEMMProvider: fix provider ID - add toggle to "Advanced" settings allowing to unlock Pro Features for QA. Available only in beta builds. - bump build number to 246 * Calens changelog updated * - rename "Purchases" to "Purchases & Subscriptions" in Settings - invoke AppStore.showManageSubscriptions() to allow direct subscription management where available, fallback to https://apps.apple.com/account/subscriptions * - fix "Select All" not working in Multiselect - adapt Multiselect to BrowserNavigationViewController * - removal of legacy code * - fix issue where OC10 accounts showed up empty in the File Provider * - NavigationContent extension of UINavigationItem - adapt SearchViewController, ClientItemViewController, ClientLocationPickerViewController and BrowserNavigationViewController to use NavigationContent instead of UINavigationItem directly - fix background color and spacing for SearchViewController.scopeViewController to align with file list - update KNOWN_ISSUES.md * - bump build number to 247 * - address static analyzer findings - address SwiftLint findings * - adapt to OCLocation.parent nullability change - unify/fix navigation titles and context.drive for ClientItemViewController - add new issue to KNOWN_ISSUES.md - make OCLocation breadcrumb generation general-purpose, so it can be used not just by ClientLocationBarController but also f.ex. dropdowns - BrowserNavigationItem: add .canTrimViewController property - update SDK * Fixes for Xcode 14 fastlane builds and resigning Fastfile: - needs setting manual code signing, because of signing swift package failures Resign Script: - seems like not all provisioning profiles contains SHA-1 values but SHA-256 values. Changed that for checking. * - AppDelegate / SceneDelegate cleanup * - BrowserNavigationBookmark - new class to capture location information that can then be used to restore/create a view controller - supports arbitrary OCDataItems - used of state restoration, sidebar selection - usable for browser navigation history compacting & view controller restore (tbd) - can be attached to view controllers via new UIViewController.navigationBookmark property - BrowserNavigationBookmark+AccountController: provide ID bridge to side bar - BrowserNavigationViewController: add support for a delegate (used to sync selection in sidebar) - BrowserNavigationHistory: fix bug in .currentItem implementation - BrowserNavigationItem: add .navigationBookmark property - DataItemBrowserNavigationBookmarkReStore protocol - allows storing and restoring view controllers individually on a per-OCDataItem basis - implementation for OCItem, OCLocation and OCSavedSearch - OCDrive: support for DataItemSelectionInteraction now uses OCLocation's implementation under the hood - AppStateActions - new, extensible and universal mechanism for building complex UI scenes with dependencies - used for state saving, state restoration, scene building and more - actions for opening a connection, navigation to the personal folder or a BrowserNavigationBookmark - or revealing an OCItem - high-level conceptual overview in accompanying README.md - ClientContext: new .scene property - CollectionViewController: implemented recordSelection() and SelectionOperation, leaving it commented out for future (re)consideration - CollectionViewAction: - new .highlight and .unhighlightAll actions - add support for "select first matching item" to .select and .highlight - AccountConnectionPool: new .activeConnections property to return active connections - AccountConnection: fix busyStatus handling bug - OCSavedSearch: allow override of uuid (needed for sidebar navigation item selection/state restoration) - URL+Extensions: refactor to allow opening links via app scheme (f.ex. https://demo.owncloud.org/f/27 via owncloud://demo.owncloud.org/f/27) - DisplayHostViewController: no longer require OCQuery or OCDataSource - creating an "internal" one if needed - update CONFIGURATION.json - update KNOWN_ISSUES.md - update ios-sdk - bump build number - remove ServerListTableViewController, ServerListTableHeaderView, OpenItemUserActivity and more source files that are no longer in use * - allow more customization when generating breadcrumbs from OCLocation, make segment composition available globally - complete implementation of "Available Offline" view - provide rich view of OCItemPolicys with path and icon - allow revealing items marked available offline in their respective location - provide "no items" views for: - Available Offline - Favorites - all search-based Quick Access views (PDF Documents, Documents, Images, Videos, Audios) - unify header views using a new ComposedMessageView category - update KNOWN_ISSUES.md * - Collection Views - add support for supplementary items via CollectionViewSupplementaryItem and CollectionViewSupplementaryCellProvider - implement TitleSupplementaryCell for section titles, including pinning support - implement ViewSupplementaryCell for arbitrary views, including pinning support - allow specifying CollectionViewSupplementaryItem on a per-section level - ClientItemViewController: adopt CollectionViewSupplementaryItem with ViewSupplementaryCell to show SortBar as pinned section header - ClientSharedByMeViewController, ClientSharedWithMeViewController, Available Offline and more: adopt headers based on supplementary cells, removing the need to use wrapping data sources in many places (efficiency win!) - remove legacy search scope code from SortBar - remove legacy Push Presentation Controller code - fix miscellaneous warnings through small code changes * - new class DataSourceCondition: - allows triggering actions based on item count of datasources - allows logical combinations of several conditions - CollectionViewController: add .coverView property and support for filled, centered and top layout - CollectionViewSection: adopt DataSourceCondition to implement .hideIfEmptyDataSource - AccountControllerSpacesGridViewController: add "No spaces" message if no spaces are shared with user - ClientSharedByMeViewController and ClientSharedWithMeViewController: add respective "no items" messages - AppRootViewController: no longer scroll highlighted sidebar items into center - ComposedMessageView: theme image views with tintColor * - DisplaySettings: provide query condition(s) implementing display settings filters (new queryConditionForDisplaySettings property) - AccountSearchScope, DriveSearchScope, ContainerSearchScope: use DisplaySettings.queryConditionForDisplaySettings to limit search results to those that should be visible - CollectionViewController: fix crash bug when sections with supplementary views are hidden * - AccountController: ensure Saved Search sidebar item is only visible if the account has saved searches - CollectionViewController.WrappedItem: add readable description for improved debugging - CollectionViewSection: fix a crash bug if an item is inserted into a child data source whose parent item is hidden or not (yet) in the collection view's data source * - add support for authenticated WebFinger and retrieval of server instances via SDK update - currently does not implement a picker for retrieved instances but always uses the first one returned by the server * - bump version to 249 * - update SDK to implement enterprise#5579, including sending a Referer header when requesting the IdP configuration - bump build number to 250 * - ClientItemViewController: ensure navigationItem.title is set by .navigationTitle for proper back button labeling in UINavigationControllers - ClientLocationPicker: fix destination of view controller pushes - make sure they get pushed to the picker, not the parent context's browser controller * - ContainerSearchScope: ensure the folder within which the scope starts is not included in the results itself * - AppProvider: lang parameter fix via SDK update (ensure ISO-639-1 (uses only 2 characters) by cutting off any differentiators (f.ex. "en-GB" becomes "en")) * - update SDK to possible address finding (38) in feature/new-navigation * - ClientLocationPickerViewController: fix vertical cancelButton constraint * - OCShare+Interactions: make unshare action available via context menu and popup, use OCCore.delete() for unsharing * - DisplayExtensionContext: subclass to allow passing the ClientContext to actions invoked from the more menu in DisplayViewController - DisplayHostViewController: use DisplayExtensionContext and create a ClientContext child with a different originatingViewController - DisplayViewController: clarify naming of method invoked when the more button is pressed in the viewer * - enlarge "reveal" and "more" accessory buttons * - bump build number to 252 - tag v12alpha1 * - fixed fastlane build error - updated needed Xcode version for fastlane builds * added a class setting key to set the default bookmark name when creating a new bookmark "bookmark.default-name" * - introduce ThemeCSS and convert existing code base to use the new APIs - progressed, but still work-in-progress * - further CSS theming advances and cleanup * - update SDK to include authenticated WebFinger fix - BookmarkViewController: consistent spacing with tabs to improve code readability * - remove unused customizedColorsByPath code * - additional CSS fixes and refinements * - further CSS color and code warning fixes * - Settings now uses insetGrouped layout - Action cards now use insetGrouped layout - Spaces grid view updates depending on size class (animated!) - StaticTableRow: CSS updates and fixes, including a retain loop that existed before - fix appearance of group disclosure chevron in sidebar when item is selected * - add ImportPasteboardAction as action for empty folders * - fix action cell background appearance in dark mode * - MakeTVG: update for latest Swift version - add space.svg + space.tvg - display "Files" (for OC10) or name of space for root folder(s) and show appropriate icon * - SDK update: fix connection validation in case WebFinger lookup is not present - fix more CSS styling issues * - add "more" button to spaces in spaces grid view - fix "Access Denied" warning icon color in sidebar - limit Paste action to writeable locations - CollectionViewSection: add missing ClientContext in CollectionViewCellConfiguration * - add new SharedKeyCommands to ownCloudAppShared.framework, to implement key commands that should be available in app and extensions - move PasscodeViewController key commands over to SharedKeyCommands * - bump build number to 253 * - address #1188 via SDK update - show space names and icons if a space's root folder is made available offline or shared, in the respective views - remove separator background color in available offline view - bump build number to 254 * - AccountControllerSpacesGridViewController: fix warning - ThemeCSS: cleanup, provide documentation in README.md * - Branding+App: add missing copyright notice - add CSS support to branding - complete first revision of CSS documentation, including an example for CSS branding * - ThemeCSS README: add missing language MD annotation * - OCResourceText+ViewProvider: fix Theme registration timing to work with CSS themeing * - CSS README.md: correct title levels * - ClientLocationPickerViewController: - fix color errors for account cells - add additional separator line on top for more visual clarity - use grouped collection background color for navigation bar - ThemeCollection: minor code cleanup * - fix finding (37) in #1162 - ImageDisplayViewController: fix new warning in Xcode 14.3 * - ItemLayout: - new enum as high-level abstraction for item layout - provides layout objects and values - ClientContext: add new itemLayout property - SortBar: add new layout-toggling button, clean up delegate methods - ClientItemViewController: - add support for grid views, ItemLayout, SortBar and ClientContext advances - move footer view to a separate section to avoid layout interference, simplify the code and provide better performance - CollectionViewSection: - cleanup code - add support for changing cell layout and style in one go, without animation - UniversalItemListCell: add support for grid layout - ViewSupplementaryCell: - prepare to support different ElementKinds (f.ex. footers, etc.) - fix zIndex to make the cell float above separators as well * - SegmentViewItem: add concept of lines to allow realizing different layouts from a single array of items - OCItem+UniversalItemListCellContentProvider: tag detail segment item .lines - UniversalItemListCell: use SegmentViewItem.lines tagging to switch between single line and two lines details - SegmentView: preparations to dynamically apply a gradient mask when views exceed the boundaries * - NamingViewController: - select file names without extension when editing names - support picking a "fallback" icon - CreateDocumentAction: show fallback icon based on mimeType of created document type * - BrowserNavigationHistory: guard instance variables items and position in a thread-safe way - BrowserNavigationItem: add viewControllerIfLoaded property to allow retrieving a view controller only if it exists and without triggering its (re)creation - ClientSidebarViewController: ensure removal of /all/ history items for a view controller when it is revoked - UIViewController+NavigationRevocation: add debug logging, respect the passed revocationTriggers * - optimize grid cell margin usage * - ClientItemViewController: make SortBar.itemLayout match ClientContext.itemLayout * - Spaces grid overview: make more (…) button resist compression, fixing layout issue with long space titles that don't fit horizontally * - ClientItemViewController: - skip driveSection creation if not necessary - show quota for personal space * - add selection checkmark for multi selection in grid view * - bump build number to 255 * - ThemeCSS: implement match-cache for massive speedup * - ClientItemViewController: show "Loading…" when the contents of a folder is still being loaded * - CSS: remove Fill from .fill colors for TVG vector images - CSS README: add section for vector icon color CSS selectors * - cosmetic code changes * - UniversalItemListCell: add titleAndDetailsHeight() method to allow determining space needed for details and title - CollectionViewSection.CellLayout: add new .fillingGrid layout that dynamically adjusts cell size to achieve best size / space fit + center cells - ItemLayout: adopt CellLayout.fillingGrid - AccountControllerSpacesGridView: adopt CellLayout.fillingGrid - bump build number ot 256 * - update KNOWN_ISSUES.md --- .swiftlint.yml | 1 + .tx/config | 1 - .xcode-version | 2 +- CHANGELOG.md | 292 ++-- KNOWN_ISSUES.md | 88 +- Podfile | 17 - Podfile.lock | 16 - changelog/11.11.0_2022-09-26/1138 | 5 + changelog/11.11.0_2022-09-26/1141 | 7 + changelog/11.11.0_2022-09-26/1146 | 5 + changelog/11.11.0_2022-09-26/1156 | 5 + changelog/11.11.0_2022-09-26/5296 | 5 + doc/BUILD_CUSTOMIZATION.md | 38 + doc/CONFIGURATION.json | 754 ++++++--- doc/images/el/keyword.strings | Bin 784 -> 788 bytes doc/images/el/title.strings | Bin 894 -> 918 bytes doc/images/ko/keyword.strings | Bin 0 -> 566 bytes doc/images/ko/title.strings | Bin 0 -> 612 bytes enterprise/resign/resignOwncloudApp | 6 +- fastlane/Fastfile | 15 +- fastlane/metadata-emm/en-US/release_notes.txt | 18 +- .../en-US/release_notes.txt | 18 +- fastlane/metadata/en-US/release_notes.txt | 18 +- fastlane/screenshots/ar/keyword.strings | Bin 0 -> 820 bytes fastlane/screenshots/ar/title.strings | Bin 0 -> 820 bytes fastlane/screenshots/ca/keyword.strings | Bin 0 -> 748 bytes fastlane/screenshots/de-DE/keyword.strings | Bin 0 -> 746 bytes fastlane/screenshots/de-DE/title.strings | Bin 0 -> 870 bytes fastlane/screenshots/de/keyword.strings | Bin 0 -> 746 bytes fastlane/screenshots/de/title.strings | Bin 0 -> 870 bytes fastlane/screenshots/de_CH/keyword.strings | Bin 0 -> 746 bytes fastlane/screenshots/de_CH/title.strings | Bin 0 -> 870 bytes fastlane/screenshots/el/keyword.strings | Bin 0 -> 788 bytes fastlane/screenshots/el/title.strings | Bin 0 -> 918 bytes fastlane/screenshots/en-GB/keyword.strings | Bin 0 -> 716 bytes fastlane/screenshots/en-GB/title.strings | Bin 0 -> 802 bytes fastlane/screenshots/es/keyword.strings | Bin 0 -> 764 bytes fastlane/screenshots/es/title.strings | Bin 0 -> 850 bytes fastlane/screenshots/et_EE/title.strings | Bin 0 -> 864 bytes fastlane/screenshots/eu/keyword.strings | Bin 0 -> 754 bytes fastlane/screenshots/eu/title.strings | Bin 0 -> 876 bytes fastlane/screenshots/fr/title.strings | Bin 0 -> 890 bytes fastlane/screenshots/gl/keyword.strings | Bin 0 -> 762 bytes fastlane/screenshots/gl/title.strings | Bin 0 -> 864 bytes fastlane/screenshots/he/keyword.strings | Bin 0 -> 680 bytes fastlane/screenshots/he/title.strings | Bin 0 -> 762 bytes fastlane/screenshots/km/keyword.strings | Bin 0 -> 730 bytes fastlane/screenshots/ko/keyword.strings | Bin 0 -> 566 bytes fastlane/screenshots/ko/title.strings | Bin 0 -> 612 bytes fastlane/screenshots/lv/title.strings | Bin 0 -> 862 bytes fastlane/screenshots/pt-BR/keyword.strings | Bin 0 -> 796 bytes fastlane/screenshots/pt-BR/title.strings | Bin 0 -> 886 bytes fastlane/screenshots/ru/keyword.strings | Bin 0 -> 792 bytes fastlane/screenshots/ru/title.strings | Bin 0 -> 874 bytes fastlane/screenshots/sq/keyword.strings | Bin 0 -> 764 bytes fastlane/screenshots/sq/title.strings | Bin 0 -> 898 bytes fastlane/screenshots/th-TH/keyword.strings | Bin 0 -> 706 bytes fastlane/screenshots/th-TH/title.strings | Bin 0 -> 844 bytes fastlane/screenshots/tr/keyword.strings | Bin 0 -> 742 bytes fastlane/screenshots/tr/title.strings | Bin 0 -> 828 bytes fastlane/screenshots/ur_PK/keyword.strings | Bin 0 -> 708 bytes fastlane/screenshots/zh-Hans/keyword.strings | Bin 0 -> 556 bytes fastlane/screenshots/zh-Hans/title.strings | Bin 0 -> 592 bytes fastlane/screenshots/zh_TW/keyword.strings | Bin 0 -> 562 bytes fastlane/screenshots/zh_TW/title.strings | Bin 0 -> 576 bytes img/filetypes-tvg/space.tvg | 1 + img/filetypes/space.svg | 8 + ios-sdk | 2 +- .../CancelLabelViewController.swift | 11 +- .../DocumentActionViewController.swift | 25 +- .../FileProviderExtension.m | 26 +- .../NSError+MessageResolution.m | 7 +- .../OCItem+FileProviderItem.m | 3 +- ownCloud Share Extension/Info.plist | 4 +- ...ift => ShareExtensionViewController.swift} | 446 +++--- .../ShareNavigationController.swift | 48 - ownCloud.xcodeproj/project.pbxproj | 1408 ++++++++--------- .../xcshareddata/xcschemes/MakeTVG.xcscheme | 2 +- .../xcschemes/ownCloud File Provider.xcscheme | 3 +- .../ownCloud File ProviderUI.xcscheme | 3 +- .../xcschemes/ownCloud Intents.xcscheme | 3 +- .../ownCloud Share Extension.xcscheme | 3 +- .../xcshareddata/xcschemes/ownCloud.xcscheme | 29 +- .../xcschemes/ownCloudApp.xcscheme | 2 +- .../ownCloudScreenshotsTests.xcscheme | 2 +- .../xcschemes/ownCloudTests.xcscheme | 11 +- .../xcshareddata/swiftpm/Package.resolved | 41 - .../AccountController+ExtraItems.swift | 73 + .../AccountController+ItemActions.swift | 172 ++ .../AppRootViewController+ItemActions.swift | 54 + .../AppRootViewController.swift | 437 +++++ ownCloud/AppDelegate.swift | 184 ++- .../BookmarkInfoViewController.swift | 9 +- .../Bookmarks/BookmarkViewController.swift | 183 ++- .../Client/Actions/Action+UserInterface.swift | 10 +- .../Actions+Extensions/CopyAction.swift | 46 +- .../CreateDocumentAction.swift | 22 +- .../ImportPasteboardAction.swift | 15 +- .../Actions+Extensions/MoveAction.swift | 33 +- .../Actions+Extensions/OpenInAction.swift | 44 +- .../Actions+Extensions/OpenSceneAction.swift | 26 +- .../PresentationModeAction.swift | 6 +- .../Actions+Extensions/UnshareAction.swift | 32 +- .../UploadMediaAction.swift | 2 +- .../Actions/EditDocumentViewController.swift | 35 +- .../Actions/ImageMetadataViewController.swift | 2 +- .../Actions/Scanner/ScanViewController.swift | 18 +- .../Client/ClientActivityViewController.swift | 52 +- ...ClientRootViewController+ItemActions.swift | 90 -- .../Client/ClientRootViewController.swift | 869 ---------- ownCloud/Client/ClientSessionManager.swift | 29 +- .../Client/ExternalBrowserBusyHandler.swift | 16 +- ...yViewController+InlineMessageSupport.swift | 46 - ...ntroller+OpenItemTableViewController.swift | 72 - ...eListTableViewController+Multiselect.swift | 215 --- .../Item Policies/ItemPolicyCell.swift | 90 -- .../ItemPolicyTableViewController.swift | 224 --- .../LibrarySharesTableViewController.swift | 68 - .../Library/LibraryTableViewController.swift | 471 ------ .../PhotoAlbumTableViewController.swift | 2 +- .../Client/PhotoSelectionViewController.swift | 2 +- .../PendingSharesTableViewController.swift | 258 --- ownCloud/Client/Viewer/DisplayExtension.swift | 4 +- .../Viewer/DisplayHostViewController.swift | 99 +- .../Client/Viewer/DisplayViewController.swift | 86 +- .../DownloadItemsHUDViewController.swift | 7 +- .../Image/ImageDisplayViewController.swift | 2 +- .../Media/MediaDisplayViewController.swift | 47 +- .../QuickLook/PreviewViewController.swift | 4 +- ownCloud/Import/ImportFilesController.swift | 318 ++-- ownCloud/Key Commands/KeyCommands.swift | 55 +- .../Licensing/Offers/LicenseOfferButton.swift | 4 + .../Licensing/Offers/LicenseOfferView.swift | 9 +- .../LicenseInAppPurchaseFeatureView.swift | 1 - .../LicenseTransactionsViewController.swift | 23 +- .../Messages/CardIssueMessagePresenter.swift | 1 + ownCloud/Messages/MessageGroupCell.swift | 33 +- ownCloud/Migration/LegacyCredentials.swift | 109 -- ownCloud/Migration/Migration.swift | 601 ------- .../Migration/MigrationActivityCell.swift | 144 -- .../Migration/MigrationViewController.swift | 175 -- ownCloud/Release Notes/ReleaseNotes.plist | 74 + .../ReleaseNotesHostViewController.swift | 32 +- .../ReleaseNotesTableViewController.swift | 4 +- ownCloud/Resources/Info.plist | 5 +- .../Resources/ar.lproj/Localizable.strings | Bin 82040 -> 81084 bytes .../Resources/cs.lproj/Localizable.strings | 10 - .../Resources/de.lproj/Localizable.strings | Bin 91618 -> 96816 bytes .../Resources/en.lproj/Localizable.strings | 92 +- .../Resources/es.lproj/Localizable.strings | Bin 90022 -> 89634 bytes .../Resources/fr.lproj/Localizable.strings | Bin 93460 -> 92328 bytes .../Resources/gl.lproj/Localizable.strings | Bin 90996 -> 91100 bytes .../Resources/he.lproj/Localizable.strings | Bin 79602 -> 79706 bytes ownCloud/Resources/ko.lproj/InfoPlist.strings | Bin 2598 -> 2294 bytes .../Resources/ko.lproj/Localizable.strings | Bin 60368 -> 70726 bytes .../Resources/mk.lproj/Localizable.strings | Bin 63220 -> 62540 bytes .../Resources/nb-NO.lproj/Localizable.strings | 10 - .../Resources/nn-NO.lproj/Localizable.strings | 10 - .../Resources/pt-BR.lproj/Localizable.strings | Bin 90066 -> 89402 bytes .../Resources/pt-PT.lproj/Localizable.strings | 10 - .../Resources/ru.lproj/Localizable.strings | Bin 90686 -> 89600 bytes .../Resources/sq.lproj/Localizable.strings | Bin 89620 -> 88496 bytes .../Resources/th-TH.lproj/Localizable.strings | Bin 82020 -> 81042 bytes ownCloud/Resources/tr.lproj/InfoPlist.strings | Bin 2774 -> 2878 bytes .../Resources/tr.lproj/Localizable.strings | Bin 67484 -> 88052 bytes .../Resources/zh-Hans.lproj/InfoPlist.strings | Bin 2482 -> 1952 bytes .../zh-Hans.lproj/Localizable.strings | Bin 51020 -> 50340 bytes .../Resources/zh_TW.lproj/Localizable.strings | Bin 65414 -> 65518 bytes .../OCBookmarkManager+Management.swift | 41 + .../OCCertificate+Extension.swift | 11 +- ownCloud/SceneDelegate.swift | 138 +- .../ServerListTableHeaderView.swift | 90 -- .../ServerListTableViewController.swift | 1071 ------------- .../ServerListTableViewController.xib | 124 -- .../Settings/AutoUploadSettingsSection.swift | 289 ++-- ownCloud/Settings/DataSettingsSection.swift | 4 +- .../Settings/DisplaySettingsSection.swift | 8 + .../Settings/LogSettingsViewController.swift | 4 +- ownCloud/Settings/MediaFilesSettings.swift | 2 +- ownCloud/Settings/MoreSettingsSection.swift | 2 +- .../Settings/PurchasesSettingsSection.swift | 2 +- .../Settings/SecuritySettingsSection.swift | 6 +- .../Settings/SettingsViewController.swift | 8 + .../UserInterfaceSettingsSection.swift | 4 +- .../StaticLoginSetupViewController.swift | 132 +- ...ingleAccountServerListViewController.swift | 3 +- .../Interface/StaticLoginViewController.swift | 5 +- .../InstantMediaUploadTaskExtension.swift | 8 +- ownCloud/Tasks/ScheduledTaskManager.swift | 1 - .../ThemeCertificateViewController.swift | 51 - ownCloud/Tools/URL+Extensions.swift | 41 +- .../UI Elements/CollapsibleProgressBar.swift | 15 +- ownCloud/UI Elements/RoundedInfoView.swift | 98 -- ownCloud/UI Elements/TextViewController.swift | 3 +- .../UIAlertController+UniversalLinks.swift | 44 - .../UIKit Extensions/UIWindow+Extension.swift | 38 - .../AppLock Settings/AppLockSettings.h | 3 + .../AppLock Settings/AppLockSettings.m | 147 +- ownCloudAppFramework/Branding/Branding.h | 2 + ownCloudAppFramework/Branding/Branding.m | 29 + .../Display Settings/DisplaySettings.h | 5 +- .../Display Settings/DisplaySettings.m | 18 + .../OCFileProviderSettings.h | 33 + .../OCFileProviderSettings.m | 55 + .../NSObject+AnnotatedProperties.h | 2 + .../NSObject+AnnotatedProperties.m | 17 + .../OCCore+LicenseEnvironment.m | 2 +- .../Environment/OCLicenseEnvironment.m | 2 +- .../App Store/OCLicenseAppStoreProvider.h | 2 + .../App Store/OCLicenseAppStoreProvider.m | 7 +- .../Providers/EMM/OCLicenseEMMProvider.m | 37 +- .../Enterprise/OCLicenseEnterpriseProvider.m | 6 - .../Licensing/Providers/OCLicenseProvider.m | 4 +- .../Providers/QA/OCLicenseQAProvider.h | 44 + .../Providers/QA/OCLicenseQAProvider.m | 160 ++ .../Resources/de.lproj/Localizable.strings | 70 + .../Search/OCQueryCondition+SearchSegmenter.h | 2 +- .../Search/OCQueryCondition+SearchSegmenter.m | 4 +- .../Search/Saved Searches/OCSavedSearch.h | 3 +- .../Search/Saved Searches/OCSavedSearch.m | 5 + .../UIViewController+HostBundleID.h | 29 + .../UIViewController+HostBundleID.m | 34 + ownCloudAppFramework/ownCloudApp.h | 5 + .../AppLock/AppLockManager.swift | 89 +- .../AppLock/PasscodeSetupCoordinator.swift | 2 +- .../AppLock/PasscodeViewController.swift | 86 +- ownCloudAppShared/Branding/Branding+App.swift | 15 +- .../AccountConnection+ItemActions.swift | 58 + .../Connection/AccountConnection.swift | 596 +++++++ .../AccountConnectionConsumer.swift | 67 + .../Connection/AccountConnectionPool.swift | 102 ++ .../AccountConnectionRichStatus.swift | 71 + .../AccountAuthenticationUpdater.swift | 17 +- ...nUpdaterPasswordPromptViewController.swift | 12 +- .../AccountConnectionAuthErrorConsumer.swift | 277 ++++ .../AccountConnectionErrorHandler.swift | 118 ++ .../Controller/AccountController.swift | 684 ++++++++ .../Controller/AccountControllerSection.swift | 31 + ...ntControllerSpacesGridViewController.swift | 85 + ...NavigationBookmark+AccountController.swift | 83 + .../Account}/Messages/MessageGroup.swift | 10 +- .../Account}/Messages/MessageSelector.swift | 20 +- ownCloudAppShared/Client/Account/README.md | 23 + ownCloudAppShared/Client/Actions/Action.swift | 60 +- .../Actions/ClientItemResolvingCell.swift | 78 - .../ClientWebAppViewController.swift | 72 +- .../CreateFolderAction.swift | 23 +- .../Implementations}/OpenInWebAppAction.swift | 62 +- .../Cells/AccountControllerCell.swift | 369 +++++ .../Collection Views/Cells/ActionCell.swift | 86 +- .../Cells/DriveGridCell.swift | 84 + .../Cells/DriveHeaderCell.swift | 50 +- .../Cells/DriveListCell.swift | 107 +- .../Cells/ExpandableResourceCell.swift | 28 +- .../Collection Views/Cells/ItemListCell.swift | 647 -------- .../Cells/SavedSearchCell.swift | 132 +- .../Cells/ThemeableCollectionViewCell.swift | 6 +- .../ThemeableCollectionViewListCell.swift | 17 +- ...UniversalItemListCellContentProvider.swift | 315 ++++ ...UniversalItemListCellContentProvider.swift | 118 ++ ...UniversalItemListCellContentProvider.swift | 190 +++ .../Cells/UniversalItemListCell.swift | 711 +++++++++ .../Collection Views/Cells/ViewCell.swift | 58 +- .../CollectionSidebarAction.swift | 146 ++ .../CollectionSidebarViewController.swift | 121 ++ .../CollectionViewAction.swift | 133 ++ .../CollectionViewCellConfiguration.swift | 10 +- ...CellProvider+StandardImplementations.swift | 74 +- .../CollectionViewController.swift | 632 +++++++- .../CollectionViewSection.swift | 481 +++++- ...CellProvider+StandardImplementations.swift | 26 + ...lectionViewSupplementaryCellProvider.swift | 57 + .../CollectionViewSupplementaryItem.swift | 34 + .../TitleSupplementaryCell.swift | 109 ++ .../ViewSupplementaryCell.swift | 76 + .../Client/Context/ClientContext.swift | 121 +- .../Client/Context/SortedItemDataSource.swift | 52 + .../OCAction+Interactions.swift | 29 +- .../OCDataItem+InteractionProtocols.swift | 9 + .../OCDrive+Interactions.swift | 27 +- .../OCItem+Interactions.swift | 114 +- .../OCItemPolicy+Interactions.swift | 77 + .../OCLocation+Interactions.swift | 91 ++ .../OCSavedSearch+Interactions.swift | 114 +- .../OCShare+Interactions.swift | 132 ++ .../DataSourceCondition.swift | 155 ++ .../ClientDirectoryPickerViewController.swift | 383 ----- .../ClientQueryViewController.swift | 965 ----------- .../ClientSpacesTableViewController.swift | 85 - .../FileListTableViewController.swift | 344 ---- .../QueryFileListTableViewController.swift | 592 ------- .../NavigationRevocationAction.swift | 136 ++ .../NavigationRevocationManager.swift | 53 + .../NavigationRevocationTrigger.swift | 133 ++ .../Client/Navigation Revocation/README.md | 35 + ...IViewController+NavigationRevocation.swift | 73 + .../Resource Sources/ResourceItemIcon.swift | 5 + .../ItemSearchSuggestionsViewController.swift | 28 +- .../Scopes/AccountSearchScope.swift | 31 +- .../OCQueryCondition+SearchToken.swift | 22 +- .../Client/Search/Scopes/SearchScope.swift | 8 +- .../Client/Search/SearchViewController.swift | 73 +- .../GroupSharingEditTableViewController.swift | 6 +- .../GroupSharingTableViewController.swift | 10 +- .../PublicLinkEditTableViewController.swift | 9 +- .../PublicLinkTableViewController.swift | 31 +- .../Client/Sharing/ShareClientItemCell.swift | 50 - .../BreadCrumbTableViewController.swift | 89 -- .../User Interface/ClientItemCell.swift | 603 ------- .../User Interface/ComposedMessageView.swift | 137 +- .../Client/User Interface/GradientView.swift | 46 +- .../IssuesCardViewController.swift | 72 +- .../Client/User Interface/ItemLayout.swift | 55 + .../Client/User Interface/MessageView.swift | 14 +- .../User Interface/NamingViewController.swift | 36 +- .../PopupButtonController.swift | 10 +- .../RoundCornerBackgroundView.swift | 27 +- .../Client/User Interface}/RoundedLabel.swift | 24 +- .../SelectionCheckmarkButton.swift | 75 + .../Client/User Interface/SortBar.swift | 186 +-- .../ClientItemViewController.swift | 647 ++++++-- .../ClientSharedByMeViewController.swift | 91 ++ .../ClientSharedWithMeViewController.swift | 96 ++ .../ClientSidebarViewController.swift | 180 +++ .../ClientLocationBarController.swift | 110 ++ .../OCLocation+Breadcrumbs.swift | 167 ++ .../ClientLocationPicker.swift | 387 +++++ .../ClientLocationPickerViewController.swift | 239 +++ .../Intent/OCLicenseManager+Setup.swift | 5 +- .../OCBookmarkManager+Locking.swift | 91 ++ .../OCBookmarkManager+Management.swift | 84 + .../SDK Extensions/OCCore+Extension.swift | 16 +- .../OCIssue+DisplayIssues.swift | 14 +- .../SDK Extensions/OCItem+Extension.swift | 21 +- .../SDK Extensions/OCMessage+Extension.swift | 4 +- .../SDK Extensions/OCShare+Extension.swift | 8 + ownCloudAppShared/Tools/VendorServices.swift | 6 + ...MutableAttributedString+AppendStyled.swift | 10 +- .../UIBarButtonItem+Extension.swift | 64 - ...llectionViewDiffableDataSource+Tools.swift | 6 + .../UIKit Extension/UIColor+Extension.swift | 40 + .../UIImageView+Thumbnails.swift | 62 - .../UINavigationItem+Extension.swift | 49 + .../UITableViewController+Extension.swift | 90 -- .../UIViewController+Extension.swift | 30 +- .../Alert View}/AlertView.swift | 96 +- .../Alert View}/AlertViewController.swift | 32 +- .../BrowserNavigationBookmark.swift | 156 ++ .../BrowserNavigationHistory.swift | 193 +++ .../BrowserNavigationItem.swift | 75 + .../BrowserNavigationViewController.swift | 521 ++++++ .../UIViewController+BrowserNavigation.swift | 32 + .../CardPresentationController.swift | 7 +- .../CardViewController.swift | 2 +- .../EmbeddingViewController.swift | 71 + .../ActionTapGestureRecognizer.swift | 36 + .../More/FrameViewController.swift | 4 +- .../More/MoreStaticTableViewController.swift | 11 +- .../User Interface/More/MoreViewHeader.swift | 44 +- .../NavigationContent.swift | 183 +++ .../NavigationContentItem.swift | 51 + .../Navigation Content/README.md | 9 + .../UINavigationItem+NavigationContent.swift | 54 + .../Progress/ProgressHUDViewController.swift | 2 +- .../ProgressIndicatorViewController.swift | 11 +- .../Progress/ProgressView.swift | 10 +- .../PushPresentationController.swift | 45 - .../PushTransition.swift | 102 -- .../PushTransitionDelegate.swift | 40 - .../SegmentView/SegmentView.swift | 329 ++++ .../SegmentView/SegmentViewItem.swift | 109 ++ .../SegmentView/SegmentViewItemView.swift | 123 ++ .../SegmentView/UIView+EmbedAndLayout.swift | 235 +++ .../User Interface/SharedKeyCommands.swift | 73 + .../Actions/AppStateActionConnect.swift | 70 + .../AppStateActionGoToPersonalFolder.swift | 70 + ...StateActionRestoreNavigationBookmark.swift | 68 + .../Actions/AppStateActionRevealItem.swift | 59 + .../State Restoration/AppStateAction.swift | 148 ++ .../NSUserActivity+SaveRestore.swift | 79 + .../State Restoration/README.md | 25 + .../StaticTableViewController.swift | 59 +- .../StaticTableView/StaticTableViewRow.swift | 215 +-- .../StaticTableViewSection.swift | 2 +- .../Theme/CSS/NSObject+ThemeCSS.swift | 165 ++ .../User Interface/Theme/CSS/README.md | 240 +++ .../Theme/CSS/ThemeCSS+AutoSelectors.swift | 121 ++ .../User Interface/Theme/CSS/ThemeCSS.swift | 415 +++++ .../Theme/CSS/ThemeCSSRecord.swift | 87 + .../Theme/CSS/UIView+ThemeCSS.swift | 72 + .../Theme/CSS/Views/ThemeCSSButton.swift | 41 + .../Theme/CSS/Views/ThemeCSSLabel.swift | 41 + .../CSS/Views/ThemeCSSProgressView.swift | 37 + .../Theme/CSS/Views/ThemeCSSTextField.swift | 44 + .../Theme/CSS/Views/ThemeCSSView.swift | 64 + .../Theme/NSObject+ThemeApplication.swift | 264 ++-- .../User Interface/Theme/TVG/TVGImage.swift | 4 + .../Theme/TVG/VectorImageView.swift | 9 + .../User Interface/Theme/Theme.swift | 17 +- .../Theme/ThemeCollection.swift | 1027 +++++++----- .../Theme/ThemeStyle+DefaultStyles.swift | 4 - .../Theme/ThemeStyle+Extensions.swift | 20 +- .../User Interface/Theme/ThemeStyle.swift | 61 +- .../User Interface/Theme/UI/ThemeButton.swift | 54 +- .../UI/ThemeCertificateViewController.swift | 54 + .../Theme/UI/ThemeNavigationController.swift | 50 +- .../Theme/UI/ThemeTableViewCell.swift | 36 +- .../User Interface/Theme/UI/ThemeView.swift | 43 +- .../User Interface/Theme/UI/ThemeWindow.swift | 1 - .../Theme/UI/ThemeableColoredView.swift | 51 - .../Theme/UI/ThemedAlertController.swift | 9 +- .../OpenItemUserActivity.swift | 54 - .../OCResourceText+ViewProvider.swift | 33 +- ownCloudScreenshotsTests/Info.plist | 22 - ownCloudScreenshotsTests/SnapshotHelper.swift | 309 ---- .../ownCloudScreenshotsTests.swift | 355 ----- ownCloudTests/EarlGrey.swift | 188 --- .../File List/CreateFolderTests.swift | 256 --- ownCloudTests/File List/FileListTests.swift | 70 - ownCloudTests/File List/FileTests.swift | 108 -- .../MockClientRootViewController.swift | 38 - .../File List/Subclasses/MockOCCore.swift | 48 - .../File List/Subclasses/MockOCQuery.swift | 22 - ownCloudTests/Login/CreateBookmarkTests.swift | 640 -------- ownCloudTests/Login/DeleteBookmarkTests.swift | 106 -- ownCloudTests/Login/EditBookmarkTests.swift | 311 ---- ownCloudTests/Resources/PropfindResponse.xml | 1 - .../Resources/PropfindResponseNewFolder.xml | 1 - ownCloudTests/Resources/test_certificate.cer | Bin 1555 -> 0 bytes ownCloudTests/Security/BiometricalTests.swift | 102 -- ownCloudTests/Security/PasscodeTests.swift | 265 ---- ownCloudTests/SettingsTests.swift | 192 --- ownCloudTests/Tools/EarlGrey+Tools.swift | 48 - .../Tools/OCBookmarkManager+Tools.swift | 57 - .../Tools/OCMockingManager+SwiftTools.swift | 18 - ownCloudTests/Tools/OwnCloudTests.swift | 40 - ownCloudTests/Tools/UtilsTests.swift | 155 -- .../LocaleDiff.xcodeproj/project.pbxproj | 289 ++++ .../xcschemes/LocaleDiff.xcscheme | 112 ++ tools/LocaleDiff/LocaleDiff/main.swift | 61 + tools/MakeTVG/main.swift | 2 +- 441 files changed, 20914 insertions(+), 17158 deletions(-) delete mode 100644 Podfile delete mode 100644 Podfile.lock create mode 100644 changelog/11.11.0_2022-09-26/1138 create mode 100644 changelog/11.11.0_2022-09-26/1141 create mode 100644 changelog/11.11.0_2022-09-26/1146 create mode 100644 changelog/11.11.0_2022-09-26/1156 create mode 100644 changelog/11.11.0_2022-09-26/5296 create mode 100644 doc/BUILD_CUSTOMIZATION.md create mode 100644 doc/images/ko/keyword.strings create mode 100644 doc/images/ko/title.strings create mode 100644 fastlane/screenshots/ar/keyword.strings create mode 100644 fastlane/screenshots/ar/title.strings create mode 100644 fastlane/screenshots/ca/keyword.strings create mode 100644 fastlane/screenshots/de-DE/keyword.strings create mode 100644 fastlane/screenshots/de-DE/title.strings create mode 100644 fastlane/screenshots/de/keyword.strings create mode 100644 fastlane/screenshots/de/title.strings create mode 100644 fastlane/screenshots/de_CH/keyword.strings create mode 100644 fastlane/screenshots/de_CH/title.strings create mode 100644 fastlane/screenshots/el/keyword.strings create mode 100644 fastlane/screenshots/el/title.strings create mode 100644 fastlane/screenshots/en-GB/keyword.strings create mode 100644 fastlane/screenshots/en-GB/title.strings create mode 100644 fastlane/screenshots/es/keyword.strings create mode 100644 fastlane/screenshots/es/title.strings create mode 100644 fastlane/screenshots/et_EE/title.strings create mode 100644 fastlane/screenshots/eu/keyword.strings create mode 100644 fastlane/screenshots/eu/title.strings create mode 100644 fastlane/screenshots/fr/title.strings create mode 100644 fastlane/screenshots/gl/keyword.strings create mode 100644 fastlane/screenshots/gl/title.strings create mode 100644 fastlane/screenshots/he/keyword.strings create mode 100644 fastlane/screenshots/he/title.strings create mode 100644 fastlane/screenshots/km/keyword.strings create mode 100644 fastlane/screenshots/ko/keyword.strings create mode 100644 fastlane/screenshots/ko/title.strings create mode 100644 fastlane/screenshots/lv/title.strings create mode 100644 fastlane/screenshots/pt-BR/keyword.strings create mode 100644 fastlane/screenshots/pt-BR/title.strings create mode 100644 fastlane/screenshots/ru/keyword.strings create mode 100644 fastlane/screenshots/ru/title.strings create mode 100644 fastlane/screenshots/sq/keyword.strings create mode 100644 fastlane/screenshots/sq/title.strings create mode 100644 fastlane/screenshots/th-TH/keyword.strings create mode 100644 fastlane/screenshots/th-TH/title.strings create mode 100644 fastlane/screenshots/tr/keyword.strings create mode 100644 fastlane/screenshots/tr/title.strings create mode 100644 fastlane/screenshots/ur_PK/keyword.strings create mode 100644 fastlane/screenshots/zh-Hans/keyword.strings create mode 100644 fastlane/screenshots/zh-Hans/title.strings create mode 100644 fastlane/screenshots/zh_TW/keyword.strings create mode 100644 fastlane/screenshots/zh_TW/title.strings create mode 100644 img/filetypes-tvg/space.tvg create mode 100644 img/filetypes/space.svg rename ownCloud Share Extension/{ShareViewController.swift => ShareExtensionViewController.swift} (52%) delete mode 100644 ownCloud Share Extension/ShareNavigationController.swift delete mode 100644 ownCloud.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ownCloud/App Controllers/AccountController+ExtraItems.swift create mode 100644 ownCloud/App Controllers/AccountController+ItemActions.swift create mode 100644 ownCloud/App Controllers/AppRootViewController+ItemActions.swift create mode 100644 ownCloud/App Controllers/AppRootViewController.swift delete mode 100644 ownCloud/Client/ClientRootViewController+ItemActions.swift delete mode 100644 ownCloud/Client/ClientRootViewController.swift delete mode 100644 ownCloud/Client/FileList Extensions/ClientQueryViewController+InlineMessageSupport.swift delete mode 100644 ownCloud/Client/FileList Extensions/FileListTableViewController+OpenItemTableViewController.swift delete mode 100644 ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift delete mode 100644 ownCloud/Client/Library/Item Policies/ItemPolicyCell.swift delete mode 100644 ownCloud/Client/Library/Item Policies/ItemPolicyTableViewController.swift delete mode 100644 ownCloud/Client/Library/LibrarySharesTableViewController.swift delete mode 100644 ownCloud/Client/Library/LibraryTableViewController.swift delete mode 100644 ownCloud/Client/Sharing/PendingSharesTableViewController.swift delete mode 100644 ownCloud/Migration/LegacyCredentials.swift delete mode 100644 ownCloud/Migration/Migration.swift delete mode 100644 ownCloud/Migration/MigrationActivityCell.swift delete mode 100644 ownCloud/Migration/MigrationViewController.swift create mode 100644 ownCloud/SDK Extensions/OCBookmarkManager+Management.swift delete mode 100644 ownCloud/Server List/ServerListTableHeaderView.swift delete mode 100644 ownCloud/Server List/ServerListTableViewController.swift delete mode 100644 ownCloud/Server List/ServerListTableViewController.xib delete mode 100644 ownCloud/Theming/ThemeCertificateViewController.swift delete mode 100644 ownCloud/UI Elements/RoundedInfoView.swift delete mode 100644 ownCloud/UIKit Extensions/UIAlertController+UniversalLinks.swift delete mode 100644 ownCloud/UIKit Extensions/UIWindow+Extension.swift create mode 100644 ownCloudAppFramework/File Provider Services/OCFileProviderSettings.h create mode 100644 ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m create mode 100644 ownCloudAppFramework/Licensing/Providers/QA/OCLicenseQAProvider.h create mode 100644 ownCloudAppFramework/Licensing/Providers/QA/OCLicenseQAProvider.m create mode 100644 ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h create mode 100644 ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m create mode 100644 ownCloudAppShared/Client/Account/Connection/AccountConnection+ItemActions.swift create mode 100644 ownCloudAppShared/Client/Account/Connection/AccountConnection.swift create mode 100644 ownCloudAppShared/Client/Account/Connection/AccountConnectionConsumer.swift create mode 100644 ownCloudAppShared/Client/Account/Connection/AccountConnectionPool.swift create mode 100644 ownCloudAppShared/Client/Account/Connection/AccountConnectionRichStatus.swift rename ownCloud/Client/ClientAuthenticationUpdater.swift => ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdater.swift (85%) rename ownCloud/Client/ClientAuthenticationUpdaterViewController.swift => ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountAuthenticationUpdaterPasswordPromptViewController.swift (90%) create mode 100644 ownCloudAppShared/Client/Account/Connection/Authentication Error Handling/AccountConnectionAuthErrorConsumer.swift create mode 100644 ownCloudAppShared/Client/Account/Controller/AccountConnectionErrorHandler.swift create mode 100644 ownCloudAppShared/Client/Account/Controller/AccountController.swift create mode 100644 ownCloudAppShared/Client/Account/Controller/AccountControllerSection.swift create mode 100644 ownCloudAppShared/Client/Account/Controller/AccountControllerSpacesGridViewController.swift create mode 100644 ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift rename {ownCloud => ownCloudAppShared/Client/Account}/Messages/MessageGroup.swift (82%) rename {ownCloud => ownCloudAppShared/Client/Account}/Messages/MessageSelector.swift (81%) create mode 100644 ownCloudAppShared/Client/Account/README.md delete mode 100644 ownCloudAppShared/Client/Actions/ClientItemResolvingCell.swift rename {ownCloud/Client/Actions/Actions+Extensions => ownCloudAppShared/Client/Actions/Implementations}/ClientWebAppViewController.swift (56%) rename ownCloudAppShared/Client/Actions/{ => Implementations}/CreateFolderAction.swift (84%) rename {ownCloud/Client/Actions/Actions+Extensions => ownCloudAppShared/Client/Actions/Implementations}/OpenInWebAppAction.swift (74%) create mode 100644 ownCloudAppShared/Client/Collection Views/Cells/AccountControllerCell.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift delete mode 100644 ownCloudAppShared/Client/Collection Views/Cells/ItemListCell.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItemPolicy+UniversalItemListCellContentProvider.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCShare+UniversalItemListCellContentProvider.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift create mode 100644 ownCloudAppShared/Client/Collection Views/CollectionSidebarAction.swift create mode 100644 ownCloudAppShared/Client/Collection Views/CollectionSidebarViewController.swift create mode 100644 ownCloudAppShared/Client/Collection Views/CollectionViewAction.swift create mode 100644 ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider+StandardImplementations.swift create mode 100644 ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryCellProvider.swift create mode 100644 ownCloudAppShared/Client/Collection Views/CollectionViewSupplementaryItem.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Supplementary Cells/TitleSupplementaryCell.swift create mode 100644 ownCloudAppShared/Client/Collection Views/Supplementary Cells/ViewSupplementaryCell.swift create mode 100644 ownCloudAppShared/Client/Context/SortedItemDataSource.swift create mode 100644 ownCloudAppShared/Client/Data Item Interactions/OCItemPolicy+Interactions.swift create mode 100644 ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift create mode 100644 ownCloudAppShared/Client/Data Item Interactions/OCShare+Interactions.swift create mode 100644 ownCloudAppShared/Client/Data Source Conditions/DataSourceCondition.swift delete mode 100644 ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift delete mode 100644 ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift delete mode 100644 ownCloudAppShared/Client/File Lists/ClientSpacesTableViewController.swift delete mode 100644 ownCloudAppShared/Client/File Lists/FileListTableViewController.swift delete mode 100644 ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift create mode 100644 ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationAction.swift create mode 100644 ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationManager.swift create mode 100644 ownCloudAppShared/Client/Navigation Revocation/NavigationRevocationTrigger.swift create mode 100644 ownCloudAppShared/Client/Navigation Revocation/README.md create mode 100644 ownCloudAppShared/Client/Navigation Revocation/UIViewController+NavigationRevocation.swift delete mode 100644 ownCloudAppShared/Client/Sharing/ShareClientItemCell.swift delete mode 100644 ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift delete mode 100644 ownCloudAppShared/Client/User Interface/ClientItemCell.swift rename {ownCloud/Issues => ownCloudAppShared/Client/User Interface}/IssuesCardViewController.swift (80%) create mode 100644 ownCloudAppShared/Client/User Interface/ItemLayout.swift rename {ownCloud/UI Elements => ownCloudAppShared/Client/User Interface}/RoundedLabel.swift (82%) create mode 100644 ownCloudAppShared/Client/User Interface/SelectionCheckmarkButton.swift rename ownCloudAppShared/Client/{Collection Views => }/View Controllers/ClientItemViewController.swift (54%) create mode 100644 ownCloudAppShared/Client/View Controllers/ClientSharedByMeViewController.swift create mode 100644 ownCloudAppShared/Client/View Controllers/ClientSharedWithMeViewController.swift create mode 100644 ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift create mode 100644 ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationBarController.swift create mode 100644 ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift create mode 100644 ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift create mode 100644 ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPickerViewController.swift create mode 100644 ownCloudAppShared/SDK Extensions/OCBookmarkManager+Locking.swift create mode 100644 ownCloudAppShared/SDK Extensions/OCBookmarkManager+Management.swift rename ownCloud/SDK Extensions/OCIssue+Extension.swift => ownCloudAppShared/SDK Extensions/OCIssue+DisplayIssues.swift (73%) rename {ownCloud => ownCloudAppShared}/SDK Extensions/OCMessage+Extension.swift (93%) delete mode 100644 ownCloudAppShared/UIKit Extension/UIBarButtonItem+Extension.swift delete mode 100644 ownCloudAppShared/UIKit Extension/UIImageView+Thumbnails.swift create mode 100644 ownCloudAppShared/UIKit Extension/UINavigationItem+Extension.swift delete mode 100644 ownCloudAppShared/UIKit Extension/UITableViewController+Extension.swift rename {ownCloud/Messages => ownCloudAppShared/User Interface/Alert View}/AlertView.swift (78%) rename {ownCloud/Messages => ownCloudAppShared/User Interface/Alert View}/AlertViewController.swift (65%) create mode 100644 ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift create mode 100644 ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationHistory.swift create mode 100644 ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationItem.swift create mode 100644 ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift create mode 100644 ownCloudAppShared/User Interface/Browser Navigation Controller/UIViewController+BrowserNavigation.swift create mode 100644 ownCloudAppShared/User Interface/EmbeddingViewController/EmbeddingViewController.swift create mode 100644 ownCloudAppShared/User Interface/Gesture Recognizer/ActionTapGestureRecognizer.swift create mode 100644 ownCloudAppShared/User Interface/Navigation Content/NavigationContent.swift create mode 100644 ownCloudAppShared/User Interface/Navigation Content/NavigationContentItem.swift create mode 100644 ownCloudAppShared/User Interface/Navigation Content/README.md create mode 100644 ownCloudAppShared/User Interface/Navigation Content/UINavigationItem+NavigationContent.swift delete mode 100644 ownCloudAppShared/User Interface/Push Presentation Controller/PushPresentationController.swift delete mode 100644 ownCloudAppShared/User Interface/Push Presentation Controller/PushTransition.swift delete mode 100644 ownCloudAppShared/User Interface/Push Presentation Controller/PushTransitionDelegate.swift create mode 100644 ownCloudAppShared/User Interface/SegmentView/SegmentView.swift create mode 100644 ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift create mode 100644 ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift create mode 100644 ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift create mode 100644 ownCloudAppShared/User Interface/SharedKeyCommands.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionConnect.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionGoToPersonalFolder.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRestoreNavigationBookmark.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/Actions/AppStateActionRevealItem.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/AppStateAction.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/NSUserActivity+SaveRestore.swift create mode 100644 ownCloudAppShared/User Interface/State Restoration/README.md create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/NSObject+ThemeCSS.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/README.md create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS+AutoSelectors.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/ThemeCSSRecord.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/UIView+ThemeCSS.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSButton.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSLabel.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSProgressView.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSTextField.swift create mode 100644 ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSView.swift create mode 100644 ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift delete mode 100644 ownCloudAppShared/User Interface/Theme/UI/ThemeableColoredView.swift delete mode 100644 ownCloudAppShared/User Interface/User Activities/OpenItemUserActivity.swift rename {ownCloud => ownCloudAppShared/User Interface}/View Providers/OCResourceText+ViewProvider.swift (78%) delete mode 100644 ownCloudScreenshotsTests/Info.plist delete mode 100644 ownCloudScreenshotsTests/SnapshotHelper.swift delete mode 100644 ownCloudScreenshotsTests/ownCloudScreenshotsTests.swift delete mode 100644 ownCloudTests/EarlGrey.swift delete mode 100644 ownCloudTests/File List/CreateFolderTests.swift delete mode 100644 ownCloudTests/File List/FileListTests.swift delete mode 100644 ownCloudTests/File List/FileTests.swift delete mode 100644 ownCloudTests/File List/Subclasses/MockClientRootViewController.swift delete mode 100644 ownCloudTests/File List/Subclasses/MockOCCore.swift delete mode 100644 ownCloudTests/File List/Subclasses/MockOCQuery.swift delete mode 100644 ownCloudTests/Login/CreateBookmarkTests.swift delete mode 100644 ownCloudTests/Login/DeleteBookmarkTests.swift delete mode 100644 ownCloudTests/Login/EditBookmarkTests.swift delete mode 100644 ownCloudTests/Resources/PropfindResponse.xml delete mode 100644 ownCloudTests/Resources/PropfindResponseNewFolder.xml delete mode 100644 ownCloudTests/Resources/test_certificate.cer delete mode 100644 ownCloudTests/Security/BiometricalTests.swift delete mode 100644 ownCloudTests/Security/PasscodeTests.swift delete mode 100644 ownCloudTests/SettingsTests.swift delete mode 100644 ownCloudTests/Tools/EarlGrey+Tools.swift delete mode 100644 ownCloudTests/Tools/OCBookmarkManager+Tools.swift delete mode 100644 ownCloudTests/Tools/OCMockingManager+SwiftTools.swift delete mode 100644 ownCloudTests/Tools/OwnCloudTests.swift delete mode 100644 ownCloudTests/Tools/UtilsTests.swift create mode 100644 tools/LocaleDiff/LocaleDiff.xcodeproj/project.pbxproj create mode 100644 tools/LocaleDiff/LocaleDiff.xcodeproj/xcshareddata/xcschemes/LocaleDiff.xcscheme create mode 100644 tools/LocaleDiff/LocaleDiff/main.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 8c6bdea78..3f98a1741 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -37,6 +37,7 @@ disabled_rules: - comment_spacing - unused_closure_parameter - nesting + - comma custom_rules: empty_line_after_guard_statement: included: ".*\\.swift" 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..6b5bab067 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -13.4.1 +14.2 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..d58fc9cb3 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,4 +1,4 @@ -# Known issues in version 12.0 alpha 2 +# Known issues ## WARNING @@ -7,44 +7,88 @@ It should only be used with dedicated test servers, test data - and test devices ## App - 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 +- [x] 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 +113,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..d76827a9c 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,258 @@ "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.classic.", + "value" : "com.owncloud.classic" + }, + { + "description" : "Extension with the identifier com.owncloud.dark.", + "value" : "com.owncloud.dark" + }, + { + "description" : "Extension with the identifier com.owncloud.dark.black.", + "value" : "com.owncloud.dark.black" + }, + { + "description" : "Extension with the identifier com.owncloud.light.", + "value" : "com.owncloud.light" + }, + { + "description" : "Extension with the identifier com.owncloud.web.dark.", + "value" : "com.owncloud.web.dark" + }, + { + "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 +1768,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 +1795,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 +1858,7 @@ "key" : "vacuum-sync-anchor-ttl", "label" : "item-policy.vacuum-sync-anchor-ttl", "status" : "debugOnly", - "type" : "bool" + "type" : "int" }, { "autoExpansion" : "none", @@ -1765,6 +2133,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 +2241,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 +2276,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 +2318,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", @@ -1923,229 +2379,5 @@ "label" : "user-settings.disallow", "status" : "advanced", "type" : "stringArray" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Indicates if the user can change the server URL for the account.", - "flatIdentifier" : "branding.profile-allow-url-configuration", - "key" : "profile-allow-url-configuration", - "label" : "Allow URL configuration", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "bool" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "The identifiers of the authentication methods allowed for this profile. Allows to f.ex. force OAuth2, or to use Basic Auth even if OAuth2 is available.", - "flatIdentifier" : "branding.profile-allowed-authentication-methods", - "key" : "profile-allowed-authentication-methods", - "label" : "Allowed authentication methods", - "possibleValues" : [ - { - "description" : "Basic Auth", - "value" : "com.owncloud.basicauth" - }, - { - "description" : "OAuth2", - "value" : "com.owncloud.oauth2" - }, - { - "description" : "OpenID Connect", - "value" : "com.owncloud.openid-connect" - } - ], - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "stringArray" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Domain names (can also include subdomain name), which are allowed as server url when adding a new account.", - "flatIdentifier" : "branding.profile-allowed-hosts", - "key" : "profile-allowed-hosts", - "label" : "Allowed Hosts", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "stringArray" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "The name that should be used for the bookmark that's generated from this profile and appears in the account list.", - "flatIdentifier" : "branding.profile-bookmark-name", - "key" : "profile-bookmark-name", - "label" : "Bookmark Name", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Text used for the onboarding button title", - "flatIdentifier" : "branding.profile-help-button-label", - "key" : "profile-help-button-label", - "label" : "Onboarding button title", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Optional URL to onboarding resources.", - "flatIdentifier" : "branding.profile-help-url", - "key" : "profile-help-url", - "label" : "Onboarding URL", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "urlString" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Identifier uniquely identifying the profile.", - "flatIdentifier" : "branding.profile-identifier", - "key" : "profile-identifier", - "label" : "Identifier", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Name of the profile during setup.", - "flatIdentifier" : "branding.profile-name", - "key" : "profile-name", - "label" : "Name", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Message shown in an alert before opening the onboarding URL.", - "flatIdentifier" : "branding.profile-open-help-message", - "key" : "profile-open-help-message", - "label" : "Open onboarding URL message", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Text that is shown when asking the user to enter their password.", - "flatIdentifier" : "branding.profile-password-auth-prompt", - "key" : "profile-password-auth-prompt", - "label" : "Password prompt", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Text that is shown to the user before opening the authentication web view (f.ex. for OAuth2, OIDC).", - "flatIdentifier" : "branding.profile-token-auth-prompt", - "key" : "profile-token-auth-prompt", - "label" : "Token authentication prompt", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "The URL of the server targeted by this profile.", - "flatIdentifier" : "branding.profile-url", - "key" : "profile-url", - "label" : "URL", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "urlString" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Text shown above the URL field when setting up an account.", - "flatIdentifier" : "branding.profile-url-prompt", - "key" : "profile-url-prompt", - "label" : "URL prompt", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" - }, - { - "autoExpansion" : "none", - "category" : "Branding", - "categoryTag" : "branding", - "classIdentifier" : "branding", - "className" : "Branding", - "description" : "Welcome message shown during account setup.", - "flatIdentifier" : "branding.profile-welcome-message", - "key" : "profile-welcome-message", - "label" : "Welcome Message", - "status" : "advanced", - "subCategory" : "Profile", - "subCategoryTag" : "profile", - "type" : "string" } ] \ No newline at end of file diff --git a/doc/images/el/keyword.strings b/doc/images/el/keyword.strings index f60e7bc496e2bfd9f306aede6cfe2b90161cbdfa..8a3dbb69f591316a4e3affd2f0a31a21c3a6e8e2 100644 GIT binary patch delta 51 ycmbQhHid122BX?U=7Y?~nYS{pXWq!Hz_16%0+BnRyp7C9fb2EQo4pv1F#-UYSr6a< delta 47 xcmbQjHi2z}2BVT2LncEGLn=ctg93viLkXBvU~mVDB{JkOBm-qO`!OD41OVlT35Wmy diff --git a/doc/images/el/title.strings b/doc/images/el/title.strings index 57713faf85c82216e860ed636a436b6533d5a6c8..2f4549f360e45dfbebeff4edd1d84b87abe162d7 100644 GIT binary patch delta 82 zcmeyzHjRCPoBIOhgUtJx4>RvzUdOzPc{{TL!x13=D3IR5ypwq&P;5W*9w6&5^AVs5 e1%|^wu|q&QpP`%~kHMKC2goXANZDA*$qWF4{TY1# delta 58 zcmbQn{*P^fo4zAM3J@wVL^9+vlrj_nS@{g*40#OB3^`yCkX$fBDo~~jL~iWmWCj3- C0u0Ll diff --git a/doc/images/ko/keyword.strings b/doc/images/ko/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..aa725dfd0bbba09b14a979f9b1dafdbd4dbe9afa GIT binary patch literal 566 zcmezWPl>^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$?f*P_uo*u{&EFjwvvFIJ#kt5`#4Z7f{>~2=SSm1GKdmXbUzopKKC5slae# zjpzvl28Ok3cEZdwLUC;x(1aYIdq6J7XByAtn&S!#GHa$BLzsi?He_=^Ay5Jik36us znEqXHP2)J@i3*^J5OY!8XN2TuWOEA`ihwRH1Dc%5P!3d601U}AtbPw*aA9x*hU|yE zTaLoqYl7nMLSX!40^OMn)DMXC+?qY4Zv2SranOaK7nsCjV! literal 0 HcmV?d00001 diff --git a/doc/images/ko/title.strings b/doc/images/ko/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..c332ce3ebe4108ab91395c5b780ed34d9808a1fd GIT binary patch literal 612 zcmaKpy-LGS9L3MjNd$3l>u|{vq(xkcyAR+f6wy|SX&OzmO9uxZp%3A3D?$r?kf8F28f~`~UuEC_zUR4YXkMd59yf7A|1IiFewl!HD;o ze5>M^_a;wh5ZL9V!3jF|ca!&lKyh^K3Upr~HB_+1ZzZl(bc4w@la_IBo<7KYmRo)> z7TEN^dnvV?>s%+pU@O&s74P6p;8O%s6AwKtkK# zzenMIU*Jm$Z_X`e3}m?$&e{5s>f-Q$!}7JBkAVf_JV CF_4}B literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..57ed3deb49160447b765fdbc876a61285464ef8b GIT binary patch literal 820 zcmbu7?Fzym7=_R8t|E2?O`<>1wT!4OM$WCYcK6j$8BO-VEMz*wL`hF!yu|b9s zH8j^FP8>BJP%v(%MGooqD*k0SbFO$I!5RmC|J<<0%IVYD3M8x3tY(}Yuk}D>WyDI7VN}7LZHgTKzkR7k|#9ctt^k`hh3YQ7ea)l*)0C=jCYybcN literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/ar/title.strings b/fastlane/screenshots/ar/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..57ed3deb49160447b765fdbc876a61285464ef8b GIT binary patch literal 820 zcmbu7?Fzym7=_R8t|E2?O`<>1wT!4OM$WCYcK6j$8BO-VEMz*wL`hF!yu|b9s zH8j^FP8>BJP%v(%MGooqD*k0SbFO$I!5RmC|J<<0%IVYD3M8x3tY(}Yuk}D>WyDI7VN}7LZHgTKzkR7k|#9ctt^k`hh3YQ7ea)l*)0C=jCYybcN literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/ca/keyword.strings b/fastlane/screenshots/ca/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..8d607293537f835a4c0853eae2c1310c92688d6b GIT binary patch literal 748 zcmaKqy-veG5QL}ZDJ(a%BtkTZA_@vE6|EI)C$jJ#wu5ShJ9n;sEznl@1)qvnJGd;5w zYKgC@H^hkCTV3#7F#h9A(Nlvp$Myh^otfg?u|@3Ph*rd{3-745B+GJp*5#zr9aX72 z4_MFht-yWf{NRxL&di^Vr(}(E$$82cxW*lhxy<2Sy*d8Z&!NMwBldQpA(r8V%or8T zh;un|b<9~(ALEf#wB(eq?q<24$T1fym>+eo%?bT_;@&I$nuZrpx;JA^&-b$;FVv^= ruhIwX$>=TIOmJ%yHqM`1`!jgmd$a!bsHhx&qQR@u-Dh};cX#p&=$wTZ literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/de-DE/keyword.strings b/fastlane/screenshots/de-DE/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..fe237c7fe214e55863a3e2851aa7ea7b549204ae GIT binary patch literal 746 zcmaKq%}#?r6ot>)r)YQrON<-i!k@ZnOiWDNHnoM8bP80kBz<`GyThOmNkbUO<=*qn zIn4K0q^T~oP^Ll^&!tveg+7(5_LWA7oUiS9TWHO?rz6rtx7?AQnXY*&)G+lxx6QAq z9O?bgJKu9Zm(zWt3)U&uIjUo(LUo*%Xp63CCs5|_Y_|z($82!oLak%Q1E(nj$Bel? zM|(ucw6mbU#9Io-DtV~gdxe#Dzy9KZzdp+nu9>wx8~cBrGxTlZ6qrVz=x-lws+~jo zQ=1+)?&aF&mOibc*KFu1A*ZAK7g$da-$JkD++}z*$nZcG8Gd%?v?s^%9C!6f-_|GQ sNbAJp2Hnhi?bNRa>J`3gpOrZrSG~hW$zEqDS)P`a>9?uaiDY0C@&Y30LU7@xRGUjNO>3JP==1s*qJIA*!Kv0VWF~Xw z%=s^KzCIHTb*Pc1nrp>6)-z+SwJOyi(kN9KVl)0mnlR7ENHowHU!p60C?{I6yQfmh zdaXjsE;d)g**R5QcM`HvPTx3RvMab#X~S8fQynl58T&Yu+*rYH7n?`wHV z?$#0pF}HzkU}T-<-eVta`H@k_x9rV6)C+K2$Lj6FILzKxL}oRh_JbR1 zj&c1aHI>ZOJpR7GZ*;$&eKzJKFtE=pKdXt=9z{;as0;3TY-ss*PX{y(bmj s`9x$%j;j`A_P8foxGS=1hTkmz2haW8DziH=O54p1j7w@*b>888134<29RL6T literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/de/keyword.strings b/fastlane/screenshots/de/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..fe237c7fe214e55863a3e2851aa7ea7b549204ae GIT binary patch literal 746 zcmaKq%}#?r6ot>)r)YQrON<-i!k@ZnOiWDNHnoM8bP80kBz<`GyThOmNkbUO<=*qn zIn4K0q^T~oP^Ll^&!tveg+7(5_LWA7oUiS9TWHO?rz6rtx7?AQnXY*&)G+lxx6QAq z9O?bgJKu9Zm(zWt3)U&uIjUo(LUo*%Xp63CCs5|_Y_|z($82!oLak%Q1E(nj$Bel? zM|(ucw6mbU#9Io-DtV~gdxe#Dzy9KZzdp+nu9>wx8~cBrGxTlZ6qrVz=x-lws+~jo zQ=1+)?&aF&mOibc*KFu1A*ZAK7g$da-$JkD++}z*$nZcG8Gd%?v?s^%9C!6f-_|GQ sNbAJp2Hnhi?bNRa>J`3gpOrZrSG~hW$zEqDS)P`a>9?uaiDY0C@&Y30LU7@xRGUjNO>3JP==1s*qJIA*!Kv0VWF~Xw z%=s^KzCIHTb*Pc1nrp>6)-z+SwJOyi(kN9KVl)0mnlR7ENHowHU!p60C?{I6yQfmh zdaXjsE;d)g**R5QcM`HvPTx3RvMab#X~S8fQynl58T&Yu+*rYH7n?`wHV z?$#0pF}HzkU}T-<-eVta`H@k_x9rV6)C+K2$Lj6FILzKxL}oRh_JbR1 zj&c1aHI>ZOJpR7GZ*;$&eKzJKFtE=pKdXt=9z{;as0;3TY-ss*PX{y(bmj s`9x$%j;j`A_P8foxGS=1hTkmz2haW8DziH=O54p1j7w@*b>888134<29RL6T literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/de_CH/keyword.strings b/fastlane/screenshots/de_CH/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..fe237c7fe214e55863a3e2851aa7ea7b549204ae GIT binary patch literal 746 zcmaKq%}#?r6ot>)r)YQrON<-i!k@ZnOiWDNHnoM8bP80kBz<`GyThOmNkbUO<=*qn zIn4K0q^T~oP^Ll^&!tveg+7(5_LWA7oUiS9TWHO?rz6rtx7?AQnXY*&)G+lxx6QAq z9O?bgJKu9Zm(zWt3)U&uIjUo(LUo*%Xp63CCs5|_Y_|z($82!oLak%Q1E(nj$Bel? zM|(ucw6mbU#9Io-DtV~gdxe#Dzy9KZzdp+nu9>wx8~cBrGxTlZ6qrVz=x-lws+~jo zQ=1+)?&aF&mOibc*KFu1A*ZAK7g$da-$JkD++}z*$nZcG8Gd%?v?s^%9C!6f-_|GQ sNbAJp2Hnhi?bNRa>J`3gpOrZrSG~hW$zEqDS)P`a>9?uaiDY0C@&Y30LU7@xRGUjNO>3JP==1s*qJIA*!Kv0VWF~Xw z%=s^KzCIHTb*Pc1nrp>6)-z+SwJOyi(kN9KVl)0mnlR7ENHowHU!p60C?{I6yQfmh zdaXjsE;d)g**R5QcM`HvPTx3RvMab#X~S8fQynl58T&Yu+*rYH7n?`wHV z?$#0pF}HzkU}T-<-eVta`H@k_x9rV6)C+K2$Lj6FILzKxL}oRh_JbR1 zj&c1aHI>ZOJpR7GZ*;$&eKzJKFtE=pKdXt=9z{;as0;3TY-ss*PX{y(bmj s`9x$%j;j`A_P8foxGS=1hTkmz2haW8DziH=O54p1j7w@*b>888134<29RL6T literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/el/keyword.strings b/fastlane/screenshots/el/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..8a3dbb69f591316a4e3affd2f0a31a21c3a6e8e2 GIT binary patch literal 788 zcmaKq-%0{e5XJ}QuBTXhf|Tecx+=Qv4VFj^$$zEgb4Wcv(QPPeij^B`SrJ%|RQ={G z>Y65p-7{x)_WS1h=KOpoBq{6SOHN8sVLy;VR!PpJC{>rKNk;N6S0M5w&AUKFLOj{# zNyv?U>XWv$qc7T3PeLM(iW(R@{A*}w>b7K!capV=FHbKOxR)_q>VpRQ4t`UI#1Y>> z``TlD(S;{HPIpw<_kfDftgERRp|I!toS;8y1Ln5l{Hx!zM!63hn01kg`5f<}ET?Ebr;iLX@}phIpTEF%W~3z?C?(U{6$oR zwg%lcmN`lfXhe=B(hgd49XKY_^>j9~y+G&xoVbD^N17R_l4JhYWNvWQZojH%0#)`0VYj`F^)4eghyI;o$%P literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/en-GB/keyword.strings b/fastlane/screenshots/en-GB/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..aba38418299a39e3a16b19b4c60895e427561d78 GIT binary patch literal 716 zcmaKq+fIW(7=-7#PeFKswlUrqFNmkBCK{98Hl_t7(gg+O?XBOxxCJU{HhW-qcIKa1 z4nMILnrow6rE2CEZE2;xHZW;aN5(;5gXp5!v^Nd5pzwg UjZew(veS$S9xCd5KEZa3zXj5Jr~m)} literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/en-GB/title.strings b/fastlane/screenshots/en-GB/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..547033c70b80a5ebb2b1fa5c9ae54c387c499e1e GIT binary patch literal 802 zcmaKq%}#?r6ot>)ryx9mCB}_$L8?t$Xib`!xFStUYa>4uXd7Q%{q6u3lr{urGQ&CF zJ$L5&%hyPkinLL#l5wsDPp+LZRc_^=SczLpnMGRiP5Hx@r(0fMA!|!!uTEDm%XL6M zMrTg8H$zou@4minbb)TfbB>%)RY?`YOsg|yzr;O+l=LbH|Kw?=;iCO?7s@99IbWze@|z6!>w@6U1o^F`Z+NWO<#>Jx^YnbokM7VB|Meqzmh){Je2mVedj?gwYT zLH!P=zE|~m4doq^C#5Pojmo9IbN+M4)8^i^)wi_k9<6jrt0<$kQ(cXzXV>)gNO**m O@viZ(TW)v1ru+a#mx_}B literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/es/keyword.strings b/fastlane/screenshots/es/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..ad1dc0f49079006d789e82aa23daa78c176f00a4 GIT binary patch literal 764 zcmaKq%}#_s5QOXOQxKlO-54*%!)A?MJ$WT$mQ|DmT=pmNZG8ss>er13MAnd*hNh>g zx_Z7oW6gA~M7b(8#8iQ&(q5%@o;fO0;kgZOiB{|zDq=;tX2n|aRgw*yNVO$r2qm%7 zk+|f%A+D&s(i!`V=ReK@Jq>76Y4{-%W*OC@)b8s%L`cT{jmv6_qU)f yxbGD%ZuO3e938!Tmm}hUDzGYafZ7;iVB0xc1%_W1@^mdjji=kyK!nTJvU literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/es/title.strings b/fastlane/screenshots/es/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..90b256dbb22f088a33bf7218463cd83b43f94d33 GIT binary patch literal 850 zcmaKq%}#_s5QOXOQ%raO<8C%yj0fGb7Y|;^1_oW_PX^fdxITjM=0UA$+!d5;2m=h= z{Z&`b*Jp$U&JbgZ3W3iJ5?&QLlxS^d2anwL3ckfixfYCw;Bd)1!WG__m-2SZ=xp_9 z(6egF$c8%=cC5H&CDl%ub%7JE3tq?QIco$endeHYH$`HI-?F}x1XI!3Ga=DiKic1u zmy^CTa&kt=gJaA)#tf{6><4D`B#I`2Gv6FO_`WsI9IJJ>r=n&=9``&Z%l(#_74xO! zzt?O2D^OcKMb2w^p_0U6YU*mukKewRBge<|n$^5b&2w5YDJim971!)A;Msf~<}PAM zPR|PJHlen6b0ov7edRbkIIx;{uR3z;iLBxLX%6%K-~K2Jhx+o!*~~S2O+x2a9%-KH kec$={gc|IxJS?ero_53Rl5vl$sI#r!I=ZB(VnO)n$f|XWtCicoIt+lN+F6nG(h08YlEi7fD8;u}u&6 zj{gVj{tA>!<7k9}DC?W=8X#J)1tV+*8 zuR{d$Nexy9Th-$d`-L16s-(<&YK~=ODO_Md#jHsF z*ZmP+nbXI_H<#q>t&_wHSRaY=QMJd!iq0(QOhr*dZBMX&~6QZZn12w>BFnvEZRVd4Pjw+XU-pd zf2Eq}Qn^ZP)bcF!%C*r>ULo%68)L>eC z%z7K^zl<|wy^Q+`8v2|*m~(u)Tn!plqe`CjpgBw>_&d~EZpG2#)gae*I!e4%Aa(zd uWjyY1ih~2)4S4paOsOiSh7PU=_$|F5Cg;ijKimdPOXd*KI(*BlOXUY#^n_pl literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/eu/title.strings b/fastlane/screenshots/eu/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..3a670b7a7e81764fc20502c8791121b28cc4ace7 GIT binary patch literal 876 zcma)*K~KU!5QXRLuh{e#2r*ua2T>%ZMoqkVAyoX#xcVsO)wZ4rlw~}{aZ;afQHnW9CR+d`ku_>#*4Vb57L>BClC$b)qcMulb3iiT0 zClbh6fpX1l;SKyO{#2i`iGomsFxSV`0hYhk~IeR)_K1`U2t>shac>oow9;;oKf}4nFK}Kn%lLStL*B8 zv`W9!9QytQ(%d;G&Ml@xEy23vB%H;bU!Xwyssrtc?^O+j>-|JUN-z6Xpkmmlr9)2U zs2g;#s@ep{_X-}^tNSC5tG0dj&uV@6z_&3DOkmr5lj=#c4>x3}!zoS$+jd5Esu#f? o(UF6c^VY7c@t)UsOLs@U&$#^`z8GfpMviW4e|B#D2g_ZBzXQ;kiU0rr literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/fr/title.strings b/fastlane/screenshots/fr/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..c64d79cafe7829f07082d7f9c76239dc6b4c46db GIT binary patch literal 890 zcmaKq&rZTX5XR^1Q#3q*5aY#oARatQd;!w51yN{`F7%NNK7{)FrmVEFl4iTx*_rwN zOuxTIHnwY<+sZ1d*;C7SD%)FW!6S_ow)WVD)7*028!|?g*gb1xQ+xG_z|D@xfv>=+ z;9Ps`zir={k%r&PQh^{FD{Ee|=)3U8**=<))xh z#GUliFF;xQ{ha+0;=+`h*^BE-PF2Zobmej3rszyS{$$CK0{uB&RMdwKkFNeM4^1SH zTT!P8{q=ta#*UoSmM1om+nGhdeL}6KT+>aH>bfxLxZg$Jl29H7O@k~ba zqur%ds3pwQVaXnq$SEm{e#d#-^ycx7cXIg+#P-kB?1o*K!cD3^-0wJTO|w<5%GQAr nMn~dj{kJ_-{jC&Vp^(v(IdX=noBclicx9DuiW1dd3N)Mc8P-aa#HUO zd_bqHr&Twc<=VqrQla(A;lR4Ihuvze7#ZHONGYx|RT;~#QbVB^&o+Q*;o8mmxC7JS({A-vH`@GEcQNVX6TL8h zkFbO{WH&Hh+X`HczVSMk-_S2{U~eki(dC4@1Kaz4HF`M7`HBO3GvVg#9vsu&g84E} Ip_f798(=1f^8f$< literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/gl/title.strings b/fastlane/screenshots/gl/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..4e0cabde71f56ace166eea5f80b6b6764c967319 GIT binary patch literal 864 zcmaKqK~BRk5JhLrDXg4;R)`H^0ToxM*i}*DmWqU?Ng!O8GjzuSc;8Od1VR&Kka{Wjs53wHIqcq%&_th?VSCma~>*Bz8X2c2Spw}!x1M??oU_0B(v$!oV|lzb4Q%&tb9@L)L)0{*bU8s kX5!TG>k+@^wGFOolBane$reQ_0}D-dFQF+OR0b_b0SFvLR9qO22agCtl8F(@oT{bu}O2uLP# zJDIfSyXV|~enhJ3P_bG{mGQjNH7nJFI_mkGz8XsWU7J&^I(tx(S^-kNX&JC@tVP4+CfmR_+xWK4eV1X+C))A6?ZiSSA#rST*pq-! z3(^W1UESm65lVxkV|B^6xihuW`x)ZlQs=t(o5ayZK6gy1Mc=n*eE$P3E0@-G_d|MI q`cz8ln5nN*Io?ps=&t9p=;-DjwVX*lq2Xp%k<)YPg_Z2-PZD3MZ=0e3 literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/he/title.strings b/fastlane/screenshots/he/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..311f4c0fc66d4d58669c2dd9230e1ecd82687583 GIT binary patch literal 762 zcmaKq(N4lZ5Jm6Hy!aJOzd#z}i}A@1Ax6LuOF=2}n?-3#6&i>QQa(bxGbq^xB%8Lo zo9;bx@9g)Nj|R@sL>GMw`Mt#*M;{|Zh;3(#HbUF$an?kO?;ii~;o+K(kEx`xl2W$f zp^}Y#`xsNd2K?o3$8 zlFzOZmlo7qcBmcFhdMsRS<_X{p`?j?Qh&|W!d3;>sm&9zU-!O(nlG26*~XT1WaPsn zE2?j)`ayzAQ%5;@xq16bFhJlW=#mC_qPu6Z+U!FU19qTuG|7ybB~>dDH9IAwGBbxI zcV{)?MJ>fgcD74T4^-aW8%Nl2T!Hy+1(;K0^TbgU7FZMZJ)>uP2i7mYd Y_x~{Tyxv9CbQSgPCETCTP4=t*1Ky^*umAu6 literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/km/keyword.strings b/fastlane/screenshots/km/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..9e578cb74f1422dd261ee9666ad6e41a73796229 GIT binary patch literal 730 zcmaJ<%Syvg6r8m`Fx!wXutnU63sLIUKM17{DW+*P(QZR2h%OXsX=@PLKX9jlxDz*S zq#vYCZfHzwEtmVqy(e>K=A7>@8wISxK?5!L+RLbDv~UU!fw}3R2G`s*b#hSE+Ej!M z3)`ADMx61Sj~wuWV}|BD<}2@LDXt@=T_MDnRYh-MO>048RaRGwd@)#-ud95K$(EdY z9CFGRc6r5HK69RZ9KwZ)91SvGB)i1nHH)Km=z zZPlT^)Y$=h!k!BELbW@<9!ja&jhvgus%AqfkA#!-Ozdpz?V{Ffe?&t6l)|}(n>kcp f3G{j~Z&5ci;R literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/ko/keyword.strings b/fastlane/screenshots/ko/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..aa725dfd0bbba09b14a979f9b1dafdbd4dbe9afa GIT binary patch literal 566 zcmezWPl>^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$?f*P_uo*u{&EFjwvvFIJ#kt5`#4Z7f{>~2=SSm1GKdmXbUzopKKC5slae# zjpzvl28Ok3cEZdwLUC;x(1aYIdq6J7XByAtn&S!#GHa$BLzsi?He_=^Ay5Jik36us znEqXHP2)J@i3*^J5OY!8XN2TuWOEA`ihwRH1Dc%5P!3d601U}AtbPw*aA9x*hU|yE zTaLoqYl7nMLSX!40^OMn)DMXC+?qY4Zv2SranOaK7nsCjV! literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/ko/title.strings b/fastlane/screenshots/ko/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..c332ce3ebe4108ab91395c5b780ed34d9808a1fd GIT binary patch literal 612 zcmaKpy-LGS9L3MjNd$3l>u|{vq(xkcyAR+f6wy|SX&OzmO9uxZp%3A3D?$r?kf8F28f~`~UuEC_zUR4YXkMd59yf7A|1IiFewl!HD;o ze5>M^_a;wh5ZL9V!3jF|ca!&lKyh^K3Upr~HB_+1ZzZl(bc4w@la_IBo<7KYmRo)> z7TEN^dnvV?>s%+pU@O&s74P6p;8O%s6AwKtkK# zzenMIU*Jm$Z_X`e3}m?$&e{5s>f-Q$!}7JBkAVf_JV CF_4}B literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/lv/title.strings b/fastlane/screenshots/lv/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..fec42a1f9c1443175035abee4b59bbf94070b31f GIT binary patch literal 862 zcmaKqKU0H15XJXeTfYE9o5o}u8@0%2XJciX!2~jdzXk%|Uj6MN0TeR~ce}TH``+8V zuTNi=aw8jwrI3m}kS$*!C&{I@lU71W?JUFE$c{B5!xv8;`F(lib|{UlIgx`|HL;3% z4YfQ@TPi+LNo(&c7Rr&Rt}#{a<%)I5cZp3(ZUwXdc_i}S5~n%JN?9ng%Dn8%Nn`3I z@KL3m-?LavdJ+*$$=Ci|!e2{CPJ-qEJHpf3Cn6n_5IpC4yT%?+$J)oJIu)&{T$RP& z8N3iHW#RVhL;t7fI#UnT0hy|1z+H`cKE^GXO+VR|oD$?{7XQsUn4{jgqXaH6q!2%q zYo~q=u4nJcGh3&!Cwt3&Al9?Fo%H^L8!_q=P`kIO%;$Tc?=ZCvdGORJ=*ugOQ9qYK h_1y%gLT7C~&S5oQ|Cw$SKiYoRZ@^1IUAXtNmT&)SmcRf2 literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/pt-BR/keyword.strings b/fastlane/screenshots/pt-BR/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..37766ce90eeaad07540c69b1b172d4a51e7dd5d2 GIT binary patch literal 796 zcmaKq%TB^T7=_Q;ry%T2q%m%c3xep@uwY9_DTLS-Y%3(btt+3vt@@prfE1B5Gt;@9 z+dto5UG;ROiI!TaW}GVJTj^D$ny|7}raY__{7p2YFF4UvM|ZTY2FyxUQ*LCsh!F-^lj^mb{IS literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/pt-BR/title.strings b/fastlane/screenshots/pt-BR/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..bd1d1bd726c285e4925bb9cec0b6bcb140154e2b GIT binary patch literal 886 zcmaKqO-{ow5QX2GQ&>F#tq>c;qAEWdgen!-Mx{whMcS04KU^2F-~jA_Z^l%lB_PZ8 z*fX9t?~Ok{T59W1iKZ%4u@5z36k2JnrQd9nDfhb>R-%-3Mnp?Nr_7eR+)atF9CI?1 z+zyCZeeb2#_)@&)DYPM~M@~aYd4Hk<);8n6I&yMU)YM$T2)v3>fG}jV;iVEcI1pxw6~o<-a)VYg=smb?q<~^QbvUR`5_%Jd+Ge_-%sdq=_l)0&_3oD DwU?bj literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/ru/keyword.strings b/fastlane/screenshots/ru/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..a004b1a7b2f867b2e484cfc550a733968e9fe507 GIT binary patch literal 792 zcmaKq$xZ@65QgheCvQH0VKRC(fssvM8d;(Ti0Bg#BVdSZ3d-9nUp1hEAn9~ZPfgFi zReyhll99A@Whhgb^L&zL##BCJA`4$xN?%64HfGnA9_yGBp#%~$LrJOAtd_K__j;#A zt!rCr+R;Et%&9EFYJg#=J8Y@fw4hhi zJJcMNUzVivB#WiL@V%IL*8Qw6waLigt`18Cm-F5m2NP*-l%@Z;j1tKw`DgNu-%k|u z=~`E2^lNai11a)%vk&A!nsWQo-Rw@1Z*+c$w^y*%H_TGCT=yNGl4H$dwn#M) u%g)}-;Woki4*DFm3!mFWUj9KnBZ)Pt1J&+EAa;j}^v7wQ46SRO4Z{jv5Hgv5RCm|C zb#MKAm!&Ed>B&fDvS5Ce7rvQ%%2bvXS;;`g7Mrl@NuP1Tjk0)>@R#MxrLO8K&UcTl z?dr}GpWQuk&aOpVM~^ovvUbR(`TM+#`X!p=eMxD134CW@}Ov8|Lx z!`2;l=bpHkxCh%`!7<}3cU|@yF8+=hLy*VRtI8Z>QKG(-Ofh5IYP}|V(?#rkjm7R21Q(TWNH3PjmN(J7lyLQsr5C%42Tw$@nplPVjLRW$xMTgv8aGw zaHGg%E+1&|1(yL%_GFHS!+%{lN9mdiQCFE{Fj1XD){2^d=M$+h-!gZ%Dg-;~ztbfn zs(%Ax-IwZwda|RRYdK_DNdT2@WOc|t83{X>k2tUK!$(fEE< literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/sq/keyword.strings b/fastlane/screenshots/sq/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..893314b7854f044f9859e0f13c6b2b386ead1c16 GIT binary patch literal 764 zcmaKq%SyvQ6o&t`PZ6>gY!Nr&qIf~DN>OlIq-kT?B#lk9_{@C=^*bk46PlKg$z?O26{@Ebjg@GrlJAQqJWIVRSLG{fO_lmu#%!#JF{2_>pbOqmLrzC}^)m@8Ru@pQ z2IG>mh89tGt|P`C&mq1Pv=Z(vrU5t^`}uFp3T&GPdUF0u8|FjjjPbH!Z_C?n`JAzn zqSHJdV!U&diprE(pnLcBpt&80Ex@Yp zkLB9!?pB|un0tzLORR4Ne{)jV{CPNQ;g{@Jo?5PW5ALITi&2t#KkGX<0Nc82sZIFS Ix21OR17oX*2mk;8 literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/sq/title.strings b/fastlane/screenshots/sq/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..aeab3e0d353a17c081d26cc9dc678abd82078a43 GIT binary patch literal 898 zcmaKq+fKqj5QhJ2pQ7Ojgcxs(7e+2jIK%|QZBwOSp{3YXh`uuKpnkIp*b10U+uiBx zeE%H2zglYRQlS#%D)~lwVdZ+)QkAo86f1SMj9sV+?+lNY0^RboG}0@+IX4o{cG{}Y zvp(=Fu`RJa5i!!7U9Jt5dtw@8!u=av@NTpI(~%OXgmX&411wJ;!Vc7fA499y_Z?{l zy&yv6{_oX92YRPbg(;q?Qy=m@&$V0=9?$MMM#a69T_CSzhz`><#=flO+iB!mCt43G z;#x(mC&v{ZVvBav0da#vUUN>xt_GIzubM78&9h6~5>0U}_fD$vaqfH^dqKUod7)Ggw?pS$Y@y+mEbvZl5l;JXO4-yODw55nae;Tb2jEKls@M%Zzyr(mXDG)inv06 z3ml_t)&g7{z~QYqId}YC6Z^b-BSp?G$2!f3eiG`hQrjlGgaSO&;B(i*IZpVdsOVyc zUYhvT@p82dNsZ`U*SSu?#}SDS=#s8DbY*`;+qq5D$)zU8*eERLlzGnk&y*_k95B5m zs;sJxGc-`a9%qM@9a`$1m7A?Tl{Z%QLHz+U4`s@T0VHYiIksaU^`wRE295JG QBX5&D4;^-+ipwABH{aW71poj5 literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/th-TH/title.strings b/fastlane/screenshots/th-TH/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..9e8b8b3bec9281c5ee2633f1cb466ac756cf7d1a GIT binary patch literal 844 zcmaKqZA-#n6o!u?C`RW?tFS=lBm`sNmP!(VRv!xb4-%;%`Bqx~`|5f&hZ~xVZO_}z zb>H{d?~fy%)TJjQnaP6pi@b8pSJe&1Hm6tAyQhw++~=z_ds)rswH|4WZydRiJheQoeQL%iU7&2Yjapfn)D+jUG&uTF3@GDVLvuh zKbCczI+ss8f5RQnxt`4F*>Gp(S^W5TIp*!+(CEmJaDjw<^ca3YuW;^@dPHs{GXCJ7 z7~%6BLiqogyiMZUZcWZMwTf`6_zpX0`tCAo!wd6{NUa2EfuCiN=TzSQA%q@s{3q7h SPrQ2A!E-n+`!=VXwW~jy6?Yo| literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/tr/keyword.strings b/fastlane/screenshots/tr/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..c8d9b7ab5f749cf7ab76279a265f88ccd66e5ac9 GIT binary patch literal 742 zcmaiyyH3ME5Jj(Begey1fFneMC5qD{0O!o3Lj2|fHNB+)`|gHyB@7) z@0pqF&yPs4PLwE9rH1!JGoDJ%D%HA^jZ)?AtRPA>XD`Tz6zYN%5pT%8BFpa0Im?Js z-c$6BgPgr2?_5XhG0$JToZAiTKGKkHwX2{+eT42SqzUmDUPClh9E5t*8^jcqJ$2CO za`9cEb^Jds)DYzr%1O{V+BKav+7cVR_9%XDKE>wRLanIFsE1R0px&B{!c`RNhvD~Q z-!qRl@Mh`r)@Y@AAveXHL~DqPUhW;F5fi*+X0_g_uVpv?w%nev8jpC|nd1|FTh0pQ z-JfGF^!GPre&&|>bK^Qq0_$e)&{;>hf#14`m0JFycu$2(DliWUR;WwoN$pN*NIUH> D$0mZW literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/tr/title.strings b/fastlane/screenshots/tr/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..d6f3387f7ab3c371eea09772d99835cfcb3fc2a0 GIT binary patch literal 828 zcma))!AiqG6h&{g{)9unz!Y&KE<~k*8^I!U6^e}#+D>YwwX`4Vr?@KWc@t|=tRf+k z$(woiymx0l-aATlq)dfM1-?T)@RVA}YaSy@<(kCUlv$<`6MXd!2rnw+;r7`O{nWs8nOnLsQV1ja>ntklPHdNzujb5Po8OlRdN)yk! zW^TL+g)AxHZ<*blt{+*qip+uk&@SmNXodQg=F}&D3~j)-iY__z*jG`ZdAW&%hTrbF z&9@VnQ%^~j)$`NuhVJwN&2a)&N6;HI>%;nWx+S)XD_PQTyDhKV+!Ju}FU zd-><=9OAlpTl0J5tjY95>w3z_HF}yHCh0f-c*pl=?n6PpV>tEAvoqM=wcVP)?s{Cq uzK2zJ`U*Xdu5J4~+2Mq^YOZ=c{i5IFgsdA+&E{UCKEPN1r9Wk@W_$vO@03yi literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/ur_PK/keyword.strings b/fastlane/screenshots/ur_PK/keyword.strings new file mode 100644 index 0000000000000000000000000000000000000000..0ffb50a7a13a4f4b1b446618e895dc81aedaeafd GIT binary patch literal 708 zcmaKqF;2rk5Ji7A1*foFU=$i`BZPz~B0=O^LjFKbMY+qVU0V?41gxFbY>TRtHo z$z&t}oZNA?wb>VIDv=E2)7BApIN3;u8=UZ;nBy2VXGbMFIK&%BOx;G#sC-Z6Mrzi8 zeK-=emyVNo?xs}D*}pnHMi^4()ZBbfwVo)u4OdN9A*I9sdgYDj8EzYWRp?u0JA^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$}1u%V#JDs&@ve1L;g*xYxflS&6}#feR>W2!!~|&jH$9473ZI$qTF5N~9vy zdd-tyrWv8QHVvpZ2k0J<%ki1hp8CyCvp%&HVG6RFkWB&kzXTi_d0$fT0NJ!ZM(lslZT*XD9%MPa0O=1~9lVxG_9U=Pu6(gPCiB;@?7G z=wO@*0chGJ4N&^0AspXM>7<1k@LoJQ%s?!0=4 F2>{?^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$};_yC7&qLQ9zfP_!JV&l#v5WI_s1mN$K2P)3*%gEa#eP^BRd;DHcTfuw@eykzTy k6P+;A%uq}#2D+&P?BhI!bR4F1MrQ^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$@?|ur!&yDW9PnsK*(o7Nk1`C~nQb1tblD5TE%uK)Z{9c40GlVKrNcR3vwR zahzEa%se9$_oe}L=Kx&=qI8>k-=S0KNGe2?kQ*YWZbw7UF5VJ4ZOm{bgOO$peic?{_|Ot{~7Wfpf8 Mf4pwjtZ0ZS0Cw1JrT_o{ literal 0 HcmV?d00001 diff --git a/fastlane/screenshots/zh_TW/title.strings b/fastlane/screenshots/zh_TW/title.strings new file mode 100644 index 0000000000000000000000000000000000000000..9676e63d1e9472a8e900f25d112c3a597fc146d2 GIT binary patch literal 576 zcmezWPl>^h!GIy2A(J7Wp%_RfG9&|GK0_%(9zzLOq?{p@AqOm$3uMJJqyWiWpb8}h z1qNFnR$^EXl;HoW@ndXD*{5k%{=DfKVM+|v3|v4NLm!As3OkcAFt@fLe<&n`7@~(_!GQvb7Bp)N2Sin#ObY~gR%v6SQpqc_;NTy-+xnj+_ zP?;3&n4S>67_BCl$tEcNE(FFrK}?LR!URmhklrO gW+>(r16@@D_HiCVIu3I>qcZ|QrYx*xD}k5-0DdufhX4Qo literal 0 HcmV?d00001 diff --git a/img/filetypes-tvg/space.tvg b/img/filetypes-tvg/space.tvg new file mode 100644 index 000000000..cc01e021e --- /dev/null +++ b/img/filetypes-tvg/space.tvg @@ -0,0 +1 @@ +{"viewBox":"{{0, 0}, {15, 15}}","defaults":{"folderFillColor":"#55739a"},"image":"\n\n\n\n\n\n\n<\/svg>\n"} \ No newline at end of file diff --git a/img/filetypes/space.svg b/img/filetypes/space.svg new file mode 100644 index 000000000..1765e7616 --- /dev/null +++ b/img/filetypes/space.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ios-sdk b/ios-sdk index 93cfe6b31..e56278355 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 93cfe6b31beb2f63624c85766f03f5ca723ad64e +Subproject commit e56278355a501fc8bd318970dd35ee49a07d6894 diff --git a/ownCloud File Provider UI/CancelLabelViewController.swift b/ownCloud File Provider UI/CancelLabelViewController.swift index c1d6c5a2a..60d5fd312 100644 --- a/ownCloud File Provider UI/CancelLabelViewController.swift +++ b/ownCloud File Provider UI/CancelLabelViewController.swift @@ -30,10 +30,13 @@ class CancelLabelViewController: UIViewController { func updateCancelLabels(with message: String) { let collection = Theme.shared.activeCollection - self.view.backgroundColor = collection.toolbarColors.backgroundColor - self.label.textColor = collection.toolbarColors.labelColor - self.button.setTitleColor(collection.toolbarColors.labelColor, for: .normal) - self.button.backgroundColor = collection.neutralColors.normal.background + + view.cssSelector = .toolbar + button.cssSelector = .cancel + + view.apply(css: collection.css, properties: [.fill]) + label.apply(css: collection.css, properties: [.stroke]) + self.label.text = message self.button.setTitle("Cancel".localized, for: .normal) } diff --git a/ownCloud File Provider UI/DocumentActionViewController.swift b/ownCloud File Provider UI/DocumentActionViewController.swift index 9ff1fe6af..dfc12bb6f 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 { @@ -69,13 +79,9 @@ class DocumentActionViewController: FPUIActionExtensionViewController { } let collection = Theme.shared.activeCollection - self.view.backgroundColor = collection.toolbarColors.backgroundColor + view.backgroundColor = collection.css.getColor(.fill, selectors: [.toolbar], for: view) - 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..d2f6e48fd 100644 --- a/ownCloud Share Extension/ShareViewController.swift +++ b/ownCloud Share Extension/ShareExtensionViewController.swift @@ -1,275 +1,118 @@ // -// 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 + self.cssSelector = .modal - 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() + } + + 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)") - }) - } - - 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) - }) - } - - 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() - }) } - 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?() + 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 } - }) - } - @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) + self?.importTo(selectedFolder: folderItem, location: folderLocation) }) - } - - 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) + contentViewController = locationPicker.pickerViewControllerForPresentation() } - 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) + self.contentViewController = progressViewController - let progressViewController = ProgressIndicatorViewController(initialProgressLabel: "Preparing…".localized, progress: nil, cancelHandler: {}) - - self.present(progressViewController, animated: false) - - 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 +162,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 +291,166 @@ class ShareViewController: MoreStaticTableViewController { }) } } + + // MARK: - Events + var willAppearDidInitialRun: Bool = false + private var _registered = false + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Register for theme + if !_registered { + _registered = true + Theme.shared.register(client: self, applyImmediately: true) + } + + // Check permission + 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: [ + .title(title, alignment: .centered), + .spacing(5), + .subtitle(message, 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 = collection.css.getColor(.fill, for: view) + } + + // 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..9e3e91b09 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,12 +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 */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; @@ -81,24 +69,19 @@ 399698ED260A3CEE00E5AEBA /* ImportPasteboardAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391220922344C30F0026C290 /* ImportPasteboardAction.swift */; }; 39969904260A3CF500E5AEBA /* CutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391220932344C30F0026C290 /* CutAction.swift */; }; 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */; }; - 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D422411EDF00B66713 /* BorderedLabel.swift */; }; 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 +92,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 +132,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 +143,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 +159,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 +183,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 */; }; @@ -260,7 +207,6 @@ DC0A358B24C0E44B00FB58FC /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; DC0A358C24C0E44B00FB58FC /* ThemeWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC243BF92317B446004FBB5C /* ThemeWindow.swift */; }; DC0A358D24C0E44B00FB58FC /* ThemedAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAB9CC823417243009091B6 /* ThemedAlertController.swift */; }; - DC0A358E24C0E44B00FB58FC /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; DC0A358F24C0E46000FB58FC /* PointerEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39664AD9242CD3A800B6121A /* PointerEffect.swift */; }; DC0A359124C0E4AF00FB58FC /* OCBookmarkManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */; }; DC0A359224C0E55800FB58FC /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F1318205A693A0029F186 /* UIColor+Extension.swift */; }; @@ -273,12 +219,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 +231,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,23 +247,37 @@ 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 */; }; + 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 */; }; DC2A128728D0725D0088A2B7 /* OCSavedSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2A127D28D06F060088A2B7 /* OCSavedSearch.m */; }; + DC2A68D529D492B300BFF393 /* space.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DC2A68D429D492B200BFF393 /* space.tvg */; }; + DC2A68D729D4E93300BFF393 /* SharedKeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */; }; 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 +285,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 +306,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 +342,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 +361,39 @@ 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 */; }; + DC8AA7BE29DC154400BFF393 /* ItemLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8AA7BD29DC154400BFF393 /* ItemLayout.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,18 +402,51 @@ 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 */; }; + DCB1B89F29C7378200BFF393 /* ThemeCSS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B89E29C7378200BFF393 /* ThemeCSS.swift */; }; + DCB1B8A229C7379C00BFF393 /* NSObject+ThemeCSS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8A129C7379C00BFF393 /* NSObject+ThemeCSS.swift */; }; + DCB1B8A429C73DB800BFF393 /* ThemeCSSRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8A329C73DB800BFF393 /* ThemeCSSRecord.swift */; }; + DCB1B8A729C75EBD00BFF393 /* ThemeCSS+AutoSelectors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8A629C75EBD00BFF393 /* ThemeCSS+AutoSelectors.swift */; }; + DCB1B8AB29C89E0900BFF393 /* ThemeCSSLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8AA29C89E0900BFF393 /* ThemeCSSLabel.swift */; }; + DCB1B8C229C8A6E500BFF393 /* ThemeCSSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8C129C8A6E500BFF393 /* ThemeCSSView.swift */; }; + DCB1B8C429C8A84000BFF393 /* UIView+ThemeCSS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8C329C8A84000BFF393 /* UIView+ThemeCSS.swift */; }; + DCB1B8C829C8F84800BFF393 /* ThemeCSSProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B8C729C8F84800BFF393 /* ThemeCSSProgressView.swift */; }; + DCB1B8CB29CD847500BFF393 /* NSObject+AnnotatedProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC5E445232654DE002E5B84 /* NSObject+AnnotatedProperties.m */; }; + DCB1B99729D1813D00BFF393 /* ThemeCSSTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B99629D1813D00BFF393 /* ThemeCSSTextField.swift */; }; + DCB1B99929D187B400BFF393 /* ThemeCSSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB1B99829D187B400BFF393 /* ThemeCSSButton.swift */; }; DCB2C05F250C1F9E001083CA /* BrandingClassSettingsSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB2C05D250C1F9E001083CA /* BrandingClassSettingsSource.h */; }; DCB2C061250C253C001083CA /* BrandingClassSettingsSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB2C05E250C1F9E001083CA /* BrandingClassSettingsSource.m */; }; + DCB32FE829E704D600BFF393 /* SelectionCheckmarkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB32FE729E704D600BFF393 /* SelectionCheckmarkButton.swift */; }; DCB458ED2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB458EB2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCB458EE2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB458EC2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m */; }; DCB459052604AD2A006A02AB /* SearchSegmentationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB459042604AD2A006A02AB /* SearchSegmentationTests.m */; }; 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 */; }; @@ -435,19 +459,15 @@ DCC085812293F66D008CC05C /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; DCC3700724D466D2008B0DEB /* DiagnosticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC3700624D466D2008B0DEB /* DiagnosticManager.swift */; }; DCC3701624D4D365008B0DEB /* OCScanJobActivity+DiagnosticGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC3701524D4D365008B0DEB /* OCScanJobActivity+DiagnosticGenerator.swift */; }; - 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 +476,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 +500,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 */; }; @@ -492,24 +511,11 @@ DCE4E43C24C19B660051722F /* FrameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236735A521217C3500E5834A /* FrameViewController.swift */; }; 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 +573,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 +600,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 +694,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 +875,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 +899,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 +927,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 +938,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 +961,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 = ""; }; @@ -1034,7 +991,6 @@ 395E16F922F03CAF00DE89A1 /* GetFileIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFileIntentHandler.swift; sourceTree = ""; }; 395E16FC22F06A7300DE89A1 /* SaveFileIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFileIntentHandler.swift; sourceTree = ""; }; 395E16FE22F172C900DE89A1 /* CreateFolderIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFolderIntentHandler.swift; sourceTree = ""; }; - 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; 3961281522F8730A0087BD3A /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 39664AD9242CD3A800B6121A /* PointerEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointerEffect.swift; sourceTree = ""; }; 3968C879239C54AC00AC28AC /* ReleaseNotesHostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReleaseNotesHostViewController.swift; sourceTree = ""; }; @@ -1043,7 +999,6 @@ 396BE4C92289500E00B254A9 /* RoundedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLabel.swift; sourceTree = ""; }; 396C82FA2319AFDD00938262 /* CollaborateAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollaborateAction.swift; sourceTree = ""; }; 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardSceneAction.swift; sourceTree = ""; }; - 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; 397754E123279EED00119FCB /* OCItem+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCItem+Extension.swift"; sourceTree = ""; }; 397754F22327A33500119FCB /* OpenSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSceneAction.swift; sourceTree = ""; }; 397E276923D04D7100117B07 /* StaticLoginSingleAccountServerListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticLoginSingleAccountServerListViewController.swift; sourceTree = ""; }; @@ -1059,7 +1014,6 @@ 39954DE023A39625006B6DC0 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; 399697F1260255B100E5AEBA /* PDFGotoPageAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFGotoPageAction.swift; sourceTree = ""; }; 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extension.swift"; sourceTree = ""; }; - 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedInfoView.swift; sourceTree = ""; }; 3998F5D422411EDF00B66713 /* BorderedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedLabel.swift; sourceTree = ""; }; 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCertificate+Extension.swift"; sourceTree = ""; }; 3999BD2D25FA2C8100DB7C47 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1072,17 +1026,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 +1087,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 +1096,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 +1114,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 +1129,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 +1162,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 +1177,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 +1192,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,26 +1236,29 @@ 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 = ""; }; + 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 = ""; }; DC2A128128D0718B0088A2B7 /* OCVault+SavedSearches.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCVault+SavedSearches.m"; sourceTree = ""; }; + DC2A68D429D492B200BFF393 /* space.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = space.tvg; path = "img/filetypes-tvg/space.tvg"; sourceTree = SOURCE_ROOT; }; + DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedKeyCommands.swift; sourceTree = ""; }; 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 +1266,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 +1299,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 +1341,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 +1367,37 @@ 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 = ""; }; + DC8AA7BD29DC154400BFF393 /* ItemLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLayout.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,12 +1409,22 @@ 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 = ""; }; + DCB1B89E29C7378200BFF393 /* ThemeCSS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSS.swift; sourceTree = ""; }; + DCB1B8A129C7379C00BFF393 /* NSObject+ThemeCSS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+ThemeCSS.swift"; sourceTree = ""; }; + DCB1B8A329C73DB800BFF393 /* ThemeCSSRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSSRecord.swift; sourceTree = ""; }; + DCB1B8A529C75D4A00BFF393 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DCB1B8A629C75EBD00BFF393 /* ThemeCSS+AutoSelectors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeCSS+AutoSelectors.swift"; sourceTree = ""; }; + DCB1B8AA29C89E0900BFF393 /* ThemeCSSLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSSLabel.swift; sourceTree = ""; }; + DCB1B8C129C8A6E500BFF393 /* ThemeCSSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSSView.swift; sourceTree = ""; }; + DCB1B8C329C8A84000BFF393 /* UIView+ThemeCSS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+ThemeCSS.swift"; sourceTree = ""; }; + DCB1B8C729C8F84800BFF393 /* ThemeCSSProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSSProgressView.swift; sourceTree = ""; }; + DCB1B99629D1813D00BFF393 /* ThemeCSSTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSSTextField.swift; sourceTree = ""; }; + DCB1B99829D187B400BFF393 /* ThemeCSSButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCSSButton.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 = ""; }; DCB3257425599ABC00058EEB /* ios_mdm_tables.adoc.tmpl */ = {isa = PBXFileReference; lastKnownFileType = text; path = ios_mdm_tables.adoc.tmpl; sourceTree = ""; }; + DCB32FE729E704D600BFF393 /* SelectionCheckmarkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionCheckmarkButton.swift; sourceTree = ""; }; DCB403EB262D9C35008CFE9E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; DCB44D7C2186F0F600DAA4CC /* ThemeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeStyle.swift; sourceTree = ""; }; DCB44D842186FEF700DAA4CC /* ThemeStyle+DefaultStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeStyle+DefaultStyles.swift"; sourceTree = ""; }; @@ -1459,8 +1436,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 +1498,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 +1526,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; }; @@ -1589,6 +1584,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 +1594,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 +1614,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 +1639,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 +1646,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 +1674,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 +1734,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 +1750,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 +1765,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 +1780,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 +1808,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 +1854,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 +1878,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 +1949,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 +1965,24 @@ 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 */, 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 +2004,6 @@ isa = PBXGroup; children = ( 399EA6EE25E6544000B6FF11 /* GroupSharingTableViewController.swift */, - 399EA6EF25E6544000B6FF11 /* ShareClientItemCell.swift */, 399EA6F025E6544000B6FF11 /* PublicLinkEditTableViewController.swift */, 399EA6F125E6544000B6FF11 /* PublicLinkTableViewController.swift */, 399EA6F225E6544000B6FF11 /* SharingTableViewController.swift */, @@ -2062,14 +2012,6 @@ path = Sharing; sourceTree = ""; }; - 399EA70525E654B400B6FF11 /* Sharing */ = { - isa = PBXGroup; - children = ( - 399EA70625E654B400B6FF11 /* PendingSharesTableViewController.swift */, - ); - path = Sharing; - sourceTree = ""; - }; 399EA74425E6575A00B6FF11 /* Notification */ = { isa = PBXGroup; children = ( @@ -2177,79 +2119,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 +2147,6 @@ 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */, 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */, 39EF06AF25D6C3FC001E1E19 /* PresentationModeAction.swift */, - DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */, - DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */, DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */, ); path = "Actions+Extensions"; @@ -2319,15 +2186,22 @@ isa = PBXGroup; children = ( DC0A35A024C1091400FB58FC /* UserInterfaceContext.swift */, + DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */, 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 = ""; @@ -2374,6 +2248,7 @@ DC8549372183B4CD00782BA8 /* ThemeStyle+Extensions.swift */, DCB44D842186FEF700DAA4CC /* ThemeStyle+DefaultStyles.swift */, DC42244F207CB2500006A2A6 /* NSObject+ThemeApplication.swift */, + DCB1B8A029C7378A00BFF393 /* CSS */, DC7DBA27207F6DFD00E7337D /* UI */, DC7DBA26207F6DEE00E7337D /* Resources */, DC7DBA21207F672700E7337D /* TVG */, @@ -2473,24 +2348,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 = "Alert View"; + sourceTree = ""; + }; + DC298CA22935854F009FA87F /* Authentication Error Handling */ = { + isa = PBXGroup; + children = ( + DC298CA029357809009FA87F /* AccountConnectionAuthErrorConsumer.swift */, + DC298C902934BDAD009FA87F /* AccountAuthenticationUpdater.swift */, + DCB6C4DD24559B1600C1EAE1 /* AccountAuthenticationUpdaterPasswordPromptViewController.swift */, ); - path = "Push Presentation Controller"; + path = "Authentication Error Handling"; sourceTree = ""; }; - DC29F09122974F8000F77349 /* FileList Extensions */ = { + DC298CAA29362534009FA87F /* Location Picker */ = { isa = PBXGroup; children = ( - DCE4E44324C1A3E30051722F /* FileListTableViewController+OpenItemTableViewController.swift */, - DCE4E44C24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift */, - DCE4E45524C1ED8D0051722F /* ClientQueryViewController+InlineMessageSupport.swift */, + DC298CA829362523009FA87F /* ClientLocationPicker.swift */, + DC298CAB29362710009FA87F /* ClientLocationPickerViewController.swift */, ); - path = "FileList Extensions"; + path = "Location Picker"; sourceTree = ""; }; DC2A127B28D06EED0088A2B7 /* Saved Searches */ = { @@ -2516,19 +2399,15 @@ 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 */, + DC3F0C2329828AB500C832DB /* Location Breadcrumbs */, + DC298CAA29362534009FA87F /* Location Picker */, ); path = "View Controllers"; sourceTree = ""; @@ -2544,18 +2423,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 +2437,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 +2501,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,12 +2665,12 @@ children = ( 39B39446238532DC00892E8D /* ThemeTableViewCell.swift */, DCE974B1207E3AF80069FC2B /* ThemeNavigationController.swift */, + DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */, DC0B37962051681600189B9A /* ThemeButton.swift */, 39B394482385334D00892E8D /* ThemeView.swift */, 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */, DC243BF92317B446004FBB5C /* ThemeWindow.swift */, DCAB9CC823417243009091B6 /* ThemedAlertController.swift */, - 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */, DC66A9F5279EEC3100792AC8 /* ResourceViewHost.swift */, ); path = UI; @@ -2761,9 +2699,6 @@ isa = PBXGroup; children = ( DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */, - DC85572A20513B8C00189B9A /* ServerListTableViewController.swift */, - DC85572B20513B8C00189B9A /* ServerListTableViewController.xib */, - 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */, ); path = "Server List"; sourceTree = ""; @@ -2772,6 +2707,7 @@ isa = PBXGroup; children = ( DC82663B28168D2800F91F7D /* ClientContext.swift */, + DC28F827294BB5ED00AC4013 /* SortedItemDataSource.swift */, ); path = Context; sourceTree = ""; @@ -2805,6 +2741,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 +2780,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,16 +2829,30 @@ path = Diagnostic; sourceTree = ""; }; - DCAEB05821F9FB0F0067E147 /* Tools */ = { + DCB1B8A029C7378A00BFF393 /* CSS */ = { isa = PBXGroup; children = ( - DCAEB05E21F9FB370067E147 /* OCBookmarkManager+Tools.swift */, - DC869A582153B1F60088977E /* OCMockingManager+SwiftTools.swift */, - 233BDEB4204FEFE500C06732 /* OwnCloudTests.swift */, - 59B09E6C21AD61F4007827B8 /* UtilsTests.swift */, - DCAEB06021F9FC510067E147 /* EarlGrey+Tools.swift */, + DCB1B8A529C75D4A00BFF393 /* README.md */, + DCB1B89E29C7378200BFF393 /* ThemeCSS.swift */, + DCB1B8A329C73DB800BFF393 /* ThemeCSSRecord.swift */, + DCB1B8A629C75EBD00BFF393 /* ThemeCSS+AutoSelectors.swift */, + DCB1B8A129C7379C00BFF393 /* NSObject+ThemeCSS.swift */, + DCB1B8C329C8A84000BFF393 /* UIView+ThemeCSS.swift */, + DCB1B8AC29C89E0F00BFF393 /* Views */, ); - path = Tools; + path = CSS; + sourceTree = ""; + }; + DCB1B8AC29C89E0F00BFF393 /* Views */ = { + isa = PBXGroup; + children = ( + DCB1B99829D187B400BFF393 /* ThemeCSSButton.swift */, + DCB1B8AA29C89E0900BFF393 /* ThemeCSSLabel.swift */, + DCB1B8C729C8F84800BFF393 /* ThemeCSSProgressView.swift */, + DCB1B99629D1813D00BFF393 /* ThemeCSSTextField.swift */, + DCB1B8C129C8A6E500BFF393 /* ThemeCSSView.swift */, + ); + path = Views; sourceTree = ""; }; DCB2C05C250C1ECD001083CA /* Branding */ = { @@ -2895,6 +2902,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 +3002,7 @@ DC2A128828D076750088A2B7 /* Search */, DC0030BE2350B1CE00BB8570 /* Tools */, DCEA7F38282D3ACA0050A3C0 /* VFS */, + DC3DDEFE287E1AA500E5586D /* UIKit Extensions */, DC774E5B22F44E4A000B11A1 /* ZIP Archive */, DC774E6522F44EA7000B11A1 /* Resources */, ); @@ -3012,12 +3069,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 */, ); @@ -3066,6 +3119,7 @@ DCE5E8942080D77F005F60CE /* image.tvg */, DCE5E8952080D77F005F60CE /* owncloud-logo.tvg */, DCE5E89C2080D780005F60CE /* package-x-generic.tvg */, + DC2A68D429D492B200BFF393 /* space.tvg */, DCB504D7221EF07E007638BE /* status-flash.tvg */, DCE5E89D2080D780005F60CE /* text-calendar.tvg */, DCE5E8992080D780005F60CE /* text-code.tvg */, @@ -3096,12 +3150,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 +3197,30 @@ 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 */, + DCB32FE729E704D600BFF393 /* SelectionCheckmarkButton.swift */, 23FA23E520BFD3D8009A6D73 /* SortBar.swift */, 3912208123436EB80026C290 /* SortMethod.swift */, + DC8AA7BD29DC154400BFF393 /* ItemLayout.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 +3248,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 +3265,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 +3295,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 +3314,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 +3375,8 @@ DCF2DA7524C82C9F0026D790 /* OCFileProviderService.h */, DC049155258C00C400DEDC27 /* OCFileProviderServiceStandby.m */, DC049154258C00C400DEDC27 /* OCFileProviderServiceStandby.h */, + DC6179E628E0578400C7C4E0 /* OCFileProviderSettings.m */, + DC6179E528E0578400C7C4E0 /* OCFileProviderSettings.h */, ); path = "File Provider Services"; sourceTree = ""; @@ -3349,9 +3396,7 @@ DC01CDCB212EDDF600FC8E38 /* TextViewController.swift */, DC6428CF2081406800493A01 /* CollapsibleProgressBar.swift */, 6EA78B8E2179B55400A5216A /* ImageScrollView.swift */, - 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */, 3998F5D422411EDF00B66713 /* BorderedLabel.swift */, - 396BE4C92289500E00B254A9 /* RoundedLabel.swift */, ); path = "UI Elements"; sourceTree = ""; @@ -3408,6 +3453,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 +3502,7 @@ DC080CDC238AE3D00044C5D2 /* App Store */, DC4331F82472E17F002DC0E5 /* EMM */, DCDC20A42399A898003CFF5B /* Enterprise */, + DC8E99DD297E8D1800594697 /* QA */, ); path = Providers; sourceTree = ""; @@ -3497,14 +3566,6 @@ path = Manager; sourceTree = ""; }; - EA9337DC2226DAE00054971F /* Settings */ = { - isa = PBXGroup; - children = ( - EA9337E22226DB070054971F /* SettingsTests.swift */, - ); - name = Settings; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3530,6 +3591,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 +3614,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 +3636,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 +3656,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 +3687,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 +3717,7 @@ buildRules = ( ); dependencies = ( + DC9B4FC829413B400037F8F8 /* PBXTargetDependency */, DC0491D8258CB00000DEDC27 /* PBXTargetDependency */, DCDC0ACE23CD185F00DFE36D /* PBXTargetDependency */, 394A0B0C22EEFD2800603813 /* PBXTargetDependency */, @@ -3663,6 +3725,7 @@ name = ownCloudAppShared; packageProductDependencies = ( DC04920A258CB06A00DEDC27 /* PocketSVG */, + DC9B4FC929413B480037F8F8 /* Down */, ); productName = ownCloudAppShared; productReference = 394A0AF922EEFC2C00603813 /* ownCloudAppShared.framework */; @@ -3709,27 +3772,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 +3879,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1330; + LastUpgradeCheck = 1420; ORGANIZATIONNAME = "ownCloud GmbH"; TargetAttributes = { 233BDE9B204FEFE500C06732 = { @@ -3867,7 +3909,7 @@ }; 394A0AF822EEFC2C00603813 = { CreatedOnToolsVersion = 11.0; - LastSwiftMigration = 1340; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; }; 39A7137F22E79C6700089423 = { @@ -3877,11 +3919,6 @@ 39DC7CCC25C2E1570001E08C = { CreatedOnToolsVersion = 12.2; }; - 59056CA922414F3C00A18A22 = { - CreatedOnToolsVersion = 10.1; - ProvisioningStyle = Automatic; - TestTargetID = 233BDE9B204FEFE500C06732; - }; DC7DBA33207F84BF00E7337D = { CreatedOnToolsVersion = 9.3; ProvisioningStyle = Automatic; @@ -3967,7 +4004,6 @@ DCC0855B2293F1FD008CC05C /* ownCloudApp */, 394A0AF822EEFC2C00603813 /* ownCloudAppShared */, DCC085632293F1FD008CC05C /* ownCloudAppTests */, - 59056CA922414F3C00A18A22 /* ownCloudScreenshotsTests */, ); }; /* End PBXProject section */ @@ -4034,7 +4070,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 +4078,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 +4085,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; }; @@ -4084,6 +4113,7 @@ DCE4E49724C1FB750051722F /* image.tvg in Resources */, DCE4E49A24C1FB750051722F /* status-flash.tvg in Resources */, DCE4E4A024C1FB750051722F /* x-office-document.tvg in Resources */, + DC2A68D529D492B300BFF393 /* space.tvg in Resources */, DCE4E49124C1FB750051722F /* folder-starred.tvg in Resources */, DCE4E4A124C1FB750051722F /* x-office-presentation.tvg in Resources */, DC2FE2DA24C30586002AFDB3 /* Localizable.strings in Resources */, @@ -4113,13 +4143,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 59056CA822414F3C00A18A22 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DCC0855A2293F1FD008CC05C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4151,30 +4174,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 +4191,7 @@ }; 23DFDCF120AEEC77003BD16B /* Update LastGitCommit key in Info.plist */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -4202,44 +4205,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 +4244,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 +4251,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 */, @@ -4319,7 +4266,6 @@ 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */, DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */, 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */, - 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */, DC82D6FA23171339001551C5 /* ScanAction.swift in Sources */, DCC3700724D466D2008B0DEB /* DiagnosticManager.swift in Sources */, 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */, @@ -4329,29 +4275,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,54 +4311,40 @@ 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 */, 025F063324AA163C009D8FC5 /* DisplayExifMetadataAction.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, - 397E276C23D05A5400117B07 /* ServerListToolCell.swift in Sources */, DCC8535823CE1236007BA3EB /* LicenseInAppProductListViewController.swift in Sources */, 23EC775B2137F3DD0032D4E6 /* OCExtensionType+Extension.swift in Sources */, DC3F4522271A23A000ED2383 /* AcknowledgementsTableViewController.swift in Sources */, @@ -4428,37 +4354,25 @@ 4CB8ADDE22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift in Sources */, 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 +4380,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 +4392,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 +4401,153 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 399EA6F625E6544100B6FF11 /* ShareClientItemCell.swift in Sources */, DC0A357E24C0E43C00FB58FC /* ThemeStyle+Extensions.swift in Sources */, + DCB1B8C829C8F84800BFF393 /* ThemeCSSProgressView.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 */, + DCB1B99929D187B400BFF393 /* ThemeCSSButton.swift in Sources */, 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */, - DCE4E44124C1A07E0051722F /* UITableViewController+Extension.swift in Sources */, - DC0A358E24C0E44B00FB58FC /* ThemeableColoredView.swift in Sources */, + DC298C9E2934D6D9009FA87F /* AccountAuthenticationUpdater.swift in Sources */, DC0A356D24C0E42200FB58FC /* PasscodeViewController.swift in Sources */, DC24B2AB25BA316D005783E2 /* Branding+App.swift in Sources */, 0234EF0E2515138B00AE921A /* PasscodeSetupCoordinator.swift in Sources */, + DCB32FE829E704D600BFF393 /* SelectionCheckmarkButton.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 */, + DCB1B8A229C7379C00BFF393 /* NSObject+ThemeCSS.swift in Sources */, + DCB1B8C429C8A84000BFF393 /* UIView+ThemeCSS.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 */, + DCB1B8C229C8A6E500BFF393 /* ThemeCSSView.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 +4557,64 @@ 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 */, + DCB1B89F29C7378200BFF393 /* ThemeCSS.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 */, + DCB1B99729D1813D00BFF393 /* ThemeCSSTextField.swift in Sources */, DCA2EDE4279B1789001F04E6 /* ResourceItemIcon.swift in Sources */, + DC8E99E8297F3BA700594697 /* ActionTapGestureRecognizer.swift in Sources */, DCB5D56B2861BEBE004AF425 /* SearchViewController.swift in Sources */, + DCB1B8A729C75EBD00BFF393 /* ThemeCSS+AutoSelectors.swift in Sources */, 0287DD7D249131E000C912CA /* AppStatistics.swift in Sources */, + DCB1B8A429C73DB800BFF393 /* ThemeCSSRecord.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 */, + DCB1B8AB29C89E0900BFF393 /* ThemeCSSLabel.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 */, + DC2A68D729D4E93300BFF393 /* SharedKeyCommands.swift in Sources */, 399EA6FA25E6544100B6FF11 /* GroupSharingEditTableViewController.swift in Sources */, + DC8AA7BE29DC154400BFF393 /* ItemLayout.swift in Sources */, + DCD68A2F291D9BE400993FF5 /* AccountControllerCell.swift in Sources */, + DC89EA6929958DF500BFF393 /* UIViewController+BrowserNavigation.swift in Sources */, DC0A358B24C0E44B00FB58FC /* ThemeRoundedButton.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4681,19 +4648,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; @@ -4713,6 +4667,7 @@ DCCD778C2604C91B00098573 /* NSDate+ComputedTimes.m in Sources */, DC66F3A623965A1400CF4812 /* NSDate+RFC3339.m in Sources */, DCF575EC2796CBDF003BEBBA /* OCImage+ViewProvider.m in Sources */, + DCB1B8CB29CD847500BFF393 /* NSObject+AnnotatedProperties.m in Sources */, DC24E0EB28B36A81002E4F5B /* OCSearchSegment.m in Sources */, DCF2DA8724C87A330026D790 /* OCCore+FPServices.m in Sources */, DC7C101224B5FD6500227085 /* OCBookmark+AppExtensions.m in Sources */, @@ -4720,6 +4675,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 +4694,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 +4749,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 +4801,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 +4855,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 +5080,7 @@ APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; APP_SHORT_VERSION = 12.0; - APP_VERSION = 228; + APP_VERSION = 256; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -5194,7 +5150,7 @@ APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; APP_SHORT_VERSION = 12.0; - APP_VERSION = 228; + APP_VERSION = 256; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -5316,7 +5272,6 @@ }; 233BDEBD204FEFE600C06732 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 03AE98EEF23B4F4F2C0FDD0F /* Pods-ownCloudTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5337,7 +5292,6 @@ }; 233BDEBE204FEFE600C06732 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D6033BC23D1129172E6D6383 /* Pods-ownCloudTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5537,65 +5491,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 +5799,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 +5907,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..2bb695c69 --- /dev/null +++ b/ownCloud/App Controllers/AppRootViewController.swift @@ -0,0 +1,437 @@ +// +// 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?.cssSelectors = [ .sidebar ] + 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: - Status Bar style + open override var childForStatusBarStyle: UIViewController? { + return contentViewController + } + + open override var contentViewController: UIViewController? { + didSet { + setNeedsStatusBarAppearanceUpdate() + } + } + + // 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() { + self.present(ThemeNavigationController(rootViewController: SettingsViewController()), 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..63379130e 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() @@ -67,7 +68,7 @@ class BookmarkInfoViewController: StaticTableViewController { let vault : OCVault = OCVault(bookmark: bookmark) OnMainThread { - let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) progressView.startAnimating() row.cell?.accessoryView = progressView } @@ -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..bcfb3602d 100644 --- a/ownCloud/Bookmarks/BookmarkViewController.swift +++ b/ownCloud/Bookmarks/BookmarkViewController.swift @@ -40,7 +40,6 @@ class BookmarkViewController: StaticTableViewController { var passwordRow : StaticTableViewRow? var tokenInfoRow : StaticTableViewRow? var deleteAuthDataButtonRow : StaticTableViewRow? - var tokenInfoView : RoundedInfoView? var showOAuthInfoHeader = false var showedOAuthInfoHeader : Bool = false var activeTextField: UITextField? @@ -68,6 +67,8 @@ class BookmarkViewController: StaticTableViewController { var bookmark : OCBookmark? var originalBookmark : OCBookmark? + var generationOptions: [OCAuthenticationMethodKey : Any]? + enum BookmarkViewControllerMode { case create case edit @@ -171,9 +172,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 } @@ -183,12 +183,15 @@ class BookmarkViewController: StaticTableViewController { self?.composeSectionsAndRows(animated: true) } - self?.nameRow?.textField?.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [.foregroundColor : Theme.shared.activeCollection.tableRowColors.secondaryLabelColor]) + if let nameRowTextField = self?.nameRow?.textField { + let placeholderColor = nameRowTextField.getThemeCSSColor(.stroke, selectors: [.placeholder]) ?? .secondaryLabel + nameRowTextField.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [.foregroundColor : placeholderColor]) + } } }, 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) @@ -239,8 +242,6 @@ class BookmarkViewController: StaticTableViewController { credentialsSection = StaticTableViewSection(headerTitle: "Credentials".localized, footerTitle: nil, identifier: "section-credentials", rows: [ usernameRow!, passwordRow! ]) - tokenInfoView = RoundedInfoView(text: "") - // Input focus tracking urlRow?.textField?.delegate = self passwordRow?.textField?.delegate = self @@ -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) @@ -892,12 +913,18 @@ class BookmarkViewController: StaticTableViewController { authMethodName = localizedAuthMethodName } - tokenInfoView?.infoText = "If you 'Continue', you will be prompted to allow the '{{app.name}}' app to open the {{authmethodName}} login page where you can enter your credentials.".localized(["authmethodName" : authMethodName]) + let tokenMessageView = ComposedMessageView.infoBox(additionalElements: [ + .text("If you 'Continue', you will be prompted to allow the '{{app.name}}' app to open the {{authmethodName}} login page where you can enter your credentials.".localized(["authmethodName" : authMethodName]), style: .system(textStyle: .body, weight: .semibold), alignment: .centered, cssSelectors: [.info]) + ]) + tokenMessageView.cssSelector = .info + tokenMessageView.backgroundView?.cssSelector = .info + tokenMessageView.backgroundInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 0, trailing: 20) + tokenMessageView.elementInsets = NSDirectionalEdgeInsets(top: 30, leading: 20, bottom: 10, trailing: 20) - self.tableView.tableHeaderView = tokenInfoView + self.tableView.tableHeaderView = tokenMessageView self.tableView.layoutTableHeaderView() } else { - self.tableView.tableHeaderView?.removeFromSuperview() + self.tableView.tableHeaderView = nil } // Continue button: show always @@ -976,7 +1003,10 @@ class BookmarkViewController: StaticTableViewController { placeholderString = bookmark.shortName } - self.nameRow?.textField?.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [.foregroundColor : Theme.shared.activeCollection.tableRowColors.secondaryLabelColor]) + if let nameRowTextField = nameRow?.textField { + let placeholderColor = nameRowTextField.getThemeCSSColor(.stroke, selectors: [.placeholder]) ?? .secondaryLabel + nameRowTextField.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [.foregroundColor : placeholderColor]) + } } // URL @@ -993,7 +1023,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 +1060,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 +1132,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/Action+UserInterface.swift b/ownCloud/Client/Actions/Action+UserInterface.swift index 8613c82d7..3b6f7aa9f 100644 --- a/ownCloud/Client/Actions/Action+UserInterface.swift +++ b/ownCloud/Client/Actions/Action+UserInterface.swift @@ -27,14 +27,14 @@ extension Action { class public func cardViewController(for item: OCItem, with context: ActionContext, progressHandler: ActionProgressHandler? = nil, completionHandler: ((Action, Error?) -> Void)? = nil) -> UIViewController? { guard let core = context.core else { return nil } - let tableViewController = MoreStaticTableViewController(style: .grouped) + let tableViewController = MoreStaticTableViewController(style: .insetGrouped) let header = MoreViewHeader(for: item, with: core) let moreViewController = FrameViewController(header: header, viewController: tableViewController) if core.connectionStatus == .online { if core.connection.capabilities?.sharingAPIEnabled == 1 { if item.isSharedWithUser || item.isShared { - let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) progressView.startAnimating() let row = StaticTableViewRow(rowWithAction: nil, title: "Searching Shares…".localized, alignment: .left, accessoryView: progressView, identifier: "share-searching") @@ -66,8 +66,6 @@ extension Action { } } - let title = NSAttributedString(string: "Actions".localized, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .heavy)]) - let actions = Action.sortedApplicableActions(for: context) actions.forEach({ @@ -82,7 +80,7 @@ extension Action { let actionsRows: [StaticTableViewRow] = actions.compactMap({return $0.provideStaticRow()}) - tableViewController.addSection(MoreStaticTableViewSection(headerAttributedTitle: title, identifier: "actions-section", rows: actionsRows)) + tableViewController.addSection(StaticTableViewSection(headerTitle: nil, identifier: "actions-section", rows: actionsRows)) return moreViewController } @@ -220,7 +218,7 @@ private extension Action { andPresent: GroupSharingTableViewController(core: core, item: item), on: context.viewController) } - }, title: title, style: .plain, image: nil, imageWidth:nil, imageTintColorKey: nil, alignment: .left, identifier: "share-add-group", accessoryView: UIImageView(image: UIImage(named: "group"))) + }, title: title, style: .plain, image: nil, imageWidth:nil, alignment: .left, identifier: "share-add-group", accessoryView: UIImageView(image: UIImage(named: "group"))) return addGroupRow } 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..741aeb09f 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift @@ -103,9 +103,11 @@ 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 } + let fallbackIcon = (fileType.mimeType != nil) ? ResourceItemIcon.iconFor(mimeType: fileType.mimeType!) : .file + OnMainThread { let documentNameViewController = NamingViewController( with: self.core, defaultName: suggestedName, stringValidator: { name in if name.contains("/") || name.contains("\\") { @@ -120,13 +122,29 @@ class CreateDocumentAction: Action { return (true, nil, nil) } - }, completion: { newFileName, _ in + }, fallbackIcon: fallbackIcon, completion: { newFileName, _ in guard let newFileName = newFileName, let core = self.core else { self.completed() return } 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..3040dd2d8 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift @@ -36,7 +36,7 @@ class ImportPasteboardAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.importpasteboard") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Paste".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreFolder, .keyboardShortcut] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreFolder, .keyboardShortcut, .emptyFolder] } override class var keyCommand : String? { return "V" } override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } @@ -47,6 +47,12 @@ class ImportPasteboardAction : Action { // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { let pasteboard = UIPasteboard.general + + if forContext.items.first?.permissions.contains(.createFolder) == false || + forContext.items.first?.permissions.contains(.createFile) == false { + return .none + } + if pasteboard.numberOfItems > 0 { return .afterMiddle } @@ -194,18 +200,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..5cc6c6c27 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() @@ -117,20 +120,13 @@ class OpenInAction: Action { sourceRect.size.height = 0.0 self.interactionController?.presentOptionsMenu(from: sourceRect, in: hostViewController.view, animated: true) - } else if let sender = self.context.sender as? UITabBarController { - var sourceRect = sender.view.frame - sourceRect.origin.y = viewController.view.frame.size.height - sourceRect.size.width = 0.0 - sourceRect.size.height = 0.0 - - self.interactionController?.presentOptionsMenu(from: sourceRect, in: sender.view, animated: true) } else if let barButtonItem = self.context.sender as? UIBarButtonItem { self.interactionController?.presentOptionsMenu(from: barButtonItem, animated: true) - } else if let cell = self.context.sender as? UITableViewCell, let clientQueryViewController = viewController as? ClientQueryViewController { - if let indexPath = clientQueryViewController.tableView.indexPath(for: cell) { - let cellRect = clientQueryViewController.tableView.rectForRow(at: indexPath) - self.interactionController?.presentOptionsMenu(from: cellRect, in: clientQueryViewController.tableView, animated: true) - } +// } else 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,24 +134,20 @@ 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) } if UIDevice.current.isIpad { - if let sender = self.context.sender as? UITabBarController { - var sourceRect = sender.view.frame - sourceRect.origin.y = viewController.view.frame.size.height - sourceRect.size.width = 0.0 - sourceRect.size.height = 0.0 - - activityController.popoverPresentationController?.sourceView = sender.view - activityController.popoverPresentationController?.sourceRect = sourceRect - } else { - activityController.popoverPresentationController?.sourceView = viewController.view - activityController.popoverPresentationController?.sourceRect = viewController.view.frame - } + activityController.popoverPresentationController?.sourceView = viewController.view + activityController.popoverPresentationController?.sourceRect = viewController.view.frame } viewController.present(activityController, animated: true, completion: nil) 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..0f24bbabf 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) + } } } @@ -235,8 +248,8 @@ class EditDocumentViewController: QLPreviewController, Themeable { } func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.navigationController?.navigationBar.backgroundColor = collection.navigationBarColors.backgroundColor - self.view.backgroundColor = collection.tableBackgroundColor + self.navigationController?.navigationBar.applyThemeCollection(collection) + self.view.backgroundColor = collection.css.getColor(.fill, selectors: [.table], for: self.view) } } diff --git a/ownCloud/Client/Actions/ImageMetadataViewController.swift b/ownCloud/Client/Actions/ImageMetadataViewController.swift index e93d0f009..405f55555 100644 --- a/ownCloud/Client/Actions/ImageMetadataViewController.swift +++ b/ownCloud/Client/Actions/ImageMetadataViewController.swift @@ -559,7 +559,7 @@ class ImageMetadataViewController: StaticTableViewController { private var imageProperties: [String : Any]? private let gpsSection = StaticTableViewSection(headerTitle: "GPS Location".localized) - private let activityIndicatorView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + private let activityIndicatorView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) public init(core inCore: OCCore, item inItem: OCItem, url:URL) { core = inCore diff --git a/ownCloud/Client/Actions/Scanner/ScanViewController.swift b/ownCloud/Client/Actions/Scanner/ScanViewController.swift index 46d906a1b..97e18eac2 100644 --- a/ownCloud/Client/Actions/Scanner/ScanViewController.swift +++ b/ownCloud/Client/Actions/Scanner/ScanViewController.swift @@ -77,8 +77,6 @@ class ScanPageCell : UICollectionViewCell, Themeable { self.layer.shadowRadius = cellShadowRadius self.layer.shadowOpacity = cellShadowOpacity self.layer.shadowOffset = cellShadowOffset - - Theme.shared.register(client: self, applyImmediately: true) } required init?(coder: NSCoder) { @@ -89,8 +87,18 @@ class ScanPageCell : UICollectionViewCell, Themeable { Theme.shared.unregister(client: self) } + private var _hasRegistered = false + open override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_hasRegistered { + _hasRegistered = true + Theme.shared.register(client: self) + } + } + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.backgroundColor = collection.tableRowColors.backgroundColor + backgroundColor = collection.css.getColor(.fill, selectors: [.table], for: nil) } } @@ -131,7 +139,7 @@ class ScanPagesCollectionViewController : UICollectionViewController, UICollecti } func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.collectionView.backgroundColor = collection.tableBackgroundColor + collectionView.backgroundColor = collection.css.getColor(.fill, selectors: [.table], for: collectionView) } required init?(coder: NSCoder) { @@ -371,7 +379,7 @@ class ScanViewController: StaticTableViewController { formatSegmentedControl?.selectedSegmentIndex = 0 formatSegmentedControl?.isUserInteractionEnabled = true formatSegmentedControl?.addTarget(self, action: #selector(updateExportFormat), for: .valueChanged) - optionsSection.add(row: StaticTableViewRow(label: "File format".localized, alignment: .left, accessoryView: formatSegmentedControl, identifier: "format")) + optionsSection.add(row: StaticTableViewRow(label: "File format".localized, accessoryView: formatSegmentedControl, identifier: "format")) // - One file per page oneFilePerPageRow = StaticTableViewRow(switchWithAction: nil, title: "Create one file per page".localized, value: false, identifier: "one-file-per-page") 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/ExternalBrowserBusyHandler.swift b/ownCloud/Client/ExternalBrowserBusyHandler.swift index ce6fee234..fdf6c6b0e 100644 --- a/ownCloud/Client/ExternalBrowserBusyHandler.swift +++ b/ownCloud/Client/ExternalBrowserBusyHandler.swift @@ -41,14 +41,14 @@ class ExternalBrowserBusyHandler: UIViewController, Themeable { } } - var infoLabel = UILabel() + var infoLabel = ThemeCSSLabel(withSelectors: [.secondary]) var cancelButton = ThemeButton(type: .custom) var cancelHandler : (() -> Void)? override func loadView() { let backgroundView = UIView() - let activityIndicator = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + let activityIndicator = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) activityIndicator.translatesAutoresizingMaskIntoConstraints = false activityIndicator.startAnimating() @@ -87,13 +87,17 @@ class ExternalBrowserBusyHandler: UIViewController, Themeable { view = backgroundView } - override func viewDidLoad() { - Theme.shared.register(client: self) + private var themeRegistered = false + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if !themeRegistered { + themeRegistered = true + Theme.shared.register(client: self) + } } func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - view.backgroundColor = collection.tableBackgroundColor - infoLabel.textColor = collection.tableRowColors.secondaryLabelColor + view.backgroundColor = collection.css.getColor(.fill, selectors: [.table], for: view) } @objc func cancel() { 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/PhotoAlbumTableViewController.swift b/ownCloud/Client/PhotoAlbumTableViewController.swift index dc261b9e3..704679006 100644 --- a/ownCloud/Client/PhotoAlbumTableViewController.swift +++ b/ownCloud/Client/PhotoAlbumTableViewController.swift @@ -66,7 +66,7 @@ class PhotoAlbumTableViewController : UITableViewController, Themeable { // This is used just to pass the selection callback to PhotoSelectionViewController when an album is selected var selectionCallback: PhotosSelectedCallback? - private let activityIndicatorView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + private let activityIndicatorView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) // MARK: - UIViewController lifecycle diff --git a/ownCloud/Client/PhotoSelectionViewController.swift b/ownCloud/Client/PhotoSelectionViewController.swift index f01da6577..fe1b5bb5e 100644 --- a/ownCloud/Client/PhotoSelectionViewController.swift +++ b/ownCloud/Client/PhotoSelectionViewController.swift @@ -380,7 +380,7 @@ class PhotoSelectionViewController: UICollectionViewController, Themeable { // MARK: - iOS13 gesture based multiple selection -@available(iOS 13, *) extension PhotoSelectionViewController { +extension PhotoSelectionViewController { override func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool { return !DisplaySettings.shared.preventDraggingFiles } 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..f13a59aef 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,69 +48,82 @@ 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 + + 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) - self.clientContext = clientContext + parentFolderQuery = query + queryDatasource = parentFolderQuery?.queryResultsDataSource - if query.state == .stopped { - self.core?.start(query) - queryStarted = true + 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) + } } } - } - } - Theme.shared.register(client: self) + self.items = self.applyMediaFilesFilter(items: allItems) + + self.updatePageViewControllerDatasource() + }, on: .main, trackDifferences: true, performInitialUpdate: true) + } } required init?(coder: NSCoder) { 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() @@ -134,9 +150,15 @@ class DisplayHostViewController: UIPageViewController { NotificationCenter.default.addObserver(self, selector: #selector(handlePlayPreviousMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) } + private var registered = false override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + if !registered { + registered = true + Theme.shared.register(client: self) + } + self.autoEnablePageScrolling() } @@ -180,7 +202,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 +274,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 +309,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 @@ -355,7 +378,7 @@ extension DisplayHostViewController: UIPageViewControllerDelegate { extension DisplayHostViewController: Themeable { func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.view.backgroundColor = collection.tableBackgroundColor + view.backgroundColor = collection.css.getColor(.fill, for: self.view) } } diff --git a/ownCloud/Client/Viewer/DisplayViewController.swift b/ownCloud/Client/Viewer/DisplayViewController.swift index 810e2b53c..9febea110 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) @@ -178,11 +180,12 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { // MARK: - Subviews / UI elements private var iconImageView = ResourceViewHost() - private var progressView = UIProgressView(progressViewStyle: .bar) + private var progressView = ThemeCSSProgressView(progressViewStyle: .bar) private var cancelButton = ThemeButton(type: .custom) - private var metadataInfoLabel = UILabel() + private var metadataInfoLabel = ThemeCSSLabel(withSelectors: [.primary, .metadata]) private var showPreviewButton = ThemeButton(type: .custom) - private var infoLabel = UILabel() + private var primaryUnviewableActionButton = ThemeButton(type: .custom) + private var infoLabel = ThemeCSSLabel(withSelectors: [.secondary]) private var connectionActivityView = UIActivityIndicatorView(style: .medium) // MARK: - Editing delegate @@ -198,6 +201,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { required init() { super.init(nibName: nil, bundle: nil) + cssSelector = .viewer } required init?(coder aDecoder: NSCoder) { @@ -221,6 +225,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 +245,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 +292,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), @@ -290,14 +305,16 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { ]) } - override func viewDidLoad() { - super.viewDidLoad() - Theme.shared.register(client: self) - } + private var _themeRegistered = false override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if !_themeRegistered { + _themeRegistered = true + Theme.shared.register(client: self) + } + startQuery() updateDisplayTitleAndButtons() @@ -348,19 +365,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 +421,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 +492,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 +527,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 +554,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 +568,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 +582,8 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { metadataInfoLabel.isHidden = true infoLabel.isHidden = true cancelButton.isHidden = true + } else { + primaryUnviewableActionButton.isHidden = !hasPrimaryUnviewableAction } case .previewFailed: @@ -689,11 +736,7 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { // MARK: - Themeable implementation func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - progressView.applyThemeCollection(collection) - cancelButton.applyThemeCollection(collection) - metadataInfoLabel.applyThemeCollection(collection) - showPreviewButton.applyThemeCollection(collection) - infoLabel.applyThemeCollection(collection) + // For subclassing } // MARK: - Query delegate @@ -728,3 +771,8 @@ class DisplayViewController: UIViewController, Themeable, OCQueryDelegate { } } } + +extension ThemeCSSSelector { + static let viewer = ThemeCSSSelector(rawValue: "viewer") + static let metadata = ThemeCSSSelector(rawValue: "metadata") +} diff --git a/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift b/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift index 2a33564db..6c3cdd481 100644 --- a/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift +++ b/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift @@ -31,7 +31,7 @@ class DownloadItemsHUDViewController: CardViewController { var messageLabel : UILabel var cancelButton : UIButton - var progressView : UIProgressView + var progressView : ThemeCSSProgressView var progressSummarizer : ProgressSummarizer weak var core : OCCore? @@ -41,8 +41,9 @@ class DownloadItemsHUDViewController: CardViewController { self.completion = completion messageLabel = UILabel() - progressView = UIProgressView(progressViewStyle: .bar) + progressView = ThemeCSSProgressView(progressViewStyle: .bar) cancelButton = ThemeButton() + cancelButton.cssSelector = .cancel progressSummarizer = ProgressSummarizer() super.init(nibName: nil, bundle: nil) @@ -207,9 +208,7 @@ class DownloadItemsHUDViewController: CardViewController { // MARK: - Themeable override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - progressView.applyThemeCollection(collection) messageLabel.applyThemeCollection(collection) - cancelButton.applyThemeCollection(collection) super.applyThemeCollection(theme: theme, collection: collection, event: event) } diff --git a/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift b/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift index eb8fe8a6c..debb8ada9 100644 --- a/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift +++ b/ownCloud/Client/Viewer/Image/ImageDisplayViewController.swift @@ -64,7 +64,7 @@ class ImageDisplayViewController : DisplayViewController { let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, - kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary + kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as [CFString : Any] as CFDictionary serialQueue.async { if let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) { 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/Client/Viewer/QuickLook/PreviewViewController.swift b/ownCloud/Client/Viewer/QuickLook/PreviewViewController.swift index f921329db..0cd7d7944 100644 --- a/ownCloud/Client/Viewer/QuickLook/PreviewViewController.swift +++ b/ownCloud/Client/Viewer/QuickLook/PreviewViewController.swift @@ -76,7 +76,7 @@ class PreviewViewController : DisplayViewController, QLPreviewControllerDataSour qlPreviewController!.view.addSubview(overlayView!) qlPreviewController!.didMove(toParent: self) - qlPreviewController?.overrideUserInterfaceStyle = Theme.shared.activeCollection.interfaceStyle.userInterfaceStyle + qlPreviewController?.overrideUserInterfaceStyle = Theme.shared.activeCollection.css.getUserInterfaceStyle() qlPreviewController!.view.translatesAutoresizingMaskIntoConstraints = false @@ -137,7 +137,7 @@ class PreviewViewController : DisplayViewController, QLPreviewControllerDataSour override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { super.applyThemeCollection(theme: theme, collection: collection, event: event) - qlPreviewController?.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle + qlPreviewController?.overrideUserInterfaceStyle = collection.css.getUserInterfaceStyle() } } diff --git a/ownCloud/Import/ImportFilesController.swift b/ownCloud/Import/ImportFilesController.swift index 251310264..b08962981 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,108 @@ 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 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: ", ") */ - completion(error) - return - } - } + if !isVisible { + // Create new picker + isVisible = true - 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)") + 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) + }) - completion(error) - return + 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 +209,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 +279,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/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index bcd27903c..5242ed513 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -1151,60 +1151,6 @@ extension PhotoSelectionViewController { } } -extension PasscodeViewController { - - public override var keyCommands: [UIKeyCommand]? { - var keyCommands : [UIKeyCommand] = [] - for i in 0 ..< 10 { - keyCommands.append( - UIKeyCommand.ported(input:String(i), - modifierFlags: [], - action: #selector(self.performKeyCommand(sender:)), - discoverabilityTitle: String(i)) - ) - } - - keyCommands.append( - UIKeyCommand.ported(input: "\u{8}", - modifierFlags: [], - action: #selector(self.performKeyCommand(sender:)), - discoverabilityTitle: "Delete".localized) - ) - - if cancelButton?.isHidden == false { - keyCommands.append( - - UIKeyCommand.ported(input: UIKeyCommand.inputEscape, - modifierFlags: [], - action: #selector(self.performKeyCommand(sender:)), - discoverabilityTitle: "Cancel".localized) - ) - } - - return keyCommands - } - - override open var canBecomeFirstResponder: Bool { - return true - } - - @objc func performKeyCommand(sender: UIKeyCommand) { - guard let key = sender.input else { - return - } - - switch key { - case "\u{8}": - deleteLastDigit() - case UIKeyCommand.inputEscape: - cancelHandler?(self) - default: - appendDigit(digit: key) - } - - } -} - extension DisplayHostViewController { override var keyCommands: [UIKeyCommand]? { @@ -1527,3 +1473,4 @@ extension FrameViewController { } } } + diff --git a/ownCloud/Licensing/Offers/LicenseOfferButton.swift b/ownCloud/Licensing/Offers/LicenseOfferButton.swift index 02dc0ee5e..9402c0067 100644 --- a/ownCloud/Licensing/Offers/LicenseOfferButton.swift +++ b/ownCloud/Licensing/Offers/LicenseOfferButton.swift @@ -35,6 +35,8 @@ class LicenseOfferButton: ThemeButton { self.buttonHorizontalPadding = 23 self.buttonCornerRadius = .round + self.cssSelector = .purchase + originalTitle = title self.setTitle(title, for: .normal) @@ -52,6 +54,8 @@ class LicenseOfferButton: ThemeButton { self.buttonVerticalPadding = 15 self.buttonCornerRadius = .medium + self.cssSelector = .purchase + self.setTitle(title, for: .normal) if let action = action { diff --git a/ownCloud/Licensing/Offers/LicenseOfferView.swift b/ownCloud/Licensing/Offers/LicenseOfferView.swift index b5ad9de7c..4e43924ee 100644 --- a/ownCloud/Licensing/Offers/LicenseOfferView.swift +++ b/ownCloud/Licensing/Offers/LicenseOfferView.swift @@ -67,7 +67,7 @@ class LicenseOfferView: UIView, Themeable { var titleLabel : UILabel? var descriptionLabel : UILabel? - var pricingDivider : UIView? + var pricingDivider : ThemeCSSView? var pricingLabel : UILabel? var purchaseButton : LicenseOfferButton? @@ -173,7 +173,7 @@ class LicenseOfferView: UIView, Themeable { descriptionLabel.textAlignment = .center pricingLabel.textAlignment = .center - pricingDivider = UIView() + pricingDivider = ThemeCSSView(withSelectors: [.separator]) pricingDivider?.translatesAutoresizingMaskIntoConstraints = false guard let pricingDivider = pricingDivider else { return } @@ -226,9 +226,6 @@ class LicenseOfferView: UIView, Themeable { titleLabel?.applyThemeCollection(collection) descriptionLabel?.applyThemeCollection(collection) pricingLabel?.applyThemeCollection(collection) - purchaseButton?.applyThemeCollection(collection, itemStyle: .purchase) - - pricingDivider?.backgroundColor = collection.tableSeparatorColor ?? .gray } @objc func takeOffer() { @@ -240,7 +237,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/Product List/LicenseInAppPurchaseFeatureView.swift b/ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift index d1fda2fac..648cf3887 100644 --- a/ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift +++ b/ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift @@ -127,7 +127,6 @@ class LicenseInAppPurchaseFeatureView: UIView, Themeable { func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { titleLabel?.applyThemeCollection(collection) descriptionLabel?.applyThemeCollection(collection) - purchaseButton?.applyThemeCollection(collection, itemStyle: .purchase) } @objc func takeOffer() { 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/Messages/MessageGroupCell.swift b/ownCloud/Messages/MessageGroupCell.swift index ad856ee53..98be4228b 100644 --- a/ownCloud/Messages/MessageGroupCell.swift +++ b/ownCloud/Messages/MessageGroupCell.swift @@ -57,8 +57,8 @@ class MessageGroupCell: ThemeTableViewCell { var applyAllContainer : UIView? var applyAllSwitch : UISwitch? - var applyAllLabel : UILabel? - var showAllLabel : UIButton? + var applyAllLabel : ThemeCSSLabel? + var showAllButton : ThemeCSSButton? var badgeLabel : RoundedLabel? var noBottomSpacing : Bool = false @@ -134,7 +134,7 @@ class MessageGroupCell: ThemeTableViewCell { } if applyAllLabel == nil { - applyAllLabel = UILabel() + applyAllLabel = ThemeCSSLabel() applyAllLabel?.translatesAutoresizingMaskIntoConstraints = false applyAllLabel?.font = UIFont.systemFont(ofSize: UIFont.systemFontSize) applyAllLabel?.text = "Apply to all".localized @@ -159,17 +159,17 @@ class MessageGroupCell: ThemeTableViewCell { badgeLabel?.isHidden = true - if showAllLabel == nil { - showAllLabel = UIButton(type: .system) - showAllLabel?.translatesAutoresizingMaskIntoConstraints = false - showAllLabel?.addTarget(self, action: #selector(showAllIssues), for: .primaryActionTriggered) + if showAllButton == nil { + showAllButton = ThemeCSSButton(type: .system) + showAllButton?.translatesAutoresizingMaskIntoConstraints = false + showAllButton?.addTarget(self, action: #selector(showAllIssues), for: .primaryActionTriggered) - applyAllContainer?.addSubview(showAllLabel!) + applyAllContainer?.addSubview(showAllButton!) } - showAllLabel?.setTitle("\("Show all".localized) (\(multiMessageCount))", for: .normal) + showAllButton?.setTitle("\("Show all".localized) (\(multiMessageCount))", for: .normal) - if setupLayout, let applyAllContainer = applyAllContainer, let applyAllSwitch = applyAllSwitch, let applyAllLabel = applyAllLabel, let showAllLabel = showAllLabel { + if setupLayout, let applyAllContainer = applyAllContainer, let applyAllSwitch = applyAllSwitch, let applyAllLabel = applyAllLabel, let showAllButton = showAllButton { NSLayoutConstraint.activate([ applyAllSwitch.topAnchor.constraint(equalTo: applyAllContainer.topAnchor, constant: applyAllSwitchVerticalInset), applyAllSwitch.leftAnchor.constraint(equalTo: applyAllContainer.leftAnchor, constant: applyAllSwitchHorizontalInset), @@ -178,9 +178,9 @@ class MessageGroupCell: ThemeTableViewCell { applyAllLabel.leftAnchor.constraint(equalTo: applyAllSwitch.rightAnchor, constant: applyAllSwitchHorizontalSpacing), applyAllLabel.centerYAnchor.constraint(equalTo: applyAllSwitch.centerYAnchor), - showAllLabel.leftAnchor.constraint(greaterThanOrEqualTo: applyAllLabel.rightAnchor, constant: showAllSwitchHorizontalInset), - showAllLabel.rightAnchor.constraint(equalTo: applyAllContainer.rightAnchor, constant: -showAllSwitchHorizontalInset), - showAllLabel.centerYAnchor.constraint(equalTo: applyAllSwitch.centerYAnchor) + showAllButton.leftAnchor.constraint(greaterThanOrEqualTo: applyAllLabel.rightAnchor, constant: showAllSwitchHorizontalInset), + showAllButton.rightAnchor.constraint(equalTo: applyAllContainer.rightAnchor, constant: -showAllSwitchHorizontalInset), + showAllButton.centerYAnchor.constraint(equalTo: applyAllSwitch.centerYAnchor) ]) } @@ -249,11 +249,4 @@ class MessageGroupCell: ThemeTableViewCell { delegate?.cell(self, showMessagesLike: message) } } - - override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - super.applyThemeCollection(theme: theme, collection: collection, event: event) - - applyAllLabel?.textColor = collection.tableRowColors.labelColor - showAllLabel?.applyThemeCollection(collection) - } } 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..e18efd0af 100644 --- a/ownCloud/Release Notes/ReleaseNotesHostViewController.swift +++ b/ownCloud/Release Notes/ReleaseNotesHostViewController.swift @@ -30,14 +30,14 @@ class ReleaseNotesHostViewController: UIViewController { private let headerHeight : CGFloat = 60.0 // MARK: - Instance Variables - var titleLabel = UILabel() + var titleLabel = ThemeCSSLabel(withSelectors: [.title]) var proceedButton = ThemeButton() var footerButton = UIButton() override func viewDidLoad() { super.viewDidLoad() - Theme.shared.register(client: self) + self.cssSelector = .releaseNotes ReleaseNotesDatasource.setUserPreferenceValue(NSString(utf8String: VendorServices.shared.appBuildNumber), forClassSettingsKey: .lastSeenReleaseNotesVersion) @@ -89,6 +89,7 @@ class ReleaseNotesHostViewController: UIViewController { bottomView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ]) + proceedButton.cssSelectors = [.proceed] proceedButton.setTitle("Proceed".localized, for: .normal) proceedButton.translatesAutoresizingMaskIntoConstraints = false proceedButton.addTarget(self, action: #selector(dismissView), for: .touchUpInside) @@ -107,6 +108,7 @@ class ReleaseNotesHostViewController: UIViewController { footerButton.titleLabel?.adjustsFontForContentSizeCategory = true footerButton.titleLabel?.numberOfLines = 0 footerButton.titleLabel?.textAlignment = .center + footerButton.cssSelectors = [.subtitle] footerButton.translatesAutoresizingMaskIntoConstraints = false footerButton.addTarget(self, action: #selector(rateApp), for: .touchUpInside) bottomView.addSubview(footerButton) @@ -122,7 +124,7 @@ class ReleaseNotesHostViewController: UIViewController { proceedButton.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor, constant: padding), proceedButton.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor, constant: padding * -1), proceedButton.heightAnchor.constraint(equalToConstant: buttonHeight), - proceedButton.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor, constant: smallPadding * -1) + proceedButton.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor, constant: smallPadding * -2) ]) NSLayoutConstraint.activate([ @@ -132,6 +134,8 @@ class ReleaseNotesHostViewController: UIViewController { containerView.bottomAnchor.constraint(equalTo: bottomView.topAnchor) ]) } + + Theme.shared.register(client: self) } deinit { @@ -155,18 +159,14 @@ class ReleaseNotesHostViewController: UIViewController { // MARK: - Themeable implementation extension ReleaseNotesHostViewController : Themeable { func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - - self.view.backgroundColor = collection.tableBackgroundColor - titleLabel.applyThemeCollection(collection, itemStyle: .title) - proceedButton.backgroundColor = collection.neutralColors.normal.background - proceedButton.setTitleColor(collection.neutralColors.normal.foreground, for: .normal) - footerButton.setTitleColor(collection.tableRowColors.labelColor, for: .normal) + view.apply(css: collection.css, selectors: nil, properties: [.fill]) + footerButton.apply(css: collection.css, properties: [.stroke]) } } 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 { @@ -264,3 +268,7 @@ extension ReleaseNotesDatasource : OCClassSettingsSupport { ] } } + +extension ThemeCSSSelector { + static let releaseNotes = ThemeCSSSelector(rawValue: "releaseNotes") +} 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 0bf4e5781b4f7577d965ae77ebfa92c3142ab7f3..8bc41a4f2fc4cf35c4b99cedf102cddac6ff1170 100644 GIT binary patch delta 117 zcmey-z`Exr%Z4Rk2`&tY3`Gpt3=MS&|NwS~R(-HJcQB*8~7J0I5}g=RlhQkw(eV zF;i4A^=K*GJC$~~2e6S}Bq0fcbCtx96nIj*r3^z>ea`-ieXuGzR>O#bH?3QdM3DvP z;4sIqEhP@m)tL}j%Z1i6{D&Y-O~Bp_jK4+~5lc~Cye-ct#IMvQTB_UsVX!B+8LYY{_d)>gyx7&5LtJC)m>A0kVS AkpKVy 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 db8e732dcb0a0254e8197fc32c8368b1b33c34f7..8f9781f0491f801873679a02c9d62274a9890466 100644 GIT binary patch delta 4808 zcmb_gT})iZ6`rdYN-ze@dfBYk+r7J3CZqwEI3S0LHV`0P|FG1HK~5}H3tq$4un>0F zD2}R4-ulqiPEMqYqPmf#x~dY|IOV~uTBUI+RsB)_Xq8G;BBfOx`cSETi26{aPQP!4 zyZ0^z3$+otJ9FopIdgu#bLL$B`m357AJp79+PwB`%iFcPht-nGtE?)iqC9^kA`iY& zx0Vl&)Rmu6�U#f;6ulQww0oL7xMCv+RxavMQ(tH-w&t&^dJu%u8Fad$fY;Pzmhm$CJ>9Y-d19O{p(I-UV%- z4HL;p!}2(H29$CER?O?gyV-SjTZSNm9?ap%@5r1KOgzVc9KwXwd`8u8*s=s6^AJcK z`OMI@QhqP^!prqx>bNC?^TRz>WbH=$I~TJj;l_&)Vj{JjYuxq14qZ8{qd2cEgsl?4 z9y^s#-dSfig<@3u)!>3OFJPav{h0IfMQ!Q&+I=#%Qg;soJ6C(HxE#2CP`N}nA2UgKfr%aR6NQFZzJ@r1_B9OAXC z-dD5Q&w14)sg?e==dZxNIQ0v>~sc zN|K80jk?jbfywhY#nhQa7=w(F%Qe9ecQN6q-vUNbkeG6LHRKFVTKn!zyGqIDVsYo# zL90Qov^4kl@!+1~R;~PoJsE6WR6eS+x7TWupS869Z`a9(q5g=AB<;tY~x)1eTGUW^;j(fUB}< zSth?9t>LUBov()E%4%!4sJXK~&^ zCKcrQn!qPEAS+jo#^hA*K6z=i0m;p(y~867ygN4U@t5K%Mdhhu0p~Xnt8QSEI*93x zg_z9J*6J+knASlhAk8&XeP6f^K~CwcIWi3Pf+(OjtYnM3NutOZN->QP}cPV{@iGr z(9zT-4fQ3T>PdaVgr+gCSV`y*c84=^?La8%$z~C`fO9i=^7w@tozM}hz0F|6wIuIT zr?N%<@k%gSL>)v?!*;fhHpHe8%g+zk^5;@Q-s@~_$!VAqY+3v*;VvY( zpUbT9%yMeng|`tnh1p=LxW7q8rS?u#ee&DR_TzU7ri2f*1mi0<)AU0QnlZ#~zv|bO zoPF}2C&Tjh9y{pPiKFox2czu6}`1VTSNgYs}A@l3) za^sbw3BRg!cgR?_0ZbB_um=}*y<2Wpn_9y z9fO*4zIYh-R%4sh5G9hB5(A*pRs1;N;S5v388~PKZ{9v+eWS_wSD)4RF?D6mn?2Tj zvbs{|{Ib<*)XlVe`&PA-(%BQb3)Ol(;JnpiwQ5Ny%k?+E)oaz(-b@TwZyr42wp+Yo z+42z;Od+?M_ZqMYhg>}uE?3^vi5Bm-}o{_wucaXB&idO?=GO^I>ce(Xqpge^b&S0%eQ3*b0R7#x&Us`=$ z&rYZzyp3W$j`1|shcS+Wn#A0%PeAyv&RkbTD{V^twwI2eEuyl2DRYy_7v%V^Z|x-$VWjUcOY2|LKk3(y2_`Uz}e zYEaA~elH?4j67Mm%1@RwW+tv9n6Yn}0uKu_sqAW4wQ@Sue4VCqEolXve@ zILz^Gtzy|{YF0uhnOlJ<`&zEi9P|0WQ_ZK#l6239(Iqpb4dUEXXCBM~r7Q;2oRWz` zBZ0T9nekr@{5<7^acj{#?l<4MdpdO$jk z_aycJZFmgWcuY0iTrLAGqXrSnlU%@=)$&9I-XxA|%()tGWzCjW0CuNwL)bVV32ajI zFw}Elmjv)qJ}(WzZ$0B!{&m!o#3* zr)hr72S8DN{l{20OAznHO9+?^m{kr%$P6q}RbV?SG8PXQ7&7Fhk;3TwK50dq+tXIv V?gUKbmkOXO@771_dA8{={tJi6&XE8B delta 1056 zcmZuwO-NKx6h1d~&{1skX@jQm94pbZC<~<@a;A`^)QpDO7>x5~@{zt*fgzW7-@WJOJKy=axgSMeUKiy$ z1Du?*O7>6}HIYtX{yrBB_EVTLl%`=i1lb7Lbd^l9xaaw50W3M3YdRGsi$*B{tpQsL z7CH^#ZsNb$e*$`)eo+$3f`vt`xM#`ciboo+#BM5q{_H zVd0;j=@hx`^JjWQz&CgrKxuqB(l-H_Qg;)sTr@`6+|~ z$FXcxx-{gyE|w!YM72{lEQ(8QYWpW$O$x0jEFrjIQmY8T;RHSjqhR^nx%!Pn?3SA12_-XRHvDvXU)x2@wY2bBlEq0z$e>zm zA%(~;yNvRcFG{`|UF{hjcp2nvl~pxqlB}YT%ADQNla>bGXsL0AbWz!^(;xLD9nIK(~f^zPnU}F=#Qd;MYiY>L1Bxy2=NMwo1%}uI{3@xuRdPwydv16$^}++ zNT2uXXCG@0u;gz3?O>T0ONGSqVlO{qED;gq`p;=tbz&wBZO-zy>ysGEL-s+Rad@WT zH6Ev~>GM=p$xy$S%G`(gSvJTP*^86e+?$$)pn}f64W+46-#RDX&slUNEbbPF4)MgP z@_N2=sdav8=6Vs;iQJ_1PkHIdKo*TZJ4kN_@>aI9_!U;z3k9Spu5&`+d%gioh~3*P zi7*g)RR0Fm*6q@p!ZE8~w#j@tcWxG~-ZEdES|;6AIAJEpvDv~eiW$L!TUy8!$Sbzl zVLuxkV5c?*c9Y#g9$#A0@oe>_?0j5%>E_MVYy=7={JxS`iJ0!}Q8DX(C!1KKZCV|D z;w;+nNePYg7pU)-%k zUaXfMUjleO@66lLHJ{=}t_ryZYwhQkKWhN`v zN>yF{%v$!;%(mk+{HkYWob`J|gfM0za{O2$}I23g4^W%=mx?f?_9>bKcVkirg(QETOs z7JF?G5YMm>1d`*gnQgW?*;kK9>N@GlfrBJQnmV-SU?J~<|NS};Ovw<1#|xh}wMfh= z)-zPQs^0(*0O&~Q;1}s_V#kZc8h)Bo5i|DhG$TbOOKCD*;PhQPF(bc43z{Z0b4dqB z+i$F|G?U6WG7Vwct&?@o2|YTXGBytVHV*oOTZPp5S3OPMuw^;rT=h9~|J>%H!Pj$X z^{zZ>eJz*Vw{03Ma5-Q6+nJ`y%VatoSer2uj7k7bbEfj-J{oIWPG4WlorlO#8c;>g zVRm3E`Ty{fsmL>$$M>d(k(aptj5HZ*LRme>dh3s4ldW&@V!J0 zRm%+Z*%BF(LE2MoQ8biFU+sU`N?w-tXwuFfJ?dDUbmy2pMVG@XvX|q+1=WlerHV4;tgO zRkACEu~A3rr1gMML{%=8E09f_I_~F`rt?zZ#XS<`*dn@gptAYbpo5Mq&YwOv<9>8} Ih5XI`AHjei2mk;8 delta 1147 zcmZuvOGuPa6uwtyFvZel#_}=h@AzQQ!RI(dj+!QBrZ!YcP9l!u_`+t)86Pc*3<-22 zc%rN3!WTkCAzaaol`V=EBDiT8BwApage{Xg_t)`7T<-thbIx~O-#O#+%BgqC=}$Q@ zpbdrUoLb(PJM9OHg$3Mtd0qhL!;je-TM1wLBydC_gHFJG*l(m{T=o{x5a_Jv118+`n6pAY!-O8k27 zY!J*27DLTY71R%v$-#`nPa$D=1PV0aY$1&r__t>(8GLd{OL_WoY8cF{$ANi03hI99 zp=L7>UK>yF_4OJGgFjw?YD~q=n=?KTu=Ry2esu*x{!Thy+UZixtx#G;VR9^Z?v#?= zI>|1mXcVlzRIyPNHHweK{-|g%^HEdt+(R|3h-M?{G#w#YX3C&Xm?4Aqu?{1xV&g`t z_v*u$Ig?hfw;420AmW5r#tKceo;@(pa4$^9H0NmTKFlCQYiSk`7}i9`yBjd<`% zf4Wvz$n}~}zQCE+$3oR09Ar7?Kh@IR+$wpf7NEjgn?yjl*B~rTU4cGia{Jih? zAJnkArt#0)LtB9MhrzHl7%ct4tW`%7qpx5Zw_Jb|zY-}&XbYyakvf!au)h~+uB}bZ zEMbPj;IAk-5U3-fADy=X)OciM1L`D_jrXw4iGf|meZZEj&fKe7PJMtVM7c@sUKHd( z-y=*x%0z7W0dsBpk^ko>*t28JZal{sZAYC0*_NBFmG}nvXgWwiDkT;*BQ= z7rr)m6-;rade zOCo(9UOT22D5Ul~%s1Pv@D@q8oD=0+bp8W(jA{a0#YjV}jI_kjgtnSSCb;~V@-b(PC zTOb!P2XBJtAMmQ6#!J|P;vW!v)w4QTk*0g5yI#F|^}2tZB(_fzyDR$7xjwiY9wAKu zrlJD5v_K`Zcd1Ao&gQW%BBCfy4x*ZJw2qyoB@9JxDa}8x?RrJyQ8r(EOul&dKisqV zOHE5xgq=i|kC6l89N1FGAH+=2`{;yw`(E?bOdWean)s%6qIMG6uE2=vfGT-L;P{tj5l@&92CxSxRFLQFXdGS~Pa z8Um=P_Rit2A372R@xzc0KbIgz=3s&t^3>cAXAVj#2r-uf!ZyCTX|~q*)pxsoj=Ct6 zepnybQ%eR@Lb(E@c=&2W*B%|1rbV#DSGJ0{)C^%|blq|paVwhdez#?NfXLun%wENR zU|33q$u;@nvqE$QXCnkI-oUB@09*85ZvX%Q delta 757 zcmdT?I|{-;6db`Hc7g|3#44E9#v|C;XcslnMPqbh1S=1bJBXzq=%uW!oqa(;v`Qlu z`h+pQhmcXg&pTaa%18K+mbBjTk%rJB_%8C^KuU(xlD1Fz~b(kKt&{=gcS`( z!dufGGbdcP(60E4M6GqB7&N0i#ljxOnA4vT{Wc}-l%2F)ael43^`ZKU+Vol({D;tW awKvT3C9{8E*sd{KWS%{JJx&u{)bIfPjFSfd diff --git a/ownCloud/Resources/ko.lproj/Localizable.strings b/ownCloud/Resources/ko.lproj/Localizable.strings index 4da92cd4bf3538d08bec3b2ec584fe56b7df7813..cbfbf99e3b53392aaeb65e540cc4eba087837d79 100644 GIT binary patch literal 70726 zcmeI5e~ez`b>}aVrKPK^8g_%ks;=~wb&8wv!!(IR+aw#~v4gudcI_cJsbM>QrB=9kd|wBHwlfprMej5um*c);fF7!DpWOzpDY;%}e(J)`>ksAlcb8$2)pV+_+t0`B_XlijF#GuhJjVR%?9F5LW@kOC z=zFh?Io9u^-^B}}Jy_gp|G^aF;MrwQAE-7}TdSKE*6ZuM$;N)NUiaQU+|Uj@cdBpH zV*|te^>2G>YyvOXg%f~(myIpjckGR*0i~hkefAT)i!Q>o&rNHfM&?tnqP+!LhdH0wn5oo9mJ4L4y|=ebAmr8m>=m?lG)E zrN?VJU2ko^Z2wVcykTuLurhqw%6#$J=_TYKW?xEEN^zdKjl#0Sf z+559o%cnZkwR7(qzTTf$^{S7l^*n6-c397t{pT}>`ZQovBwTZ>u5|*sM|jZeu2)8C zSnf$9F}KfraB}&a&%D?;0SCRk$6d92JZAGk&y3yf8kDqnEWKYbyRFYP z>+99PbD6TYy}o?z(|w$7vA2I@)Zb;RmW_JYAQNpxp_m&AcdBo9Up(s&v{OVjhfyAPt^P`tCm;h9@yiC5%kW@X|0l@%J>2dEuY$d zyi@J%UOjhU{_?M=if1CENo&Bi0bh?2{e4OxxG6nHX?_5YjdL2JjXQ@OkMi+)#UY z{vIDuG043&XUjjtQ}I}7tmZNJ1J4qj?q2^5wGKr>WLtOxKN6Kl$~M^>Jofu79_iHm zch|B51+m_eCWi$pj%-R}^0s6Az&iMCN8P%qPcxU^VBX&Mr*5dW%}b8Z+d%i|%$RX) z3Cs3lA0B+NwjS@Bgu>AWdT$>2rfK=P`B!GQ&F?=7)kaU8pPoDgXK(+D^V7dEF8zZf zu6h+;CUiKIeY*GAV}EE8tZ2M!?uXkcByf?IvZ0$9tZZ^FT8pP@2R?0mo#>}H=rDbnmS!eT&863nTg2Q$C zFfEx!b)QK-Vb#5DZpAEkFXy@5eD1w{+oV}`7+*B~iafGbJ2SKQ$XM^#kDDU``R1CR z(1V^uAbM(#B9_utHfghQIhh8iAw5M5mLXg9>V?BAjL$xrb8VjKvDM<62W+)Lzp?Js zKm7TR+*=0njn)Fo<`^fEBpWj5liqgjrD;}=Pm0do(D(F4Yx&EvCQG2`h6wx-TLD&7 zk)fzbY)oE-wGo##XU@`yG+^H}A`;ka{gKJl{@%8?x6MzQKk<8gi{kd(qrqWRQEWFn zQX9mFVzum{Z?&~VZ0A1Ipueu3*=sL*4v!1eQ=DGT3_j4O<@&OZs1@c|iCM>t279d^ zGT<>K+B$e<#niU9EfSkOeavyW)~E(2DxW4T9POnI%Q$(X!373)*VNu>eer(ucl500 ze$acyG+}kH+m8}f*HQ%cz(4D3USuM#>r?OS$BUf%=%B;5!A2)%fws>vAdrpB{Lfi$ zhmTAi=Bc#W3y05}{ysnb<`K10maHvru-;aL-xe8!!Q8^edWL%z^i7U8Mo&zhL7%KT zYuSm@S9iZOU(QF~k+=tcKhJ>9f5O(EDP*41Os4BFEcv=eY-BQ%S?dKAr0vG$BeIo^ zh`6h%&)cm**$a)3C7M1f$HR8Oi%_hkFw zPtwTprl2abp~s)B+p?MrH+mO**51q!EWy;I{haIehH2fkb$$Q|!*BD|p>b}qCy&@q zMTsSTUOw^e_;AA>>V2A(M@E>pYoN;S?eIHlx_QgZz``IKU2(E?S zN-9Tts7H?E)TB#Q*{if3(NDG`_uFiKHjXbhZ;{$ZV9?SAK z^E|)$)&uju+kdL|8WG9fRqKQO_C9dc{( z-qYxjz`0w!k~?HG(4AqChP8;%;hIi$)$Cu-I8T7Z-S!{)UfJT{+pWgGiuu|`=u|JB ztrl{<0V5PackZy!Wc%StjqLu7XZe9QG%CM0bClAgIV0u%?Y^R~d$kW&H`wg#b)eVK z&e9ZkJY*mu+Lc#)PwkZq%Quq;MlRSf%eoh+h#iS2mydh2_!|>{GC^)&^3)lNGuu7i zD}(+)TZQ)gSP2q;B`cELdPXTIeEF*jAC5Q!+_h(iyFGlmPW9>TWM4O}we`@x%nf+V z>3TfYjm~P%ziYeMP^i{174CvARhwH@D47^ zyz%G^I*+J&+r09A?t>4`eYcIVry#Z-|50(+s<7z`@lSw zxP2AU>%t|`Tq!Gy?Q=0Y@!cS-C7$-y63&ht<14qsZjrQz)5r8W%Rm?W zILFnp`R&I>=79olvk66L8Cg2brl`bs2tylw!>~!F41ET6(PfcYaQWX?=Z{OAPafPh zk6*=8`5$=5*Nu-G9U|B3xDPECYZJ}A#oY!0bHiCp?7z@^{RBG1pUdKu*lyA@vbxdF z_eIInMsz-$p3PhXnGh_p)6n7ZaYY%QuTw2M{H>$0&)|BkIZ9*_T|-CSA+o{nkmhqL z0mp8m9+X4(l0z+VBA#QHC$bhkhVT7|;xSfa^od8&G#hPKb7LXDzD|c6+0(fAz$>P% zox5&ax_w(euK{$tF^%>k_(r3Gd+lJUKYuTu`=C3FtIH^pXchgYxE!Ctt>)S~H!xP) zGh!2uAAe{i#LQH?dcN5cm(2Hz!Tvq&oev@A^IQdy>LjgBirx2bN_t1 zEo?@K+DpPeQhh|MMt(B8yCuK1tqA+LSOmp-@J3vJXU&+kczbk(WrQCVi?UYNJnQ*w zc_Pw3o(*S@Lzx8WN%$8{hV>Id_^V2VO z$z5-Fl^)VF9|yGXDUdt7aLtuKd|`ZXm^YeUz-uO(Dhm)Z2iLqe^WITm;6i`zc%+YM z*$;M6(D64U4EhscEn+8p8+jq#cbK*AEZ7YEk52WOiDUmmnQzBAIdA+7GEo^*u_Lm# z_Bk~^nWuY=r?6rZXAWXJn)zh$V_RRSF1RH7Q)l{dRFkT* zqtRNKQ9}K1R{2=v1iI?rn%ITw=hJ5v0>jeIYFUM;SxH^I-e5+Pnj#y=Zt2FBQr5+C;BIEh9d1E;WC=vEBB;N6*<_ z;P_nM{x|2!Rz;Bz`zz=T_c~R*p*(t7%?GUQJEGVUBnwJObI3Lag-Q=sH91*IJhEdY zc(mBE>IgM9WcwqMW!J8LXMXT|)`omOI+<#f*w^+|m%9S!04g+rF}R-*)W5XhT5a~r z1@#TJK2Qcsevq`vgZ1b^J+h<3B90Sb1e$^iMOENEJm#AXzgw*b)s7zL;bXx~@(hqI z#=9v!&5&e_E@a#5)xCIj?-?+A`uOQ%aL3**o)G?h=9NyRT@6JDf!|Jr4g$-s&wQ!Z zj8Y}>9*HU*@Y+1Dj#KtlyLYOh2;RnjQ>5P}*%#xRl3$Q($(i;{B;Topxc(3DvM(Q8H4xuKT}Gx0XM4|wcQ@_Gh z8S||%jN|3ORn*ntdERILxgLmqD`uBjRl<`!MVeGPlop(oP#@Q^ak52qewBVB9#VZC zlt#0mg~G~5Z4Lb-9rB;#cj67G&QLmEI#c!6O-}PF9k&7WL^qc*uj-7a=a7c+8LE87 zddnz#{N%RmBMP9g?WgJ z%bm2BPi!_XF80&h-s68omLK~s`bLB(?ZaUJk;PQSUlN1_5@EwK5esxmHjK~*SeGFEvmS%Y-E?H*m}sGT{Rbbe}3YCx#h(U zu9WeEgA#o2q}8tR^*?!TAxH$RFEba!$rvdUj8p z4}jBmSfxoRiP4jY5W2qDd%1g{mvuI_Pq}_#4d)lv&uw!l9?mxyj zK5AMSai_5C5TfNb8%3kiMl}IYJ!~`-m$YULzwBJsR^}r?WvsK;ZOzW83QU>3=Xx`B z?bvnG-~`Pa^!InK^pw8cXW-NVZ9C4TSFmn_#l82959i;JR85coa+5>h6Pigh)8 zP|p_vC!QEOseN78Wa2$!In!;d_s@d1$`>r2eJE+(q_QjxlEj`t#)Hq%Hm|Q&6)|99 zsWfAi`)V#MSEjXv;4Z;fK6C2RXx$j~v+_Gs0*)a>Y+hW3a&wM7;DLPc_;u4l4QRFFf?;SP# zxq9!-Ba^dd`jt^sOQHeqH3-nZv4StnpC7-qf6AuZ@rhQ0J32@x*5bhhd6I3HJtSM= zIKZ#{HKQy!Xd)Z0mhp-TvP@nX;JONHsffl`o@vgAP!k(DHviGw=oHzdPj|bM+H?A~ zdM@@zpgnk^HPw2GX0)GEMljhvayC!UqbY{vx#e#V$6@8j@a62+`xAHcGcR7-hXz6l zwC`L}f%-nI-qroHN$YA1(0aY?0R$zQ&*hVh))KAEN7;Vxje6W=^l)p8=U!4QYXRYV zrJV%ed?&k?!XwavS*pLa~rZ_-r{Z>g^a&)ZGgC3zvkT(mZm>C_0=(81M?|kbocYvz_jvW^W4&$0X)pFZJQx|wGDlYC z3;Bz<2q@PrvVxly-hx|v@%_CGubsAh;7^WRHA@t@SXNFuo#d&ok8tdj&Z^90Ip;>h z72^j^s13^gG!6Jaeb;gaZ}v}!=3ZZH^ve4Bi;ML8yxm<;&X;@gcKDngp~FG& z9vUO}*88j9$}pGbkisK@2e+GKwquPqk`me}&L&DiGL&!Oxm!%Q9^TPuB0L*B0Ox4! zSkM-!fxCG_D@CK!oshj5tZf1csK&o=tg1}LYCcuI>b-Y9Jh=bp`0V*{ICXXg3%ubq zKWp@2?>4Kb9Q1F?m{pjJx&u~*FGn>mlFXW$9$4`8Sa0+MRrKyzVN>zLLsCo1#1+t;<GMj7rpppE^FV1f3+uJ4wpnveW zz#_kM9t7H}4iSw)ZIo)%hgAl+W*1k-deLEIcA>RAUYDD^-tc+hWHd0k`8TJZ?FHwT zUOJVz%Gd>b;wWZ`)i2&kG+b*db$y860H1<`fEn3`f{`bTS+fqh@{xducmhwG|5e$A zCx`C9Ge?v7Q_i9yq4+(tCbC*-7{`b8N@H}>K5Ow{|H*F@`1!@oBMc*<_<=!}7bhnB zCp575kw=R*&F31+_3_sWj6|A0teZCKyuMKT1+C2y^< zmm^Wn(vBKW%3lm9Hk_U7$NKU>+sBO_$Q!ptJZYzjUU+A$7akl(>JU-b! zf8y^s=ftVJEO~A3M8SANj^W-k`rsS&*kD|)yNv>pU!91YF!gBCGx(Fyrukf>Z8d)$ z`vK;n?fL1KrnmR+fXHp!2T)A`Kb0A8_U%+JpLlM;w|B~JFg{^t1MQmQ(5R8~7DRjE+ql-nUboIg?LrkgX{rxDUFW{tP1q17m?7tg3t- zw01dD*53gq)^vCcSKHWuMeWQ)rVTyl*x6vvpab*GBAWHO>8hU}@Yc)&h4Hi^`W(b; zb!2MNFoof%xgT^eYf9c zt+jw3^U1k50Rfck!-?Alxiw}E{^?Yi7q`@Ogcq0E`|7t=y*j!OvoQbNwT8rEqD|ol z;(K^oUc2Z*%#Jl624<%YuZ7m^4Pke~UiiKv`+c z4-VrCUN!rJy3%BsRS@b<19reZdi~Q~vixv09wN275zR++#D@ppJIZb`qmmn7-Kt%9 zw)YduS}mXZ)3jlO4N{%mQG9ykJA*%V+H-Py@(*sZ_dy@*lX5B=PyUSj zN2e=!2Iqy}s=i>q^Q;L&Tp9?l3k+SlL_Rc5d@=#Qtz%Ea{^>&uUVw^&Q8 z(`8PPpjUUxT9VzmxD-huv|eCPU*~F%qIm8St4QTBtC@m3$tC#hVZ$XgG}YYk$y29} zi-%Ud`psihqCwB)U2;&)XWAK5&FgjJ$}L@G9OUn}zf~Fcf%)^(%eot0buMb-VYrUx zME3MYDyWY0wRI{gN5zA0+Hc6jS1os}{MR+7o}YC|L9URLj=?Nh(%n^0pLlP2u+pE3 zTrf&joE`GeoK%89$?VTBrLDZLu#3)(=o9IY9Kq&{3$w{zlQc-{!99z^r`QJ#2`8z7 zMVWEer|>J>N6wXX!n=|;GPoWCi({oBfm(8>r*6|-7ZO2AKJivqk>_)R2YFBI#%V^! z0=aM2sq8(FgBPR{&zHZ6P>^Pz(%C`Iod7XUzz^@~B{}0ao(RtH5;JC5KX1_1{3hA= zbq}LE*&#H_!`34-&oDaD&OITrx4PpK+k+pX-2~QGY7(I4#zn12IWjQOq)J{FaHDHn z+ugTd(c^r5+bW%#eb4cR8nQV3BmNMiaJ}c5nwk(?`g+hqr7aZ)2DXG_;>ev1i};Cz zNP<3*kWVD!DU-I45G)8j)t&aNO|Kz+%A|+I#%3ypCN|~vIBehGNrSfWvaXe;bg9-} zm#hcgx4fUo_UBkx9vIOUy2&-JLyFEF^!>9|lWinhf%R*6&jYKIZJ%AJ>5kQtQyzI^ zeC9w;6|V6HyttPk+_*375o2hiHha>BYk4H<55=~aZj9O<_!^9bHyG>9c?GW>D`8cP z*Is8ZYFbzL@_0}A(m_g!Pk7s`@nX0AxgL9f7p&&4Q*nc-duFqZlN6rkA#;UoPy{5u zS-xm{)~Bs~;L&gR!K*uU)Tuz+9+?^77vI5ESW8t;<@{yN9x#@lZzL9?X3gkcSEngw zL%Gpo5=r@x2jzH}|q)Pr`l2kY(U_*jhTgIpff=2rTQq?BRKPJbbtu z%^{yaIPsjlX5?p_Rw((Ob*;AEWGLXpVR91q&yhi%Q(SP5&4j0&HTK6#1)@>;xcrQI zc#kj2jFg}fmXSKv=-6|f_MXa!DxV97dbXxX2{@OT@q5uSXlWu5>{nAtV-9hcM<(!+ z;)Bppn$w|^4CZRSPxMZyCqL0UpXeQBs*%S}^bS6?YmbjZ?&j#ajaQX{+dw^%@5{s zO~L-&b^U$v+iRUu#&ChN_P4Gzr4ypH|E<}XJGdlTX)o089f;flR7U%_7Hnz*uLoLf ze8K*|oibH!KN-lJ#fREu_T z9*WwfwFay6G4NCI&utY{ld}FD_HR(~%~L-k?^NGPQqH;A`ju6p8x03Mk$r~q#N*@= znbl)(VpO;=W*Pl+ZWj7K-b9-NWAK#tTtt;FOI`*OMC2>$g31vBgXwJeE45_2uyN+XrlsqM5gysIjU z82QqAX{Nhu9A#?Q|0}(2)vj>j%s4vTHh#NdQobaR!Fx!g?!y!Yc^3AMELGbX%%4t< zo1PfFoyV)#@qwWVKOeR6<3<;GeA2k#yLrZK5a(1qBbTb0AFM8S{f|eYb3AAVtpNy> z9g~FML&I%ZPTKoriTtxuPoIc$Sa|cybd%u)FO-ZN9uB$)KXwp?JeF6%a8jvL<&lxu z)r}E@KWOh3&+k#i2Oat z(Jj_iKHc{Ew2I$vDecdlZR}CdN;etO%FaO~TC(rde2d?SEah4G$`Kq*`+Qb&O^f8X%NNLqtVIO=X%oeu9*H@uU6)SMGAMku$p8Q?0S( z%pN*Xxyr~Y#XZRTj8g30l825FwG)gF8)JCw1{jf^a2l7V^kY@na%BK?hZ6TjP?!8y zhoAjRYMfOI{h8NZn*P-M)yKwqyt{gCnVqWxC*nDxzUDnjfu-y{Acyxad?Aax0pBMA z2kYt#4J+ZhAz%~i1TQR`BL*Bj$2S~<`#e@4>qH-X4LA{5+icIc%K=#$ep3uJ)$*%` z`8wPp6`q#QC=Lp2f6lyqWGd{k=ci%~iW2a{oR{%j$IKQ1GjTN(T}+Fu_~Ef1pPt=w z2AY9Mm-W0mw${cX(rP~er0+y}<(tPjUsw2 zilV>!WBI#65ryF|lpddK8a^1@fK7|B+{g1Tb-IRg+~VQJ7aJt0{=F~PX$v>77GJ-F ziVjmbn~y9T%I#f97~Z?~#Y*^SHP%~cHiw&e;+vC$r%pK&@ZdGK z*5%C2>YrS-7qb%sb1HVhFSIjsgZH~dJ{X)6S)r@qtaD%m?^I`O^KCO*thMUbsm+X- zmNmj<;7wo8%BR?V_jT!NJc+rI&~_fOh*6cD zUe&fqE4b7GF*S8qy;z-pLGZbL#^7s4Cv+`8@mP?XXwOOA$y24iz`tp4=W81^97$Ib zJ>*+@w7Ub3iQ|06yk=}aH}`}`7EtUtcR8){=7P9CR+#A<(c|gh=0I4l8&AR2hhS}zQ}L{jQ$PLlmE+3ni{t(_C{vLZ z$;#saU93;e%dfW9*!+Mv<3x8hVsm^B+H(ehJDmdV`)8i-?+|i-jDNXHIv-CRdq{RI zN4!vY__q-_Vf?8#-aFboE)PdvAr3CtXe-t33Z7QX8BqD{#Lb?A66V%u@Qq|IQ$o^{ zBMmGC-n-xR=Q&NUL2>RurEM2=6oODEBU|9BxlL)!$h3 zCS1Ucg8CDBLz91s^e)fvxkW=`xSe;sj~~l3>{8AtEv2nAFMc7Xk#+YB(Ry(DuT0=` zaTeX>Xs~sbror}`&61HBB2MKT0&wS>U535)Twz=9JmUx5=pSiZncS6?ocsD1!i_Q(A8Gqh#f-i_Z9rffADPkASw+xJs5mb6Ri zExuO!3Pe`9&4&m7reC|T)wlqzQoQ_B%^S+wDK6NTIMM6*iJiy@`pt5TjwY2435Sx^ z(Y_IWp`xvvbrqD10W3OUerEO+qp#kO5x^hxZz19%6RC5bZgaP9WfiN;yCy5^v3Vj| z3-c~bjysQkf>Y4uZTt3|U?cM<2t^4_aK-m^HrFd{W;#R0@H-ru^@c5Ms&b5emsYeL z_FikrR%mDc-mU08?7d?GKb71Dbc^`gDHT!szP`?hxYscTow?({wNlIurtrOlDJ-aB zPo8g^*WL4cZKO#_`OjAh*DjMUW#OWpF`z{D+**r^oo~hOWNp9>8BydCA1235gKOi7 z)qUCB)^P2ych1_^PKo+I=yC%H8EemL9q9d{*B)LnUWw;BV!a`y@_U^6L=#%Sb3c;&=5k9nSa=`=@Ed3@)Rns*h;%nkcRI?F_*oAY6&aI6<@RW2G0 zFAWEml&jNfs50>CpDIWsN?JE1fpFF>5%~_z+S5 zjrWSqE;ZROy-0PDVyaRbZY?O{l^}s{zeg9?SB}SCIerKBL%u(Ml{R@89O04Zv}p;= z#YPRdn&Y^}&N?+(m(>rd+kv~X&X}Ay*}u_ha6RZ?kMghx%2CQE&<=c@Y*ifro|`gI z@7y!zw$1AsT%NN=C*?1F_$rqMAC|ib8_!(#>V4*6*Rol{ zFMl!dhHY_K){v23i8@;}2O9M;`&n`$)N|hUj4-Vj zmsrwujq6qHOQxUeFn^zOA=zUhkAYqqKlI8hI*BS}KW&ZH)i*w`n|{~sH1)48x%Bh2 zX=_Iej=Z;_ImZvR35jeKES>=QJI z$fr|9&VaiMft^fQw9WC6zi)rr)cIt7i45iQ_z7s=3mS$8zVOEl&9Va78)i3j_;>*m zBiNS?4xR7kMcM13PU+&9xp+(An`~~_(~IGIX|0c7s+KOrZArsNF&nBb9s0gPsz%B$NHVvA!=k#=xF% zZXxd<;h`#9n5mXEKM(^exTQkTYDn$7ZRH$Z=?#v?<_9aiwQrYaK(^4GcoM2H&b!f! z$IjpT6Stj4 zy-=q~@Wl&rL-09HW@Wk ztAz}rRprO6ZRJ zk-pWeZ}j>;(Sf@5OxX;o0Qt=|NB&lxXNI2by*rJ>E~~R3SIzFR`=PMQNEB_t7uWrt z+>T9_1b)E7!_RZyt?G7;%9!)%Vfv}V58?4wh4x33?c z1_WxEPyLiFhhcT`R+7cl#>Ls9_^n5-i-;qT3|SZCkNT;A5)P-@7`c#a5hw4fa%Idf za*mI46{-+X_Vb}%zzPP(7xcl^CC!~LhzrS##oKT7VwYTA$6`E2MXrzKt=HBTGiREJ zR#m+_tZn42+A|6of%C6;GVNnUgpy@JJK$|_w$C(td?e3(>cE#g$~=D72c>_|e)3Ls ze7L??`WVCYiQ6u(7*u5*@BU(z ze9L^F@1PGeyoDq+b1!~ndsG$kFSWf!*SBw^aq$#(7}d2a%3Nh8e6h0~S2~WMQKgJ1 z?Hq|;R#yL$*NSgIVGGnlo*TI**$zD=Gah%A;gxN!r4?@s`@wqg!Q85#ada~XHV7<` zho^oYP8LSoGox*Jdl&LEJZ3_sGB?xP>n*aJ!I7M{;9vg|Jm>PypBd{>DGi=RtOhl{ z4(VQ)SIe?adb8_-2gWyzeGgGzUV5~C{{9R(Gv;ECg4-XTw4JHJF97j{xt_tbv`Mw+8e=c^mb4EW@*rRImU<7=BVp&T_B%j(LF zXJ48=_G6!UvGMXatBn#w;rU$KirLS+wql{8(1#OaoMth4t} zBd^M<0I@t7>N1nexa2wHOQCCs;&_4wOwCu*I)a7n@qc0dOm*a4yF0=&#G1p`6LpoU zn)aQ-rLHJvl!LQ;O=b2uV?vosqR{NcdB5>P9~|fqe_>KRi8VLwc`|< zMjiuf=P`B0vklL~#$n&X@e(-p^P%k{WeXU6uqgp`Zj&L*t+pDjT0)Mm)Dsw>XM3X z@N8VBLahAb8c)y- z-8p!YGdgHWzr{H3RIaLHcbA2g7GM7F`3q+KID-^Bl}ni~)_m)Pf3wwh!5v?omA$uh z4$?=AdacsCR~#W5SdOc9#2=DNuVcuO4VF;%2l%(jmH(wCMBjL# zKQVefYlufd4JeXbQZ@VFfgRaR?itW5e8uVI6Q7x=_c9mGCE}@brp`|tQ6YP>duWzO zj3`6jg^oH4$C7`;i3o%`H^KK8_$Gr~H_m_yJmz8ivKR%}T~oX?toNXf zB_6)NmO*g4yRN0k@`JCd{14ucC<~8sI#yNy>?@)c^<5TxuTLuj^Jx3b&;PVmEe~dc z;jWbTHcq8x4xH)^nXs|qFjXe7OBI>cGG_;!Z>mQK{j+r0k%cU8goG!f?lbFV-Seob z1KG-u1GJGUy8WhwX6B4@XmI|{T4T%S&K{EI1U_rB4+m!cSN#fbWq3Zm5uuoSfvSPv z9E0zC>)bRMATrp0Y|q(y_WVd>nO!1%1vz>s&*xl9A92%0<0c|?RVn)no>MGGUrJkz zN-lt^s-=y)wALDV#IU5Dprwty)b<+vE`u5_XJ4*4enc$!MAXuIIQ(TTNw%{%g1M~aPTj7Erwa_6S$>TX2YxW7KaA*!b>gnwxk0O)$>Gnm#YW% z;sz-b-Y$^^(JitCHzRA!y*&1*Mr#WRcFMl;VsA3JFZGKcBJO1mfk>K@`pn_JnS8ya z?bxlXVazwmU~%~Ro937Ija}Sn)z#U*`vi-z*Lvw#6el7b#GYjQ;jngEheip0b$<`- z0GDaknlY3i4LfqVyd1nqWJcspmIg2A9~Pf}g5#y`-d`RUyT37FdIN6)%TJ`Q^WW}I zw7=a{b3Xe=I`0%#gnMahXWnjdcdgwvLXHe_+g$&$lzw@io4cW-E`b{DC+&ascj5nB z4WqjQsRr^$4T~4P8af$^itOOc#Ol1NT~!UfLib|iLrc#;-uL_%ZJ)t`*Otc`nLS;9 z9X!U&H+_s6HGlv;aq+R_L$r5Oan{FbK`e9Q=b$$9JZla;@OqRZ9JIep?=9vIKPo(oZS&7qwMco= zFGg>-@JpXTcLfj5!5Z4IUiyTKL+)6-)98U;04Gy3nk&EYSBL_%i|#v-;zHjAYErty zC_brthwLYCrw#YBwPDogX_YYK&En(*>s?$g_-eH8z%7fzyy2)%aQ@wDZHcE@pVzO1 z9e2+ChT-XPd++%~ENu8l!U_2aKYbC{pFF6nKk<_8rO7)7<@rO02@3^0UXultpb+$N zh?JqAx~0@XZ=U*IKX$m&-jTUuh0U7Ews-PD@ITQh)ZV)X>GaMyM^4Zr-ifnT5~Yrn?kKbjkz;;y1k zce|5b7Y}6~oq@CFj&G>%YdAmt?(~JjV<*VyKRWZ$^owW5XUOn&kN^B6`SO>QSP-v;j*eocedr*%*r0B-~iiDF58r` z9{0B8o|}xK(hhlyk{1_iBP{0HVr(O^CiY>I{Yy?XSCU>kccWFYJbmJ~>sppLInDgY zB{m>%%q=0V7kqak-o7-xy?1E#inWJ0Qe&iiop_`S9Y-o~i>WxEE4 z$Bswkc*Y%f;~?>@fBk|+##$g3R7#X*$94Z;eIvo~$y29{U(uW;jmV91mPtckFnHP=@jsd6Yyr$+}V~}vm5I1N* zvFtmIojG~@{RwW`HQy`U(Vr^{vz!$!5srjs#XY2NUI(dtI)hVj3wwi5#j-9GqvMjG>XR0%od;8Uf zd!#i}{@>5YE|z`r9i+EgAHH>r#g(-Ve7tblzx+^jW5$g&R(9En?y^3duPkw}!LnC- zySG&O*K#K5dp=9QquHsowT^)0g?j!*85k_tQ?<v=p# zGEI2rVejuSF2t6Xd(P~(sSeq50_+7UGQbx@ALlF?_75M3OxXC$>#yOzqV3whY42Xw z69$ibMD!8AiyA}`v95=YjL-c1&7gtnRlFuV7ks9{uS|S6;kWgL&Lu)qWP^^NXPo2B zR!L`2xcQ*`j(o;oz&B_0%kj+%c3*%;E8>nX#YH8bNSe%(_z}Co&zxIUpR^6SgJW@I z+;JgqFzC^KPXH%%@Pd`Q)}4v@+@1Tds+iDTxK1+TQDND^QMnh!SWtM>cAlpujjQb& zI1vL0YUi>I)?c;Jv5V}!B{HLYlJcE_!}$8u^8P*XxpiAhpO-T*+-Cq~;UV!UzHR=N z$mA)$ueImkO#db|-&=^W;|cnU_&~OmYP~j_8_%Wh^7nJ^Gg$DU)syTgu$7#>)b@f# zU$a|l6r!S89<}7wU-eD<-MwvxP0^E6$9rnc?QK=T ze({!<1~IxDZv>cctbW`6GjF*A#9;Uj41RfH-(1KG2gKg9q*IOxmG$;GMI+LhHkShB z#bMJ|=aC)sZ0rt^HAib>`SApZ!JcZYq=dgy{i}}l(Uvk&@b8t4lCd!;#c-EmL*iiQ z5nrVYYci}w!00Vjz3L|yhieYzROF`o7B&Kja{6zvzreRY-)nk6{<~9-WndNC-6%Z*`oonXFNy= zKheThY&_1zGhRM-z`WRN=icXhMfbM36|-2my>?r|irH=Eqb#5J>7?5_&e>&}F=N~A z>7IiZtY@}lR3d=AfY$E-Zl2|Nws+eU`=LNoN_{(@uQhFioIYk+7OgFh-np*rZO~`c ztL_Ud&eud}xe_M0dcDnlQ=jjf_{lsC-p3EAS_b?K!)fl5+htz5uZ9PY$T=8`r5v#D zb`vw^v5x)tPmdn#Z8M5{EF=m$9L@7ZNU`$D57*cYtoa?#&%S;2552dy)oxXJ?kV=C z_8P=^SUO?CjuF1AXPA&MW<>w8nrs=y-X1-%zm6`Mo!dB6^_{cYTS?+w3mm2WRw7RFB7RQ5V@2kI7GSLjMPA3pGnydF9A7uvEybBl)(O88%|@%|`d<(ft=uj}3oMLdco3r*muu=dSOa{*N@EnNYJEpd zUtuQi8SkH-JKfJ)fi<*Nyp5d%JKJu!ZF=|Tr&soS@3pmND;J|=eY`YE`?~Wf7U$_E z_s2ZaZQd@tFTS4okl~0dAiG{zCGse6={Th^^^?kl0zsa)!d&bdVte4w)bcYlZ0m9|wnK(s3kJqb-o}7;( zckz|e{A^Z_VUZQjD|bvZ7h^-&@avWNllP!=r{Gol9kNEWM-o*BN9M8_FDr)7UM^TG zcQ>F8R1iOUo`LEP$#>g|$vHA=mg8mTy0%un)X|xG$xZ=r?w$RKYv=B=+dIkN+;xsq z>N*3U)9j9?ZS$O8Q8s{Y7BtVYyYGYLa=pXvM8xPG_oX72<9UgoNn|jb)`urD`vUZEsuom;xo+#UmewY4K>0jK#{*MMI)SE0|% zr+Gtl`C&^lw2tlY_~7CD4Ig;DyX`mq4Gb598)&ZFgZcXj9h~nx01Y=8uR%4{!GW=$ zo4<41w)=EJExwuvo$~3xOT_|q9(a7tzZ+V7yihc8?pBDX#PzM)s*JB;JQx@cZFFT= zckSJ3aV<)W>AIvc;g>XPRv>XVR_^9bQIl!*HCvai7$CAYHnv<*~xM{y1^%b6q)fO$ma-;4WqRlcQ-7vCv%S!uBoF+H{nzPqL7A-Im%a8FT#y!0NL;f|UU z;bYF7k_(Tu9{5Z4Z9Y8s^1@xY*b*{ko-g-o#vs;}Jtu3>#5?`T2$APNtD_C_PDjlC z-`{@Ww4zR@_o%_j+|lOz^s@f9Ox9N0EL*kC#=OTyf71R%PKorR{bFr|58a}pjJm9d z_h$dR?XCKa)^q3Y-^LSK$PSBN-~?5J``Fm%!FL)tzZvA8PaQj8x3|80;>kY8Mf<_^ zt96!xb%r0=KI||s>^6Gjn#SQR1mCdtox1Yy^VJuP66|ruT9@9#F-PQw@8LchmcCho zJUX@NY^VDERLCA#d2EeyTR<@QMr&DbG3KTOssAlFyv9O&#{M(ePjoN2S@H8;`!{4M zG@bO#pp2n!Lc)|UA_s<^KszDfIV)|q?)R=6*Vnu^^y@`88b9%^XMC7gW%*PqB=4!6 z(A;u=@4Eh1ew(-FGtR5)d>|UWtmr-U&b#T(OXOek z-fa+&oxH#PduKJQ{~Bwz)IQD$NKD4{)>?*mq88LqDk6(F{vGGGcj`rjscVRb6Ma^` z^M$iMGHq0SQ%XwnIU}}FllX(`5j;4z=0UY}{kK7j{7i5XtD`HjROe@EMV@b`VfF@F z6T1@X6XOnhuio0-Ag7NuQq3E4N)wVblE2g*<6d5G&AfGyIf?VcQIR3ZaFjT?@{ukL zqAQ1Fa0EeskH_8uTumIU8obO)qHj~yFSXxNXTw^de?3P+>=B%G@mbsZYxcN>(B&gW z*3FeKpE%#I;@ncp0d|xszBaDZ4mW!n;0@iB<>sn6P9H2gN8RbCyI94^Q)l`&gL&T5 z*D6gF@C+*pq@hqq1q{OC*5 zuA3Zl(GGn1+tQYYHq3TkaI;t4^h$^v&|5L~Vr@i8a#ivS$)fo#X2$;T78+rb^~zP- z&{sL1fMc;X8U;;9#a4Tt4qA(3Y#r<8};o8xBj>Ke#5AJ zul=8SGC!l0V=DXZyA8W!jTPZZ0|7-FzkJQOYOB53U~r-}9e1&MZ^6xFE{E_Cc5P?P zU($`4N1mD6-mhd@z2Fn$!{KSf$lss3wSPi=bM1qcTI~nv)||DeUs#vB)6+QZerJ06 zpZZ@b+M1ree5mBJ&9)Y5UxLe;^+3&hPw-akFtfVBN}^`ZSbwu_h)M;=Fc1+*qmh6` z@w6;7a|EOr@0^qIGc45KDFfM=3JqG0yQNxfmVel2Rrdp{flpp;)n&?}6IaAK0~XbhQJb@@H)ho= zROnEJsT()z`idj`-43myAaV!+v-16y5AmhIrmwznA44f)|pc$Hxc3u<0yiG{7+r~M6 zY--T|9&1YlQ&Z0Kj)zy5a$-Q=;pCf;P#C}8Wz6q4P(_n+$f|2nAF6t(X!6PwgTYSk z`mXE91;+uDiI?Frd?tWlmfBu5gj|e$hLeveKDi%@CZSzXk6~t2>{Y zY-Yj1l<%tPW-L4u=Stx*W9?Ii-#g053#yO5d1(Al>|l%YN?AORI@GpK*KwRqXU}~RWM|uMJ3DV) z?s@l~d+z!9?l~u2&C*!U8jC&Vajtm&Ex-j|}$%+x{3iLjuQ)JJ8IGgkh!`t7Qg^ zzN$p+%ocq2feLD?+Rw}j>PHhKnibqy&ckgMe|avC%i?m0Mz~6Y5|s`cRXpq}HCl;o zBKnrff>D(o>R>B$!3HRw(BYEWCa6NnCCJoRga%EG1WK(2W&34F(8gFML>jsIoSZXq zuW%dam~5hWZUMayXV9cwj?7bG^1jRZi#%6;`fnZJk86+}>Oxtl2|t8x3{2(nxgt{1 z%|#`W=j-fFFAJ4Jj)*K#b|56D0ClY1qrL>VJ+p=i;8g4z= zSjeH|sce)#l?tiF3y&p+q>wW$l_W(=W4X}#w4VeDYdh@ER7@iYxVY(Yq(8G97i=r> zrOgQI+z#01w!&|oQ5i={x#^5-Iw_C&&^9jJ^%W38~C)uGWVpGNM-r*WavA2^=T zFbvD!iVMP6tPX>*1~}tX(8uYajN^$8M`D~27vef`Db5S8QjIZZ3=%gHzcUgE@eN3d zuOSQ4(HUt#LP9t;yS@lmO}4WTVfDh5i20`$oeK-ly>K-$61%W(Q6qj>WJYvSk}8=T zwT>L-B1E`-0jW;1qb|t^(-|eo{-nXZqzKgiAqe`e02DkYMRQUR_{CD(Nz!5c;^;{c z$+Vqe*v;Ye;`GoIg7gNO$f8h}62chr81|B}EO}Ot@7#4F4%cyrN-mKt<8tT`yvbfa zp=(LKM2JlpkO+w>T@vB`^3&5Lso0bj3TawBqLX#tS7l@ADjQl>CDO{br$caMl?lf5 zG-RY}NH;Ya)5Ar*8`Hy>--Z0FLMi$#1PGq2OM$rKc3^of|0KgAPcK6w8}qq>Dc52J z&F6Dl@K|1m>c9DLG}n%%TqA~a+cB1V2IF}-*t${{rWCYniX}WrgI>ui|0N=t}39%L;TE-*&Lb*?;@qhiTJn_yQ|2!D{HuWRE^ucZ+n@N5@ z09&`!Omg_XE0MT85-HowsM($g>BmY8ZCBKm+5DL^a?rDVHiF8tRZAY2$&0QRRW2*9 z!1eMlc&3}-+UcM-0-Y@y^nD2BNM5d{-2dtn-XOWpmuF}gV zK4{HdWkpey9!*ur=&RBq#@_^fZw0pP<cN%tt%mxIZIHdOOI*{}g^O$Yx-irspp>LgC|oSuVo$2OuA5p`TC=EH*HKfrrL1I6Kh&|(U@ zFK4VLAQjU6_5tb@+&D2mo@ElA9GWkJ%$GaZ49he=ZU*^6S}!K&uOa$jF@c(QHKQn;Jx))WdSpB+_Lkt;jnWfxeT`{=a3G;_)*f)Lke9+o?eb zn$MgMT2A|!@p*Ir0kwL&7G-a*N9WsWjKAHDgB0P;mLznyIN-cb_vUPpb_ualyGFu1d<5;RC})k5EDM0Tr^MzXLqnrJIpTT{{2>LNZpoPw5k zwTcum(KA<)7JX+8s1g^x+4s|Z}Y>IzeI{1fxW{mb_Dj0Y_Y?5`r;D7e9=!T zS%!<3Lj!n%7=tPtmlKe1*?0erUQWc(%du1`^)Pi&LK7>zX|C_+C1P;1#PB}E<$unK zoH!8t7*2JWNv_&oEb_>T490j@5^i;+2=2dD`=R5?N@Vt^asP@M#-0qM_L#7*$A;P- z9^E}w^!4Z=?R7xjYs1UE5y-M}tyhDs{W`e&c{KKyV7%W%wMGTU zje^NOyz_=ekxFh|O^UNdfbF?|!;0}6c9ahgItP?U8qgCRj@<)BTpF;mf{uP#%Q>fl z?xQd&P6`@%od2jo7BAK?-1)~wXa-|YF&KfWK{GlB!?Y{t7}i1XG_t%jS@!P_hQseB z564Ztf(6q#PWWXObFLn@3$tI$3q zS4{0~&^Khm;E-OBe^N0GZsO*Ljl#L%CQ0o`Dr!b-s2kxawMbzaHo)?kBltz}%Cms7 zj)!H$wU{&fB&t74K=Wrt+M~hwPZg3B%=|R8c0>WgXarQFQE-n^;U6_3uZqWqqb@3| zIvQgH+vmIlrZFSZ#}cu6ESmIGV_?jR?lC*okGG?9JPpggs>3&5s^R{s2;sL+;q+}Y zzPTMyJE9NVKyhT1oIs3guRe={uMO~gO^8j`tjHw=*O8j6S=>O-%Oj`e(04xZv$VgK zR(*Xgb2)3`c+SGj75m*daz6_|v1l;QE%C{M#1XRNC)TE%Gx3it?t#=4S}UZ7No4oo z;pO~k4j<+h(fNg>Kg-(ePEqRIF_}AgPOOZgc`T_HP7(Yg8-I4+LV|np~nIwA^sxQp|}7^@D3Mg?fZX{O`7`(-qe^(^}{dW^_}%&oGgm09x)$J tMs^2HmC~m#je3a67DJRZ#im~@>}O;#wb(P4z~ci73$%?By1y&)|2K~Iq@(}< diff --git a/ownCloud/Resources/mk.lproj/Localizable.strings b/ownCloud/Resources/mk.lproj/Localizable.strings index 18a22480dc2d9c2fa75859f1dd7d553b164bd633..e176dacf024a5c2a373cad531707f92ef1ecbe47 100644 GIT binary patch delta 14 WcmezJmif#J<_$;QZO-}F!3qFH2?(12 delta 606 zcmX@}g89o^<_$;QIl3?;GL!(Z0z)oCCPO+y5tyIJkk60@6iWm0^MNu73^@#`K-olw zWQIy0FOi`D2o)H#81yH9G#BPnVDN?Lp8U~TRMHWsDjBG+6lg{XP!&{;A#w6U4q;x1 z&H$hp#SF!h4U2>)$8oULD=R)5M}~Zr32L|0mW>Am_&V(ZI5!=!rcis4`CWMIf(mUK5x_$tw)4lD$sQ>AGiVC znh6d;kc&%zZbuDmkY8c0CPpp9UMwo>ffj*+J{1(SKqFBjtrRGh3XD(%pwl3cj^A2R P&9Y`-*sQ ztW$wz_hF{8C>vl3(^v@KG7GREy`eG3X%#!;cJo(Bzmv_g>>5S}F<)U3%u|iK$1X~i zMWz_;)-4iTZ%=W0ST9Co%n7n?%r*0locNjAaY%q%z+e+54Prtut{dY6M6+j?lha6-UX5U#13qvew%#S^@z;c~baS}r+22H7iok%~3 zsl=^Ua1F93l++b*73M2;)>DqmT99iCtHPBZ#SNfsWLFS@WY&clbf(}#xwNNq6%LHM ztW>W-!yY$$vX>h-#^alyRgqkj972xN0U13*P%1aJT|3k4YZo^J#jS*+2vL0^i8Pq| ze&ME!WnWkrGOoJd{)la4JL?m1;v?f)6?%yeweSP;qDoaLAwPn+-{4HELBYKj$we4- zjZif_qvmfgA`DF+#IU{#8K7RQl?w#C84BPfWKnK(E29b5@l8JBF$+`y>wK<>Z}7I=SpG6ZyOZsOEXqhZ(5YQ%>nf2q8x=wyX<~x#zQ#5E+QN5x?S8BjsEG3s``w}ClLn})%Yn9~JpG=J=jo4Y zd5Rvd=I5=A2pEF!WG+lRu<4_vT!Mv)YIErl{#hPPtTXM7TUAf1AX{;rIB^>__G zt^e?IhXvF1M`hfdKaI6g z;cH6{Jic4aH)cxI%&fGQXZu3d@8vq7R>~`^`|mw1D!uBreyz^AGR-9-EHZAF@bA%1 zzwqm>pg64G`Ngnu&%5=#fY_AT1 zd=`2+C?aYhUEMxGV>Ci>GAM~xj3!CaFyt6b;5AMrtOgCyB}fKcz>(BUd)38m?k;N8D2(O3F{2$G_}OFC-DO#<(ugH(=xXD(7$8Mn8pH`NvTag(Ujtv*p&e*!fm zP>q2*33NYv#}UCmG)brRt$xvdg@Yf1L)tD{(nio{SWH0j=?aB$lapx42Yd%UU!eu) zqyNE}uxvmCa-97Q2%*9;n=!64iBJ+87T7h_zV4b+Mv*?gUgW><%={2qN#GPuBL$sF zakX~7N*(HUAIL^#MAqNH6nO&7Tg=918Rd$yGV3L!)Rw_2bu&@gn4j+yFq|(1=CixO zbIf(6)}C)a+Y0Lu=y)zucqeI;Yr!YcF=IKGgf(sJc}%A58N+t5bsVRNAByU$yMQ`w8C ZdXLxC@`CefafyZDFiDBbY`b{c_y_I2?e72p 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 5eaf6914f41a0b54326f3d53596d519da9ed5fd9..fb7f3dd1e3c23ab84b86b2e4947c627efda8d880 100644 GIT binary patch delta 117 zcmdmYgtcK0>xLU~2`&tY3`Gpt3ZCa3;4dPxfhrgN ze9%k2%=LEl^BCXaEZ^X1oaW_t%IJ&JM5`|7vw)6us!R8d9=gmKPKuq@y=_a2$a=oh zc!0Kb1v6;>1j8#BXHyBo9(#qXcHMgy(|WYDw=<|k)2;bPaDikE$m2+~@WtsbJlw{j zYP_3s+P{!LTfI`b)DfLVLq|#B&vk*|R5s~u>LEo`yxpt0|7H5(6(J*tqV7*Y8}OAO zwh2b+Lgdx`0eQV2k)OZ9hDqaW(ml2yVvZ*`#Yu=X0?WdBBzuR^{465l5O@-q$#w)* zo8BU=V#3+gZD5X%qX8|0YcC<*;w9fc#GWP)y$heO^ajqZ>QGZ%993(pM^dGi@YV2l zu=|-+JRE;g1?yBH#S0U>fgw%5kT`cEh7`W*63&F>Yb>(l!_R<(r6^`Njq?a7XF+fb t%ZC-iej7fM?x;wLXW$8>fgtDn7UDTB1VV92e)Jh~`0(Pv$*9(IQ^w@{^YPqp xBN&P&Z@eHr`O|qn6tTkdqLY&@TtXPO+5X~NZcdm%shflEMuuOkPvm;Ga7^;qQwnWEEqFdC~OSB!Sk zVqvFnDBA6);^WS@0l?SY--!W0mn!1LUdDg*eQ~9D0}MU79<7CBT$tc<&Z z7maF|VHIyBdW@S+>-aRfOjqa>P0}3VC1f^Hd9Erja6Lquw z>Huv4xGp|MGUU5P*MK%j^ODyS>XD6{8smviOXH$_eVE-wA0-fC)ei5di5Nx}k8HJdenuI| z9&J^@GFg|ZY;Fprtcr)Jvv)!&28h{M>QUX6WL;#0O-#!any^J+B3@vBaxuGo;a(1} z*AZh2Y|4JYMogS7S*uISIRwL_;|8Lp%yeKw?o`7U$-^7V{#3_Y%Eh)ZC$dR3!#p%F z=>dm#)4<6^EU>vLp1z%!V$qpXUW&dBR_u*n1#B{{?;GH>l0!;b6rP=!wgJU*@wS*w V-yI7Ht3k delta 769 zcmZuvO-mb56ul$)5VhAu8pEVu#wj5nB${Xxb(687nuSo%E<&MoNURP-V?I;_7ybja zk2os@T~`oX=uX|capR)9Lc8n_=*}}QUj)Kq-rRTIz2}{G?)&lI_2U<%Me9_SYm17s1?)T4MeJx)pf&88l%svDOxniKXq3jNk90DC=a9LBsw(iM zCYN0rRebh%MDcia&TxWtQS$o+jYj<^Fjc4`N{2o%yD&7-!%_CKf%zTlG$*+;;H*fE zb?(ooE#NGZ@;GT(Tpg(P*O=n5&*u~tPj|iI*UP|&!9k1w$63XkY_YYJiRj;uyRPUW z;CD;hK8h*+4-KK*1Hn-q;TaA?QW&d9i{>Max1=eKh$`fC7%1YkQ0u~=H@f9JX69fk z3_x|K!8^d#V7`V+$_9zwwNSsKD8WNK#a=P!Q;ihAG2mU5=q*9=K!b9^$HGDN80xYBS|yDAF#lGG+^ZC$oqL;+$5%=)C{QvtOKE zbEMwNG<0U5dxjM_8b@@)=4P z@)>f0>=K4dhHRjGK3Kj8C<5YDGW-G3ISi>l9l1cV7^o@_h!udWL60MImx|K)FIkW~s6*9FoAK(TzFND5FqpCJuM765I@2g`$ON(AZz$(1o=>VOT+ z2Rk4WY+(sdDagPipt59!Y=(58qw;}<0<}zz

>U2diK>7@I@uIU|RLLzi-SN0FZd Xihwbd42+#3SaiYzM~T6jfr|kE+X;5B diff --git a/ownCloud/Resources/tr.lproj/Localizable.strings b/ownCloud/Resources/tr.lproj/Localizable.strings index 2ce2cbd363c92c379e35310024d9e09c72345b8f..2181c111aabc5b27ca4056d9cbd0c994f3f7d08f 100644 GIT binary patch delta 20179 zcmb8134Gnvb?4ucWLcJFZBI|nvMlsuTX@%EAja6jNVdGhYb*jWC2?d+F{-s$3yvMa zD`}QyLQKpFHA&qtr1aC7Kw`w3PFq^QZcLa?LlUMbNkhWcd~k;nH)IkAQUPaM(>pPupXy#=$kMuzpzXrwzbEQjaBXY@uoBSY!C z3L0m{+9N~yXFz|F`g>4^^vkZd6LS2=`GuX4^O5Fgul~rX*Dp^$T6QifU-`pwY08^d zw_9g)>v$y;>52454(j~Q$N~K`pbPa!Mx<_YLVjM=R&z+-4@MT~`xgCK5NVB!L||Y+ zWNoBSo~e(gkH^|_Wc!*@DVSd=KaDR*ZBr6M`qQgFeL87GhxfT3CiX|VlqEM^iL~pd z9vzX4Y}cg^I%a~!HtS~?+!yW95gQ}pkz;I8DR&zK`g@0?*{O58bxen{{*!;MFFC4f zBqMtx7j<;EKI>ebkf9eUq;yH?u1+0?;GpaIu+ib4VZ|ayG zZiP;jP^Xh&z|~%TgUXwfFI3oFqyrF2y>c*b)_I+a zJV)dg^Y+Qm59dnpj{Mmd9X)7uN=tP@R^=t5gYv2BlQG-rtvahy<(|}K`eopU)y$g7 zEZw<8N8@tY{D)>-L<{oF{Hs>Gf9dGk^v5KI%sL{s=$?=0vp)SB6HL0_Sa+{(VOUDm z*UnC=wGXI#MkD8;T<@jW;dmHQBfUlE4Lf7St>k2sbb{*Q-FegmYRn8-;#1%UannI)~d7)D(g5VE_zg6|6;twjM*D8PO6BY^f9@k zDl*(VslSKavwk-h0XpZFPd!*8mG_pf<}lb+RsX7n!6vBJC;^mfqV6+9*}BMR~gq?A3{3% z$*&T)GyFBkV`~zU^J3-f`p9*W#kxo3Q+~9jPL^#hE@P(OJ%ZX-K9TH*(hDPRHmpKt zzsh8vtX!FpA8#tHF>b=NjgFIq{OJqbGPGje;-qqc{<`!xTpx?vtbem_4{Qz!Bifny4`Fd?CTS^(D)wMg7NkTUKcSmX!>3fwfO5UF33Kq@G^(`h3 zIS4Q}<*nz+oStZYRB4&v^;9^vy^2`2R?)3_Z!F!EytY^hXU;E4D$*0%L2ETIS-PPn zfdSy$N0hfNTX3+Xx*+yMTrj{1oR5sF$BpzL)E)V-?gh#K z9+7arn|;L58j4(=3^?ARBLJxb`eXTz#*G#gUvu<;{t(c!u?X|Bq^ZyOB_qHA)Br|~ zMC8S}l{uFuWlPhd_!ebj&@mWFuH++4o$`aPmbdh|ZFWX}U+06krb@(v|FKyUGW1D- zz#yK2?1yA=^Juh7s@GS`L(Mzoxa3#&=v&*yxElvf^}9rDNM0(Am9Q5~L)q1fmP#un9Y9Oo(}{4(%cf0omUptnhrsQLr&^2dq_w3s-Byp zL-N%AwQ}LAkI4t$%!{s&U#xzA8M4Mb2{n$MH2;tI9Fp2jM}t}a@vhOkw_f&MA6+cj z3!8ywZ~e56I9W^JJxq~DkHAv8fA#TbfhQuz?q8H%wI)ATUOig8mvD)Fk!oYn;U&z% z0cN}dm0O+yj*)<4$K=R9r5|>CB#UZ}u9xN~W74+ned#yWU7I5te_kqgw^UTMspyF9 z{6=4-**JK2dgc1RjHaM?z`ei|0?Xw&TERm+Qtnp67}E94AHYK}7}Eo3_>2fZ9G61q z-vnyonta_xn!geDRs6DM>+BT8#-JJwo^nuW;a!9VsF07r$_LwuuBg|?$Odg8Hx!D7e6$GOL$XJA-!1R7#^vF*N95&=SKd7* zU!H9*mrdmwut>#^FYjCe<2 znQ(@O2cFj_WJ8#4pR|s}a#f}hT~xi8u?P)%{m$vEeDea!FfFYgPNe*^2}(-MT+oE! z9im++nSr8x(t~bS)C3y}2R>6=NI27|&8@f!=G~H{QUEeiX9GUgn^uC*(Jv2f{p@Oh z8$&b{L84?*fI70}Dbat4YI6dlSx5H0`%g4^E}z|@#y;c>eq2hneRQ#>XQc)jg&K}v zy2GE;P^IpfSRQCBFK>Hg-$rHJO4~LaPO~B~>Hn7J@f7>Wux<&T((jPi&u&n0bTo zfjdgWcMIi{`NioA2Och*4ISDHnv==cfmqEJ-Od5!hY-`P{3Iipsrs*NQv2Pi^eg@E zjxKD|DOQ@?GGzjxQ}--C`F@o&bQP?%d@4JHnuGw>1|>XoQBOnpt+uFy7nqjO4qDR= zg`9mbQJr0=v?5`pOEee)p2`gKyYw?O6rS{HT;Bd!{uNM}%D-uhO5eiwM4$rZr0;M| zEF?7NIO=xt*`cT8o68Drn7Wsc09Cy{PBlk~K_Ikcu$Hn6>NAUDE?PUQhJQJ=X};tv zTNX`9Yst#>*;}@Xn7YRjCrtmCE`$-A3G&7^#Iu7EofDIrhPT9@b=e3k^(YRzcae)S zHe6feH;qH3pB=tAI@?@@ifd4!J7RKiqWQ2~HL-YL)DJU9J~BNuWOFVK{k%m?g( zM{B_x)+ZRUg83ZNR_Do`_2sLPA3U?5 zl?!wg&g)YmOTS`Ic4XM<80?eJJi1nHf3%?5M~7!!5-|k|YcTao(9nR`vaS6$Q7|xl zzN#{1^=5bE8f9>k%7YMsKa*>b&muLX)TqZ)fel$(!AD(-4n9+Z2IKr& z7dRaq&6%Y0nuInuimxDOk-DHOc-e-!l?(cQ<_6~Py-JNnj`Akavf2$MWVi~n7v%fA zMG>IHJ}qrDGuIy|hwwCg-yg2cS=p`wS;*&NB!?Ic9y{*-4rkzRec}^yOgs31=Jir? zzFr>qR2re0gD@mkgfYdaEH$7l7V=9HuaMGq`Bjt z?5$2wqEA1EMyDk?>Du?{|NF_0=E!X;isZ^?D&+R^d^xjt-on6D=`I{m5_lIa zHzqqjl8?TYkegyf+T9QOMmMT7VF&LaQ(~_K9)bg^1XKbYc>l_x>qeMAn`wn>!OhOF zsm+{7bfbMMlsri}HlInU_3^H*OXBOF7?gCV(hC1K>d#3Je*T8)8qO)GcG)80rFyb7K@}pW2NG9!OYv>tMpa0F`~mL! zs!Cl$-a5TD<*O^RMPdZZP@ns%i9;5Tg%&^tXnX-AW zN=k8mw>q^0ejQ4~bXaua*HvxPvDUM&EO4UU zlajGDHP|pKSk1RjK?PO-BvRY6ow3ct3qE6)&<^uvZ!8G;oXOU5{Sh5Mv0A!)>>mr{ z;lH?Mrg{9RT=nSQ*+kk<4W#H04*b@uM2;54iGgG|fIkrq3m{*{2H2#*zhQOO5v*ea zI&@n;r1Q{6x2rU=I?eeyK+Wsq9W$e`nXHDF%=vy}BYA;Y2g3JQ!4+4t7Wvd?^a>uU*l?XN3ln$iwQ^XY^% zrSoJ}OL-o_FMaOOS90oFRha&0*?a>9vuws5@eXdGA(3xL_1}5A^8NmI( zic!d=Z#r|v<;yaDwpe>m?~-|~#WM0_Z*@{z2-}s=e&yl@K4c=C$c>ECCf zOk}B?Sa(n!`bB}f`>9H4db(I%eDbndOv$n^@ZYkwkYP#P7h`&21zdRtu|}hT77=A4 zYRO{{RJDiQ$|}7Zb>>PJJUk$h`dFfgJdyZ_IwJxw&({;XZziG*x~FPA3B}05Gy?1i z=-_m%63#xo&~{uM$0g`G@aR`Q{)&4vZuW zp!j~7(^R7^U~h8<^z0R=$En7h)0u8?Fl5*3oHt0_EaR{|0$Tuhhy_Sv(t6J2&CIN{ zqk4aDtVC1MAaZ&sG<g^VAVt7n#Wev*kovg>3K z_(7dEn%eZII9Jz{J9fsU;A2Hn`ke0e2L&}j36CLL0Ym>1zQ(2QxkkBWX`zPHWMoq1 zBR$WpkHHB!gVznVtAIGqoD>K0`McMweD}F)Hqrj`*Ncj<&l!V%k75F1p{TnHmox3)6QU|~AwHW)5&+(Wd}i)NFKm@#nT9~3-V;r~!Q zud3$H0y+80-=r4kB?t0wOe!dS!q2SpMOuk7HoEba5q-bW0UM@Z#_D(0 zp-=4c0fxenj?TX!Q!!viVhLR&)M3HYR_6y~DZ%1RiaR!YVx~Ov>3E7`PIAD0 z{lo8Yc?t_;px82(oIoqA48QXX@6e4PmPWnI0rcCaU|otiyo6OJu68M%%>0`*v-n+m z6MpHAOrd4Rm*)It)*Z@{_PwNed%VnDX;cO*lT=1z<(Cub-RJ)xuiYPU=y{tCAn;la zYwaw5{u6E*fUNj3Mgmf+@5rH_G^Q85c5z0oUZs@g;aWM>QYiD*SLii7%aT;QGV4F; z zj3?i~_~GBaVZTq$X}}7?K)3w+Lv<@d zWuTK>P#*d@LGIlpcNN7G;qnw}K!hd-GWJ?b`afSF|K&d)ja%J>CXsxmc9Q0mv3gH6 zOvPt~fQD;x%UDT59vJ_e-1_yojQ~kdh*A`zp+hPj4p?a&9c4EvF#PzCbbg{ru4~U* zZIQz#EP)KdD`QQgYP~$sZ|BSsLSSVsRt6Pb~&( zK(A<-vWr$TnzdWXcAI&ne&!}Gu@^_6QH$wvXT-P4{jKIpj?2*#ru&BC^4f5rB)?Ev z&TTplg|P4DC@JXO%PQrXbMt3Sw5Ft_JF%DrFG*C{MMG+mQuF4Gx#|USVk}lVnX)^E zY};9((wjSj>|$6_WmWREH!ExWU1RpROXXo`ndykh9iNKLaPFq$tMq2H_X%PHbdJh> zrxJQEEiboQFQ}rvo#zS~CpMttk*T!|xbcEF+8W-8jrG0KwzGU525WT9(1IIy%6Nri z+%H&hMVRqT&ac4D1|?w?vsXw+&1|AB*o5yeOnAwqx0J0Dxp!kx!e6BHXKR21K?}dgEc2_jYhZ6Y}ef?wnt#aVW*-0_uI%BTKbTjApf zD^9=_JFv4D65Kkm6};;;sN6}4s0+DhkHN7TegjlvRQSk1CWgAB3fd+-BAN8%4WZMP zR^OJxc`^CqpA{o_N!~X<1v#>LN?8`Oa7!Wz1Ffo);7Sw$e`VspdldU7u|GLjB2WW<7Vb3px(DrGG~o;B=_0Mk0A*un<`pR2f(jwX>Klqy%gj8&?N*D{ zBMb~YNG@0Eb}YU0v1);fX#RUaY#1B#qXO95qi=p=f=Mc?Wqp^tH6yV+Jj230U}kCz zP69Ya9+2q%Sk|reAju?S52u!V|L3S0QBa#~)b|!sDF*_M_v%P9HH@AT%gVP6`W&hF zybj(pMNO%gEOmqNew|=xpS=zaHm!JAq8SD_gU%={8a34;)Kg$VZfS^->1J^1!)UCQ zv1=pS_~y?RB}XR7_ie0V5B4+NJ9fmp4N3^z88SHemyl?MpUo_&nM3hFii~>=%Z`?+ z)LtD0E9eDd1lh8&-^=ZKXM|y0+?1Kq?=}D*?UDDXJ#BJSMm68TaP1ph!9Id9y3V!A z3>eg>@}m`Fz6TayY<}rhr*Q0<3^i1~%14$~%c}YcjSMWiNoRO122Z?@wmZFo?S_Bh ztpxAPQ~k=pbGr)QOl4w?iMJ^wY1a*au&I-$N?~H^CAPdt=~0OTe!Y(IgQ~k*LL1!W zyc9DXjiks4tr^J;ThG9yh$OXBlZ;#uxk`Wc>7PJqVWQbIr_S+m)jTuh6l8@NfJ(Lo zFa?D1Qfq_sD}34a=x$M!bFa{|+v?_#S(}Z9XVVYq)X3|-!hCtFCQ7?>7409H@A?PE znASk1!^(X?ipCT z^9)c$0JmPbzm8^D#am3rzC6V*2&10t1}A=EQIAWp0txI@iJ)Z)96;gjk5$N{1%;`g zL+WYx8~2R1Zk3l}@FtMtb{$0!r)5Df4!ghU0)vN^(f5HgV?G~nSG%U#kA+OjR6(X7 zY6P~lLl-moWuccY^sFlTJV~C&KegzYhh?c2jsubGQnm=o9;CehHYi;xL00GfZ}93JOUN(}8X_O}q`I0QNBW-ZP~la1ll{>=geFG7Frl zOP?*)UHQIH*vf1MIAAHCmmb~>Y9{!iG^ehFXO=S{?3aJ9t?lzH0Wmn$xWcHFA>nVP z(h;ySIADuD>9llp72MLkR1#doPzaOGuO<#nzaob|%~7IE_Ucms?zdexvtH_VT`vr# zBy}`qFhiQQoCBbOK=ZU0CS?z5ayPlL)FZunbh4jfqQ$ZN-HgE2fAdlV7htkmo;V&~ z8d_q~3KVB#dz?9`RdBChX2w8e=4~~4JWEdDtz?h0KSG$D3=nDHkb@83PqLMLe=6Hv zaTV6Fm}?i~=!Qdshz){LenA(mx>GJ-L(BM7A#{!35c#VVqrfQ8-wp6UZxXROf}ise^)rDd!1k?1?=9##Xa<)-8=3P{6N-j>VzCWn4TUsmVs0?BHw(@8o=f3>LKpix7;sq3;uHn6%al6>B(O<8Z~2K04~T z8jIwv!uV>Oly6b8dvuCG2|L!d_q!uV;LL;=*iK4ab!@dgow(hi6bK}YB$qTC2Dg}p z{;9UKuwibBCKT-_I@e?+^p?z8t#q~X-^ui>9tmnJY>TdW7zwduE3x=$wXI^@giIvm za(c2hzSABqCIZ6<(&(*v+7`%s@^jm0@D#g62-*70em}r~a2e7Ongm?Z!3V5|LJRvb z{=H=sz_ORN!JLI7z6u3wA&p@<(Gsr;KoDz3<^Y#+A$@<-s1ON=^=Tgpx?zd7s-_48 zxTbjlIwJcFR_40kQJo7sW`8fP)!7W+Erc#dy{t{sfxm5sD#Da*6!^>jI$|L8>oe16 zkWmfW_xZ9FyH!t3uU9I>Vrw?wt~emYr+0W(A+y17*pd}co5Q|qXPwxrK@JT)-$)8; z?Y$~J-`_x{DnBXfs&40~%o-_n!b+dh>xc`~=p-!RcHIuyVUUz;RD?hKMs4c6&a~PF zB)Qaf2UR3MCmLttSl5Bf){HNMR5l$Tx)(^Moip9+Vja%5GSe|5{U%32ZLkbQB4;q1 zE!h}|=IcM3zwA{#?bn~MfrpaMte=fjd!1nYi1yHYz((h-)4y&EJw}#BZfEG}wuz@5p{JYW?6>3E zE*q35&Q?j$$!lI&IYL|Ro|ms-oeouHli zU1(znSpiV1DgOnzPsm zF_{N0;Qcpiq&^S)nR5XCODUTXSZIf`L49F&vg{j9pa)09M1|mgXEGj~;mN_}Wi@u2 znJsW`U_gta0oY*~I3AbJJ)bAHozvS#jd>+-X;|;}A-nEO$f?Ohl+}g21;0n|?;a$QT&!**2DeIatdb}jVXH)0z#)};{+v;9ZAoBt3 z6@fI_S+diS46IGm0-zvtc1}(7b;Xf%*2ZMjn~Ti%q$boAX_>cF4 zow^REXj8`mPI!U!g8-!9i(R&Y^NsiE3;sJZ{v#M}o0)Nv$6XvuUii1@LHz|s~!b{q!R0$b%%C^$@pkFCUP zS%cW7#@hZ|uR%6ht!dK6RO8!S44ZYKG2NzXY__pUH&tYHT3JHW?HbcGQ`mDKx^3hB zv3Ys--goZ#_?_Q5_r7~)Vn<##pFI|@e-Q6ptov-^qMq8rbpJPm(JCSFXd(x=jb=C}^g&Rfm+S>_HnuN1+Vbp&%+r{mgLy+!QC8Eqvl zYBoK`Wg=Y6F2R(m1Q)X3<@E?0%RNY6uIV2=RbkbAYa@gwOPq-tL?=I*Xy&*<(Sqko z8qiqc!&jOO+WHr;bG-#e)(0@WzLIAfN=qFmEOqMP(iszOl!bAl{7twj?TD6#VEsu5 z)8%%(sB(}HvY0XSOujz#OhOE8lM`F%F;rbo!oH2`;Ev=V5?P~P*jQrH%eI^*{mZSF z3Gsn*)%6TRy&DWNT0}QLnvu6ADtYeYOuL1L$qMnKcGP~5;rH+y;#pa=i$+5;Uuj@* z;PJFh-f0pYhJ~WHMTA8kd+Efoz_M76iiyqJGqM}lmu7ftGjtR+E$pj7>=A}H(TZfx z<24J#dWdfiTlBI=O|3JSs{Q4Oyi8HM7Ssmq10X!0tb!lqv9!IAU2J z+w3t?Ycy;$E{h^_7mu2e(k+%Z@heB3rt~^hlySpZi>_ZXN&7Kr)rhu(G z8CMU}t!Pnx8`!Sn2eDKX<-0?1g6oOQ*=tsPZ%w}mf!ZO2Yb)Wc+j}&NQgNxSA6M(P zW2io&fAaIF17~|2sJNJf!;8%4de2%)dkpr1j6p?ISQNiM=GP_4S)xu+FTaXqjd3Zi zzpt%FU-px39w;N6+LxOAhyp&|_i^_B;fVGAg5M2pL1fT@%>Aj0x)@Na(98DM(~<@b ztb%jD;l$8V3=TP$eotoXb%_u*9!SI815N}EBqb`66v?W4dBuBwX8C)#{7Nepz3SE* zU!67S(+7Pqn0dVt;lo3C>F`Us|BV%91b&^X=Nv0G!87beWY~d|!=0EJPRE+#9+V!} z^t-=ds$EVyZv5M>UpjfnB)?uwcEn#x&`aO8#>73xan-$5{Z8KE2W!!-PrOsE;o=1c zUieddS}%iBy`qv@IO?jOgQ5c+AFVCu;l5Zn^h&mnypnvUZ6x*Ja5gb5`&Tc6)wyVy z>dH>GQQg#Jbj`xqXkv*`MNDkX$kRj_6QjQPnc79%E)c*DIkR^6bz zIP=%ldgG-J+ZitOt6pJ@Y&~2%6qH13* zzq%9WuiBCGk0gvuCSdHQ1!pIj)N85wwrduGZL(5r>SL4RW|a6-F}YPkh_$M&TX6bj z5~gmx$hT?;nO}GiXt2L|%Y(1JEJV+(EiB4J?LReGr+pZYxKKQu8MjtgxOv-gV)_eG zu&d-b^G#f?V>}?F)uE4?DMKHZ)hviNuqPN4icV|1kxl<)0KP8)f z;qI~6>`hFwqEh`%%6YBsG1Xh&9hB$sw`c~UJL2`R`wlbS|8^TH=J?m~tsNzEZRnrN z!3T35T$N2D+l}oOXbM&y2wwvYoRI-gmGihH8PQ8~T%iJ_3huu^wYoDN_a_1BD ziaeA{W%5Wa70QG>9&CB!mIG=PNuhLku!!R2)jVoTu4a(xzSWH14abwoUiy`6^-_*J z?Pa{@yyTQoFO^7B0eNLx0lAVlbB?==%Sv6dL9t67FQ9CBs(@_LwSx7h3n*1yDPSM` z=~pYeT(^R}G2A_JMSDnZJ@WIk1w}$)T0xkt3_v;MpP?lqXlwCbinD VWT({UQTbspjmXQzbjYmw@IRyS$|V2* diff --git a/ownCloud/Resources/zh-Hans.lproj/InfoPlist.strings b/ownCloud/Resources/zh-Hans.lproj/InfoPlist.strings index 1f48b880c0ac155e44028a1b1e490d7e90314db6..76e7fe67be8659d18c295e8a3f144e916652340a 100644 GIT binary patch delta 199 zcmdlaynug03yT_C%%rGb|Mt{xeq4SE{suuS5?ac5(-#J1gfUES>`cjaP7j#IWU!fw zHHC3<94nuKMmS%{hhY5>jnec&_oAMVW1SOcw3NMSToDA)&tx!JhCK+RP!7LBpfZ`s cdK@;yDCOhyk_e4A3j$e%aFh~*H3Js|0GS3xRR910 delta 763 zcmdT?yAAqjwYPJxPreFRw0p%Di*#lxZRjDD@T{igYoRw`{CZchJC c)DGO?s9mv0uUjP6A diff --git a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings index 7ecdb064222c9f4898ddacc76ed65e9323ee2bea..06d5f7335ae8deaa65f9b75d92bc34de52c5a491 100644 GIT binary patch delta 14 WcmX@p$GoJId4tUU%|{M)umS)xwg%n+ delta 592 zcmZ3|$$X}dd4tUU$#HI6p}7p14CxF-42cXSKvq6O9)kiy8jzpQkPBqxFr)%y6B&{j zDuKL2h5{f|V9;XFXW(K`VsHeCCj(WM0(F%D#eHFBO?H$N*2gIew@Hb?8fp&E+{p*S zg!z?#rq}{8$ZV*XGE9{x(3WDL-bAQvp+FTmK-&`;QYHub3s2_L=aMF*X2DF3dblgW z?#KiB4`C}dIfxsPWl|YZaOi=^Ky$aT$S1A^} delta 14 WcmaF&pSkTn^M)I)n;qQN?F0ZmXb0v1 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/SDK Extensions/OCCertificate+Extension.swift b/ownCloud/SDK Extensions/OCCertificate+Extension.swift index 3ae4bfc04..18a9d9a41 100644 --- a/ownCloud/SDK Extensions/OCCertificate+Extension.swift +++ b/ownCloud/SDK Extensions/OCCertificate+Extension.swift @@ -28,29 +28,30 @@ extension OCCertificate { var color = UIColor.red var shortDescription = "" var longDescription = "" + let css = Theme.shared.activeCollection.css switch status { case .none: break case .error: - color = Theme.shared.activeCollection.errorColor + color = css.getColor(.stroke, selectors: [.error], for: nil) ?? .systemRed // Theme.shared.activeCollection.errorColor shortDescription = "Error".localized longDescription = "\("Validation Error".localized) \(error.localizedDescription)" case .reject: - color = Theme.shared.activeCollection.errorColor + color = css.getColor(.stroke, selectors: [.error], for: nil) ?? .systemRed // Theme.shared.activeCollection.errorColor shortDescription = "Rejected".localized longDescription = "Certificate was rejected by user.".localized case .promptUser: - color = Theme.shared.activeCollection.warningColor + color = css.getColor(.stroke, selectors: [.warning], for: nil) ?? .systemYellow shortDescription = "Warning".localized longDescription = "Certificate has issues.\nOpen 'Certificate Details' for more informations.".localized case .passed: - color = Theme.shared.activeCollection.successColor + color = css.getColor(.stroke, selectors: [.success], for: nil) ?? .systemGreen shortDescription = "Passed".localized longDescription = "No issues found. Certificate passed validation.".localized case .userAccepted: - color = Theme.shared.activeCollection.warningColor + color = css.getColor(.stroke, selectors: [.warning], for: nil) ?? .systemYellow shortDescription = "Accepted".localized longDescription = "Certificate may have issues, but was accepted by user.\nOpen 'Certificate Details' for more informations.".localized } 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/DataSettingsSection.swift b/ownCloud/Settings/DataSettingsSection.swift index 5aa557cb9..347afc117 100644 --- a/ownCloud/Settings/DataSettingsSection.swift +++ b/ownCloud/Settings/DataSettingsSection.swift @@ -97,7 +97,7 @@ class DataSettingsSection: SettingsSection { // MARK: - Cellular settings UI func pushCellularSettings() { - let cellularSettingsViewController = CellularSettingsViewController(style: .grouped) + let cellularSettingsViewController = CellularSettingsViewController(style: .insetGrouped) cellularSettingsViewController.changeHandler = { [weak self] in self?.cellularRow?.cell?.detailTextLabel?.text = self?.cellularSummary @@ -108,7 +108,7 @@ class DataSettingsSection: SettingsSection { // MARK: - Local copy expiration UI func pushLocalCopyExpirationSettings() { - let localCopyExpirationViewController = StaticTableViewController(style: .grouped) + let localCopyExpirationViewController = StaticTableViewController(style: .insetGrouped) let localCopyExpirationSelectionSection = StaticTableViewSection(headerTitle: "Delete unused local copies".localized, footerTitle: "Time measured since uploading, editing, downloading or viewing the respective file through this device. Does not apply to files downloaded via the Available Offline feature. Local copies may be deleted before the given period of time has passed, f.ex. because there's a newer version of a file on the server - or through the manual deletion of offline copies. Also, local copies may not be deleted after the given period of time has passed, f.ex. if an action is performed on it, the file is still in use - or the account holding the file hasn't been used in the app.".localized) var timeIntervals : [Int] = [ 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/LogSettingsViewController.swift b/ownCloud/Settings/LogSettingsViewController.swift index 284183373..d614e0813 100644 --- a/ownCloud/Settings/LogSettingsViewController.swift +++ b/ownCloud/Settings/LogSettingsViewController.swift @@ -84,7 +84,7 @@ class LogSettingsViewController: StaticTableViewController { let logsRow = StaticTableViewRow(subtitleRowWithAction: { [weak self] (_, _) in let logFilesViewController = LogFilesViewController(style: .plain) self?.navigationController?.pushViewController(logFilesViewController, animated: true) - }, title: "Browse".localized, accessoryType: .disclosureIndicator, identifier: "viewLogs") + }, title: "Browse".localized, accessoryType: .disclosureIndicator, identifier: "viewLogs") logBrowseSection?.add(row: logsRow) logBrowseSection?.footerTitle = "The last 10 archived logs are kept on the device - with each log covering up to 24 hours of usage. When sharing please bear in mind that logs may contain sensitive information such as server URLs and user-specific information.".localized @@ -92,7 +92,7 @@ class LogSettingsViewController: StaticTableViewController { } // Privacy - // TODO: Reactivate the below code when the code base is reviewed in terms of correct masking of private data + // Reactivate the below code when the code base is reviewed in terms of correct masking of private data #if false if logPrivacySection == nil { logPrivacySection = StaticTableViewSection(headerTitle: "Privacy".localized) diff --git a/ownCloud/Settings/MediaFilesSettings.swift b/ownCloud/Settings/MediaFilesSettings.swift index 614e31165..01388d7e6 100644 --- a/ownCloud/Settings/MediaFilesSettings.swift +++ b/ownCloud/Settings/MediaFilesSettings.swift @@ -62,7 +62,7 @@ class MediaFilesSettingsSection: SettingsSection { } private func pushMediaUploadSettings() { - let mediaUploadSettingsViewController = MediaUploadSettingsViewController(style: .grouped) + let mediaUploadSettingsViewController = MediaUploadSettingsViewController(style: .insetGrouped) self.viewController?.navigationController?.pushViewController(mediaUploadSettingsViewController, animated: true) } } diff --git a/ownCloud/Settings/MoreSettingsSection.swift b/ownCloud/Settings/MoreSettingsSection.swift index e2c788258..5120c5eb9 100644 --- a/ownCloud/Settings/MoreSettingsSection.swift +++ b/ownCloud/Settings/MoreSettingsSection.swift @@ -88,7 +88,7 @@ class MoreSettingsSection: SettingsSection { } acknowledgementsRow = StaticTableViewRow(rowWithAction: { (row, _) in - row.viewController?.navigationController?.pushViewController(AcknowledgementsTableViewController(style: .grouped), animated: true) + row.viewController?.navigationController?.pushViewController(AcknowledgementsTableViewController(style: .insetGrouped), animated: true) }, title: "Acknowledgements".localized, accessoryType: .disclosureIndicator, identifier: "acknowledgements") var buildType = "release".localized 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/Settings/SecuritySettingsSection.swift b/ownCloud/Settings/SecuritySettingsSection.swift index 0a2f616ad..5b74348c7 100644 --- a/ownCloud/Settings/SecuritySettingsSection.swift +++ b/ownCloud/Settings/SecuritySettingsSection.swift @@ -110,7 +110,7 @@ class SecuritySettingsSection: SettingsSection { frequencyRow = StaticTableViewRow(subtitleRowWithAction: { [weak self] (row, _) in if let vc = self?.viewController { - let newVC = StaticTableViewController(style: .grouped) + let newVC = StaticTableViewController(style: .insetGrouped) newVC.title = "Lock application".localized let frequencySection = StaticTableViewSection(headerTitle: "Lock application".localized, footerTitle: "If you choose \"Immediately\" the App will be locked, when it is no longer in foreground.".localized) @@ -174,9 +174,7 @@ class SecuritySettingsSection: SettingsSection { // Creation of certificate management row certificateManagementRow = StaticTableViewRow(rowWithAction: { (row, _) in - let certificateManagementViewController = CertificateManagementViewController(style: UITableView.Style.grouped) - - row.viewController?.navigationController?.pushViewController(certificateManagementViewController, animated: true) + row.viewController?.navigationController?.pushViewController(CertificateManagementViewController(style: .insetGrouped), animated: true) }, title: "Certificates".localized, accessoryType: .disclosureIndicator, identifier: "Certificates") } diff --git a/ownCloud/Settings/SettingsViewController.swift b/ownCloud/Settings/SettingsViewController.swift index 01198a9d6..03c2e4c74 100644 --- a/ownCloud/Settings/SettingsViewController.swift +++ b/ownCloud/Settings/SettingsViewController.swift @@ -23,6 +23,14 @@ import ownCloudAppShared class SettingsViewController: StaticTableViewController { + init() { + super.init(style: .insetGrouped) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() self.navigationItem.title = "Settings".localized diff --git a/ownCloud/Settings/UserInterfaceSettingsSection.swift b/ownCloud/Settings/UserInterfaceSettingsSection.swift index b0d4c6e03..06dd04ab4 100644 --- a/ownCloud/Settings/UserInterfaceSettingsSection.swift +++ b/ownCloud/Settings/UserInterfaceSettingsSection.swift @@ -57,7 +57,7 @@ class UserInterfaceSettingsSection: SettingsSection { } func pushThemeStyleSelector() { - let styleSelectorViewController = StaticTableViewController(style: .grouped) + let styleSelectorViewController = StaticTableViewController(style: .insetGrouped) styleSelectorViewController.navigationItem.title = "Theme".localized if let styleSelectorSection = styleSelectorViewController.sectionForIdentifier("theme-style-selection") { @@ -97,6 +97,6 @@ class UserInterfaceSettingsSection: SettingsSection { } func pushLogSettings() { - self.viewController?.navigationController?.pushViewController(LogSettingsViewController(style: .grouped), animated: true) + self.viewController?.navigationController?.pushViewController(LogSettingsViewController(style: .insetGrouped), animated: true) } } 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/StaticLoginSingleAccountServerListViewController.swift b/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift index e54148bad..915e123bd 100644 --- a/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift @@ -358,8 +358,7 @@ class StaticLoginSingleAccountServerListViewController: ServerListTableViewContr } @IBAction override func settings() { - let viewController : SettingsViewController = SettingsViewController(style: .grouped) - let navigationController : ThemeNavigationController = ThemeNavigationController(rootViewController: viewController) + let navigationController : ThemeNavigationController = ThemeNavigationController(rootViewController: SettingsViewController()) // Prevent any in-progress connection from being shown resetPreviousBookmarkSelection() diff --git a/ownCloud/Static Login/Interface/StaticLoginViewController.swift b/ownCloud/Static Login/Interface/StaticLoginViewController.swift index 3562c556e..45bda2a16 100644 --- a/ownCloud/Static Login/Interface/StaticLoginViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginViewController.swift @@ -352,8 +352,7 @@ class StaticLoginViewController: UIViewController, Themeable, StateRestorationCo } @objc func settings() { - let viewController : SettingsViewController = SettingsViewController(style: .grouped) - let navigationViewController : ThemeNavigationController = ThemeNavigationController(rootViewController: viewController) + let navigationViewController : ThemeNavigationController = ThemeNavigationController(rootViewController: SettingsViewController()) self.present(navigationViewController, animated: true, completion: nil) } @@ -366,7 +365,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/Tasks/ScheduledTaskManager.swift b/ownCloud/Tasks/ScheduledTaskManager.swift index c830dca55..bcbce554d 100644 --- a/ownCloud/Tasks/ScheduledTaskManager.swift +++ b/ownCloud/Tasks/ScheduledTaskManager.swift @@ -307,7 +307,6 @@ class ScheduledTaskManager : NSObject { // MARK: - Background tasks handling for iOS >= 13 -@available(iOS 13, *) extension ScheduledTaskManager { static let backgroundRefreshInterval = TimeInterval(15 * 60) diff --git a/ownCloud/Theming/ThemeCertificateViewController.swift b/ownCloud/Theming/ThemeCertificateViewController.swift deleted file mode 100644 index a6b162e8e..000000000 --- a/ownCloud/Theming/ThemeCertificateViewController.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ThemeCertificateViewController.swift -// ownCloud -// -// Created by Felix Schwarz on 23.08.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 ownCloudUI -import ownCloudAppShared - -class ThemeCertificateViewController: OCCertificateViewController, Themeable { - override func viewDidLoad() { - super.viewDidLoad() - - Theme.shared.register(client: self, applyImmediately: true) - } - - deinit { - Theme.shared.unregister(client: self) - } - - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.tableView.backgroundColor = collection.tableGroupBackgroundColor - self.tableView.separatorColor = collection.tableSeparatorColor - - self.sectionHeaderTextColor = collection.tableRowColors.secondaryLabelColor - - self.lineTitleColor = collection.tableRowColors.secondaryLabelColor - self.lineValueColor = collection.tableRowColors.labelColor - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell : UITableViewCell = super.tableView(tableView, cellForRowAt: indexPath) - - cell.backgroundColor = Theme.shared.activeCollection.tableRowColors.backgroundColor - - return cell - } -} 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/UI Elements/CollapsibleProgressBar.swift b/ownCloud/UI Elements/CollapsibleProgressBar.swift index 02b1d41e1..d5d140d14 100644 --- a/ownCloud/UI Elements/CollapsibleProgressBar.swift +++ b/ownCloud/UI Elements/CollapsibleProgressBar.swift @@ -27,7 +27,7 @@ private struct CollapsibleProgressBarUpdate { class CollapsibleProgressBar: UIView, Themeable { var contentView : UIView = UIView() var fillView : UIView = UIView() - var progressView : UIProgressView = UIProgressView() + var progressView : ThemeCSSProgressView = ThemeCSSProgressView() var progressLabelView : UILabel = UILabel(frame: CGRect.zero) var contentViewHeight : CGFloat = 0 var heightConstraint : NSLayoutConstraint? @@ -165,12 +165,15 @@ class CollapsibleProgressBar: UIView, Themeable { // MARK: - Theming func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - fillView.backgroundColor = collection.toolbarColors.backgroundColor - contentView.backgroundColor = collection.toolbarColors.backgroundColor - progressLabelView.textColor = collection.toolbarColors.labelColor + let fillColor = collection.css.getColor(.fill, selectors: [.toolbar], for: self) // collection.toolbarColors.backgroundColor + let strokeColor = collection.css.getColor(.stroke, selectors: [.toolbar], for: self) // collection.toolbarColors.labelColor - progressView.trackTintColor = collection.toolbarColors.backgroundColor?.lighter(0.1) - progressView.tintColor = collection.toolbarColors.tintColor + fillView.backgroundColor = fillColor + contentView.backgroundColor = fillColor + progressLabelView.textColor = strokeColor + +// progressView.trackTintColor = collection.toolbarColors.backgroundColor?.lighter(0.1) +// progressView.tintColor = collection.toolbarColors.tintColor } // MARK: - Collapsing diff --git a/ownCloud/UI Elements/RoundedInfoView.swift b/ownCloud/UI Elements/RoundedInfoView.swift deleted file mode 100644 index 72e8293b4..000000000 --- a/ownCloud/UI Elements/RoundedInfoView.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// RoundedInfoView.swift -// ownCloud -// -// Created by Matthias Hühne on 19.03.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 RoundedInfoView: UIView, Themeable { - - // MARK: - Constants - let cornerRadius : CGFloat = 10.0 - let borderWidth : CGFloat = 1.0 - let horizontalPadding : CGFloat = 10.0 - let verticalPadding : CGFloat = 20.0 - let verticalLabelPadding : CGFloat = 10.0 - let font : UIFont = UIFont.systemFont(ofSize: 14) - - // MARK: - Instance Variables - var infoText : String = "" { - didSet { - label.text = infoText - } - } - var messageThemeApplierToken : ThemeApplierToken? - var backgroundView = UIView() - var label = UILabel() - - init(text: String) { - super.init(frame: CGRect.zero) - infoText = text - Theme.shared.register(client: self, applyImmediately: true) - setupView() - } - - 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 - } - } - - private func setupView() { - backgroundView.layer.cornerRadius = cornerRadius - backgroundView.layer.borderWidth = borderWidth - backgroundView.translatesAutoresizingMaskIntoConstraints = false - self.addSubview(backgroundView) - - label.textAlignment = .center - label.font = font - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - self.addSubview(label) - - NSLayoutConstraint.activate([ - backgroundView.leftAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leftAnchor, constant: horizontalPadding), - backgroundView.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -horizontalPadding), - backgroundView.topAnchor.constraint(equalTo: self.topAnchor, constant: verticalPadding), - backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0), - backgroundView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0), - - label.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: horizontalPadding), - label.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: -horizontalPadding), - label.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: verticalLabelPadding), - label.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -verticalLabelPadding), - label.widthAnchor.constraint(greaterThanOrEqualToConstant: 0), - label.heightAnchor.constraint(greaterThanOrEqualToConstant: 0) - ]) - } - - // MARK: - Theme support - - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - backgroundView.backgroundColor = Theme.shared.activeCollection.tableRowColors.backgroundColor - backgroundView.layer.borderColor = Theme.shared.activeCollection.tableRowBorderColor?.cgColor - label.textColor = Theme.shared.activeCollection.tableRowColors.labelColor - } - -} diff --git a/ownCloud/UI Elements/TextViewController.swift b/ownCloud/UI Elements/TextViewController.swift index 3c093e241..e668324a7 100644 --- a/ownCloud/UI Elements/TextViewController.swift +++ b/ownCloud/UI Elements/TextViewController.swift @@ -55,7 +55,6 @@ class TextViewController: UIViewController, Themeable { } func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - textView?.textColor = collection.tableRowColors.labelColor - textView?.backgroundColor = collection.tableBackgroundColor + textView?.apply(css: collection.css, properties: [.stroke, .fill]) } } 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..c30f92b9a 100644 --- a/ownCloudAppShared/AppLock/PasscodeViewController.swift +++ b/ownCloudAppShared/AppLock/PasscodeViewController.swift @@ -93,6 +93,12 @@ public class PasscodeViewController: UIViewController, Themeable { } } + if keypadButtonsEnabled { + self.cssSelectors = [ .modal, .passcode ] + } else { + self.cssSelectors = [ .modal, .passcode, .disabled ] + } + self.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .update) } } @@ -107,18 +113,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,13 +148,15 @@ 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 super.init(nibName: "PasscodeViewController", bundle: Bundle(for: PasscodeViewController.self)) + self.cssSelector = .passcode + self.modalPresentationStyle = .fullScreen } @@ -162,33 +170,45 @@ public class PasscodeViewController: UIViewController, Themeable { self.title = VendorServices.shared.appName self.cancelButton?.setTitle("Cancel".localized, for: .normal) + self.cancelButton?.cssSelector = .cancel + + self.messageLabel?.cssSelector = .title + self.passcodeLabel?.cssSelector = .code + self.errorMessageLabel?.cssSelector = .subtitle + self.timeoutMessageLabel?.cssSelectors = [.title, .timeout] self.message = { self.message }() 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 - } + if let biometricalSecurityName = LAContext().supportedBiometricsAuthenticationName() { + self.biometricalButton?.accessibilityLabel = biometricalSecurityName + } - if #available(iOS 13.4, *) { - for button in keypadButtons! { + if let keypadButtons { + let keypadFont = UIFont.systemFont(ofSize: 34) + + for button in keypadButtons { + if button != deleteButton { + button.cssSelector = .digit + } + button.titleLabel?.font = keypadFont PointerEffect.install(on: button, effectStyle: .highlight) } - PointerEffect.install(on: cancelButton!, effectStyle: .highlight) - PointerEffect.install(on: deleteButton!, effectStyle: .highlight) - PointerEffect.install(on: biometricalButton!, effectStyle: .highlight) + + deleteButton?.cssSelector = .backspace } PointerEffect.install(on: cancelButton!, effectStyle: .highlight) PointerEffect.install(on: deleteButton!, effectStyle: .highlight) + PointerEffect.install(on: biometricalButton!, effectStyle: .highlight) } public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -316,27 +336,23 @@ public class PasscodeViewController: UIViewController, Themeable { return .darkContent } - return Theme.shared.activeCollection.statusBarStyle + return Theme.shared.activeCollection.css.getStatusBarStyle(for: self) ?? .default } open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - - lockscreenContainerView?.backgroundColor = collection.tableBackgroundColor + lockscreenContainerView?.apply(css: collection.css, properties: [.fill]) messageLabel?.applyThemeCollection(collection, itemStyle: .title, itemState: keypadButtonsEnabled ? .normal : .disabled) - errorMessageLabel?.applyThemeCollection(collection, itemStyle: .message, itemState: keypadButtonsEnabled ? .normal : .disabled) - passcodeLabel?.applyThemeCollection(collection, itemStyle: .title, itemState: keypadButtonsEnabled ? .normal : .disabled) - timeoutMessageLabel?.applyThemeCollection(collection, itemStyle: .message, itemState: keypadButtonsEnabled ? .normal : .disabled) - - for button in keypadButtons! { - button.applyThemeCollection(collection, itemStyle: .bigTitle) - } - deleteButton?.themeColorCollection = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: collection.neutralColors.normal.background, background: .clear)) + messageLabel?.apply(css: collection.css, properties: [.stroke]) + errorMessageLabel?.apply(css: collection.css, properties: [.stroke]) + passcodeLabel?.apply(css: collection.css, properties: [.stroke]) + timeoutMessageLabel?.apply(css: collection.css, properties: [.stroke]) - biometricalImageView?.tintColor = collection.tintColor - - cancelButton?.applyThemeCollection(collection, itemStyle: .defaultForItem) + if let deleteButton { + // Biometrical image should get same tint color as deleteButton + biometricalImageView?.tintColor = collection.css.getColor(.stroke, for: deleteButton) + } } } @@ -352,3 +368,11 @@ extension PasscodeViewController: UITextFieldDelegate { return false } } + +extension ThemeCSSSelector { + static let passcode = ThemeCSSSelector(rawValue: "passcode") + static let digit = ThemeCSSSelector(rawValue: "digit") + static let code = ThemeCSSSelector(rawValue: "code") + static let backspace = ThemeCSSSelector(rawValue: "backspace") + static let timeout = ThemeCSSSelector(rawValue: "timeout") +} diff --git a/ownCloudAppShared/Branding/Branding+App.swift b/ownCloudAppShared/Branding/Branding+App.swift index 084b6b265..c79741eba 100644 --- a/ownCloudAppShared/Branding/Branding+App.swift +++ b/ownCloudAppShared/Branding/Branding+App.swift @@ -6,6 +6,16 @@ // Copyright © 2021 ownCloud GmbH. All rights reserved. // +/* + * Copyright (C) 2021, 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 @@ -251,16 +261,17 @@ extension Branding { extension Branding { func generateThemeStyle(from theme: [String : Any], generic: [String : Any]) -> ThemeStyle? { - let style = theme["ThemeStyle"] as? String ?? "contrast" + let style = theme["ThemeStyle"] as? String ?? ThemeCollectionStyle.light.rawValue let identifier = theme["Identifier"] as? String ?? "com.owncloud.branding" let name = theme["Name"] as? String ?? "ownCloud-branding-theme" + let cssRecordStrings = theme["cssRecords"] as? [String] if let themeStyle = ThemeCollectionStyle(rawValue: style), let darkBrandColor = theme["darkBrandColor"] as? String, let lightBrandColor = theme["lightBrandColor"] as? String { let colors = theme["Colors"] as? NSDictionary let styles = theme["Styles"] as? NSDictionary - return ThemeStyle(styleIdentifier: identifier, localizedName: name.localized, lightColor: lightBrandColor.colorFromHex ?? UIColor.red, darkColor: darkBrandColor.colorFromHex ?? UIColor.blue, themeStyle: themeStyle, customizedColorsByPath: nil, customColors: colors, genericColors: generic as NSDictionary?, interfaceStyles: styles) + return ThemeStyle(styleIdentifier: identifier, localizedName: name.localized, lightColor: lightBrandColor.colorFromHex ?? UIColor.red, darkColor: darkBrandColor.colorFromHex ?? UIColor.blue, themeStyle: themeStyle, customColors: colors, genericColors: generic as NSDictionary?, interfaceStyles: styles, cssRecordStrings: cssRecordStrings) } return nil 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..0ec750df9 --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/AccountControllerSpacesGridViewController.swift @@ -0,0 +1,85 @@ +// +// 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: AccountControllerSpacesGridViewController.cellLayout(for: .current)) + + 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), + .title("No spaces".localized, 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) + }) + } + } + + static func cellLayout(for traitCollection: UITraitCollection) -> CollectionViewSection.CellLayout { + return .fillingGrid(minimumWidth: 260, maximumWidth: 300, computeHeight: { width in + return floor(width * 3 / 4) + }, cellSpacing: NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), sectionInsets: NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5), center: true) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + OnMainThread { + self.spacesSection.cellLayout = AccountControllerSpacesGridViewController.cellLayout(for: self.traitCollection) + } + } + + 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..ba096440e --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/AccountControllerCell.swift @@ -0,0 +1,369 @@ +// +// 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 = ThemeCSSLabel(withSelectors: [.title]) + public var detailLabel: UILabel = ThemeCSSLabel(withSelectors: [.description]) + 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 + + cssSelectors = [.account] + iconView.cssSelectors = [.icon] + disconnectButton.cssSelectors = [.disconnect] + + 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) + + if let backgroundColor = collection.css.getColor(.fill, for: self) { + backgroundConfig.backgroundColor = backgroundColor + } + if let cornerRadius = collection.css.getCGFloat(.cornerRadius, for: self) { + backgroundConfig.cornerRadius = cornerRadius + } + + var disconnectButtonConfig = disconnectButton.configuration + disconnectButtonConfig?.baseBackgroundColor = collection.css.getColor(.fill, for: disconnectButton) + disconnectButtonConfig?.baseForegroundColor = collection.css.getColor(.stroke, for: disconnectButton) + disconnectButton.configuration = disconnectButtonConfig + + 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) + } + })) + } +} + +public extension ThemeCSSSelector { + static let account = ThemeCSSSelector(rawValue: "account") + static let disconnect = ThemeCSSSelector(rawValue: "disconnect") +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift index f84f3866c..fe40d75ce 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ActionCell.swift @@ -53,6 +53,12 @@ class ActionCell: ThemeableCollectionViewCell { } var type : OCActionType = .regular { didSet { + switch type { + case .warning: cssSelectors = [.action, .warning] + case .destructive: cssSelectors = [.action, .destructive] + case .regular: cssSelectors = [.action] + } + if superview != nil { applyThemeCollectionToCellContents(theme: Theme.shared, collection: Theme.shared.activeCollection, state: .normal) } @@ -67,6 +73,8 @@ class ActionCell: ThemeableCollectionViewCell { } func configure() { + cssSelectors = [.action] + iconView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false @@ -138,13 +146,15 @@ class ActionCell: ThemeableCollectionViewCell { } override func updateConfiguration(using state: UICellConfigurationState) { + super.updateConfiguration(using: state) + let collection = Theme.shared.activeCollection 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 = collection.css.getColor(.fill, state: [.highlighted], for: self) } else { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.normal.background : UIColor(white: 0, alpha: 0.05) + backgroundConfig?.backgroundColor = collection.css.getColor(.fill, for: self) } backgroundConfig?.cornerRadius = 8 @@ -155,8 +165,10 @@ class ActionCell: ThemeableCollectionViewCell { override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { super.applyThemeCollectionToCellContents(theme: theme, collection: collection, state: state) - titleLabel.textColor = (type == .destructive) ? collection.destructiveColors.normal.foreground : collection.tintColor - iconView.tintColor = (type == .destructive) ? collection.destructiveColors.normal.foreground : collection.tintColor + let tintColor = collection.css.getColor(.stroke, for: self) + + iconView.tintColor = tintColor + titleLabel.textColor = tintColor setNeedsUpdateConfiguration() } @@ -186,11 +198,73 @@ 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: cell.cssSelectors = [.warning] + case .destructive: cell.cssSelectors = [.destructive] + case .regular: cell.cssSelectors = [] + } + + backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell() + + 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)) + } + } + + cell.accessories = accessories + cell.contentConfiguration = content + cell.backgroundConfiguration = backgroundConfiguration + cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) + }) + } + 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) } @@ -198,3 +272,7 @@ extension ActionCell { })) } } + +extension ThemeCSSSelector { + static let action = ThemeCSSSelector(rawValue: "action") +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift new file mode 100644 index 000000000..5df4b55b0 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift @@ -0,0 +1,84 @@ +// +// 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 +import ownCloudSDK + +class DriveGridCell: DriveHeaderCell { + var moreButton: UIButton = UIButton() + var moreAction: OCAction? { + didSet { + moreButton.isHidden = (moreAction == nil) + } + } + + 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 + + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 10) + var buttonConfig = UIButton.Configuration.filled() + buttonConfig.image = UIImage(systemName: "ellipsis", withConfiguration: symbolConfig) + buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6) + buttonConfig.buttonSize = .mini + buttonConfig.cornerStyle = .capsule + + moreButton.configuration = buttonConfig + moreButton.translatesAutoresizingMaskIntoConstraints = false + moreButton.addAction(UIAction(handler: { [weak self] _ in + self?.moreAction?.run() + }), for: .primaryActionTriggered) + moreButton.isHidden = true + moreButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + contentView.addSubview(moreButton) + } + + override func configureLayout() { + super.configureLayout() + + moreButton.setContentHuggingPriority(.required, for: .horizontal) + + NSLayoutConstraint.activate([ + titleLabel.centerYAnchor.constraint(equalTo: moreButton.centerYAnchor), + titleLabel.trailingAnchor.constraint(equalTo: moreButton.leadingAnchor, constant: -10), + moreButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10) + ]) + } + + override var subtitle: String? { + didSet { + subtitleLabel.text = subtitle ?? " " // Ensure the grid cells' titles align by always showing a subtitle - if necessary, an empty one + } + } + + override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { + super.applyThemeCollectionToCellContents(theme: theme, collection: collection, state: state) + + moreButton.apply(css: collection.css, properties: [.stroke, .fill]) + } +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift index 9ab37ecde..c2b4952cb 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveHeaderCell.swift @@ -31,12 +31,16 @@ class DriveHeaderCell: DriveListCell { weak var collectionViewController: CollectionViewController? var collectionItemRef: CollectionViewController.ItemRef? + var coverImageHeightConstraint : NSLayoutConstraint? + deinit { Theme.shared.unregister(client: self) coverObservation?.invalidate() } override func configure() { + cssSelectors = [.header, .drive] + darkBackgroundView.translatesAutoresizingMaskIntoConstraints = false darkBackgroundView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.4) @@ -53,36 +57,44 @@ class DriveHeaderCell: DriveListCell { textOuterSpacing = 16 coverImageResourceView.fallbackView = nil - coverImageHeightConstraint = coverImageResourceView.heightAnchor.constraint(greaterThanOrEqualToConstant: 160) + coverImageResourceView.cssSelector = .cover + if let suggestedCellHeight { + coverImageHeightConstraint = coverImageResourceView.heightAnchor.constraint(greaterThanOrEqualToConstant: suggestedCellHeight) + } contentView.insertSubview(darkBackgroundView, belowSubview: titleLabel) - Theme.shared.register(client: self, applyImmediately: true) - coverObservation = coverImageResourceView.observe(\ResourceViewHost.contentStatus, options: [.initial], changeHandler: { [weak self] viewHost, _ in self?.darkBackgroundView.isHidden = (viewHost.contentStatus != .fromResource) self?.recomputeHeight() }) } - 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() + }) } } } override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { - coverImageResourceView.backgroundColor = collection.lightBrandColor + coverImageResourceView.backgroundColor = collection.css.getColor(.fill, for: coverImageResourceView) // Different look (unified with navigation bar, problematic in light mode): // coverImageResourceView.backgroundColor = collection.navigationBarColors.backgroundColor @@ -92,10 +104,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), @@ -104,23 +113,28 @@ class DriveHeaderCell: DriveListCell { titleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: textOuterSpacing), titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: textOuterSpacing), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -textOuterSpacing), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -textOuterSpacing).with(priority: .defaultHigh), // make constraint "overridable" for DriveGridCell subclass titleLabel.bottomAnchor.constraint(equalTo: subtitleLabel.topAnchor, constant: -textInterSpacing), subtitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: textOuterSpacing), - subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -textOuterSpacing), + subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -textOuterSpacing).with(priority: .defaultHigh), // make constraint "overridable" for DriveGridCell subclass subtitleLabel.bottomAnchor.constraint(equalTo: coverImageResourceView.bottomAnchor, constant: -textOuterSpacing), darkBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 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) } } + +extension ThemeCSSSelector { + static let drive = ThemeCSSSelector(rawValue: "drive") + static let cover = ThemeCSSSelector(rawValue: "cover") +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift index 7ced88c96..f06073f30 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,98 @@ 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, driveItem, cellConfiguration in + if let presentable = OCDataRenderer.default.renderItem(driveItem, 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) + + // More item button action + if let clientContext = cellConfiguration.clientContext, let moreItemHandling = clientContext.moreItemHandler, let drive = driveItem as? OCDrive { + cell.moreAction = OCAction(title: "", icon: nil, action: { [weak moreItemHandling] (action, options, completion) in + clientContext.core?.cachedItem(at: drive.rootLocation, resultHandler: { error, item in + if let item { + OnMainThread { + moreItemHandling?.moreOptions(for: item, at: .moreFolder, context: clientContext, sender: action) + } + } + completion(error) + }) + }) + } + } + }) + + 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 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.backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell() + cell.contentConfiguration = content + cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) + } + 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..ddd52b990 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ExpandableResourceCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ExpandableResourceCell.swift @@ -24,8 +24,6 @@ class ExpandableResourceCell: UICollectionViewListCell, Themeable { super.init(frame: frame) configure() configureLayout() - - Theme.shared.register(client: self, applyImmediately: true) } required init?(coder: NSCoder) { @@ -36,9 +34,20 @@ class ExpandableResourceCell: UICollectionViewListCell, Themeable { Theme.shared.unregister(client: self) } + private var _registered: Bool = false + + override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_registered { + _registered = true + Theme.shared.register(client: self, applyImmediately: true) + } + } + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.contentView.backgroundColor = collection.tableBackgroundColor - expandButton.tintColor = collection.tintColor + self.contentView.backgroundColor = collection.css.getColor(.fill, for: self) + expandButton.tintColor = collection.css.getColor(.stroke, selectors: [.button], for: self) } var resourceView: UIView? { @@ -74,6 +83,8 @@ class ExpandableResourceCell: UICollectionViewListCell, Themeable { } func configure() { + self.cssSelectors = [.expandable] + expandButton.translatesAutoresizingMaskIntoConstraints = false shadowView.translatesAutoresizingMaskIntoConstraints = false @@ -134,7 +145,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() + }) } } @@ -171,3 +185,7 @@ extension ExpandableResourceCell { } } + +extension ThemeCSSSelector { + static let expandable = ThemeCSSSelector(rawValue: "expandable") +} 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..82c576c0d 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift @@ -32,35 +32,39 @@ class SavedSearchCell: ThemeableCollectionViewCell { } let iconView = UIImageView() - let titleLabel = UILabel() + let titleLabel = ThemeCSSLabel(withSelectors: [.title]) + 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 { - if superview != nil { - applyThemeCollectionToCellContents(theme: Theme.shared, collection: Theme.shared.activeCollection, state: .normal) - } + segmentView.items = items ?? [] } } func configure() { + cssSelector = .savedSearch + 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 +83,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,58 +94,118 @@ 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) ] } +} - override func updateConfiguration(using state: UICellConfigurationState) { - let collection = Theme.shared.activeCollection - var backgroundConfig = backgroundConfiguration?.updated(for: state) +extension OCSavedSearch { + private var queryConditionsForDisplay: [OCQueryCondition] { + let searchSegments = (searchTerm as NSString).segmentedForSearch(withQuotationMarks: true) + var queryConditions: [OCQueryCondition] = [] - if state.isHighlighted || state.isSelected || (state.cellDropState == .targeted) { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.highlighted.background : UIColor(white: 0, alpha: 0.10) - } else { - backgroundConfig?.backgroundColor = (type == .destructive) ? collection.destructiveColors.normal.background : UIColor(white: 0, alpha: 0.05) + for searchSegment in searchSegments { + if let queryCondition = OCQueryCondition.forSearchSegment(searchSegment) { + queryConditions.append(queryCondition) + } } - backgroundConfig?.cornerRadius = 8 - - backgroundConfiguration = backgroundConfig + return queryConditions } - override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { - super.applyThemeCollectionToCellContents(theme: theme, collection: collection, state: state) + 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 + } +} - titleLabel.textColor = (type == .destructive) ? collection.destructiveColors.normal.foreground : collection.tintColor - iconView.tintColor = (type == .destructive) ? collection.destructiveColors.normal.foreground : collection.tintColor +extension OCSavedSearch { + var displayName: String { + return (isNameUserDefined && name.count > 0) ? name : (isTemplate ? "Search template".localized : "Saved search".localized) + } - setNeedsUpdateConfiguration() + 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.backgroundConfiguration = .listSidebarCell() + cell.contentConfiguration = content + cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) + } + 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) + } })) } } + +extension ThemeCSSSelector { + public static let savedSearch = ThemeCSSSelector(rawValue: "savedSearch") +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewCell.swift index 4fe651ae8..2a641994b 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewCell.swift @@ -36,8 +36,8 @@ class ThemeableCollectionViewCell: UICollectionViewCell, Themeable { } } - open override func willMove(toSuperview newSuperview: UIView?) { - super.willMove(toSuperview: newSuperview) + open override func didMoveToWindow() { + super.didMoveToWindow() if !themeRegistered { // Postpone registration with theme until we actually need to. Makes sure self.applyThemeCollection() can take all properties into account @@ -63,7 +63,7 @@ class ThemeableCollectionViewCell: UICollectionViewCell, Themeable { } open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.applyThemeCollection(Theme.shared.activeCollection) + self.applyThemeCollection(collection, cellState: configurationState) self.applyThemeCollectionToCellContents(theme: theme, collection: collection, state: ThemeItemState(selected: self.isSelected)) } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewListCell.swift index 1fc42a505..d91b80b59 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/ThemeableCollectionViewListCell.swift @@ -20,7 +20,7 @@ import UIKit open class ThemeableCollectionViewListCell: UICollectionViewListCell, Themeable { private var themeRegistered : Bool = false - public var updateLabelColors : Bool = true + public var updateColors : Bool = true override init(frame: CGRect) { super.init(frame: frame) @@ -36,11 +36,13 @@ open class ThemeableCollectionViewListCell: UICollectionViewListCell, Themeable } } - open override func willMove(toSuperview newSuperview: UIView?) { - super.willMove(toSuperview: newSuperview) + open override func didMoveToWindow() { + super.didMoveToWindow() - if !themeRegistered { + if !themeRegistered, window != nil { // Postpone registration with theme until we actually need to. Makes sure self.applyThemeCollection() can take all properties into account + automaticallyUpdatesBackgroundConfiguration = false + automaticallyUpdatesContentConfiguration = false Theme.shared.register(client: self, applyImmediately: true) themeRegistered = true } @@ -59,11 +61,16 @@ open class ThemeableCollectionViewListCell: UICollectionViewListCell, Themeable } } + open override func updateConfiguration(using state: UICellConfigurationState) { + super.updateConfiguration(using: state) + self.applyThemeCollection(Theme.shared.activeCollection, cellState: state) + } + open func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { } open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.applyThemeCollection(Theme.shared.activeCollection) + self.applyThemeCollection(collection, cellState: configurationState) self.applyThemeCollectionToCellContents(theme: theme, collection: collection, state: ThemeItemState(selected: self.isSelected)) } 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..0e9729e60 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift @@ -0,0 +1,315 @@ +// +// 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.scaledImageFitting(in: CGSize(width: 32, height: 16)), style: .plain, lines: [.singleLine, .primary]) + segmentItem.insets = .zero + segmentItem.alpha = cloudStatusIconAlpha + detailItems.append(segmentItem) + } + + // - Sharing + if isSharedWithUser || sharedByUserOrGroup { + let segmentItem = SegmentViewItem(with: UIImage(named: "group")?.scaledImageFitting(in: CGSize(width: 32, height: 16)), style: .plain, lines: [.singleLine, .primary]) + segmentItem.insets = .zero + detailItems.append(segmentItem) + } + + if sharedByPublicLink { + let segmentItem = SegmentViewItem(with: UIImage(named: "link")?.scaledImageFitting(in: CGSize(width: 32, height: 16)), style: .plain, lines: [.singleLine, .primary]) + 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 + } + + let primaryLineSizeSegment = SegmentViewItem(with: nil, title: detailString, style: .plain, titleTextStyle: .footnote, lines: [.primary]) + primaryLineSizeSegment.insets = .zero + detailItems.append(primaryLineSizeSegment) + + let secondaryLineDateSegment = SegmentViewItem(with: nil, title: lastModifiedLocalizedCompact, style: .plain, titleTextStyle: .footnote, lines: [.secondary]) + secondaryLineDateSegment.insets = .zero + detailItems.append(secondaryLineDateSegment) + + detailString += " - " + lastModifiedLocalized + + let detailSegment = SegmentViewItem(with: nil, title: detailString, style: .plain, titleTextStyle: .footnote, lines: [.singleLine]) + 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..acac8ce77 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItemPolicy+UniversalItemListCellContentProvider.swift @@ -0,0 +1,118 @@ +// +// 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 : ((location?.isDriveRoot == true) ? .drive : .folder) + + // Title + if location?.isDriveRoot == true, let driveID = location?.driveID, let drive = context?.core?.drive(withIdentifier: driveID), let driveName = drive.name { + content.title = .drive(name: driveName) + } else 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..d9dcf802f --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCShare+UniversalItemListCellContentProvider.swift @@ -0,0 +1,190 @@ +// +// 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 : (itemLocation.isDriveRoot ? .drive : .folder) + } + + // Title + if itemLocation.isDriveRoot, let driveID = itemLocation.driveID, let drive = context?.core?.drive(withIdentifier: driveID), let driveName = drive.name { + content.title = .drive(name: driveName) + } else 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, cssSelectors: [.accessory, .copyToClipboard], 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 (_, accessory) = cell.makeAccessoryButton(image: OCSymbol.icon(forSymbolName: "checkmark.circle"), title: "Accept".localized, accessibilityLabel: "Accept share".localized, cssSelectors: [.accessory, .accept], 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 + }) + } + })) + + accessories.append(accessory) + } + + if state == .pending { + let (_, accessory) = cell.makeAccessoryButton(image: OCSymbol.icon(forSymbolName: "minus.circle"), title: "Decline".localized, accessibilityLabel: "Decline share".localized, cssSelectors: [.accessory, .decline], 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 + }) + } + })) + + 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) + } + })) + } +} + +extension ThemeCSSSelector { + static let copyToClipboard = ThemeCSSSelector(rawValue: "copyToClipboard") + static let accept = ThemeCSSSelector(rawValue: "accept") + static let decline = ThemeCSSSelector(rawValue: "decline") +} diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift new file mode 100644 index 000000000..bf7fc7c83 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift @@ -0,0 +1,711 @@ +// +// 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) + case drive(name: String) + } + + public enum Icon { + case file + case folder + case drive + 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 detailSegmentPrimaryView: SegmentView = SegmentView(with: [], truncationMode: .clipTail) + private var hasSecondaryDetailView = false + open lazy var detailSegmentSecondaryView: SegmentView? = { + let view = SegmentView(with: [], truncationMode: .clipTail) + view.translatesAutoresizingMaskIntoConstraints = false + hasSecondaryDetailView = true + return view + }() + + 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() { + detailSegmentPrimaryView.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + iconView.translatesAutoresizingMaskIntoConstraints = false + + titleLabel.cssSelector = .title + + contentView.addSubview(titleLabel) + contentView.addSubview(detailSegmentPrimaryView) + contentView.addSubview(iconView) + } + + var cellStyle: CollectionViewCellStyle.StyleType = .tableCell { + didSet { + if cellStyle != oldValue { + updateLayoutConstraints() + } + } + } + + static func titleAndDetailsHeight(withSecondarySegment: Bool) -> CGFloat { + return withSecondarySegment ? 84 : 64 + } + + open func updateLayoutConstraints() { + if let cellConstraints { + NSLayoutConstraint.deactivate(cellConstraints) + self.cellConstraints = nil + } + + var constraints: [NSLayoutConstraint] + + if cellStyle == .gridCell { + let useSecondarySegmentView = true + + let horizontalMargin: CGFloat = 10 + let verticalMargin: CGFloat = 5 + let iconTextMargin: CGFloat = 5 + let titleDetailsSpacing: CGFloat = 4 + let detailsDetailsSpacing: CGFloat = 2 + + let titleAndDetailsHeight: CGFloat = UniversalItemListCell.titleAndDetailsHeight(withSecondarySegment: useSecondarySegmentView) + + titleLabel.numberOfLines = 2 + titleLabel.textAlignment = .center + titleLabel.lineBreakMode = .byTruncatingMiddle + titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + titleLabel.setContentHuggingPriority(.defaultLow, for: .vertical) + titleLabel.font = UIFont.systemFont(ofSize: UIFont.labelFontSize * 0.8) + + detailSegmentPrimaryView.setContentHuggingPriority(.required, for: .vertical) + + constraints = [ + iconView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: horizontalMargin), + iconView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -horizontalMargin), + iconView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalMargin), + iconView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -titleAndDetailsHeight), + + titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: iconTextMargin), + titleLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: horizontalMargin), + titleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -horizontalMargin), + titleLabel.bottomAnchor.constraint(equalTo: detailSegmentPrimaryView.topAnchor, constant: -titleDetailsSpacing), + + detailSegmentPrimaryView.leadingAnchor.constraint(greaterThanOrEqualTo: self.contentView.leadingAnchor, constant: horizontalMargin), + detailSegmentPrimaryView.trailingAnchor.constraint(lessThanOrEqualTo: self.contentView.trailingAnchor, constant: -horizontalMargin), + detailSegmentPrimaryView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor), + + separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.contentView.trailingAnchor) + ] + + if useSecondarySegmentView, let detailSegmentSecondaryView { + if detailSegmentSecondaryView.superview == nil { + contentView.addSubview(detailSegmentSecondaryView) + } + + constraints.append(contentsOf: [ + detailSegmentSecondaryView.topAnchor.constraint(equalTo: detailSegmentPrimaryView.bottomAnchor, constant: detailsDetailsSpacing), + + detailSegmentSecondaryView.leadingAnchor.constraint(greaterThanOrEqualTo: self.contentView.leadingAnchor, constant: horizontalMargin), + detailSegmentSecondaryView.trailingAnchor.constraint(lessThanOrEqualTo: self.contentView.trailingAnchor, constant: -horizontalMargin), + detailSegmentSecondaryView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor) + ]) + } + } else { + let horizontalMargin : CGFloat = 15 + let verticalLabelMargin : CGFloat = 10 + let verticalIconMargin : CGFloat = 10 + let spacing : CGFloat = 15 + let iconViewWidth : CGFloat = 40 + let verticalLabelMarginFromCenter : CGFloat = 1 + + titleLabel.numberOfLines = 1 + titleLabel.textAlignment = .left + titleLabel.lineBreakMode = .byTruncatingTail + titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + titleLabel.font = UIFont.systemFont(ofSize: UIFont.labelFontSize) + + if hasSecondaryDetailView { + detailSegmentSecondaryView?.removeFromSuperview() + hasSecondaryDetailView = false + } + + constraints = [ + 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), + detailSegmentPrimaryView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor), + detailSegmentPrimaryView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + + titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalLabelMargin), + titleLabel.bottomAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: -verticalLabelMarginFromCenter), + detailSegmentPrimaryView.topAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: verticalLabelMarginFromCenter), + detailSegmentPrimaryView.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 primaryDetailSegments: [SegmentViewItem]? { + didSet { + detailSegmentPrimaryView.items = primaryDetailSegments ?? [] + } + } + + var secondaryDetailSegments: [SegmentViewItem]? { + didSet { + detailSegmentSecondaryView?.items = secondaryDetailSegments ?? [] + } + } + + open func set(title: String?, isFileName: Bool = false, small: Bool? = nil) { + var effectiveSmall = small + + if effectiveSmall == nil, cellStyle == .gridCell { + effectiveSmall = true + } + + self.title = attributedTitle(for: title, isFileName: isFileName, small: effectiveSmall ?? false) + } + + func attributedTitle(for title: String?, isFileName: Bool, small: Bool = false) -> 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, small: small) + .appendNormal(".", small: small) + .appendNormal(pathExtension, small: small) + } else { + return NSMutableAttributedString() + .appendBold(title as String, small: small) + } + } + + 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 .drive: + iconViewProvider = ResourceItemIcon.drive + + 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) + + case .drive(name: let name): + set(title: name) + } + } else { + set(title: nil) + } + } + + // Details + if onlyFields == nil || onlyFields?.contains(.details) == true { + if let details = content?.details { + if cellStyle == .gridCell { + primaryDetailSegments = details.filtered(for: [.primary], includeUntagged: false) + secondaryDetailSegments = details.filtered(for: [.secondary], includeUntagged: false) + } else { + primaryDetailSegments = details.filtered(for: [.singleLine], includeUntagged: true) + } + } else { + if cellStyle == .gridCell { + primaryDetailSegments = nil + secondaryDetailSegments = nil + } else { + primaryDetailSegments = 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 cellStyle == .gridCell { + self.accessories = [] + } else { + 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 + + if let cellStyleType = configuration?.style.type { + self.cellStyle = cellStyleType + } + + 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 + + button.cssSelectors = [.accessory, .more] + + 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 + + button.cssSelectors = [.accessory, .reveal] + + 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) + + button.cssSelectors = [.accessory] + + 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) + + progressView.cssSelectors = [.accessory, .progress] + + 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, cssSelectors: [ThemeCSSSelector]? = [.accessory], 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) + } + + 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) + } + + button.cssSelectors = cssSelectors + + button.applyThemeCollection(Theme.shared.activeCollection) + + return (button, .customView(configuration: UICellAccessory.CustomViewConfiguration(customView: button, placement: .trailing(displayed: .whenNotEditing)))) + } + + // MARK: - Prepare for reuse + open override func prepareForReuse() { + super.prepareForReuse() + + primaryDetailSegments = 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.css.getColor(.fill, state: [.highlighted], for: self)?.withAlphaComponent(0.5) + } else { + backgroundConfig?.backgroundColor = collection.css.getColor(.fill, for: self) + } + + backgroundConfiguration = backgroundConfig + + // Multiselection in grid cell layout + if state.isEditing, cellStyle == .gridCell { + let checkmarkSize = CGSize(width: 24, height: 24) + + if selectionCheckmarkView == nil { + let iconFrame = iconView.frame + + selectionCheckmarkView = SelectionCheckmarkView(frame: CGRect(x: iconFrame.origin.x + ((iconFrame.size.width - checkmarkSize.width)/2.0), y: iconFrame.origin.y + ((iconFrame.size.height - checkmarkSize.height)/2.0), width: checkmarkSize.width, height: checkmarkSize.height)) + selectionCheckmarkView?.translatesAutoresizingMaskIntoConstraints = false + selectionCheckmarkView?.setContentCompressionResistancePriority(.required, for: .vertical) + selectionCheckmarkView?.setContentCompressionResistancePriority(.required, for: .horizontal) + } + + UIView.performWithoutAnimation { + if let selectionCheckmarkView, selectionCheckmarkView.superview == nil { + contentView.addSubview(selectionCheckmarkView) + contentView.addConstraints([ + selectionCheckmarkView.widthAnchor.constraint(equalToConstant: checkmarkSize.width), + selectionCheckmarkView.heightAnchor.constraint(equalToConstant: checkmarkSize.height), + selectionCheckmarkView.centerXAnchor.constraint(equalTo: iconView.centerXAnchor), + selectionCheckmarkView.centerYAnchor.constraint(equalTo: iconView.centerYAnchor) + ]) + } + + selectionCheckmarkView?.isSelected = isSelected + } + } else { + selectionCheckmarkView?.removeFromSuperview() + selectionCheckmarkView = nil + } + } + + private var selectionCheckmarkView: SelectionCheckmarkView? + + open override func applyThemeCollectionToCellContents(theme: Theme, collection: ThemeCollection, state: ThemeItemState) { + titleLabel.apply(css: collection.css, state: state.cssState, properties: [.stroke]) + + if let moreButton { + moreButton.tintColor = collection.css.getColor(.stroke, for: moreButton) + } + if let revealButton { + revealButton.tintColor = collection.css.getColor(.stroke, for: revealButton) + } + if let messageButton { + messageButton.tintColor = collection.css.getColor(.stroke, for: messageButton) + } + + 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 + } + } +} + +extension ThemeCSSSelector { + static let more = ThemeCSSSelector(rawValue: "more") + static let reveal = ThemeCSSSelector(rawValue: "reveal") +} 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..3f61ce4a5 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/CollectionSidebarViewController.swift @@ -0,0 +1,121 @@ +// +// 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.css.getColor(.fill, for: collectionView) // collection.tableGroupBackgroundColor + } + + open override var cssAutoSelectors: [ThemeCSSSelector] { + return [.sidebar, .collection] + } +} + +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..8d2cbe4e8 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() + }) } }) } @@ -99,11 +108,56 @@ public extension CollectionViewCellProvider { } cell.contentConfiguration = content + cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) 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.backgroundConfiguration = .listSidebarCell() + cell.contentConfiguration = content + cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) + + 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..f9d1a3a8a 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift @@ -20,20 +20,23 @@ import UIKit import ownCloudApp import ownCloudSDK -open class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDragDelegate, UICollectionViewDropDelegate, Themeable { +open class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDragDelegate, UICollectionViewDropDelegate, Themeable, ThemeCSSAutoSelector { public var clientContext: ClientContext? 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) @@ -62,7 +65,9 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } deinit { - Theme.shared.unregister(client: self) + if _themeRegistered { + Theme.shared.unregister(client: self) + } } // MARK: - View configuration @@ -79,7 +84,12 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public func configureLayout() { if usesStackViewRoot, let stackView = stackView { collectionView.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(collectionView) + + let safeAreaView = ThemeCSSView() + safeAreaView.translatesAutoresizingMaskIntoConstraints = false + safeAreaView.embed(toFillWith: collectionView, enclosingAnchors: safeAreaView.safeAreaAnchorSet) + + stackView.addArrangedSubview(safeAreaView) } else { collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(collectionView) @@ -153,23 +163,23 @@ 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() - - 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.css.getColor(.fill, for: collectionView) + + 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?.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 self?.provideEmptyFallbackCell(for: indexPath, item: collectionItemRef) + 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 && !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) } } - collectionViewDataSource.apply(snapshot, animatingDifferences: animatingDifferences) + // 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,67 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } } + // MARK: - Events + private var _navigationBarWasHidden: Bool? + + private var _themeRegistered = false + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if !_themeRegistered { + _themeRegistered = true + Theme.shared.register(client: self, applyImmediately: true) + } + + 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: - Update cell layout + public func updateCellLayout(animated: Bool = false) { + collectionView.setCollectionViewLayout(createCollectionViewLayout(), animated: animated) + } + // MARK: - Themeing public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { if event != .initial { - collectionView.setCollectionViewLayout(createCollectionViewLayout(), animated: false) + updateCellLayout(animated: false) } + + collectionView.backgroundColor = collection.css.getColor(.fill, for: collectionView) + coverRootView?.backgroundColor = collection.css.getColor(.fill, for: collectionView) + } + + open var cssAutoSelectors: [ThemeCSSSelector] { + return [.collection] } } 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..bbb5e7ec8 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift @@ -19,11 +19,17 @@ 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 fillingGrid(minimumWidth: CGFloat, maximumWidth: CGFloat? = nil, computeHeight: (_ width: CGFloat) -> CGFloat, cellSpacing: NSDirectionalEdgeInsets? = nil, sectionInsets: NSDirectionalEdgeInsets? = nil, center: Bool = false) case custom(generator: ((_ collectionViewController: CollectionViewController?, _ layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection)) func collectionLayoutSection(for collectionViewController: CollectionViewController? = nil, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { @@ -43,28 +49,39 @@ public class CollectionViewSection: NSObject { config.footerMode = footerMode } - switch listAppearance { - case .plain: - config.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + let css = Theme.shared.activeCollection.css + var themedBackgroundColor: UIColor? + var isGrouped = false + + switch listAppearance { case .grouped, .insetGrouped: - config.backgroundColor = Theme.shared.activeCollection.tableGroupBackgroundColor + isGrouped = true default: break } + if let collectionViewController, let cssColor = css.getColor(.fill, selectors: isGrouped ? [.grouped] : nil, for: collectionViewController) { + themedBackgroundColor = cssColor + } + + config.backgroundColor = themedBackgroundColor + // 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 +107,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 +180,83 @@ 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 + + case .fillingGrid(let minimumWidth, let maximumWidth, let computeHeight, let cellInsets, let sectionInsets, let center): + let effectiveContentSize = layoutEnvironment.container.effectiveContentSize + let availableWidth = effectiveContentSize.width - (sectionInsets?.leading ?? 0) - (sectionInsets?.trailing ?? 0) + let cellInsets = cellInsets ?? NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) + let sectionInsets = sectionInsets ?? .zero + var groupInsets: NSDirectionalEdgeInsets = .zero + + Log.debug("GRID: Available width: \(availableWidth) - contentSize: \(effectiveContentSize)") + + // Determine item size depending on width + var totalItemWidth = minimumWidth + cellInsets.leading + cellInsets.trailing + var maxItemCount = floor(availableWidth / totalItemWidth) + + if let maximumWidth, maximumWidth > minimumWidth { + let maxTotalWidth = floor(availableWidth / maxItemCount) + let maximumTotalWidth = maximumWidth + cellInsets.leading + cellInsets.trailing + let maxAllowedTotalWidth = maxTotalWidth > maximumTotalWidth ? maximumTotalWidth : maxTotalWidth + + totalItemWidth = maxAllowedTotalWidth + } + + if center { + let unusedWidth = availableWidth - (maxItemCount * totalItemWidth) + let extraLeadingTrailingSpace = floor(unusedWidth / 2.0) + + groupInsets.leading += extraLeadingTrailingSpace + groupInsets.trailing += extraLeadingTrailingSpace + } + + let itemHeight = computeHeight(totalItemWidth - cellInsets.leading - cellInsets.trailing) + cellInsets.top + cellInsets.bottom + + Log.debug("GRID: Section insets: \(sectionInsets) - itemWidth: \(totalItemWidth) - itemHeight: \(itemHeight)") + + // Put layout together + let itemWidthDimension: NSCollectionLayoutDimension = .absolute(totalItemWidth) + let itemHeightDimension: NSCollectionLayoutDimension = .absolute(itemHeight) + + let itemSize = NSCollectionLayoutSize(widthDimension: itemWidthDimension, heightDimension: itemHeightDimension) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.contentInsets = cellInsets + + let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeightDimension), subitems: [ item ]) + group.contentInsets = groupInsets + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = sectionInsets + + section.interGroupSpacing = 0 + + return section + // Custom case .custom(let generator): return generator(collectionViewController, layoutEnvironment) @@ -168,13 +269,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() } } @@ -188,10 +300,11 @@ public class CollectionViewSection: NSObject { return _cellStyle } set { + let oldValue = _cellStyle _cellStyle = newValue - if _cellStyle != newValue { - OnMainThread { + if _cellStyle != oldValue { + OnMainThread(inline: !_animateCellLayoutChange) { self.collectionViewController?.reload(sections: [self], animated: false) } } @@ -201,17 +314,48 @@ 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 var cellLayout: CellLayout { + didSet { + collectionViewController?.updateCellLayout(animated: _animateCellLayoutChange) + } + } + private var _animateCellLayoutChange = true - 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 +365,32 @@ 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) + } + } + // 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 +408,79 @@ 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 = contentDataSource { + let clientContext = clientContext ?? collectionViewController.clientContext - if let cellProvider = cellProvider, let dataSource = dataSource { 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 +490,234 @@ 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 cell == nil { - cell = collectionViewController.provideEmptyFallbackCell(for: indexPath, item: collectionItemRef) + 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() + + 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) +} + +extension CollectionViewSection { + func adopt(itemLayout: ItemLayout) { + let animateCellLayoutChange = _animateCellLayoutChange + + UIView.performWithoutAnimation { + _animateCellLayoutChange = false + cellStyle = itemLayout.cellStyle + cellLayout = itemLayout.sectionCellLayout(for: collectionViewController?.traitCollection ?? .current) + _animateCellLayoutChange = animateCellLayoutChange + } } } 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..8e5eb3cd8 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Supplementary Cells/TitleSupplementaryCell.swift @@ -0,0 +1,109 @@ +// +// 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) + ]) + } + + cssSelector = .sectionHeader + } + + private var themeRegistered = false + open override func didMoveToWindow() { + super.didMoveToWindow() + + if !themeRegistered { + // Postpone registration with theme until we actually need to. Makes sure self.applyThemeCollection() can take all properties into account + Theme.shared.register(client: self, applyImmediately: true) + themeRegistered = true + } + } + + // MARK: - Themeing + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + label?.applyThemeCollection(collection, itemStyle: .system(textStyle: .title3, weight: .bold)) + backgroundColor = collection.css.getColor(.fill, for: self) + } + + // 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..e6bab7ab2 --- /dev/null +++ b/ownCloudAppShared/Client/Collection Views/Supplementary Cells/ViewSupplementaryCell.swift @@ -0,0 +1,76 @@ +// +// 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 supplementaryViewElementKinds: [CollectionViewSupplementaryItem.ElementKind] = [ .view ] + + for supplementaryViewElementKind in supplementaryViewElementKinds { + let viewSupplementaryCellRegistration = UICollectionView.SupplementaryRegistration(elementKind: supplementaryViewElementKind) { supplementaryView, elementKind, indexPath in + } + + CollectionViewSupplementaryCellProvider.register(CollectionViewSupplementaryCellProvider(for: supplementaryViewElementKind, 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, elementKind: CollectionViewSupplementaryItem.ElementKind = .view, alignment: NSRectAlignment = .top) -> CollectionViewSupplementaryItem { + let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(view.frame.size.height)) + let supplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: elementKind, alignment: alignment) + + if pinned { + supplementaryItem.pinToVisibleBounds = true + supplementaryItem.zIndex = 200 + } + + return CollectionViewSupplementaryItem(supplementaryItem: supplementaryItem, content: view) + } +} diff --git a/ownCloudAppShared/Client/Context/ClientContext.swift b/ownCloudAppShared/Client/Context/ClientContext.swift index 4f0d026a5..8b14274d1 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,19 @@ 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 itemLayout: ItemLayout? /* public var sortMethod : SortMethod? { didSet { @@ -139,23 +175,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 +206,12 @@ 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 + itemLayout = inParent?.itemLayout permissions = inParent?.permissions permissionHandlers = inParent?.permissionHandlers @@ -237,7 +283,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 +294,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 +306,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..dfdbf71c7 --- /dev/null +++ b/ownCloudAppShared/Client/Navigation Revocation/UIViewController+NavigationRevocation.swift @@ -0,0 +1,73 @@ +// +// 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] = [] + + // Log.debug("Register revokation of view controller: \(self) \(self.navigationItem.titleLabelText ?? "?")") + + if let bookmarkUUID = context.accountConnection?.bookmark.uuid { + if revocationTriggers.contains(.connectionClosed) { + events.append(.connectionClosed(bookmarkUUID: bookmarkUUID)) + } + + if revocationTriggers.contains(.driveRemoved) { + 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 { + if let navigationRevocationHandler { + navigationRevocationHandler.handleRevocation(event: event, context: context, for: self) + } else { + Log.warning("navigation revocation triggered, but navigationRevocationHandler is gone") + } + } else { + Log.warning("navigation revocation triggered, but viewController is gone") + } + }).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..e1aed0479 100644 --- a/ownCloudAppShared/Client/Resource Sources/ResourceItemIcon.swift +++ b/ownCloudAppShared/Client/Resource Sources/ResourceItemIcon.swift @@ -24,6 +24,11 @@ public class ResourceItemIcon: OCResource, OCViewProvider { public static let folder : ResourceItemIcon = ResourceItemIcon(iconName: "folder") public static let file : ResourceItemIcon = ResourceItemIcon(iconName: "file") + public static let drive : ResourceItemIcon = ResourceItemIcon(iconName: "space") + + public static func iconFor(mimeType: String) -> ResourceItemIcon { + return ResourceItemIcon(iconName: OCItem.iconName(for: mimeType) ?? "file") + } public convenience init(iconName: String, identifier: String? = nil) { 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..167496771 100644 --- a/ownCloudAppShared/Client/Search/SearchViewController.swift +++ b/ownCloudAppShared/Client/Search/SearchViewController.swift @@ -161,36 +161,19 @@ 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() + let rootView = ThemeCSSView(withSelectors: [.searchSuggestions]) 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 @@ -207,7 +190,6 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl open override func viewDidLoad() { super.viewDidLoad() - Theme.shared.register(client: self, applyImmediately: true) searchField.translatesAutoresizingMaskIntoConstraints = false searchField.addTarget(self, action: #selector(searchFieldContentsChanged), for: .editingChanged) @@ -228,8 +210,15 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl } } + private var _registered = false open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + if !_registered { + _registered = true + Theme.shared.register(client: self, applyImmediately: true) + } + OnMainThread { self.searchField.becomeFirstResponder() } @@ -239,23 +228,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 +253,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 +320,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 +341,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 +437,10 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl // MARK: - Theme support public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.view.backgroundColor = collection.navigationBarColors.backgroundColor + searchField.applyThemeCollection(collection) } } + +extension ThemeCSSSelector { + static let searchSuggestions = ThemeCSSSelector(rawValue: "searchSuggestions") +} diff --git a/ownCloudAppShared/Client/Sharing/GroupSharingEditTableViewController.swift b/ownCloudAppShared/Client/Sharing/GroupSharingEditTableViewController.swift index 862bfcb8a..6dbf61a16 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 = [ @@ -309,7 +309,7 @@ open class GroupSharingEditTableViewController: StaticTableViewController { let section = StaticTableViewSection(headerTitle: "", footerTitle: footer) section.add(rows: [ StaticTableViewRow(buttonWithAction: { [weak self] (row, _) in - let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) progressView.startAnimating() row.cell?.accessoryView = progressView diff --git a/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift b/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift index 3f5d28639..b73da67d7 100644 --- a/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift +++ b/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift @@ -151,10 +151,10 @@ open class GroupSharingTableViewController: SharingTableViewController, UISearch super.viewDidLayoutSubviews() // Needs to be done here, because of an iOS 13 bug. Do not move to viewDidLoad! - let attributedStringColor = [NSAttributedString.Key.foregroundColor : Theme.shared.activeCollection.searchBarColors.secondaryLabelColor] + let attributedStringColor = [NSAttributedString.Key.foregroundColor : Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.secondary], for: searchController?.searchBar.searchTextField) ?? .placeholderText] let attributedString = NSAttributedString(string: "Add email or name".localized, attributes: attributedStringColor) searchController?.searchBar.searchTextField.attributedPlaceholder = attributedString - searchController?.searchBar.searchTextField.textColor = Theme.shared.activeCollection.searchBarColors.labelColor + searchController?.searchBar.searchTextField.textColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.searchField, .label], for:nil) } open override func viewDidAppear(_ animated: Bool) { @@ -184,11 +184,11 @@ open class GroupSharingTableViewController: SharingTableViewController, UISearch self.actionSection = section let declineRow = StaticTableViewRow(buttonWithAction: { (row, _) in - let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) 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 { @@ -518,6 +518,6 @@ open class GroupSharingTableViewController: SharingTableViewController, UISearch open override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { super.applyThemeCollection(theme: theme, collection: collection, event: event) - self.searchController?.searchBar.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle + self.searchController?.searchBar.overrideUserInterfaceStyle = collection.css.getUserInterfaceStyle() } } diff --git a/ownCloudAppShared/Client/Sharing/PublicLinkEditTableViewController.swift b/ownCloudAppShared/Client/Sharing/PublicLinkEditTableViewController.swift index 50481b139..dd173e953 100644 --- a/ownCloudAppShared/Client/Sharing/PublicLinkEditTableViewController.swift +++ b/ownCloudAppShared/Client/Sharing/PublicLinkEditTableViewController.swift @@ -537,15 +537,18 @@ open class PublicLinkEditTableViewController: StaticTableViewController { if let shareURL = share.url { deleteSection.add(rows: [ StaticTableViewRow(buttonWithAction: { (row, _) in + let originalColor = row.cell?.textLabel?.textColor + UIPasteboard.general.url = shareURL row.cell?.textLabel?.text = shareURL.absoluteString row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 15.0) - row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowColors.secondaryLabelColor + row.cell?.textLabel?.textColor = row.cell?.textLabel?.getThemeCSSColor(.stroke, selectors: [.secondary], state: [.disabled]) ?? .secondaryLabel row.cell?.textLabel?.numberOfLines = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { row.cell?.textLabel?.text = "Copy Public Link".localized row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 17.0) - row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tintColor + row.cell?.textLabel?.textColor = originalColor row.cell?.textLabel?.numberOfLines = 1 } }, title: "Copy Public Link".localized, style: .plain) @@ -554,7 +557,7 @@ open class PublicLinkEditTableViewController: StaticTableViewController { deleteSection.add(rows: [ StaticTableViewRow(buttonWithAction: { [weak self] (row, _) in - let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium) progressView.startAnimating() row.cell?.accessoryView = progressView diff --git a/ownCloudAppShared/Client/Sharing/PublicLinkTableViewController.swift b/ownCloudAppShared/Client/Sharing/PublicLinkTableViewController.swift index 074796b1a..e8dc3b9cd 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)) @@ -177,15 +184,17 @@ open class PublicLinkTableViewController: SharingTableViewController { let privateLinkRow = StaticTableViewRow(buttonWithAction: { (row, _) in UIPasteboard.general.url = url + let originalColor = row.cell?.textLabel?.textColor + row.cell?.textLabel?.text = url.absoluteString row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 15.0) - row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowColors.secondaryLabelColor + row.cell?.textLabel?.textColor = row.cell?.textLabel?.getThemeCSSColor(.stroke, selectors: [.secondary], state: [.disabled]) ?? .secondaryLabel row.cell?.textLabel?.numberOfLines = 0 _ = NotificationHUDViewController(on: self, title: "Private Link".localized, subtitle: "URL was copied to the clipboard".localized, completion: { row.cell?.textLabel?.text = "Copy Private Link".localized row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 17.0) - row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tintColor + row.cell?.textLabel?.textColor = originalColor row.cell?.textLabel?.numberOfLines = 1 }) }, title: "Copy Private Link".localized, style: .plain) @@ -237,9 +246,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 +278,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..4f3873313 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,15 +41,19 @@ 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? + public var textView: ThemeCSSLabel? public var imageView: UIImageView? public var progress: Progress? - public var progressBar: UIProgressView? + public var progressBar: ThemeCSSProgressView? public var activityIndicatorView: UIActivityIndicatorView? @@ -59,7 +64,7 @@ public class ComposedMessageElement: NSObject { if _view == nil { switch kind { case .title, .subtitle, .text: - textView = UILabel() + textView = ThemeCSSLabel(withSelectors: cssSelectors) textView?.translatesAutoresizingMaskIntoConstraints = false textView?.numberOfLines = 0 @@ -87,6 +92,8 @@ public class ComposedMessageElement: NSObject { imageView?.contentMode = .scaleAspectFit imageView?.translatesAutoresizingMaskIntoConstraints = false + imageView?.cssSelectors = cssSelectors + let rootView = UIView() rootView.translatesAutoresizingMaskIntoConstraints = false rootView.addSubview(imageView!) @@ -141,20 +148,28 @@ 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.css.getColor(.stroke, for: imageView) + } + }) + _view = rootView case .divider: - let dividerView = UIView() + let dividerView = ThemeCSSView(withSelectors: cssSelectors ?? [.separator]) dividerView.translatesAutoresizingMaskIntoConstraints = false dividerView.heightAnchor.constraint(equalToConstant: 1).isActive = true - dividerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2) + _view = dividerView case .progressBar(let progress, let relativeWidth): - let progressView = UIProgressView(progressViewStyle: .default) + let progressView = ThemeCSSProgressView(progressViewStyle: .default) progressView.translatesAutoresizingMaskIntoConstraints = false progressView.observedProgress = progress + progressView.cssSelectors = cssSelectors + let rootView = UIView() rootView.translatesAutoresizingMaskIntoConstraints = false rootView.addSubview(progressView) @@ -192,6 +207,8 @@ public class ComposedMessageElement: NSObject { progressView.translatesAutoresizingMaskIntoConstraints = false progressView.progress = progress + progressView.cssSelectors = cssSelectors + let rootView = UIView() rootView.translatesAutoresizingMaskIntoConstraints = false rootView.addSubview(progressView) @@ -225,6 +242,7 @@ public class ComposedMessageElement: NSObject { case .activityIndicator(let style, let size): let activityIndicator = UIActivityIndicatorView(style: style) activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.cssSelectors = cssSelectors let rootView = UIView() rootView.translatesAutoresizingMaskIntoConstraints = false @@ -265,6 +283,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.cssSelectors = cssSelectors + + var buttonConfig = UIButton.Configuration.filled() + buttonConfig.title = text + buttonConfig.cornerStyle = .large + button.configuration = buttonConfig + + button.addAction(action, for: .primaryActionTriggered) + + _view = button } } @@ -296,7 +328,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() @@ -306,51 +338,67 @@ public class ComposedMessageElement: NSObject { } } - static public func title(_ title: String, alignment: Alignment = .leading, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + static public func title(_ title: String, alignment: Alignment = .leading, cssSelectors: [ThemeCSSSelector]? = [.title], insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { let element = ComposedMessageElement(kind: .title, alignment: alignment, insets: altInsets) element.text = title element.style = .system(textStyle: .title3, weight: .bold) + element.cssSelectors = cssSelectors return element } - static public func subtitle(_ subtitle: String, alignment: Alignment = .leading, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + static public func subtitle(_ subtitle: String, alignment: Alignment = .leading, cssSelectors: [ThemeCSSSelector]? = [.subtitle], insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { let element = ComposedMessageElement(kind: .subtitle, alignment: alignment, insets: altInsets) element.text = subtitle element.style = .systemSecondary(textStyle: .body, weight: nil) + element.cssSelectors = cssSelectors return element } - static public func text(_ text: String, font: UIFont? = nil, style: ThemeItemStyle?, alignment: Alignment = .leading, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + static public func text(_ text: String, font: UIFont? = nil, style: ThemeItemStyle?, alignment: Alignment = .leading, cssSelectors: [ThemeCSSSelector]? = nil, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { let element = ComposedMessageElement(kind: .text, alignment: alignment, insets: altInsets) element.text = text element.font = font element.style = style + element.cssSelectors = cssSelectors + + return element + } + + static public func button(_ title: String, action: UIAction, alignment: Alignment = .leading, cssSelectors: [ThemeCSSSelector]? = nil, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + let element = ComposedMessageElement(kind: .button(action: action), alignment: alignment, insets: altInsets) + element.text = title + element.cssSelectors = cssSelectors 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) + static public func image(_ image: UIImage, size: CGSize?, adaptSizeToRatio: Bool = false, alignment: Alignment = .centered, cssSelectors: [ThemeCSSSelector]? = nil, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + let element = ComposedMessageElement(kind: .image(image: image, imageSize: size, adaptSizeToRatio: adaptSizeToRatio), alignment: alignment, insets: altInsets) + element.cssSelectors = cssSelectors + + return element } - static public func progressBar(with relativeWidth: CGFloat = 1.0, progress: Progress?, alignment: Alignment = .centered, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + static public func progressBar(with relativeWidth: CGFloat = 1.0, progress: Progress?, alignment: Alignment = .centered, cssSelectors: [ThemeCSSSelector]? = nil, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { let element = ComposedMessageElement(kind: .progressBar(progress: progress, relativeWidth: relativeWidth), alignment: alignment, insets: altInsets) element.progress = progress + element.cssSelectors = cssSelectors return element } - static public func progressCircle(with progress: Progress, alignment: Alignment = .centered, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { + static public func progressCircle(with progress: Progress, alignment: Alignment = .centered, cssSelectors: [ThemeCSSSelector]? = nil, insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { let element = ComposedMessageElement(kind: .progressCircle(progress: progress), alignment: alignment, insets: altInsets) element.progress = progress + element.cssSelectors = cssSelectors return element } static public func activityIndicator(with alignment: Alignment = .centered, style: UIActivityIndicatorView.Style? = nil, size: CGSize = CGSize(width: 30, height: 30), insets altInsets: NSDirectionalEdgeInsets? = nil) -> ComposedMessageElement { - let effectiveStyle = style ?? Theme.shared.activeCollection.activityIndicatorViewStyle + let effectiveStyle = style ?? Theme.shared.activeCollection.css.getActivityIndicatorStyle() ?? .medium return ComposedMessageElement(kind: .activityIndicator(style: effectiveStyle, size: size), alignment: alignment, insets: altInsets) } @@ -384,9 +432,10 @@ public class ComposedMessageView: UIView, Themeable { } } - init(elements: [ComposedMessageElement]) { + public init(elements: [ComposedMessageElement]) { super.init(frame: .zero) self.translatesAutoresizingMaskIntoConstraints = false + self.cssSelectors = [.message] self.elements = elements } @@ -403,22 +452,32 @@ public class ComposedMessageView: UIView, Themeable { public override func willMove(toSuperview newSuperview: UIView?) { if !_didSetupContent { _didSetupContent = true - embedAndLayoutElements() - - Theme.shared.register(client: self, applyImmediately: true) } super.willMove(toSuperview: newSuperview) } - public override func willMove(toWindow newWindow: UIWindow?) { - super.willMove(toWindow: newWindow) + private var _clientRegistered = false + public override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil { + if !_didSetupContent { + _didSetupContent = true + embedAndLayoutElements() + } + + if !_clientRegistered { + _clientRegistered = true + Theme.shared.register(client: self, applyImmediately: true) + } + } guard let elements = elements else { return } for element in elements { - element.elementInView = (newWindow != nil) + element.elementInView = (window != nil) } } @@ -488,27 +547,47 @@ public extension ComposedMessageView { var elements: [ComposedMessageElement] = [] if let image = image { - let imageElement: ComposedMessageElement = .image(image, size: CGSize(width: 48, height: 48), alignment: .centered) + let imageElement: ComposedMessageElement = .image(image, size: CGSize(width: 48, height: 48), alignment: .centered, cssSelectors: [.icon]) imageElement.insets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10) elements.append(imageElement) } - if let title = title { + if let title { elements.append(.title(title, alignment: .centered)) } - if let subtitle = subtitle { + if let subtitle { elements.append(.subtitle(subtitle, alignment: .centered)) } + if let additionalElements { + elements.append(contentsOf: additionalElements) + } + let infoBoxView = ComposedMessageView(elements: elements) infoBoxView.elementInsets = NSDirectionalEdgeInsets(top: 30, leading: 20, bottom: 30, trailing: 20) infoBoxView.backgroundInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0) - infoBoxView.backgroundView = RoundCornerBackgroundView(fillColorPicker: { theme, collection, event in - return collection.tableGroupBackgroundColor - }) + infoBoxView.backgroundView = RoundCornerBackgroundView() + infoBoxView.backgroundView?.cssSelectors = [.background] + infoBoxView.cssSelectors = [.message, .infoBox] infoBoxView.translatesAutoresizingMaskIntoConstraints = false return infoBoxView } + + static func sectionHeader(titled title: String) -> ComposedMessageView { + let headerView = ComposedMessageView(elements: [ + .spacing(10), + .title(title, alignment: .leading, cssSelectors: [.sectionHeader], insets: .zero) + ]) + headerView.elementInsets.leading = 15 + headerView.elementInsets.bottom = 5 + + return headerView + } +} + +extension ThemeCSSSelector { + static let infoBox = ThemeCSSSelector(rawValue: "infoBox") + static let message = ThemeCSSSelector(rawValue: "message") } diff --git a/ownCloudAppShared/Client/User Interface/GradientView.swift b/ownCloudAppShared/Client/User Interface/GradientView.swift index 4dcd6a4e4..44e2e6e3e 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) { @@ -66,15 +86,9 @@ public class GradientView : UIView { } public class ShadowBarView : GradientView, Themeable { - public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - if let tableTintColor = collection.tableRowColors.tintColor { - self.colors = [tableTintColor.withAlphaComponent(0).cgColor, tableTintColor.withAlphaComponent(0.1).cgColor, tableTintColor.withAlphaComponent(0.25).cgColor] - } - } - public init() { super.init(with: [UIColor(hex: 0, alpha: 0).cgColor, UIColor(hex: 0, alpha: 0.10).cgColor, UIColor(hex: 0, alpha: 0.25).cgColor], locations: [0.0, 0.9, 1.0]) - Theme.shared.register(client: self, applyImmediately: true) + cssSelector = .shadow } required public init?(coder: NSCoder) { @@ -84,4 +98,20 @@ public class ShadowBarView : GradientView, Themeable { deinit { Theme.shared.unregister(client: self) } + + private var _registered: Bool = false + public override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_registered { + _registered = true + Theme.shared.register(client: self, applyImmediately: true) + } + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + if let shadowColor = collection.css.getColor(.fill, for: self) { + self.colors = [shadowColor.withAlphaComponent(0).cgColor, shadowColor.withAlphaComponent(0.1).cgColor, shadowColor.withAlphaComponent(0.25).cgColor] + } + } } diff --git a/ownCloud/Issues/IssuesCardViewController.swift b/ownCloudAppShared/Client/User Interface/IssuesCardViewController.swift similarity index 80% rename from ownCloud/Issues/IssuesCardViewController.swift rename to ownCloudAppShared/Client/User Interface/IssuesCardViewController.swift index 5dabe7554..2adac5949 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,14 +36,16 @@ class CardCellBackgroundView : UIView { } } -class CardHeaderView : UIView, Themeable { - var label : UILabel +public class CardHeaderView : UIView, Themeable { + public var label : ThemeCSSLabel - init(title: String) { - label = UILabel() + public init(title: String) { + label = ThemeCSSLabel() label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize * 1.4, weight: .bold) label.text = title + label.setContentHuggingPriority(.required, for: .vertical) label.setContentHuggingPriority(.defaultLow, for: .horizontal) label.setContentCompressionResistancePriority(.required, for: .vertical) @@ -67,35 +68,30 @@ class CardHeaderView : UIView, Themeable { fatalError("init(coder:) has not been implemented") } - deinit { - Theme.shared.unregister(client: self) - } - - override func didMoveToSuperview() { - super.didMoveToSuperview() + private var _themeRegistered = false + public override func didMoveToWindow() { + super.didMoveToWindow() - if self.superview != nil { + if window != nil, !_themeRegistered { + _themeRegistered = true Theme.shared.register(client: self) } } - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize * 1.4, weight: .bold) - label.textColor = collection.tableRowColors.labelColor - - self.backgroundColor = collection.tableBackgroundColor + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.apply(css: collection.css, properties: [.fill]) } } -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 +101,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 +185,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 } @@ -215,10 +217,10 @@ class IssuesCardViewController: StaticTableViewController { if row.cell?.accessoryType == .disclosureIndicator { // On iOS 13+, chevrons created via .accessoryType are not using the .tintColor anymore let chevronImageView = UIImageView(image: UIImage(systemName: "chevron.right")) - (row.cell as? ThemeTableViewCell)?.accessoryView = chevronImageView + row.cell?.accessoryView = chevronImageView } - (row.cell as? ThemeTableViewCell)?.cellStyler = cellStyler + row.cell?.cellStyler = cellStyler section.add(row: row) } @@ -231,19 +233,20 @@ 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) + issuesViewController.cssSelector = .issues let headerView = CardHeaderView(title: issuesViewController.headerTitle ?? "") let alertView = AlertView(localizedTitle: "", localizedDescription: "", contentPadding: 15, options: issuesViewController.options) let frameViewController = FrameViewController(header: headerView, footer: alertView, viewController: issuesViewController) - alertView.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + alertView.backgroundColor = Theme.shared.activeCollection.css.getColor(.fill, for: issuesViewController.tableView) issuesViewController.alertView = alertView hostViewController.present(asCard: frameViewController, animated: true, withHandle: false, dismissable: false) { @@ -258,10 +261,13 @@ 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 - alertView?.backgroundColor = collection.tableBackgroundColor + tableView.applyThemeCollection(collection) } } + +extension ThemeCSSSelector { + static let issues = ThemeCSSSelector(rawValue: "issues") +} diff --git a/ownCloudAppShared/Client/User Interface/ItemLayout.swift b/ownCloudAppShared/Client/User Interface/ItemLayout.swift new file mode 100644 index 000000000..45347ee5d --- /dev/null +++ b/ownCloudAppShared/Client/User Interface/ItemLayout.swift @@ -0,0 +1,55 @@ +// +// ItemLayout.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 04.04.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 enum ItemLayout { + // MARK: - Layouts + case list + case grid + + // MARK: - Layout information + public func sectionCellLayout(for traitCollection: UITraitCollection, collectionView: UICollectionView? = nil) -> CollectionViewSection.CellLayout { + switch self { + case .list: + return .list(appearance: .plain, contentInsets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + case .grid: + let titleAndDetailsHeight = UniversalItemListCell.titleAndDetailsHeight(withSecondarySegment: true) + + return .fillingGrid(minimumWidth: 130, computeHeight: { width in + return (width * 3 / 4) + titleAndDetailsHeight + }, cellSpacing: NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5), sectionInsets: NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5), center: true) + } + } + + public var cellStyleType: CollectionViewCellStyle.StyleType { + switch self { + case .list: + return .tableCell + + case .grid: + return .gridCell + } + } + + public var cellStyle: CollectionViewCellStyle { + return CollectionViewCellStyle(with: cellStyleType) + } +} diff --git a/ownCloudAppShared/Client/User Interface/MessageView.swift b/ownCloudAppShared/Client/User Interface/MessageView.swift index df372d048..4982cc3b7 100644 --- a/ownCloudAppShared/Client/User Interface/MessageView.swift +++ b/ownCloudAppShared/Client/User Interface/MessageView.swift @@ -150,13 +150,6 @@ open class MessageView: UIView { messageImageView = imageView messageTitleLabel = titleLabel messageMessageLabel = messageLabel - - messageThemeApplierToken = Theme.shared.add(applier: { [weak self] (_, collection, _) in - self?.messageView?.backgroundColor = collection.tableBackgroundColor - - self?.messageTitleLabel?.applyThemeCollection(collection, itemStyle: .bigTitle) - self?.messageMessageLabel?.applyThemeCollection(collection, itemStyle: .bigMessage) - }) } if messageView?.superview == nil { @@ -168,6 +161,13 @@ open class MessageView: UIView { self.mainView.addSubview(rootView) + messageThemeApplierToken = Theme.shared.add(applier: { [weak self] (_, collection, _) in + self?.messageView?.backgroundColor = collection.css.getColor(.fill, selectors: [.table], for: self?.messageView) + + self?.messageTitleLabel?.applyThemeCollection(collection, itemStyle: .bigTitle) + self?.messageMessageLabel?.applyThemeCollection(collection, itemStyle: .bigMessage) + }) + self.composeViewBottomConstraint = rootView.bottomAnchor.constraint(equalTo: self.mainView.safeAreaLayoutGuide.bottomAnchor) if keyboardHeight > 0 { self.composeViewBottomConstraint.constant = self.mainView.safeAreaInsets.bottom - keyboardHeight diff --git a/ownCloudAppShared/Client/User Interface/NamingViewController.swift b/ownCloudAppShared/Client/User Interface/NamingViewController.swift index d8342eac5..7aff8f858 100644 --- a/ownCloudAppShared/Client/User Interface/NamingViewController.swift +++ b/ownCloudAppShared/Client/User Interface/NamingViewController.swift @@ -22,7 +22,7 @@ import ownCloudSDK public typealias StringValidatorResult = (Bool, String?, String?) public typealias StringValidatorHandler = (String) -> StringValidatorResult -open class NamingViewController: UIViewController, Themeable { +open class NamingViewController: UIViewController { weak open var item: OCItem? weak open var core: OCCore? open var completion: (String?, NamingViewController) -> Void @@ -52,14 +52,17 @@ open class NamingViewController: UIViewController, Themeable { private let thumbnailSize = CGSize(width: 150.0, height: 150.0) - public init(with item: OCItem? = nil, core: OCCore? = nil, defaultName: String? = nil, stringValidator: StringValidatorHandler? = nil, completion: @escaping (String?, NamingViewController) -> Void) { + open var fallbackIcon: OCResource? + + public init(with item: OCItem? = nil, core: OCCore? = nil, defaultName: String? = nil, stringValidator: StringValidatorHandler? = nil, fallbackIcon: OCResource? = nil, completion: @escaping (String?, NamingViewController) -> Void) { self.item = item self.core = core self.completion = completion self.stringValidator = stringValidator self.defaultName = defaultName + self.fallbackIcon = fallbackIcon - blurView = UIVisualEffectView(effect: UIBlurEffect(style: Theme.shared.activeCollection.backgroundBlurEffectStyle)) + blurView = UIVisualEffectView(effect: UIBlurEffect(style: Theme.shared.activeCollection.css.getBlurEffectStyle())) stackView = UIStackView(frame: .zero) @@ -68,7 +71,7 @@ open class NamingViewController: UIViewController, Themeable { thumbnailImageView = ResourceViewHost() nameContainer = UIView(frame: .zero) - nameTextField = UITextField(frame: .zero) + nameTextField = ThemeCSSTextField() nameTextField.accessibilityIdentifier = "name-text-field" textfieldCenterYAnchorConstraint = nameTextField.centerYAnchor.constraint(equalTo: nameContainer.centerYAnchor) @@ -78,16 +81,14 @@ open class NamingViewController: UIViewController, Themeable { thumbnailHeightAnchorConstraint = thumbnailImageView.heightAnchor.constraint(equalToConstant: 150) super.init(nibName: nil, bundle: nil) - - Theme.shared.register(client: self, applyImmediately: true) } - convenience public init(with item: OCItem, core: OCCore? = nil, stringValidator: StringValidatorHandler? = nil, completion: @escaping (String?, NamingViewController) -> Void) { - self.init(with: item, core: core, defaultName: nil, stringValidator: stringValidator, completion: completion) + convenience public init(with item: OCItem, core: OCCore? = nil, stringValidator: StringValidatorHandler? = nil, fallbackIcon: OCResource? = nil, completion: @escaping (String?, NamingViewController) -> Void) { + self.init(with: item, core: core, defaultName: nil, stringValidator: stringValidator, fallbackIcon: fallbackIcon, completion: completion) } - convenience public init(with core: OCCore? = nil, defaultName: String, stringValidator: StringValidatorHandler? = nil, completion: @escaping (String?, NamingViewController) -> Void) { - self.init(with: nil, core: core, defaultName: defaultName, stringValidator: stringValidator, completion: completion) + convenience public init(with core: OCCore? = nil, defaultName: String, stringValidator: StringValidatorHandler? = nil, fallbackIcon: OCResource? = nil, completion: @escaping (String?, NamingViewController) -> Void) { + self.init(with: nil, core: core, defaultName: defaultName, stringValidator: stringValidator, fallbackIcon: fallbackIcon, completion: completion) } required public init?(coder aDecoder: NSCoder) { @@ -96,13 +97,6 @@ open class NamingViewController: UIViewController, Themeable { deinit { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardDidShowNotification, object: nil) - Theme.shared.unregister(client: self) - } - - open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - nameTextField.backgroundColor = collection.tableBackgroundColor - nameTextField.textColor = collection.tableRowColors.labelColor - nameTextField.keyboardAppearance = collection.keyboardAppearance } override open func viewDidLoad() { @@ -120,7 +114,7 @@ open class NamingViewController: UIViewController, Themeable { core.vault.resourceManager?.start(thumbnailRequest) } else { nameTextField.text = defaultName - thumbnailImageView.activeViewProvider = ResourceItemIcon.folder + thumbnailImageView.activeViewProvider = (fallbackIcon as? OCViewProvider) ?? ResourceItemIcon.folder } // Navigation buttons @@ -150,7 +144,7 @@ open class NamingViewController: UIViewController, Themeable { thumbnailImageView.widthAnchor.constraint(equalTo: thumbnailImageView.heightAnchor), thumbnailImageView.centerXAnchor.constraint(equalTo: thumbnailContainer.centerXAnchor), thumbnailImageView.centerYAnchor.constraint(equalTo: thumbnailContainer.centerYAnchor) - ]) + ]) // Thumbnail container View thumbnailContainer.translatesAutoresizingMaskIntoConstraints = false @@ -350,6 +344,10 @@ extension NamingViewController: UITextFieldDelegate { textField.selectedTextRange = nameTextField.textRange(from: nameTextField.beginningOfDocument, to:position) + } else if let name = textField.text, + let range = name.range(of: ".", options: .backwards), + let position: UITextPosition = nameTextField.position(from: nameTextField.beginningOfDocument, offset: range.lowerBound.utf16Offset(in: name)) { + textField.selectedTextRange = nameTextField.textRange(from: nameTextField.beginningOfDocument, to:position) } else { textField.selectedTextRange = nameTextField.textRange(from: nameTextField.beginningOfDocument, to: nameTextField.endOfDocument) } diff --git a/ownCloudAppShared/Client/User Interface/PopupButtonController.swift b/ownCloudAppShared/Client/User Interface/PopupButtonController.swift index 83a2c8d12..7bf21beef 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 @@ -100,6 +100,8 @@ open class PopupButtonController : NSObject { button.setContentHuggingPriority(.required, for: .horizontal) button.showsMenuAsPrimaryAction = true + + button.cssSelectors = [.popupButton] } convenience init(with choices: [PopupButtonChoice], selectedChoice: PopupButtonChoice? = nil, selectFirstChoice: Bool = false, dropDown: Bool = false, staticTitle: String? = nil, titleCustomizer: TitleCustomizer? = nil, selectionCustomizer: SelectionCustomizer? = nil, choiceHandler: ChoiceHandler? = nil) { @@ -148,6 +150,8 @@ open class PopupButtonController : NSObject { ]) _updateTitleFromSelectedChoice() + + Theme.shared.register(client: self, applyImmediately: true) } private func _updateTitleFromSelectedChoice() { @@ -188,4 +192,8 @@ open class PopupButtonController : NSObject { button.sizeToFit() } } + + open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + button.applyThemeCollection(collection) + } } diff --git a/ownCloudAppShared/Client/User Interface/RoundCornerBackgroundView.swift b/ownCloudAppShared/Client/User Interface/RoundCornerBackgroundView.swift index 6c5c9e38a..4601b7f82 100644 --- a/ownCloudAppShared/Client/User Interface/RoundCornerBackgroundView.swift +++ b/ownCloudAppShared/Client/User Interface/RoundCornerBackgroundView.swift @@ -41,7 +41,7 @@ public class RoundCornerBackgroundView: UIView, Themeable { setNeedsDisplay() } } - var fillColor: UIColor { + var fillColor: UIColor? { didSet { setNeedsDisplay() } @@ -54,7 +54,7 @@ public class RoundCornerBackgroundView: UIView, Themeable { typealias ThemeColorPicker = (_ theme: Theme, _ collection: ThemeCollection, _ event: ThemeEvent) -> UIColor? - init(with radius: CornerRadius = .standard, fillColor: UIColor = .systemGroupedBackground, fillColorPicker: ThemeColorPicker? = nil) { + init(with radius: CornerRadius = .standard, fillColor: UIColor? = nil, fillColorPicker: ThemeColorPicker? = nil) { self.fillColor = fillColor self.cornerRadius = radius @@ -64,8 +64,8 @@ public class RoundCornerBackgroundView: UIView, Themeable { self.fillColorPicker = fillColorPicker - if fillColorPicker != nil { - Theme.shared.register(client: self, applyImmediately: true) + if fillColor != nil { + _registered = true // Do not register for dynamic coloring when a fill color is provided } } @@ -75,15 +75,30 @@ public class RoundCornerBackgroundView: UIView, Themeable { public override func draw(_ rect: CGRect) { let bezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: cornerRadius.corners, cornerRadii: cornerRadius.radii) - fillColor.setFill() + fillColor?.setFill() bezierPath.fill() } + private var _registered = false + public override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_registered { + _registered = true + + Theme.shared.register(client: self, applyImmediately: true) + } + } + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - if let fillColorPicker = fillColorPicker { + if let fillColorPicker { if let pickedColor = fillColorPicker(theme, collection, event) { fillColor = pickedColor } + } else { + if let cssColor = collection.css.getColor(.fill, for: self) { + fillColor = cssColor + } } } } 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/SelectionCheckmarkButton.swift b/ownCloudAppShared/Client/User Interface/SelectionCheckmarkButton.swift new file mode 100644 index 000000000..7b1d1e140 --- /dev/null +++ b/ownCloudAppShared/Client/User Interface/SelectionCheckmarkButton.swift @@ -0,0 +1,75 @@ +// +// SelectionCheckmarkButton.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 12.04.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 + +class SelectionCheckmarkView: UIImageView, Themeable { + private var _themeRegistered = false + public override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_themeRegistered { + _themeRegistered = true + + cssSelector = .selectionCheckmark + Theme.shared.register(client: self) + } + } + + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + updateImage() + } + + var isSelected: Bool = false { + didSet { + updateImage() + } + } + + func updateImage() { + var name: String + var colors: [UIColor] + + let fillColor = getThemeCSSColor(.fill, state: isSelected ? [.selected] : nil) ?? .systemBlue + let strokeColor = getThemeCSSColor(.stroke, state: isSelected ? [.selected] : nil) ?? .white + + colors = [strokeColor, fillColor] + + if isSelected { + name = "checkmark.circle.fill" + } else { + name = "circle.fill" + } + + let symbolConfig = UIImage.SymbolConfiguration(paletteColors: colors) + let symbolImage = UIImage(systemName: name, withConfiguration: symbolConfig) + + let shadowColor = isSelected ? strokeColor : fillColor + + layer.shadowColor = shadowColor.cgColor + layer.shadowOpacity = 1.0 + layer.shadowOffset = .zero + layer.shadowRadius = 2 + + image = symbolImage + } +} + +extension ThemeCSSSelector { + static let selectionCheckmark = ThemeCSSSelector(rawValue: "selectionCheckmark") +} diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index fd350ca2f..fd1185168 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -7,67 +7,29 @@ // /* -* 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 - } -} +import ownCloudSDK 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() + func sortBar(_ sortBar: SortBar, itemLayout: ItemLayout) + func sortBarToggleSelectMode(_ sortBar: SortBar) } -public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate { - +public class SortBar: ThemeCSSView { weak public var delegate: SortBarDelegate? { didSet { updateSortButtonTitle() @@ -79,15 +41,14 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate let leftPadding: CGFloat = 16.0 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 rightDisplayModeButtonPadding: CGFloat = 8.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 changeItemLayoutButton: UIButton? public var allowMultiSelect: Bool = true { didSet { updateSelectButtonVisibility() @@ -109,30 +70,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,50 +92,41 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } } - public var searchScope : SortBarSearchScope { + public var itemLayout: ItemLayout = .list { didSet { - delegate?.searchScope = searchScope - searchScopeSegmentedControl?.selectedSegmentIndex = searchScope.rawValue + switch itemLayout { + case .grid: changeItemLayoutButton?.setImage(OCSymbol.icon(forSymbolName: "list.bullet"), for: .normal) + case .list: changeItemLayoutButton?.setImage(OCSymbol.icon(forSymbolName: "square.grid.2x2"), for: .normal) + } } } // MARK: - Init & Deinit - - public init(frame: CGRect = .zero, sortMethod: SortMethod, searchScope: SortBarSearchScope = .local) { + public init(frame: CGRect = .zero, sortMethod: SortMethod) { selectButton = UIButton() + changeItemLayoutButton = UIButton() sortButton = UIButton(type: .system) - searchScopeSegmentedControl = SegmentedControl() self.sortMethod = sortMethod - self.searchScope = searchScope super.init(frame: frame) + self.cssSelector = .sortBar - if let sortButton = sortButton, let searchScopeSegmentedControl = searchScopeSegmentedControl, let selectButton = selectButton { + if let sortButton, let selectButton, let changeItemLayoutButton { sortButton.translatesAutoresizingMaskIntoConstraints = false selectButton.translatesAutoresizingMaskIntoConstraints = false - searchScopeSegmentedControl.translatesAutoresizingMaskIntoConstraints = false + changeItemLayoutButton.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) - ]) + self.addSubview(changeItemLayoutButton) // Sort Button sortButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline) sortButton.titleLabel?.adjustsFontForContentSizeCategory = true + sortButton.cssSelector = .sorting sortButton.semanticContentAttribute = (sortButton.effectiveUserInterfaceLayoutDirection == .leftToRight) ? .forceRightToLeft : .forceLeftToRight sortButton.setImage(UIImage(named: "chevron-small-light"), for: .normal) sortButton.setContentHuggingPriority(.required, for: .horizontal) @@ -237,8 +165,9 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate sortButton.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -rightPadding) ]) + // Select Button selectButton.setImage(UIImage(named: "select"), for: .normal) - selectButton.tintColor = Theme.shared.activeCollection.favoriteEnabledColor + selectButton.cssSelector = .multiselect selectButton.addTarget(self, action: #selector(toggleSelectMode), for: .touchUpInside) selectButton.accessibilityLabel = "Enter multiple selection".localized selectButton.isPointerInteractionEnabled = true @@ -249,11 +178,24 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate selectButton.heightAnchor.constraint(equalToConstant: sideButtonsSize.height), selectButton.widthAnchor.constraint(equalToConstant: sideButtonsSize.width) ]) + + // Disply Mode Button + changeItemLayoutButton.setImage(OCSymbol.icon(forSymbolName: "square.grid.2x2"), for: .normal) + changeItemLayoutButton.cssSelector = .itemLayout + changeItemLayoutButton.addTarget(self, action: #selector(toggleDisplayMode), for: .touchUpInside) + changeItemLayoutButton.accessibilityLabel = "Toggle layout".localized + changeItemLayoutButton.isPointerInteractionEnabled = true + + NSLayoutConstraint.activate([ + changeItemLayoutButton.centerYAnchor.constraint(equalTo: self.centerYAnchor), + changeItemLayoutButton.trailingAnchor.constraint(lessThanOrEqualTo: selectButton.leadingAnchor, constant: -rightDisplayModeButtonPadding), + changeItemLayoutButton.heightAnchor.constraint(equalToConstant: sideButtonsSize.height), + changeItemLayoutButton.widthAnchor.constraint(equalToConstant: sideButtonsSize.width) + ]) } // Finalize view setup self.accessibilityIdentifier = "sort-bar" - Theme.shared.register(client: self) selectButton?.isHidden = !showSelectButton } @@ -262,17 +204,13 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate fatalError("init(coder:) has not been implemented") } - deinit { - Theme.shared.unregister(client: self) - } - // MARK: - Theme support + public override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.sortButton?.apply(css: collection.css, properties: [.stroke]) + self.selectButton?.apply(css: collection.css, properties: [.stroke]) + self.changeItemLayoutButton?.apply(css: collection.css, properties: [.stroke]) - 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 + super.applyThemeCollection(theme: theme, collection: collection, event: event) } // MARK: - Sort Direction Title @@ -290,23 +228,21 @@ 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() + delegate?.sortBarToggleSelectMode(self) } - // MARK: - UIPopoverPresentationControllerDelegate - @objc open func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { - return .none - } + @objc private func toggleDisplayMode() { + var newItemLayout: ItemLayout = (itemLayout == .grid) ? .list : .grid - @objc open func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) { - popoverPresentationController.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + itemLayout = newItemLayout + delegate?.sortBar(self, itemLayout: newItemLayout) } } + +public extension ThemeCSSSelector { + static let sortBar = ThemeCSSSelector(rawValue: "sortBar") + static let multiselect = ThemeCSSSelector(rawValue: "multiselect") + static let itemLayout = ThemeCSSSelector(rawValue: "itemLayout") + static let sorting = ThemeCSSSelector(rawValue: "sorting") +} diff --git a/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift similarity index 54% rename from ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift rename to ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift index db024d51b..a50585ff2 100644 --- a/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift +++ b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift @@ -26,45 +26,70 @@ 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 itemsListDataSource: OCDataSource? // typically query.queryResultsDataSource or .itemsDatasource + public var itemSectionDataSource: OCDataSourceComposition? + public var itemSection: CollectionViewSection? - public var driveSection : CollectionViewSection? + public var footerSectionDataSource: OCDataSourceArray = OCDataSourceArray() + public var footerSection: CollectionViewSection? - public var driveSectionDataSource : OCDataSourceComposition? - public var singleDriveDatasource : OCDataSourceComposition? - private var singleDriveDatasourceSubscription : OCDataSourceSubscription? - public var driveAdditionalItemsDataSource : OCDataSourceArray = OCDataSourceArray() + public var driveSection: CollectionViewSection? - public var emptyItemListDataSource : OCDataSourceArray = OCDataSourceArray() - public var emptyItemListDecisionSubscription : OCDataSourceSubscription? - public var emptyItemListItem : ComposedMessageView? + 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 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: ThemeCSSLabel? + + public var location: OCLocation? + + private var stateObservation: NSKeyValueObservation? + private var queryStateObservation: NSKeyValueObservation? + private var queryRootItemObservation: NSKeyValueObservation? + + private var viewControllerUUID: UUID - private var stateObservation : NSKeyValueObservation? - private var queryRootItemObservation : NSKeyValueObservation? + private var coreConnectionStatusObservation : NSKeyValueObservation? - public init(context inContext: ClientContext?, query inQuery: OCQuery, highlightItemReference: OCDataItemReference? = nil) { + 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 +119,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,38 +154,53 @@ 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 { - return true - } + if let drive = itemRecord?.item as? OCDrive, + drive.identifier == itemControllerContext.drive?.identifier { + return true } return false }) - // Create combined data source from drive + additional items - driveSectionDataSource = OCDataSourceComposition(sources: [ singleDriveDatasource!, driveAdditionalItemsDataSource ]) + if itemControllerContext.drive?.specialType == .space { // limit to spaces, do not show header for f.ex. the personal space or the Shares Jail space + // Create combined data source from drive + additional items + driveSectionDataSource = OCDataSourceComposition(sources: [ singleDriveDatasource!, driveAdditionalItemsDataSource ]) - // Create drive section from combined data source - driveSection = CollectionViewSection(identifier: "drive", dataSource: driveSectionDataSource, cellStyle: .init(with: .header), cellLayout: .list(appearance: .plain)) + // Create drive section from combined data source + 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: [contentsDataSource]) + + let itemLayout = itemControllerContext.itemLayout ?? .list + let itemSectionCellStyle = CollectionViewCellStyle(from: itemLayout.cellStyle, changing: { cellStyle in + if showRevealButtonForItems { + cellStyle.showRevealButton = true + } + }) + + itemSection = CollectionViewSection(identifier: "items", dataSource: itemSectionDataSource, cellStyle: itemSectionCellStyle, cellLayout: itemLayout.sectionCellLayout(for: .current), clientContext: itemControllerContext) + + footerSection = CollectionViewSection(identifier: "items-footer", dataSource: footerSectionDataSource, cellStyle: ItemLayout.list.cellStyle, cellLayout: ItemLayout.list.sectionCellLayout(for: .current), clientContext: itemControllerContext) - if let driveSection = driveSection { + if let driveSection { sections.append(driveSection) } - if let queryItemDataSourceSection = itemSection { - sections.append(queryItemDataSourceSection) + if let itemSection { + sections.append(itemSection) + } + + if let footerSection { + sections.append(footerSection) } } @@ -162,13 +212,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 +236,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), + .title(emptyItemListTitleLocalized ?? "No contents".localized, alignment: .centered), + .spacing(5), + .subtitle(emptyFolderMessage, alignment: .centered) ]) emptyItemListItem?.elementInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 2, trailing: 0) @@ -196,19 +258,54 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, .spacing(25), .progressCircle(with: indeterminateProgress), .spacing(25), - .text("Loading…".localized, style: .system(textStyle: .title3, weight: .semibold), alignment: .centered) + .title("Loading…".localized, alignment: .centered) ]) + folderRemovedListItem = ComposedMessageView(elements: [ + .image(OCSymbol.icon(forSymbolName: "nosign")!, size: CGSize(width: 64, height: 48), alignment: .centered), + .title("Folder removed".localized, alignment: .centered), + .spacing(5), + .subtitle("This folder no longer exists on the server.".localized, alignment: .centered) + ]) + + footerItem = UIView() + footerItem?.translatesAutoresizingMaskIntoConstraints = false + + footerFolderStatisticsLabel = ThemeCSSLabel(withSelectors: [.sectionFooter, .statistics]) + 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() + + // Observe connection status + if let core = itemControllerContext.core { + coreConnectionStatusObservation = core.observe(\OCCore.connectionStatus, options: .initial) { [weak self, weak core] (_, _) in + OnMainThread { [weak self, weak core] in + if let connectionStatus = core?.connectionStatus { + self?.coreConnectionStatus = connectionStatus + } + } + } } } @@ -219,80 +316,84 @@ 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?.itemLayout = clientContext?.itemLayout ?? .list sortBar?.showSelectButton = true - itemsLeadInDataSource.setVersionedItems([ sortBar! ]) + if let sortBar { + itemSection?.boundarySupplementaryItems = [ + .view(sortBar, pinned: true) + ] + } // 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) + } + } + } + + public override func addStacked(child viewController: UIViewController, position: CollectionViewController.StackedPosition, relativeTo: UIView? = nil) { + var relativeToView = relativeTo + var stackedPosition = position + + if viewController == actionsBarViewController, let locationBarView = locationBarViewController?.view { + stackedPosition = .top + relativeToView = locationBarView + } + + super.addStacked(child: viewController, position: stackedPosition, relativeTo: relativeToView) + } + + 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) } } @@ -300,12 +401,18 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, public func updateAdditionalDriveItems(from subscription: OCDataSourceSubscription) { let snapshot = subscription.snapshotResettingChangeTracking(true) - if let core = clientContext?.core, - let firstItemRef = snapshot.items.first, - let itemRecord = try? subscription.source?.record(forItemRef: firstItemRef), - let drive = itemRecord.item as? OCDrive, - let driveRepresentation = OCDataRenderer.default.renderItem(drive, asType: .presentable, error: nil) as? OCDataItemPresentable, + guard let core = clientContext?.core, + let firstItemRef = snapshot.items.first, + let itemRecord = try? subscription.source?.record(forItemRef: firstItemRef), + let drive = itemRecord.item as? OCDrive else { return } + + driveQuota = drive.quota + + guard drive.specialType == .space else { return } // limit to spaces, do not show header for f.ex. the personal space or the Shares Jail space + + if let driveRepresentation = OCDataRenderer.default.renderItem(drive, asType: .presentable, error: nil) as? OCDataItemPresentable, let descriptionResourceRequest = try? driveRepresentation.provideResourceRequest(.coverDescription) { + 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))") @@ -320,9 +427,18 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, var _actionProgressHandler : ActionProgressHandler? + // MARK: - Connection status + var coreConnectionStatus: OCCoreConnectionStatus? { + didSet { + if coreConnectionStatus != oldValue { + recomputeContentState() + } + } + } + // 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 +471,26 @@ 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 > 0 { self.contentState = .hasContent + self.folderStatistics = snapshot?.specialItems?[.folderStatistics] as? OCStatistic + } else if (numberOfItems == nil) || + ((self.query != nil) && (self.query?.rootItem == nil) && (self.query?.isCustom != true)) || + ((self.query != nil) && (self.query?.state == .started)) || + ((self.query != nil) && (self.query?.state == .waitingForServerReply) && self.clientContext?.core?.connectionStatus == .online) { + 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 +500,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 +508,20 @@ 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) - itemsLeadInDataSource.setVersionedItems([ ]) + refreshEmptyActions() + sortBar?.isHidden = true + footerSectionDataSource.setVersionedItems([ ]) case .loading: var loadingItems : [OCDataItem] = [ ] @@ -415,22 +530,44 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, loadingItems.append(loadingListItem) } emptyItemListDataSource.setItems(loadingItems, updated: nil) - itemsLeadInDataSource.setVersionedItems([ ]) + sortBar?.isHidden = true + footerSectionDataSource.setVersionedItems([ ]) + + case .removed: + var folderRemovedItems : [OCDataItem] = [ ] + + if let folderRemovedListItem = folderRemovedListItem { + folderRemovedItems.append(folderRemovedListItem) + } + emptyItemListDataSource.setItems(folderRemovedItems, updated: nil) + sortBar?.isHidden = true + footerSectionDataSource.setVersionedItems([ ]) case .hasContent: emptyItemListDataSource.setItems(nil, updated: nil) - if let sortBar = sortBar { - itemsLeadInDataSource.setVersionedItems([ sortBar ]) + sortBar?.isHidden = false + + if searchActive == true { + footerSectionDataSource.setVersionedItems([ ]) + } else { + if let footerItem { + footerSectionDataSource.setVersionedItems([ footerItem ]) + } } emptySectionHiddenNew = true case .searchNonItemContent: emptyItemListDataSource.setItems(nil, updated: nil) - itemsLeadInDataSource.setVersionedItems([ ]) + sortBar?.isHidden = true + footerSectionDataSource.setVersionedItems([ ]) itemSectionHiddenNew = true } + if changeFromOrToRemoved { + updateNavigationBarButtonItems() + } + if (itemSectionHidden != itemSectionHiddenNew) || (emptySectionHidden != emptySectionHiddenNew) { updateSections(with: { sections in self.itemSection?.hidden = itemSectionHiddenNew @@ -440,7 +577,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 +644,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 +694,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") @@ -488,23 +717,19 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, let comparator = sortMethod.comparator(direction: sortDirection) query?.sortComparator = comparator -// customSearchQuery?.sortComparator = comparator -// -// if (customSearchQuery?.queryResults?.count ?? 0) >= maxResultCount { -// updateCustomSearchQuery() -// } } - public func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SortBarSearchScope) { - // only for SortBarDelegate protocol conformance - } + public func sortBar(_ sortBar: SortBar, itemLayout: ItemLayout) { + if itemLayout != clientContext?.itemLayout { + clientContext?.itemLayout = itemLayout - public func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { - self.present(presentViewController, animated: animated, completion: completionHandler) + itemSection?.clientContext?.itemLayout = itemLayout + itemSection?.adopt(itemLayout: itemLayout) + } } // MARK: - Multiselect - public func toggleSelectMode() { + public func sortBarToggleSelectMode(_ sortBar: SortBar) { if let clientContext = clientContext, clientContext.hasPermission(for: .multiselection) { isMultiSelecting = !isMultiSelecting } @@ -513,6 +738,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 +762,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 +801,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 +815,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 +842,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 +1008,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 @@ -749,10 +1030,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, if let savedTemplates = vault.savedSearches?.filter({ savedSearch in return savedSearch.isTemplate }), savedTemplates.count > 0 { - let savedSearchTemplatesHeaderView = ComposedMessageView(elements: [ - .spacing(10), - .text("Saved search templates".localized, style: .system(textStyle: .headline), alignment: .leading, insets: .zero) - ]) + let savedSearchTemplatesHeaderView = ComposedMessageView.sectionHeader(titled: "Saved search templates") savedSearchTemplatesHeaderView.elementInsets = .zero suggestionItems.append(savedSearchTemplatesHeaderView) @@ -763,10 +1041,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, if let savedSearches = vault.savedSearches?.filter({ savedSearch in return !savedSearch.isTemplate }), savedSearches.count > 0 { - let savedSearchTemplatesHeaderView = ComposedMessageView(elements: [ - .spacing(10), - .text("Smart folders".localized, style: .system(textStyle: .headline), alignment: .leading, insets: .zero) - ]) + let savedSearchTemplatesHeaderView = ComposedMessageView.sectionHeader(titled: "Saved searches".localized) savedSearchTemplatesHeaderView.elementInsets = .zero suggestionItems.append(savedSearchTemplatesHeaderView) @@ -798,8 +1073,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } searchResultsContent = nil searchViewController = nil - - itemSectionDataSource?.setInclude(true, for: itemsLeadInDataSource) + sortBar?.isHidden = false } // MARK: - SearchViewControllerDelegate @@ -836,7 +1110,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 +1120,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 +1172,75 @@ 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) + } +} + +extension ThemeCSSSelector { + static let statistics = ThemeCSSSelector(rawValue: "statistics") } diff --git a/ownCloudAppShared/Client/View Controllers/ClientSharedByMeViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientSharedByMeViewController.swift new file mode 100644 index 000000000..5b5c7a56c --- /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), + .title(title, 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..1eda0225c --- /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), + .title("No items shared with you".localized, 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..62b88f1dd --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift @@ -0,0 +1,180 @@ +// +// 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 + +extension ThemeCSSSelector { + static let logo = ThemeCSSSelector(rawValue: "logo") +} + +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)") + + // A view controller may appear more than once in history, so if a view controller is to be removed, + // make sure that all history items for it are removed + while let historyItem = history.item(for: viewController) { + history.remove(item: historyItem, completion: nil) + } + } + } + + // 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() -> ThemeCSSView { + let logoImage = UIImage(named: "branding-login-logo") + let logoImageView = UIImageView(image: logoImage) + logoImageView.cssSelector = .icon + 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 = ThemeCSSLabel() + 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 = ThemeCSSView(withSelectors: [.logo]) + logoContainer.translatesAutoresizingMaskIntoConstraints = false + logoContainer.addSubview(logoImageView) + logoContainer.addSubview(logoLabel) + logoContainer.setContentHuggingPriority(.required, for: .horizontal) + logoContainer.setContentHuggingPriority(.required, for: .vertical) + + let logoWrapperView = ThemeCSSView() + 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 + if !VendorServices.shared.isBranded, let logoColor = collection.css.getColor(.stroke, for: logoImageView) { + logoImageView.image = logoImageView.image?.tinted(with: logoColor) + } + }) + + 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..61d238907 --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationBarController.swift @@ -0,0 +1,110 @@ +// +// 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 + +extension ThemeCSSSelector { + static let locationBar = ThemeCSSSelector(rawValue: "locationBar") +} + +open class ClientLocationBarController: UIViewController, Themeable { + public var location: OCLocation + public var clientContext: ClientContext + + public var seperatorView: ThemeCSSView? + 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 loadView() { + view = ThemeCSSView(withSelectors: [.toolbar, .locationBar]) + } + + open override func viewDidLoad() { + super.viewDidLoad() + + seperatorView = ThemeCSSView(withSelectors: [.separator]) + 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) + ]) + } + } + + var _themeRegistered: Bool = false + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if !_themeRegistered { + _themeRegistered = true + 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) { + seperatorView?.apply(css: collection.css, properties: [.fill]) + + if let backgroundFillColor = collection.css.getColor(.fill, for: view) { + segmentView?.scrollViewOverlayGradientColor = backgroundFillColor.cgColor + } + } +} 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..99313a4e8 --- /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"), style: .chevron) + 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..491cb6601 --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift @@ -0,0 +1,387 @@ +// +// 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, cssSelectors: [.title]) + + var elements: [ComposedMessageElement] = [ headerViewTitleElement! ] + + if let headerSubTitle { + headerViewSubtitleElement = .text(headerSubTitle, style: .systemSecondary(textStyle: .body), alignment: .leading, cssSelectors: [.subtitle]) + 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 + viewController.cssSelector = .accountList + + case .account: + title = "Account".localized + viewController.cssSelector = .accountList + 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) + } + } +} + +extension ThemeCSSSelector { + static let accountList = ThemeCSSSelector(rawValue: "accountList") +} 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..e9bac5a1e --- /dev/null +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPickerViewController.swift @@ -0,0 +1,239 @@ +// +// 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 + + self.cssSelector = .locationPicker + + 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 topSeparatorLine: UIView = ThemeCSSView(withSelectors: [.separator]) + var bottomSeparatorLine: UIView = ThemeCSSView(withSelectors: [.separator]) + + override func viewDidLoad() { + super.viewDidLoad() + + let showCancelButton = locationPicker.headerView != nil + + bottomBarContainer.translatesAutoresizingMaskIntoConstraints = false + selectButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.translatesAutoresizingMaskIntoConstraints = false + promptLabel.translatesAutoresizingMaskIntoConstraints = false + topSeparatorLine.translatesAutoresizingMaskIntoConstraints = false + bottomSeparatorLine.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(bottomSeparatorLine) + view.addSubview(bottomBarContainer) + + var constraints: [NSLayoutConstraint] = [] + var leadingButtonAnchor = selectButton.leadingAnchor + + if let headerView = locationPicker.headerView { + headerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(headerView) + + leadingButtonAnchor = cancelButton.leadingAnchor + + headerView.addSubview(topSeparatorLine) + + constraints.append(contentsOf: [ + headerView.leftAnchor.constraint(equalTo: view.leftAnchor), + headerView.rightAnchor.constraint(equalTo: view.rightAnchor), + headerView.topAnchor.constraint(equalTo: view.topAnchor), + + topSeparatorLine.leftAnchor.constraint(equalTo: headerView.leftAnchor), + topSeparatorLine.rightAnchor.constraint(equalTo: headerView.rightAnchor), + topSeparatorLine.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + topSeparatorLine.heightAnchor.constraint(equalToConstant: 1), + + 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), + + bottomSeparatorLine.leftAnchor.constraint(equalTo: bottomBarContainer.leftAnchor), + bottomSeparatorLine.rightAnchor.constraint(equalTo: bottomBarContainer.rightAnchor), + bottomSeparatorLine.topAnchor.constraint(equalTo: bottomBarContainer.topAnchor), + bottomSeparatorLine.heightAnchor.constraint(equalToConstant: 1) + ]) + + NSLayoutConstraint.activate(constraints) + } + + private var registered = false + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if !registered { + registered = true + 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.css.getColor(.fill, for:view) + } +} + +extension ThemeCSSSelector { + static let locationPicker = ThemeCSSSelector(rawValue: "locationPicker") +} 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/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift b/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift index dfb081a12..314ea823f 100644 --- a/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift +++ b/ownCloudAppShared/SDK Extensions/OCItem+Extension.swift @@ -118,7 +118,11 @@ extension OCItem { if iconName == nil { if self.type == .collection { - iconName = "folder" + if isRoot, driveID != nil { + iconName = "space" + } else { + iconName = "folder" + } } else { iconName = "file" } @@ -145,6 +149,12 @@ extension OCItem { return OCItem.dateFormatter.string(from: lastModified) } + public var lastModifiedLocalizedCompact: String { + guard let lastModified = self.lastModified else { return "" } + + return OCItem.compactDateFormatter.string(from: lastModified) + } + static private let byteCounterFormatter: ByteCountFormatter = { let byteCounterFormatter = ByteCountFormatter() byteCounterFormatter.allowsNonnumericFormatting = false @@ -160,6 +170,15 @@ extension OCItem { return dateFormatter }() + static private let compactDateFormatter: DateFormatter = { + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .short + dateFormatter.locale = Locale.current + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + public var sharedByPublicLink : Bool { if self.shareTypesMask.contains(.link) { return true 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/NSMutableAttributedString+AppendStyled.swift b/ownCloudAppShared/UIKit Extension/NSMutableAttributedString+AppendStyled.swift index 5e9e89884..606393f8e 100644 --- a/ownCloudAppShared/UIKit Extension/NSMutableAttributedString+AppendStyled.swift +++ b/ownCloudAppShared/UIKit Extension/NSMutableAttributedString+AppendStyled.swift @@ -21,19 +21,21 @@ import UIKit public extension NSMutableAttributedString { var boldFont: UIFont { return UIFont.preferredFont(forTextStyle: .headline) } var normalFont: UIFont { return UIFont.preferredFont(forTextStyle: .subheadline) } + var smallBoldFont: UIFont { return UIFont.preferredFont(forTextStyle: .subheadline, with: .semibold) } + var smallNormalFont: UIFont { return UIFont.preferredFont(forTextStyle: .subheadline) } - func appendBold(_ value:String) -> NSMutableAttributedString { + func appendBold(_ value:String, small: Bool = false) -> NSMutableAttributedString { let attributes:[NSAttributedString.Key : Any] = [ - .font : boldFont + .font : small ? smallBoldFont : boldFont ] self.append(NSAttributedString(string: value, attributes:attributes)) return self } - func appendNormal(_ value:String) -> NSMutableAttributedString { + func appendNormal(_ value:String, small: Bool = false) -> NSMutableAttributedString { let attributes:[NSAttributedString.Key : Any] = [ - .font : normalFont + .font : small ? smallNormalFont : normalFont ] self.append(NSAttributedString(string: value, attributes:attributes)) 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..dda5fa3fe 100644 --- a/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift @@ -100,4 +100,44 @@ extension UIColor { return (String(format: "\(leadIn)%02x%02x%02x", Int(selfRed*255.0), Int(selfGreen*255.0), Int(selfBlue*255.0))) } + + public var brightness: CGFloat? { + guard let components = cgColor.components, components.count > 2 else { return nil } + return ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + } + + public func isLight() -> Bool { + if let brightness, brightness > 0.5 { + return true + } + return false + } + + public func contrast(to otherColor: UIColor) -> CGFloat? { + if let brightness, let otherBrightness = otherColor.brightness { + if brightness > otherBrightness { + return (brightness + 0.05) / (otherBrightness + 0.05) + } else { + return (otherBrightness + 0.05) / (brightness + 0.05) + } + } + + return nil + } + + public func preferredContrastColor(from colors: [UIColor]) -> UIColor? { + var highestContrastColor: UIColor? + var highestContrast: CGFloat = 0 + + for color in colors { + if let contrast = contrast(to: color) { + if (contrast > highestContrast) || (highestContrastColor == nil) { + highestContrastColor = color + highestContrast = contrast + } + } + } + + return highestContrastColor + } } 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..ea34ff531 --- /dev/null +++ b/ownCloudAppShared/UIKit Extension/UINavigationItem+Extension.swift @@ -0,0 +1,49 @@ +// +// 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: ThemeCSSLabel? { + let (items, _) = navigationContent.items(withIdentifier: "ios16-truncated-title-fix") + return items.first?.titleView as? ThemeCSSLabel + } + + 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 = ThemeCSSLabel(withSelectors: [.title]) + navigationTitleLabel.font = UIFont.systemFont(ofSize: UIFont.buttonFontSize, weight: .semibold) + navigationTitleLabel.lineBreakMode = .byTruncatingMiddle + 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/UITableViewController+Extension.swift b/ownCloudAppShared/UIKit Extension/UITableViewController+Extension.swift deleted file mode 100644 index eb236353d..000000000 --- a/ownCloudAppShared/UIKit Extension/UITableViewController+Extension.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// UITableViewController+Extension.swift -// ownCloud -// -// Created by Matthias Hühne on 26.02.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 extension UITableViewController { - - func addThemableBackgroundView() { - // UITableView background view is nil for default. Set a UIView with clear color to can insert a subview above - let backgroundView = UIView.init(frame: self.tableView.frame) - backgroundView.backgroundColor = UIColor.clear - self.tableView.backgroundView = backgroundView - - // This view is needed to stop flickering when scrolling (white line between UINavigationBar and UITableView header - let coloredView = ThemeableColoredView(frame: CGRect(x: 0, y: -self.view.frame.size.height, width: self.view.frame.size.width, height: self.view.frame.size.height + 1)) - coloredView.translatesAutoresizingMaskIntoConstraints = false - - self.tableView.insertSubview(coloredView, aboveSubview: self.tableView.backgroundView!) - - NSLayoutConstraint.activate([ - coloredView.topAnchor.constraint(equalTo: self.tableView.topAnchor, constant: -self.view.frame.size.height), - coloredView.leftAnchor.constraint(equalTo: self.tableView.leftAnchor), - coloredView.widthAnchor.constraint(equalTo: self.tableView.widthAnchor), - coloredView.heightAnchor.constraint(equalToConstant: self.view.frame.size.height + 1) - ]) - } - - func colorSection(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath, borderColor: UIColor?) { - let cornerRadius: CGFloat = 10.0 - let layer: CAShapeLayer = CAShapeLayer() - let pathRef: CGMutablePath = CGMutablePath() - - let bounds: CGRect = cell.bounds.insetBy(dx: 0, dy: 0) - - if indexPath.row == 0 && indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - pathRef.addRoundedRect(in: bounds, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: .identity) - } else if indexPath.row == 0 { - pathRef.move(to: CGPoint(x: bounds.minX, y: bounds.maxY)) - pathRef.addArc(tangent1End: CGPoint(x: bounds.minX, y: bounds.minY), - tangent2End: CGPoint(x: bounds.midX, y: bounds.minY), - radius: cornerRadius) - - pathRef.addArc(tangent1End: CGPoint(x: bounds.maxX, y: bounds.minY), - tangent2End: CGPoint(x: bounds.maxX, y: bounds.midY), - radius: cornerRadius) - pathRef.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY)) - } else if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - pathRef.move(to: CGPoint(x: bounds.minX, y: bounds.minY)) - pathRef.addArc(tangent1End: CGPoint(x: bounds.minX, y: bounds.maxY), - tangent2End: CGPoint(x: bounds.midX, y: bounds.maxY), - radius: cornerRadius) - - pathRef.addArc(tangent1End: CGPoint(x: bounds.maxX, y: bounds.maxY), - tangent2End: CGPoint(x: bounds.maxX, y: bounds.midY), - radius: cornerRadius) - pathRef.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY)) - } else { - pathRef.move(to: CGPoint(x: bounds.minX, y: bounds.minY)) - pathRef.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY)) - - pathRef.move(to: CGPoint(x: bounds.maxX, y: bounds.maxY)) - pathRef.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY)) - } - - layer.path = pathRef - layer.strokeColor = borderColor?.cgColor ?? UIColor.lightGray.cgColor - layer.lineWidth = 1.0 - layer.fillColor = UIColor.clear.cgColor - - let backgroundView: UIView = UIView(frame: bounds) - backgroundView.layer.insertSublayer(layer, at: 0) - backgroundView.backgroundColor = .clear - cell.backgroundView = backgroundView - } -} 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 78% rename from ownCloud/Messages/AlertView.swift rename to ownCloudAppShared/User Interface/Alert View/AlertView.swift index 162072c76..92b8d6327 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: ThemeCSSView { + 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 : ThemeCSSLabel = ThemeCSSLabel() + public var headerContainer : ThemeCSSView = ThemeCSSView(withSelectors: [.header]) - var titleLabel : UILabel = UILabel() - var descriptionLabel : UILabel = UILabel() - var optionStackView : UIStackView? + public var titleLabel : UILabel = ThemeCSSLabel(withSelectors: [.title]) + public var descriptionLabel : UILabel = ThemeCSSLabel(withSelectors: [.description]) + 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 @@ -67,20 +66,16 @@ class AlertView: UIView, Themeable { super.init(frame: .zero) - prepareViewAndConstraints() + self.cssSelector = .alert - Theme.shared.register(client: self, applyImmediately: true) + prepareViewAndConstraints() } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - Theme.shared.unregister(client: self) - } - - func createOptionViews() { + public func createOptionViews() { var optionIdx : Int = 0 for option in options { @@ -91,6 +86,17 @@ class AlertView: UIView, Themeable { optionButton.translatesAutoresizingMaskIntoConstraints = false optionButton.accessibilityIdentifier = option.accessibilityIdentifier + switch option.type { + case .cancel: + optionButton.cssSelector = .cancel + + case .destructive: + optionButton.cssSelector = .destructive + + case .regular, .default: + optionButton.cssSelector = .confirm + } + optionButton.setContentHuggingPriority(.required, for: .vertical) optionButton.setContentCompressionResistancePriority(.required, for: .vertical) @@ -102,13 +108,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 +128,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 @@ -225,30 +231,4 @@ class AlertView: UIView, Themeable { ]) } } - - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.headerLabel.applyThemeCollection(collection) - self.titleLabel.applyThemeCollection(collection) - self.descriptionLabel.applyThemeCollection(collection) - - self.headerContainer.backgroundColor = collection.navigationBarColors.backgroundColor - self.headerLabel.textColor = collection.navigationBarColors.secondaryLabelColor - - var idx : Int = 0 - - for optionView in self.optionViews { - switch options[idx].type { - case .cancel: - optionView.themeColorCollection = collection.neutralColors - - case .destructive: - optionView.themeColorCollection = collection.destructiveColors - - case .regular, .default: - optionView.themeColorCollection = collection.approvalColors - } - - idx += 1 - } - } } diff --git a/ownCloud/Messages/AlertViewController.swift b/ownCloudAppShared/User Interface/Alert View/AlertViewController.swift similarity index 65% rename from ownCloud/Messages/AlertViewController.swift rename to ownCloudAppShared/User Interface/Alert View/AlertViewController.swift index 2a971e6fc..5181f45fe 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) { - self.view.backgroundColor = collection.tableBackgroundColor + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.view.apply(css: collection.css, properties: [.fill]) } - 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..895923918 --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationHistory.swift @@ -0,0 +1,193 @@ +// +// 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) { + OCSynchronized(self) { + 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? { + var presentItem: BrowserNavigationItem? + + OCSynchronized(self) { + if position > 0 { + position -= 1 + presentItem = items[position] + } + } + + if let presentItem { + present(item: presentItem, with: .toPrevious, completion: completion) + } else { + completion?(false) + } + + return presentItem + } + + @discardableResult open func moveForward(completion: CompletionHandler? = nil) -> BrowserNavigationItem? { + var presentItem: BrowserNavigationItem? + + OCSynchronized(self) { + if position < items.count - 1 { + position += 1 + presentItem = items[position] + } + } + + if let presentItem { + present(item: presentItem, with: .toNext, completion: completion) + } else { + completion?(false) + } + + return presentItem + } + + open func item(for viewController: UIViewController?) -> BrowserNavigationItem? { + var foundItem: BrowserNavigationItem? + + OCSynchronized(self) { + for item in items { + if item.viewControllerIfLoaded == viewController { + foundItem = item + break + } + } + } + + return foundItem + } + + @discardableResult open func remove(item: BrowserNavigationItem, completion: BrowserNavigationHistory.CompletionHandler?) -> Bool { + var didRemove = false + + OCSynchronized(self) { + 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) + } + + didRemove = true + } + } + + if didRemove { + 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..71e4bb5d6 --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationItem.swift @@ -0,0 +1,75 @@ +// +// 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 viewControllerIfLoaded: UIViewController? { + return _viewController + } + + 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..bc0631725 --- /dev/null +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift @@ -0,0 +1,521 @@ +// +// 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, ThemeCSSAutoSelector { + var navigationView: UINavigationBar = UINavigationBar() + var contentContainerView: UIView = UIView() + var contentContainerLidView: UIView = UIView() + + var sideBarSeperatorView: ThemeCSSView = ThemeCSSView(withSelectors: [.separator]) + + 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.cssSelector = .content + 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 = [ + ] + } + + private var _themeRegistered = false + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if !_themeRegistered { + _themeRegistered = true + 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() + + let done = { + self.delegate?.browserNavigation(viewController: self, contentViewControllerDidChange: self.contentViewController) + completion?(true) + } + + if needsSideBarLayout { + OnMainThread { + UIView.animate(withDuration: 0.3, animations: { + self.updateSideBarLayoutAndAppearance() + self.view.layoutIfNeeded() + }, completion: { _ in + done() + }) + } + } else { + done() + } + } + + // 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 { + didSet { + setNeedsStatusBarAppearanceUpdate() + } + } + 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.sidebarViewController?.view.layoutIfNeeded() + 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) + view.apply(css: collection.css, properties: [.fill]) + } + + public var cssAutoSelectors: [ThemeCSSSelector] = [.splitView] + + // MARK: - Status Bar style + open override var preferredStatusBarStyle: UIStatusBarStyle { + var statusBarStyle: UIStatusBarStyle? + + if isSideBarVisible, let sidebarViewController { + statusBarStyle = Theme.shared.activeCollection.css.getStatusBarStyle(for: sidebarViewController) + } else if let contentViewController { + statusBarStyle = Theme.shared.activeCollection.css.getStatusBarStyle(for: contentViewController) + } + + if statusBarStyle == nil { + statusBarStyle = Theme.shared.activeCollection.css.getStatusBarStyle(for: self) + } + + return statusBarStyle ?? super.preferredStatusBarStyle + } + + open override var childForStatusBarStyle: UIViewController? { + return nil + } +} + +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..d7b621355 100644 --- a/ownCloudAppShared/User Interface/Card Presentation Controller/CardPresentationController.swift +++ b/ownCloudAppShared/User Interface/Card Presentation Controller/CardPresentationController.swift @@ -147,9 +147,8 @@ final class CardPresentationController: UIPresentationController, Themeable { } func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - overStretchView.backgroundColor = collection.tableGroupBackgroundColor - dragHandleView.backgroundColor = collection.tableSeparatorColor - + overStretchView.backgroundColor = collection.css.getColor(.fill, selectors: [.grouped, .table], for:overStretchView) + dragHandleView.backgroundColor = collection.css.getColor(.fill, selectors: [.separator], for:dragHandleView) } private func offset(for position: CardPosition, translatedBy: CGFloat = 0, allowOverStretch: Bool = false) -> CGFloat { @@ -417,7 +416,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/Card Presentation Controller/CardViewController.swift b/ownCloudAppShared/User Interface/Card Presentation Controller/CardViewController.swift index 0002e21af..bf6fed1f0 100644 --- a/ownCloudAppShared/User Interface/Card Presentation Controller/CardViewController.swift +++ b/ownCloudAppShared/User Interface/Card Presentation Controller/CardViewController.swift @@ -29,7 +29,7 @@ open class CardViewController: UIViewController, Themeable, CardPresentationSizi // MARK: - Themeable open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - view.backgroundColor = collection.tableBackgroundColor + view.apply(css: collection.css, selectors: [.table], properties: [.fill]) } // MARK: - CardPresentationSizing 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/FrameViewController.swift b/ownCloudAppShared/User Interface/More/FrameViewController.swift index 61b5c2ffa..382df945e 100644 --- a/ownCloudAppShared/User Interface/More/FrameViewController.swift +++ b/ownCloudAppShared/User Interface/More/FrameViewController.swift @@ -138,7 +138,7 @@ open class FrameViewController: UIViewController, CardPresentationSizing { extension FrameViewController: Themeable { public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.headerView.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor - self.view.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + self.headerView.backgroundColor = collection.css.getColor(.fill, for: self.headerView) + self.view.backgroundColor = collection.css.getColor(.fill, selectors: [.table], for: self.view) } } diff --git a/ownCloudAppShared/User Interface/More/MoreStaticTableViewController.swift b/ownCloudAppShared/User Interface/More/MoreStaticTableViewController.swift index 5cdd2aeee..cc6c4292d 100644 --- a/ownCloudAppShared/User Interface/More/MoreStaticTableViewController.swift +++ b/ownCloudAppShared/User Interface/More/MoreStaticTableViewController.swift @@ -25,6 +25,8 @@ open class MoreStaticTableViewController: StaticTableViewController { override public init(style: UITableView.Style) { themeApplierTokens = [] super.init(style: style) + + cssSelectors = [.more] } deinit { @@ -37,8 +39,8 @@ open class MoreStaticTableViewController: StaticTableViewController { open override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { if let title = (sections[section] as? MoreStaticTableViewSection)?.headerAttributedTitle { - let containerView = UIView() - let label = UILabel() + let containerView = ThemeCSSView(withSelectors: [.sectionHeader]) + let label = ThemeCSSLabel(withSelectors: [.label]) label.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(label) NSLayoutConstraint.activate([ @@ -46,7 +48,7 @@ open class MoreStaticTableViewController: StaticTableViewController { label.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10), label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10), label.rightAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.rightAnchor, constant: -20) - ]) + ]) label.attributedText = title @@ -66,7 +68,7 @@ open class MoreStaticTableViewController: StaticTableViewController { if (sections[section] as? MoreStaticTableViewSection)?.headerAttributedTitle != nil { return UITableView.automaticDimension } - return 0.0 + return UITableView.automaticDimension } open override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { @@ -79,7 +81,6 @@ open class MoreStaticTableViewController: StaticTableViewController { open override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { super.applyThemeCollection(theme: theme, collection: collection, event: event) - self.tableView.separatorColor = self.tableView.backgroundColor } } diff --git a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift index 2b9ea0a5a..c7ca3a2cf 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() @@ -56,8 +56,6 @@ open class MoreViewHeader: UIView { self.translatesAutoresizingMaskIntoConstraints = false - Theme.shared.register(client: self) - render() } @@ -79,8 +77,6 @@ open class MoreViewHeader: UIView { self.translatesAutoresizingMaskIntoConstraints = false - Theme.shared.register(client: self) - render() } @@ -89,6 +85,8 @@ open class MoreViewHeader: UIView { } private func render() { + cssSelectors = [.more, .header] + titleLabel.translatesAutoresizingMaskIntoConstraints = false detailLabel.translatesAutoresizingMaskIntoConstraints = false iconView.translatesAutoresizingMaskIntoConstraints = false @@ -179,7 +177,19 @@ open class MoreViewHeader: UIView { print("Error: \(error)") } } else { - titleLabel.attributedText = NSAttributedString(string: item.name ?? "", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) + var itemName = item.name + + if item.isRoot { + if let core, core.useDrives, let driveID = item.driveID { + if let drive = core.drive(withIdentifier: driveID) { + itemName = drive.name + } + } else { + itemName = "Files".localized + } + } + + titleLabel.attributedText = NSAttributedString(string: itemName ?? "", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) let byteCountFormatter = ByteCountFormatter() byteCountFormatter.countStyle = .file @@ -230,14 +240,26 @@ open class MoreViewHeader: UIView { public func updateFavoriteButtonImage() { if item.isFavorite == true { + favoriteButton.cssSelectors = [.favorite] favoriteButton.setImage(UIImage(named: "star"), for: .normal) - favoriteButton.tintColor = Theme.shared.activeCollection.favoriteEnabledColor favoriteButton.accessibilityLabel = "Unfavorite item".localized } else { + favoriteButton.cssSelectors = [.disabled, .favorite] favoriteButton.setImage(UIImage(named: "unstar"), for: .normal) - favoriteButton.tintColor = Theme.shared.activeCollection.favoriteDisabledColor favoriteButton.accessibilityLabel = "Favorite item".localized } + + favoriteButton.tintColor = Theme.shared.activeCollection.css.getColor(.stroke, for: favoriteButton) + } + + private var _hasRegistered = false + open override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_hasRegistered { + _hasRegistered = true + Theme.shared.register(client: self) + } } } @@ -245,10 +267,12 @@ extension MoreViewHeader: Themeable { public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { titleLabel.applyThemeCollection(collection) detailLabel.applyThemeCollection(collection, itemStyle: .message) - activityIndicator.style = collection.activityIndicatorViewStyle + activityIndicator.style = collection.css.getActivityIndicatorStyle(for: activityIndicator) ?? .medium if adaptBackgroundColor { - backgroundColor = collection.tableBackgroundColor + backgroundColor = collection.css.getColor(.fill, for: self) } + + updateFavoriteButtonImage() } } 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..7cfd22a14 --- /dev/null +++ b/ownCloudAppShared/User Interface/Navigation Content/UINavigationItem+NavigationContent.swift @@ -0,0 +1,54 @@ +// +// 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 +import ownCloudApp + +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/Progress/ProgressHUDViewController.swift b/ownCloudAppShared/User Interface/Progress/ProgressHUDViewController.swift index 52dff3a14..d03b8edd8 100644 --- a/ownCloudAppShared/User Interface/Progress/ProgressHUDViewController.swift +++ b/ownCloudAppShared/User Interface/Progress/ProgressHUDViewController.swift @@ -120,7 +120,7 @@ open class ProgressHUDViewController: UIViewController { } override open var preferredStatusBarStyle : UIStatusBarStyle { - return Theme.shared.activeCollection.statusBarStyle + return Theme.shared.activeCollection.css.getStatusBarStyle(for: self) ?? .default } override open func viewWillAppear(_ animated: Bool) { diff --git a/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift b/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift index b0260bbe9..3b2841b97 100644 --- a/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift +++ b/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift @@ -22,7 +22,7 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { open var cancelled : Bool = false open var cancelHandler : (() -> Void)? - open var progressView : UIProgressView + open var progressView : ThemeCSSProgressView open var activityIndicator : UIActivityIndicatorView open var label : UILabel open var titleLabel : UILabel? @@ -65,7 +65,7 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { } public init(initialTitleLabel: String? = nil, initialProgressLabel: String?, progress : Progress?, cancelLabel: String? = nil, cancelHandler: (() -> Void)? = nil) { - progressView = UIProgressView(progressViewStyle: .bar) + progressView = ThemeCSSProgressView(progressViewStyle: .bar) progressView.translatesAutoresizingMaskIntoConstraints = false activityIndicator = UIActivityIndicatorView(style: .large) @@ -96,6 +96,7 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { if cancelHandler != nil { cancelButton = ThemeButton(type: .system) + cancelButton?.cssSelector = .cancel cancelButton?.translatesAutoresizingMaskIntoConstraints = false cancelButton?.setTitle(cancelLabel ?? "Cancel".localized, for: .normal) @@ -208,13 +209,11 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { } open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.view.backgroundColor = collection.tableBackgroundColor + self.view.backgroundColor = collection.css.getColor(.fill, for: self.view) ?? .white - self.progressView.applyThemeCollection(collection) self.titleLabel?.applyThemeCollection(collection, itemStyle: .title, itemState: .normal) self.label.applyThemeCollection(collection) - self.cancelButton?.applyThemeCollection(collection) - self.activityIndicator.style = collection.activityIndicatorViewStyle + self.activityIndicator.style = collection.css.getActivityIndicatorStyle(for: self.activityIndicator) ?? .medium } open func update(progress: Float? = nil, text: String? = nil) { diff --git a/ownCloudAppShared/User Interface/Progress/ProgressView.swift b/ownCloudAppShared/User Interface/Progress/ProgressView.swift index 248aba7b6..2c2d333da 100644 --- a/ownCloudAppShared/User Interface/Progress/ProgressView.swift +++ b/ownCloudAppShared/User Interface/Progress/ProgressView.swift @@ -18,7 +18,9 @@ import UIKit -public class ProgressView: UIView, Themeable, CAAnimationDelegate { +public class ProgressView: UIView, Themeable, CAAnimationDelegate, ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] = [ .progress ] + var backgroundCircleLayer : CAShapeLayer = CAShapeLayer() var foregroundCircleLayer : CAShapeLayer = CAShapeLayer() var stopButtonLayer : CAShapeLayer = CAShapeLayer() @@ -155,10 +157,10 @@ public class ProgressView: UIView, Themeable, CAAnimationDelegate { backgroundCircleLayer.fillColor = nil stopButtonLayer.strokeColor = nil - foregroundCircleLayer.strokeColor = collection.progressColors.foreground.cgColor - backgroundCircleLayer.strokeColor = collection.progressColors.background.cgColor + foregroundCircleLayer.strokeColor = collection.css.getColor(.stroke, for: self)?.cgColor + backgroundCircleLayer.strokeColor = collection.css.getColor(.fill, for: self)?.cgColor - stopButtonLayer.fillColor = collection.tintColor.cgColor + stopButtonLayer.fillColor = collection.css.getColor(.fill, selectors: [.button], for: self)?.cgColor ?? foregroundCircleLayer.strokeColor } @objc private func cancel() { 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..0e9433417 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentView.swift @@ -0,0 +1,329 @@ +// +// 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 + +extension ThemeCSSSelector { + static let segments = ThemeCSSSelector(rawValue: "segments") +} + +public class SegmentView: ThemeView, ThemeCSSAutoSelector { + public let cssAutoSelectors: [ThemeCSSSelector] = [.segments] + + 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 borderMaskView: 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.borderMaskView = maskView + 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 + } + } + +// private var allViewsFullyVisible: Bool = false { +// didSet { +// if allViewsFullyVisible != oldValue, let borderMaskView { +// if allViewsFullyVisible { +// borderMaskView.removeFromSuperview() +// mask = nil +// } else { +//// embed(toFillWith: borderMaskView) +//// mask = borderMaskView +// } +// } +// } +// } +// +// private func evaluateBorderMaskNecessity() { +// if borderMaskView != nil, !isScrollable { +// if let lastViewFrame = items.last?.view?.frame, +// let firstViewFrame = items.first?.view?.frame { +// let bounds = bounds +// +// if firstViewFrame.origin.x >= bounds.origin.x, +// (lastViewFrame.origin.x + lastViewFrame.size.width) <= (bounds.origin.x + bounds.size.width) { +// allViewsFullyVisible = true +// } else { +// allViewsFullyVisible = false +// } +// } +// } +// } +// +// public override func didMoveToWindow() { +// super.didMoveToWindow() +// +// OnMainThread { +// self.evaluateBorderMaskNecessity() +// } +// } +// +// public override func layoutSubviews() { +// super.layoutSubviews() +// self.evaluateBorderMaskNecessity() +// } + + public override var bounds: CGRect { + didSet { + OnMainThread { + self.scrollToTruncationTarget() + // self.evaluateBorderMaskNecessity() + } + } + } +} diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift new file mode 100644 index 000000000..72b5edbe9 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift @@ -0,0 +1,109 @@ +// +// 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 chevron + case token + } + + public enum Line: Int { + case singleLine + case primary + case secondary + } + + 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 lines: [Line]? //!< Optional Lines that can be used to separate content into multiple lines (used f.ex. for grid cell layouts to use a single array for single line and multi line views) + + 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, lines: [Line]? = 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.lines = lines + self.representedObject = representedObject + self.weakRepresentedObject = weakRepresentedObject + self.gestureRecognizers = gestureRecognizers + } +} + +extension [SegmentViewItem] { + func filtered(for lines: [SegmentViewItem.Line], includeUntagged: Bool) -> [SegmentViewItem] { + return filter({ item in + if let itemLines = item.lines { + for line in lines { + if itemLines.contains(line) { + return true + } + } + + return false + } + + return includeUntagged + }) + } +} diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift new file mode 100644 index 000000000..91af23540 --- /dev/null +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift @@ -0,0 +1,123 @@ +// +// 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, ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + switch item?.style { + case .plain: return [ .item, .plain ] + case .label: return [ .item, .label ] + case .token: return [ .item, .token ] + case .chevron: return [ .item, .separator ] + default: return [.item] + } + } + + 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?.cssSelector = .icon + 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 = ThemeCSSLabel(withSelectors: [.title]) + 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 iconView { + iconView.tintColor = collection.css.getColor(.stroke, for: iconView) + } + + backgroundColor = collection.css.getColor(.fill, for: self) + } +} 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/SharedKeyCommands.swift b/ownCloudAppShared/User Interface/SharedKeyCommands.swift new file mode 100644 index 000000000..fc47ad73b --- /dev/null +++ b/ownCloudAppShared/User Interface/SharedKeyCommands.swift @@ -0,0 +1,73 @@ +// +// SharedKeyCommands.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 29.03.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 + +extension PasscodeViewController { + // Moved over from KeyCommands.swift to be usable across app and extensions + public override var keyCommands: [UIKeyCommand]? { + var keyCommands : [UIKeyCommand] = [] + for i in 0 ..< 10 { + keyCommands.append( + UIKeyCommand.ported(input:String(i), + modifierFlags: [], + action: #selector(self.performKeyCommand(sender:)), + discoverabilityTitle: String(i)) + ) + } + + keyCommands.append( + UIKeyCommand.ported(input: "\u{8}", + modifierFlags: [], + action: #selector(self.performKeyCommand(sender:)), + discoverabilityTitle: "Delete".localized) + ) + + if cancelButton?.isHidden == false { + keyCommands.append( + + UIKeyCommand.ported(input: UIKeyCommand.inputEscape, + modifierFlags: [], + action: #selector(self.performKeyCommand(sender:)), + discoverabilityTitle: "Cancel".localized) + ) + } + + return keyCommands + } + + override open var canBecomeFirstResponder: Bool { + return true + } + + @objc func performKeyCommand(sender: UIKeyCommand) { + guard let key = sender.input else { + return + } + + switch key { + case "\u{8}": + deleteLastDigit() + case UIKeyCommand.inputEscape: + cancelHandler?(self) + default: + appendDigit(digit: key) + } + + } +} 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/StaticTableView/StaticTableViewController.swift b/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewController.swift index df2865672..d12250452 100644 --- a/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewController.swift +++ b/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewController.swift @@ -160,9 +160,7 @@ open class StaticTableViewController: UITableViewController, Themeable { // MARK: - View Controller override open func viewDidLoad() { super.viewDidLoad() - extendedLayoutIncludesOpaqueBars = true - Theme.shared.register(client: self) } public var willDismissAction : ((_ viewController: StaticTableViewController) -> Void)? @@ -180,10 +178,16 @@ open class StaticTableViewController: UITableViewController, Themeable { } } + private var _themeRegistered = false override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + hasBeenPresentedAtLeastOnce = true - super.viewWillAppear(animated) + if !_themeRegistered { + _themeRegistered = true + Theme.shared.register(client: self) + } } deinit { @@ -261,27 +265,48 @@ open class StaticTableViewController: UITableViewController, Themeable { } // MARK: - Theme support + func applyColor(headerFooterView view: UIView, color: UIColor) { + if let label = view as? UILabel { + label.textColor = color + } else if let headerView = view as? UITableViewHeaderFooterView { + headerView.textLabel?.textColor = color + } + } + open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { self.tableView.applyThemeCollection(collection) - } - public override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - guard let sectionColor = Theme.shared.activeCollection.tableSectionHeaderColor else { return } + var headerTextColor: UIColor? + var footerTextColor: UIColor? - if let label = view as? UILabel { - label.textColor = sectionColor - } else if let headerView = view as? UITableViewHeaderFooterView { - headerView.textLabel?.textColor = sectionColor + for sectionIdx in 0.. Void)? private var updateViewAppearance : ((_ row: StaticTableViewRow) -> Void)? - public var cell : UITableViewCell? + public var cell : ThemeTableViewCell? public var selectable : Bool = true @@ -102,8 +101,6 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { return section?.viewController } - private var themeApplierToken : ThemeApplierToken? - public var index : Int? { return section?.rows.firstIndex(of: self) } @@ -129,9 +126,10 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { override public init() { type = .row super.init() + cssSelectors = [.table, .cell] } - convenience public init(rowWithAction: StaticTableViewRowAction?, title: String, subtitle: String? = nil, image: UIImage? = nil, imageWidth: CGFloat? = nil, imageTintColorKey : String = "labelColor", alignment: NSTextAlignment = .left, messageStyle: StaticTableViewRowMessageStyle? = nil, recreatedLabelLayout cellLayouter: ThemeTableViewCell.CellLayouter? = nil, leadingAccessoryView: UIView? = nil, accessoryType: UITableViewCell.AccessoryType = .none, identifier : String? = nil, accessoryView: UIView? = nil) { + convenience public init(rowWithAction: StaticTableViewRowAction?, title: String, subtitle: String? = nil, image: UIImage? = nil, imageWidth: CGFloat? = nil, alignment: NSTextAlignment = .left, messageStyle: StaticTableViewRowMessageStyle? = nil, recreatedLabelLayout cellLayouter: ThemeTableViewCell.CellLayouter? = nil, leadingAccessoryView: UIView? = nil, accessoryType: UITableViewCell.AccessoryType = .none, identifier : String? = nil, accessoryView: UIView? = nil) { self.init() type = .row @@ -169,14 +167,17 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { themeCell.accessoryView = accessoryView } - if let cell = self.cell { + if let cell { PointerEffect.install(on: cell.contentView, effectStyle: .hover) } - themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in - self?.cell?.imageView?.tintColor = themeCollection.tableRowColors.value(forKeyPath: imageTintColorKey) as? UIColor - self?.cell?.accessoryView?.tintColor = themeCollection.tableRowColors.labelColor - }) + themeCell.cellCustomizer = { (cell, styleSet) in + let css = styleSet.collection.css + let labelColor = css.getColor(.stroke, selectors: [.label], for: cell) + + cell.imageView?.tintColor = labelColor + cell.accessoryView?.tintColor = labelColor + } themeCell.accessibilityIdentifier = identifier @@ -194,40 +195,58 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.identifier = identifier self.cell = ThemeTableViewCell(withLabelColorUpdates: false) - self.cell?.textLabel?.text = title - self.cell?.textLabel?.textAlignment = alignment + + var cellContent = cell?.defaultContentConfiguration() + cellContent?.text = title + cellContent?.textProperties.alignment = (alignment == .center) ? .center : .natural + self.cell?.contentConfiguration = cellContent self.cell?.accessoryView = accessoryView self.cell?.accessibilityIdentifier = identifier - if let cell = self.cell { + if let cell { PointerEffect.install(on: cell.contentView, effectStyle: .hover) } - themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in + cell?.cellCustomizer = { (cell, styleSet) in + let css = styleSet.collection.css var textColor, selectedTextColor, backgroundColor, selectedBackgroundColor : UIColor? - textColor = themeCollection.tableRowColors.labelColor - backgroundColor = themeCollection.tableRowColors.backgroundColor + textColor = css.getColor(.stroke, for: cell) // tableRowColors.labelColor + backgroundColor = css.getColor(.fill, for: cell) // tableRowColors.backgroundColor - self?.cell?.textLabel?.textColor = textColor + var contentConfig = cell.contentConfiguration as? UIListContentConfiguration - if selectedTextColor != nil { - self?.cell?.textLabel?.highlightedTextColor = selectedTextColor + if let textColor { + contentConfig?.textProperties.color = textColor + if let selectedTextColor { + contentConfig?.textProperties.colorTransformer = UIConfigurationColorTransformer({ [weak cell] (color) in + if cell?.configurationState.isHighlighted == true { + return selectedTextColor + } + return textColor + }) + } } - if backgroundColor != nil { + cell.contentConfiguration = contentConfig - self?.cell?.backgroundColor = backgroundColor - } + var backgroundConfig = cell.backgroundConfiguration - if selectedBackgroundColor != nil { - let selectedBackgroundView = UIView() + if let backgroundColor { + backgroundConfig?.backgroundColor = backgroundColor - selectedBackgroundView.backgroundColor = selectedBackgroundColor + if let selectedBackgroundColor { + backgroundConfig?.backgroundColorTransformer = UIConfigurationColorTransformer({ [weak cell] (color) in + if cell?.configurationState.isHighlighted == true { + return selectedBackgroundColor + } + return backgroundColor + }) + } - self?.cell?.selectedBackgroundView? = selectedBackgroundView + cell.backgroundConfiguration = backgroundConfig } - }, applyImmediately: true) + } if rowWithAction != nil { self.action = rowWithAction @@ -236,7 +255,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { } } - convenience public init(rowWithAction: StaticTableViewRowAction?, title: String, alignment: NSTextAlignment = .left, image: UIImage? = nil, imageTintColorKey : String = "labelColor", accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, accessoryView: UIView?, identifier: String? = nil) { + convenience public init(rowWithAction: StaticTableViewRowAction?, title: String, alignment: NSTextAlignment = .left, image: UIImage? = nil, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, accessoryView: UIView?, identifier: String? = nil) { self.init() type = .row @@ -246,10 +265,12 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { guard let cell = self.cell else { return } - cell.textLabel?.text = title - cell.textLabel?.textAlignment = alignment + var cellContent = cell.defaultContentConfiguration() + cellContent.text = title + cellContent.textProperties.alignment = (alignment == .center) ? .center : .natural + cellContent.image = image + self.cell?.contentConfiguration = cellContent cell.accessoryType = accessoryType - self.cell?.imageView?.image = image cell.accessibilityIdentifier = identifier if rowWithAction != nil { self.action = rowWithAction @@ -272,13 +293,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { ]) } - if let cell = self.cell { - PointerEffect.install(on: cell.contentView, effectStyle: .hover) - } - - themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in - self?.cell?.imageView?.tintColor = themeCollection.tableRowColors.value(forKeyPath: imageTintColorKey) as? UIColor - }) + PointerEffect.install(on: cell.contentView, effectStyle: .hover) } convenience public init(subtitleRowWithAction: StaticTableViewRowAction?, title: String, subtitle: String? = nil, style : UITableViewCell.CellStyle = .subtitle, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, identifier : String? = nil, withButtonStyle : Bool = false) { @@ -293,7 +308,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.cell?.accessibilityIdentifier = identifier - if let cell = self.cell { + if let cell { PointerEffect.install(on: cell.contentView, effectStyle: .hover) } @@ -306,15 +321,18 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { } if withButtonStyle { - themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in - let textColor = themeCollection.tableRowColors.labelColor + cell?.cellCustomizer = { (cell, styleSet) in + let css = styleSet.collection.css - self?.cell?.textLabel?.textColor = textColor - self?.cell?.detailTextLabel?.textColor = textColor + let textColor = css.getColor(.stroke, for: cell.textLabel) // themeCollection.tableRowColors.labelColor + let highlightTextColor = css.getColor(.stroke, state: [.highlighted], for: cell.textLabel) // themeCollection.tableRowHighlightColors.labelColor - self?.cell?.textLabel?.highlightedTextColor = themeCollection.tableRowHighlightColors.labelColor - self?.cell?.detailTextLabel?.highlightedTextColor = themeCollection.tableRowHighlightColors.labelColor - }, applyImmediately: true) + cell.textLabel?.textColor = textColor + cell.detailTextLabel?.textColor = textColor + + cell.textLabel?.highlightedTextColor = highlightTextColor + cell.detailTextLabel?.highlightedTextColor = highlightTextColor + } } } @@ -345,7 +363,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.cell?.accessibilityIdentifier = groupIdentifier + "." + accessibilityIdentifier } - if let cell = self.cell { + if let cell { PointerEffect.install(on: cell.contentView, effectStyle: .hover) } @@ -380,7 +398,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.cell?.detailTextLabel?.numberOfLines = 0 } - if let cell = self.cell { + if let cell { PointerEffect.install(on: cell.contentView, effectStyle: .hover) } @@ -430,7 +448,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.textFieldAction = action self.value = textValue - let cellTextField : UITextField = UITextField() + let cellTextField : UITextField = ThemeCSSTextField() cellTextField.translatesAutoresizingMaskIntoConstraints = false @@ -464,17 +482,10 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.updateViewAppearance = { [weak cellTextField] (row) in cellTextField?.isEnabled = row.enabled - cellTextField?.textColor = row.enabled ? Theme.shared.activeCollection.tableRowColors.labelColor : Theme.shared.activeCollection.tableRowColors.secondaryLabelColor } self.textField = cellTextField - themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in - cellTextField.textColor = (self?.enabled == true) ? themeCollection.tableRowColors.labelColor : themeCollection.tableRowColors.secondaryLabelColor - cellTextField.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [.foregroundColor : themeCollection.tableRowColors.secondaryLabelColor]) - cellTextField.keyboardAppearance = themeCollection.keyboardAppearance - }) - cellTextField.accessibilityLabel = accessibilityLabel } @@ -518,7 +529,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { } // MARK: - Labels - convenience public init(label: String, alignment: NSTextAlignment = .left, accessoryView: UIView? = nil, identifier: String? = nil) { + convenience public init(label: String, accessoryView: UIView? = nil, identifier: String? = nil) { self.init() type = .label @@ -527,7 +538,6 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.cell = ThemeTableViewCell(style: .default, reuseIdentifier: nil) self.cell?.textLabel?.text = label self.cell?.textLabel?.numberOfLines = 0 - self.cell?.textLabel?.textAlignment = alignment if accessoryView != nil { self.cell?.accessoryView = accessoryView @@ -610,24 +620,25 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.init(customView: paddingView, identifier: identifier) - self.themeApplierToken = Theme.shared.add(applier: { [weak titleLabel, weak messageLabel, weak paddingView, weak iconView] (_, themeCollection, _) in + cell?.cellCustomizer = { [weak titleLabel, weak messageLabel, weak paddingView, weak iconView] (cell, styleSet) in + let css = styleSet.collection.css var textColor, backgroundColor, tintColor : UIColor? switch style { case .plain: - textColor = themeCollection.tableRowColors.tintColor + textColor = cell.getThemeCSSColor(.stroke) // collection.tableRowColors.tintColor tintColor = textColor - backgroundColor = themeCollection.tableRowColors.backgroundColor + backgroundColor = cell.getThemeCSSColor(.fill) // collection.tableRowColors.backgroundColor case .text: - textColor = themeCollection.tableRowColors.labelColor - tintColor = themeCollection.tableRowColors.labelColor - backgroundColor = themeCollection.tableRowColors.backgroundColor + textColor = cell.getThemeCSSColor(.stroke, selectors: [.label, .primary]) // collection.tableRowColors.labelColor + tintColor = textColor + backgroundColor = cell.getThemeCSSColor(.fill) // collection.tableRowColors.backgroundColor case .confirmation: - textColor = themeCollection.approvalColors.normal.foreground + textColor = css.getColor(.stroke, selectors: [.confirm], for: cell) tintColor = textColor - backgroundColor = themeCollection.approvalColors.normal.background + backgroundColor = css.getColor(.fill, selectors: [.confirm], for: cell) case .warning: textColor = .black @@ -635,9 +646,9 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { backgroundColor = .systemYellow case .alert: - textColor = themeCollection.destructiveColors.normal.foreground + textColor = css.getColor(.stroke, selectors: [.destructive], for: cell) tintColor = textColor - backgroundColor = themeCollection.destructiveColors.normal.background + backgroundColor = css.getColor(.fill, selectors: [.destructive], for: cell) case let .custom(customTextColor, customBackgroundColor, customTintColor): textColor = customTextColor @@ -646,7 +657,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { } if textColor == nil { - textColor = themeCollection.tableRowColors.tintColor + textColor = cell.getThemeCSSColor(.stroke) ?? .systemPink // collection.tableRowColors.tintColor } titleLabel?.tintColor = textColor @@ -662,7 +673,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { iconView?.image = iconView?.image?.tinted(with: tintColor) } } - }, applyImmediately: true) + } self.selectable = false self.cell?.selectionStyle = .none @@ -725,7 +736,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { // MARK: - Buttons - convenience public init(buttonWithAction action: StaticTableViewRowAction?, title: String, style: StaticTableViewRowButtonStyle = .proceed, image: UIImage? = nil, imageWidth : CGFloat? = nil, imageTintColorKey : String? = nil, alignment: NSTextAlignment = .center, identifier : String? = nil, accessoryView: UIView? = nil, accessoryType: UITableViewCell.AccessoryType = .none) { + convenience public init(buttonWithAction action: StaticTableViewRowAction?, title: String, style: StaticTableViewRowButtonStyle = .proceed, image: UIImage? = nil, imageWidth : CGFloat? = nil, alignment: NSTextAlignment = .center, identifier : String? = nil, accessoryView: UIView? = nil, accessoryType: UITableViewCell.AccessoryType = .none) { self.init() type = .button @@ -761,30 +772,27 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.cell?.accessibilityIdentifier = identifier - if let cell = self.cell { + if let cell { PointerEffect.install(on: cell.contentView, effectStyle: .hover) } - themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in + cell?.cellCustomizer = { (cell, styleSet) in + let css = styleSet.collection.css var textColor, selectedTextColor, backgroundColor, selectedBackgroundColor : UIColor? switch style { case .plain: - textColor = themeCollection.tableRowColors.labelColor - backgroundColor = themeCollection.tableRowColors.backgroundColor - - case .plainNonOpaque: - textColor = themeCollection.tableRowColors.tintColor - backgroundColor = themeCollection.tableRowColors.backgroundColor + textColor = css.getColor(.stroke, for: cell) // themeCollection.tableRowColors.labelColor + backgroundColor = css.getColor(.fill, for: cell) // themeCollection.tableRowColors.backgroundColor case .proceed: - textColor = themeCollection.neutralColors.normal.foreground - backgroundColor = themeCollection.neutralColors.normal.background - selectedBackgroundColor = themeCollection.neutralColors.highlighted.background + textColor = css.getColor(.stroke, selectors: [.proceed], for: cell) // themeCollection.neutralColors.normal.foreground + backgroundColor = css.getColor(.fill, selectors: [.proceed], for: cell) // themeCollection.neutralColors.normal.background + selectedBackgroundColor = css.getColor(.fill, selectors: [.proceed, .highlighted], for: cell) // themeCollection.neutralColors.highlighted.background case .destructive: - textColor = UIColor.red - backgroundColor = themeCollection.tableRowColors.backgroundColor + textColor = css.getColor(.stroke, selectors: [.destructive, .label], for: cell) // UIColor.red + backgroundColor = css.getColor(.fill, for: cell) // themeCollection.tableRowColors.backgroundColor case let .custom(customTextColor, customSelectedTextColor, customBackgroundColor, customSelectedBackgroundColor): textColor = customTextColor @@ -793,18 +801,18 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { selectedBackgroundColor = customSelectedBackgroundColor } - self?.cell?.textLabel?.tintColor = textColor - self?.cell?.textLabel?.textColor = textColor - self?.cell?.imageView?.tintColor = (imageTintColorKey != nil) ? themeCollection.tableRowColors.value(forKeyPath: imageTintColorKey!) as? UIColor : textColor - self?.cell?.accessoryView?.tintColor = textColor - self?.cell?.tintColor = themeCollection.tintColor + cell.textLabel?.tintColor = textColor + cell.textLabel?.textColor = textColor + cell.imageView?.tintColor = textColor + cell.accessoryView?.tintColor = textColor + cell.tintColor = textColor // themeCollection.tintColor if selectedTextColor != nil { - self?.cell?.textLabel?.highlightedTextColor = selectedTextColor + cell.textLabel?.highlightedTextColor = selectedTextColor } if backgroundColor != nil { - self?.cell?.backgroundColor = backgroundColor + cell.backgroundColor = backgroundColor } if selectedBackgroundColor != nil { @@ -812,9 +820,9 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { selectedBackgroundView.backgroundColor = selectedBackgroundColor - self?.cell?.selectedBackgroundView? = selectedBackgroundView + cell.selectedBackgroundView = selectedBackgroundView } - }, applyImmediately: true) + } self.action = action } @@ -839,7 +847,10 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { datePickerView.maximumDate = maximumDate datePickerView.accessibilityIdentifier = identifier datePickerView.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: UIControl.Event.valueChanged) - datePickerView.setValue(Theme.shared.activeCollection.tableRowColors.labelColor, forKey: "textColor") + + if let labelColor = datePickerView.getThemeCSSColor(.stroke, selectors: [.label]) { + datePickerView.setValue(labelColor, forKey: "textColor") + } self.cell = ThemeTableViewCell(style: .default, reuseIdentifier: nil) self.cell?.selectionStyle = .none @@ -850,7 +861,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { datePickerView.layoutIfNeeded() - if let cell = self.cell { + if let cell { var constraints : [NSLayoutConstraint] = [ datePickerView.leftAnchor.constraint(equalTo: cell.contentView.safeAreaLayoutGuide.leftAnchor), datePickerView.rightAnchor.constraint(equalTo: cell.contentView.safeAreaLayoutGuide.rightAnchor), @@ -883,7 +894,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { slider.value = value slider.accessibilityIdentifier = identifier slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged) - slider.tintColor = Theme.shared.activeCollection.tintColor + slider.tintColor = Theme.shared.activeCollection.css.getColor(.stroke, for: slider) cell = ThemeTableViewCell(style: .default, reuseIdentifier: nil) cell?.selectionStyle = .none @@ -893,7 +904,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.value = value self.action = action - if let cell = self.cell { + if let cell { NSLayoutConstraint.activate([ slider.leftAnchor.constraint(equalTo: cell.contentView.safeAreaLayoutGuide.leftAnchor, constant: 20), slider.rightAnchor.constraint(equalTo: cell.contentView.safeAreaLayoutGuide.rightAnchor, constant: -20), @@ -917,7 +928,7 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { self.action = action - if let cell = self.cell { + if let cell { var constraints : [NSLayoutConstraint] = [] customView.translatesAutoresizingMaskIntoConstraints = false @@ -942,12 +953,4 @@ open class StaticTableViewRow : NSObject, UITextFieldDelegate { @objc open func actionTriggered(_ sender: UIView) { action?(self, sender) } - - // MARK: - Deinit - - deinit { - if themeApplierToken != nil { - Theme.shared.remove(applierForToken: themeApplierToken) - } - } } diff --git a/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewSection.swift b/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewSection.swift index 44ee8243c..cfe9b70e5 100644 --- a/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewSection.swift +++ b/ownCloudAppShared/User Interface/StaticTableView/StaticTableViewSection.swift @@ -187,7 +187,7 @@ open class StaticTableViewSection: NSObject { public func selectedValue(forGroupIdentifier groupIdentifier: String) -> Any? { for row in rows { if row.groupIdentifier == groupIdentifier { - if row.cell?.accessoryType == UITableViewCell.AccessoryType.checkmark { + if row.cell?.accessoryType == .checkmark { return (row.value) } } diff --git a/ownCloudAppShared/User Interface/Theme/CSS/NSObject+ThemeCSS.swift b/ownCloudAppShared/User Interface/Theme/CSS/NSObject+ThemeCSS.swift new file mode 100644 index 000000000..2edba24a9 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/NSObject+ThemeCSS.swift @@ -0,0 +1,165 @@ +// +// NSObject+ThemeCSS.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 19.03.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 ThemeCSSAutoSelector { + var cssAutoSelectors: [ThemeCSSSelector] { get } +} + +public protocol ThemeCSSChangeObserver { + func cssSelectorsChanged() +} + +extension NSObject { + private struct AssociatedKeys { + static var cssSelectors = "_cssSelectors" + } + + public var activeThemeCSS: ThemeCSS { + return Theme.shared.activeCollection.css + } + + public func getThemeCSSColor(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil) -> UIColor? { + return activeThemeCSS.getColor(property, selectors: selectors, state: stateSelectors, for: self) + } + + public var cssSelectors: [ThemeCSSSelector]? { + set { + objc_setAssociatedObject(self, &AssociatedKeys.cssSelectors, newValue, .OBJC_ASSOCIATION_RETAIN) + + if let changeObserver = self as? ThemeCSSChangeObserver { + changeObserver.cssSelectorsChanged() + } + } + + get { + var effectiveSelectors: [ThemeCSSSelector] = objc_getAssociatedObject(self, &AssociatedKeys.cssSelectors) as? [ThemeCSSSelector] ?? [] + + if let autoSelector = self as? ThemeCSSAutoSelector { + effectiveSelectors.insert(contentsOf: autoSelector.cssAutoSelectors, at: (effectiveSelectors.count > 0) ? (effectiveSelectors.count-1) : 0) + } + + if let viewController = self as? UIViewController, viewController.parent == nil, viewController.presentingViewController != nil { + effectiveSelectors.insert(.modal, at: 0) + } + + return effectiveSelectors + } + } + + public var cssSelector: ThemeCSSSelector? { + get { + return cssSelectors?.first + } + set { + if let newValue { + cssSelectors = [newValue] + } else { + cssSelectors = nil + } + } + } + + private var _parentCSSObject: NSObject? { + if let view = self as? UIView { + if let viewController = view.next as? UIViewController, viewController.view == self { + return viewController + } + + return view.superview + } + + if let viewController = self as? UIViewController { + return viewController.view.superview // viewController.parent?.view + } + + return nil + } + + public var cascadingStyleSelectors: [ThemeCSSSelector] { + var obj: NSObject? = self + var cascadingStyleSelectors: [ThemeCSSSelector] = [.all] + + while obj != nil { + if let styleSelectors = obj?.cssSelectors { + for addSelector in styleSelectors.reversed() { + if !cascadingStyleSelectors.contains(addSelector) { + cascadingStyleSelectors.insert(addSelector, at: 1) + } + } + } + + obj = obj?._parentCSSObject + } + + return cascadingStyleSelectors + } + + @objc public var cssDescription: String { + let selectors = (cascadingStyleSelectors.compactMap({ selector in + return selector.rawValue as NSString + }) as NSArray).componentsJoined(by: ".") + + let properties: [ThemeCSSProperty] = [ + .stroke, .fill, .cornerRadius + ] + + var records = "" + + for property in properties { + records += "- \(property.rawValue): " + if let record = Theme.shared.activeCollection.css.get(property, for: self) { + records += "\((record.selectors.compactMap({ selector in selector.rawValue }) as NSArray).componentsJoined(by: ".")) -> \(record.value != nil ? record.value! : "-")" + } else { + records += "-" + } + records += "\n" + } + + return "Selectors: \(selectors)\nMatching:\n\(records)" + } + + @objc public func cssDescription(extraSelectors: [String]? = nil, stateSelectors: [String]? = nil) -> String { + let selectors = (cascadingStyleSelectors.compactMap({ selector in + return selector.rawValue as NSString + }) as NSArray).componentsJoined(by: ".") + + let properties: [ThemeCSSProperty] = [ + .stroke, .fill, .cornerRadius + ] + + var records = "" + + for property in properties { + records += "- \(property.rawValue): " + if let record = Theme.shared.activeCollection.css.get(property, + selectors: extraSelectors?.compactMap({string in return ThemeCSSSelector(rawValue: string)}), + state: stateSelectors?.compactMap({string in return ThemeCSSSelector(rawValue: string)}), + for: self) { + records += "\((record.selectors.compactMap({ selector in selector.rawValue }) as NSArray).componentsJoined(by: ".")) -> \(record.value != nil ? record.value! : "-")" + } else { + records += "-" + } + records += "\n" + } + + return "Selectors: \(selectors)\nMatching:\n\(records)" + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/README.md b/ownCloudAppShared/User Interface/Theme/CSS/README.md new file mode 100644 index 000000000..aeb32303f --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/README.md @@ -0,0 +1,240 @@ +# Theme CSS + +## Overview + +`ThemeCSS` brings CSS-style styling to the `UIViewController`/`UIView` (henceforth summarized as "elements") tree, by allowing to attach CSS-style selectors to them via new `cssSelector` and `cssSelectors` properties. + +After an element has settled in its place in the view tree, traversing the tree from an element to the tree's root element allows building a *Selector Path*, which can then be used to find the *most specific* value for a styling property. + +## Implementation + +### `ThemeCSSAutoSelector` +The `ThemeCSSAutoSelector` protocol is used to add class-specific selectors to commonly used, system-provided view classes automatically, f.ex. `label` for `UILabel`, or `cell` for `UICollectionReusableView`. + +### `Theme` registration +To receive notifications on Theme changes and to perform initial styling, elements opt into themeing by registering with `Theme`, with an initial themeing taking place at the time of registration. + +To ensure the full, applicable *Selector Path* can be built correctly, themeing should only occur once a view has arrived in its final place in the view structure. + +This is typically the case when `UIView.didMoveToWindow()` or `UIViewController.viewWillAppear(_:)` are called respectively. Appropriate registration code could therefore look like this: + +#### Example for `UIView` +```swift +private var _themeRegistered = false +public override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_themeRegistered { + _themeRegistered = true + Theme.shared.register(client: self) + } +} +``` + +#### Example for `UIViewController` +```swift +private var _themeRegistered = false +open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if !_themeRegistered { + _themeRegistered = true + Theme.shared.register(client: self, applyImmediately: true) + } +} +``` + +### Using CSS selectors + +#### Assigning selectors to elements +A single selector can be assigned to an element via the `cssSelector` property, like f.ex. + +```swift +tokenView.cssSelector = .token +``` + +If more than one selector should be assigned to an alement, the `cssSelectors` property can be used, like f.ex. + +```swift +tableView.cssSelector = [.collection, .tableView] +``` + +#### Defining new selectors +New selectors can be defined by using extensions, like f.ex. + +```swift +extension ThemeCSSSelector { + static let releaseNotes = ThemeCSSSelector(rawValue: "releaseNotes") +} +``` + +#### Defining property values +Property values are encapsulated in `ThemeCSSRecord`s, which consist of +- `selectors`: an array of `ThemeCSSSelector`s that is later matched with the *Selector Path* of elements +- `property`: the property for which a value is defined (most commonly `.stroke` and `.fill`) +- `value`: the value of the property, which can take virtually any type +- `important`: if true, the record receives an extra boost to its matching score + +Example of how to add styling records to a `ThemeCSS` instance: +```swift +css.add(records: [ + ThemeCSSRecord(selectors: [.account], property: .fill, value: accountCellSet.backgroundColor), + ThemeCSSRecord(selectors: [.account, .title], property: .stroke, value: accountCellSet.labelColor), + ThemeCSSRecord(selectors: [.account, .description], property: .stroke, value: accountCellSet.secondaryLabelColor), + ThemeCSSRecord(selectors: [.account, .disconnect], property: .stroke, value: accountCellSet.tintColor), + ThemeCSSRecord(selectors: [.account, .disconnect], property: .fill, value: accountCellSet.labelColor), +]) +``` + +It is also possible to pass a `nil` value, to "remove" a value for a certain *Selector Path*. + +### Retrieving property values +There are several ways to retrieve values: + +#### Directly from an element +Using the `getThemeCSSColor(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil) -> UIColor?` method: +```swift +let fillColor = view.getThemeCSSColor(.fill) +``` + +This method is a convenience way for typed access to the `ThemeCSS` instance of `Theme.shared.activeCollection`. + +#### Typed, from the `ThemeCSS` instance +Using its typed `getPROPERTY(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> VALUE?` family of methods: +```swift +let fillColor = collection.css.getColor(.fill, for: view) +``` + +This family of methods also carries out conversion from other types, where appropriate: + +Method name | Allowed types +--------------------------------|-------------- +getColor | `UIColor`, `String` (in hex `RRGGBB` or `#RRGGBB` notations, f.ex. `abcdef` or `#abcdef`) +getInteger | `Int` +getCGFloat | `CGFloat` +getBool | `Boolean`, `String` (`true` and `false`) +getUserInterfaceStyle | `UIUserInterfaceStyle`, `Int`, `String` (`unspecified`, `light`, `dark`) +getStatusBarStyle | `UIStatusBarStyle`, `Int`, `String` (`default`, `lightContent`, `darkContent`) +getBarStyle | `UIBarStyle`, `Int`, `String` (`default`, `black`) +getKeyboardAppearance | `UIKeyboardAppearance`, `Int`, `String` (`default`, `light`, `dark`) +getActivityIndicatorStyle | `UIActivityIndicatorView.Style`, `Int`, `String` (`medium`, `large`) +getBlurEffectStyle | `UIBlurEffect.Style`, `Int`, `String` (`regular`, `light`, `dark`) + +#### Raw, from the `ThemeCSS` instance +Using the `get(_ property: ThemeCSSProperty, selectors additionalSelectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> ThemeCSSRecord?` method, it is possible to retrieve the best matching `ThemeCSSRecord` for the property: +```swift +let record = collection.css.get(.fill, for: view) +let fillColor = record.value as? UIColor +``` + +### "Virtual" properties and states +While most elements only have a foreground (`.stroke`) and background (`.fill`) color, some elements require greater flexibility - or should look different depending on their state. + +This is where additional selectors come in. All methods described above therefore allow passing additional +- `selectors` for sub elements, which are appended to the end of the *Selector Path* for an element +- `state` for state information, which are inserted *before* the last selector in the *Selector Path* (already including additional selectors) + +#### Example for sub elements +A text field needs a color for placeholder text that's different from the color for typed text: +```swift +let placeholderColor = css.getColor(.stroke, selectors: [.placeholder], for: textField) +``` + +If the *Selector Path* for `textField` is `.modal .textField`, adding the `.placeholder` selector now requests the `.stroke` color for `.modal .textField .placeholder`. + +#### Example for different states +A button should be able to use a different background color when pressed: +```swift +let backgroundColor = button.getThemeCSSColor(.fill, state: isPressed ? [.highlighted] : nil) +``` + +If the *Selector Path* for `button` is `.modal .button`, adding the `.highlighted` state selector now requests the `.stroke` color for `.modal .highlighted .textField`. + +### How matching works +The best matching record to derive property values from is determined through the following rules: +- a record is only considered if: + - *all* of the record's Selectors are also contained in the *Selector Path* + - the record's property matches the requested property +- records whose last element is identical to that of the *Selector Path* are preferred (+100) +- the specifity - and therefore weight - of selectors increases (+10) from the beginning to the end of the *Selector Path* (in `.sidebar .cell .label .account`, `.label` is weighted higher than `.cell` - at 30 vs 20) +- records with the `important` property are preferred (+1000) +- if two or more records reach the same score, the one that was last added to the `ThemeCSS` instance will be used (allows override, f.e.x. through `Branding.plist`) + +## Debugging selectors and matching +If a view doesn't use the expected values for the respective properties, this can different reasons: +- themeing is performed at a time where the final *Selector Path* can't be built. Debug by setting a breakpoint and check if the element hierarchy has been correctly established at that point. +- a record other than the expected one is determined as most specific. This can be fixed by adding a more specific record, or changing the request. + +To make debugging straightforward, you can use `cssDescription` and `cssDescription(extraSelectors: [String]? = nil, stateSelectors: [String]? = nil)` in the debugger, which will output the applicable records and values for the `.stroke`, `.fill` and `.cornerRadius` properties. + +### Usage in practice + +Combining this with view debugging turns this into a fast, flexible debugging tool: +- enter *Debug View Hierarchy* in Xcode (button that shows a stack of rectangles in the bottom bar) +- right-click the element you'd like to inspect and pick use *Reveal in Debug Navigator* from the popup menu +- right-click the revealed element in the sidebar and pick *Copy* from the popup menu +- type `po [PASTEHERE cssDescription]` behind `(lldb)` into the Debug Console, whereby you replace `PASTEHERE` with the clipboard contents, of course + +Example: +``` +(lldb) po [((UIView *)0x11bcf9150) cssDescription] +Selectors: all.splitView.content.collection.cell.sortBar +Matching: +- stroke: collection.cell -> UIExtendedSRGBColorSpace 0.305882 0.521569 0.784314 1 +- fill: collection.cell -> UIExtendedGrayColorSpace 1 1 +- cornerRadius: - +``` + +A way to change the value of the `stroke` property, then, would be to add a more specific record, f.ex. for `collection.cell.sortBar` - or possibly for `sortBar` directly (the last selector is weighted much higher). + +## Adding styling via branding +Additional CSS records can be added through the usual branding options, including `Branding.plist`, by providing an array of css records following this format: + +``` +selector1.selector2….property: value +``` + +Example for a `Branding.plist` that fills the logo in the sidebar's navigation bar with red color: + +```xml +branding.theme-definitions$[0].cssRecords + + sidebar.navigationBar.logo.stroke: #ff0000 + +``` + +See *Debugging selectors and matching* > *Usage in practice* above for how to determine the selectors of a view on screen. + +#### Icon color CSS selectors +Icon colors are now also configured via CSS selectors: + +TVG/Legacy Color | CSS selector string +------------------|------------------------------------ +`folderFillColor` | `vectorImage.folderColor.fill` +`fileFillColor` | `vectorImage.fileColor.fill` +`logoFillColor` | `vectorImage.logoColor.fill` +`iconFillColor` | `vectorImage.iconColor.fill` +`symbolFillColor` | `vectorImage.symbolColor.fill` + +### Reference +#### Selectors +These selectors are used to differentiate beteween different areas: + +Selector | Description +-------------|--------------- +`all` | Matches all elements +`sidebar` | All elements in the sidebar part of the splitview. +`content` | All elements in the content part of the splitview. +`modal` | All modal and standalone views, including views in app extensions. + +#### Properties +Property | Type | Description / Values +---------|--------|------------ +`stroke` | color | Color used for f.ex. text and tinting. In hex string notation. Use `none` for no color. +`fill` | color | Color used for f.ex. background fill. In hex string notation. Use `none` for no color. +`cornerRadius` | float | Corner radius (not widely used) +`style` | `UIUserInterfaceStyle` | `unspecified`, `light`, `dark` +`barStyle` | `UIBarStyle` | `default`, `black` +`statusBarStyle` | `UIStatusBarStyle` | `default`, `lightContent`, `darkContent` +`blurEffectStyle` | `UIBlurEffect.Style` | `regular`, `light`, `dark` +`keyboardAppearance` | `UIKeyboardAppearance` | `default`, `light`, `dark` +`activityIndicatorStyle` | `UIActivityIndicatorView.Style` | `medium`, `large` diff --git a/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS+AutoSelectors.swift b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS+AutoSelectors.swift new file mode 100644 index 000000000..66b3700d8 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS+AutoSelectors.swift @@ -0,0 +1,121 @@ +// +// ThemeCSS+AutoSelectors.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 19.03.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 + +extension UILabel: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.label] + } +} + +extension UICollectionReusableView: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.cell] + } +} + +extension UICollectionView: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.collection] + } +} + +extension UITableView: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + switch style { + case .grouped: return [.grouped, .table] + case .insetGrouped: return [.insetGrouped, .table] + default: return [.table] // includes .plain + } + } +} + +extension UINavigationBar: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.navigationBar] + } +} + +extension UIToolbar: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.toolbar] + } +} + +extension UITabBar: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.tabBar] + } +} + +extension UIProgressView: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.progress] + } +} + +extension UIButton: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + if (self as? ThemeButton) != nil { + return [.filled, .button] + } else { + return [.button] + } + } +} + +extension UITextField: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + if (self as? UISearchTextField) != nil { + return [.textField, .searchField] + } else { + return [.textField] + } + } +} + +extension UITextView: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.textView] + } +} + +extension UISegmentedControl: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.segmentedControl] + } +} + +extension UIDatePicker: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.datePicker] + } +} + +extension UISlider: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.slider] + } +} + +extension UIAlertController: ThemeCSSAutoSelector { + public var cssAutoSelectors: [ThemeCSSSelector] { + return [.alert] + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift new file mode 100644 index 000000000..0ff61cc7a --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift @@ -0,0 +1,415 @@ +// +// ThemeCSS.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 19.03.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 + +// MARK: - Selectors +public struct ThemeCSSSelector: RawRepresentable, Equatable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + // Catch-all selector that *everything* matches + public static let all = ThemeCSSSelector(rawValue: "all") + + // Presented view controllers + public static let modal = ThemeCSSSelector(rawValue: "modal") + + // Splitview region selectors (sidebar + presented content) + public static let sidebar = ThemeCSSSelector(rawValue: "sidebar") + public static let content = ThemeCSSSelector(rawValue: "content") + public static let separator = ThemeCSSSelector(rawValue: "separator") + + // Bars on top and bottom + public static let navigationBar = ThemeCSSSelector(rawValue: "navigationBar") + public static let toolbar = ThemeCSSSelector(rawValue: "toolbar") + public static let tabBar = ThemeCSSSelector(rawValue: "tabbar") + + // Content structure + public static let table = ThemeCSSSelector(rawValue: "table") + public static let collection = ThemeCSSSelector(rawValue: "collection") + public static let header = ThemeCSSSelector(rawValue: "header") + public static let footer = ThemeCSSSelector(rawValue: "footer") + public static let sectionHeader = ThemeCSSSelector(rawValue: "sectionHeader") + public static let sectionFooter = ThemeCSSSelector(rawValue: "sectionFooter") + public static let cell = ThemeCSSSelector(rawValue: "cell") + public static let accessory = ThemeCSSSelector(rawValue: "accessory") + + // Content Elements + public static let splitView = ThemeCSSSelector(rawValue: "splitView") + public static let alert = ThemeCSSSelector(rawValue: "alert") + public static let label = ThemeCSSSelector(rawValue: "label") + public static let button = ThemeCSSSelector(rawValue: "button") + public static let slider = ThemeCSSSelector(rawValue: "slider") + public static let progress = ThemeCSSSelector(rawValue: "progress") + public static let activityIndicator = ThemeCSSSelector(rawValue: "activityIndicator") + public static let textField = ThemeCSSSelector(rawValue: "textField") + public static let textView = ThemeCSSSelector(rawValue: "textView") + public static let searchField = ThemeCSSSelector(rawValue: "searchField") + public static let datePicker = ThemeCSSSelector(rawValue: "datePicker") + public static let popupButton = ThemeCSSSelector(rawValue: "popupButton") + public static let placeholder = ThemeCSSSelector(rawValue: "placeholder") + public static let segmentedControl = ThemeCSSSelector(rawValue: "segmentedControl") + public static let group = ThemeCSSSelector(rawValue: "group") + public static let icon = ThemeCSSSelector(rawValue: "icon") + public static let item = ThemeCSSSelector(rawValue: "item") + public static let background = ThemeCSSSelector(rawValue: "background") + public static let border = ThemeCSSSelector(rawValue: "border") + public static let shadow = ThemeCSSSelector(rawValue: "shadow") + + // Purpose + public static let title = ThemeCSSSelector(rawValue: "title") + public static let subtitle = ThemeCSSSelector(rawValue: "subtitle") + public static let description = ThemeCSSSelector(rawValue: "description") + + public static let primary = ThemeCSSSelector(rawValue: "primary") + public static let secondary = ThemeCSSSelector(rawValue: "secondary") + public static let tertiary = ThemeCSSSelector(rawValue: "tertiary") + + public static let info = ThemeCSSSelector(rawValue: "info") + public static let warning = ThemeCSSSelector(rawValue: "warning") + public static let critical = ThemeCSSSelector(rawValue: "critical") + public static let error = ThemeCSSSelector(rawValue: "error") + public static let success = ThemeCSSSelector(rawValue: "success") + + public static let confirm = ThemeCSSSelector(rawValue: "confirm") + public static let destructive = ThemeCSSSelector(rawValue: "destructive") + public static let cancel = ThemeCSSSelector(rawValue: "cancel") + public static let proceed = ThemeCSSSelector(rawValue: "proceed") + public static let purchase = ThemeCSSSelector(rawValue: "purchase") + public static let favorite = ThemeCSSSelector(rawValue: "favorite") + + public static let plain = ThemeCSSSelector(rawValue: "plain") + public static let token = ThemeCSSSelector(rawValue: "token") + + // States + public static let highlighted = ThemeCSSSelector(rawValue: "highlighted") + public static let selected = ThemeCSSSelector(rawValue: "selected") + public static let disabled = ThemeCSSSelector(rawValue: "disabled") + public static let filled = ThemeCSSSelector(rawValue: "filled") + + // Configurations + public static let grouped = ThemeCSSSelector(rawValue: "grouped") + public static let insetGrouped = ThemeCSSSelector(rawValue: "insetGrouped") +} + +// MARK: - Properties +public struct ThemeCSSProperty: RawRepresentable, Equatable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + // Colors + public static let stroke = ThemeCSSProperty(rawValue: "stroke") + public static let fill = ThemeCSSProperty(rawValue: "fill") + + // Integers + public static let cornerRadius = ThemeCSSProperty(rawValue: "cornerRadius") + + // Others + public static let style = ThemeCSSProperty(rawValue: "style") // UIUserInterfaceStyle + public static let barStyle = ThemeCSSProperty(rawValue: "barStyle") // UIBarStyle + public static let statusBarStyle = ThemeCSSProperty(rawValue: "statusBarStyle") // UIStatusBarStyle + public static let blurEffectStyle = ThemeCSSProperty(rawValue: "blurEffectStyle") // UIBlurEffect.Style + public static let keyboardAppearance = ThemeCSSProperty(rawValue: "keyboardAppearance") // UIKeyboardAppearance + public static let activityIndicatorStyle = ThemeCSSProperty(rawValue: "activityIndicatorStyle") // UIActivityIndicatorView.Style +} + +// MARK: - CSS +open class ThemeCSS: NSObject { + open var records: [ThemeCSSRecord] = [] + private var _cachedMatches: [ThemeCSSAddress : ThemeCSSRecord] = [:] + + open func add(record: ThemeCSSRecord) { + records.append(record) + OCSynchronized(self) { + _cachedMatches.removeAll() // Clear cache + } + } + + open func add(records: [ThemeCSSRecord]) { + self.records.append(contentsOf: records) + OCSynchronized(self) { + _cachedMatches.removeAll() // Clear cache + } + } + + open func get(_ property: ThemeCSSProperty, for selectors: [ThemeCSSSelector]) -> ThemeCSSRecord? { + var bestRecord: ThemeCSSRecord? + var bestRecordScore: Int = -1 + + // Search cache + let cssAddress = selectors.address(with: property) + + var cachedMatch: ThemeCSSRecord? + OCSynchronized(self) { + cachedMatch = _cachedMatches[cssAddress] + } + if let cachedMatch { + // Return cached match + return cachedMatch + } + + // Determine best match from records + for record in records { + let score = record.score(for: selectors, property: property) + + if score != -1, score >= bestRecordScore { // prefer later records over previous records to allow overriding by records added later without having to replace the previous one + bestRecordScore = score + bestRecord = record + } + } + + // Store match in cache + OCSynchronized(self) { + _cachedMatches[cssAddress] = bestRecord + } + + return bestRecord + } + + open func get(_ property: ThemeCSSProperty, selectors additionalSelectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> ThemeCSSRecord? { + var selectors = (object as? NSObject)?.cascadingStyleSelectors ?? [] + + if let additionalSelectors { + selectors.append(contentsOf: additionalSelectors) + } + + if let stateSelectors { + // The last selector is weighted higher because it typically is used to describe the type + // To not interfere with this convention, selectors representing state are inserted before the last selector + selectors.insert(contentsOf: stateSelectors, at: (selectors.count > 0) ? selectors.count - 1 : 0) + } + + if object == nil, !selectors.contains(.all) { + // If no object is provided, ensure .all is included in the selectors + selectors.insert(.all, at: 0) + } + + return get(property, for: selectors) + } + + open func getColor(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> UIColor? { + let value = get(property, selectors: selectors, state: stateSelectors, for: object)?.value + + if let color = value as? UIColor { + return color + } + + if let string = value as? String { + if string == "none" { + return nil + } + if let hexColor = string.colorFromHex { + return hexColor + } + } + + return nil + } + + open func getInteger(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> Int? { + return get(property, selectors: selectors, state: stateSelectors, for: object)?.value as? Int + } + + open func getCGFloat(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> CGFloat? { + return get(property, selectors: selectors, state: stateSelectors, for: object)?.value as? CGFloat + } + + open func getBool(_ property: ThemeCSSProperty, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> Bool? { + let value = get(property, selectors: selectors, state: stateSelectors, for: object)?.value + + if let bool = value as? Bool { + return bool + } + + if let string = value as? String { + switch string { + case "true": return true + case "false": return false + default: break + } + } + + return nil + } + + open func getUserInterfaceStyle(selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject? = nil) -> UIUserInterfaceStyle { + let value = get(.style, selectors: selectors, state: stateSelectors, for: object)?.value + + if let style = value as? UIUserInterfaceStyle { + return style + } + + if let intValue = value as? Int { + // Convert Int values to UIUserInterfaceStyle if needed + return UIUserInterfaceStyle(rawValue: intValue) ?? .unspecified + } + + if let stringValue = value as? String { + // Convert String values to UIUserInterfaceStyle if needed + switch stringValue { + case "unspecified": return .unspecified + case "light": return .light + case "dark": return .dark + default: break + } + } + + return .unspecified + } + + open func getStatusBarStyle(selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> UIStatusBarStyle? { + let value = get(.statusBarStyle, selectors: selectors, state: stateSelectors, for: object)?.value + + if let style = value as? UIStatusBarStyle { + return style + } + + if let intValue = value as? Int { + // Convert Int values to UIStatusBarStyle if needed + return UIStatusBarStyle(rawValue: intValue) + } + + if let stringValue = value as? String { + // Convert String values to UIStatusBarStyle if needed + switch stringValue { + case "default": return .default + case "lightContent": return .lightContent + case "darkContent": return .darkContent + default: break + } + } + + return nil + } + + open func getBarStyle(selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject?) -> UIBarStyle? { + let value = get(.barStyle, selectors: selectors, state: stateSelectors, for: object)?.value + + if let style = value as? UIBarStyle { + return style + } + + if let intValue = value as? Int { + // Convert Int values to UIBarStyle if needed + return UIBarStyle(rawValue: intValue) + } + + if let stringValue = value as? String { + // Convert String values to UIBarStyle if needed + switch stringValue { + case "default": return .default + case "black": return .black + default: break + } + } + + return nil + } + + open func getKeyboardAppearance(selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject? = nil) -> UIKeyboardAppearance { + let value = get(.keyboardAppearance, selectors: selectors, state: stateSelectors, for: object)?.value + + if let style = value as? UIKeyboardAppearance { + return style + } + + if let intValue = value as? Int { + // Convert Int values to UIKeyboardAppearance if needed + return UIKeyboardAppearance(rawValue: intValue) ?? .default + } + + if let stringValue = value as? String { + // Convert String values to UIKeyboardAppearance if needed + switch stringValue { + case "default": return .default + case "light": return .light + case "dark": return .dark + default: break + } + } + + return .default + } + + open func getActivityIndicatorStyle(selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject? = nil) -> UIActivityIndicatorView.Style? { + let value = get(.activityIndicatorStyle, selectors: selectors, state: stateSelectors, for: object)?.value + + if let style = value as? UIActivityIndicatorView.Style { + return style + } + + if let intValue = value as? Int { + // Convert Int values to UIActivityIndicatorView.Style if needed + return UIActivityIndicatorView.Style(rawValue: intValue) + } + + if let stringValue = value as? String { + // Convert String values to UIActivityIndicatorView.Style if needed + switch stringValue { + case "medium", "white", "gray": return .medium + case "whiteLarge", "large": return .large + default: break + } + } + + return nil + } + + open func getBlurEffectStyle(selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, for object: AnyObject? = nil) -> UIBlurEffect.Style { + let value = get(.blurEffectStyle, selectors: selectors, state: stateSelectors, for: object)?.value + + if let style = value as? UIBlurEffect.Style { + return style + } + + if let intValue = value as? Int, let style = UIBlurEffect.Style(rawValue: intValue) { + // Convert Int values to UIBlurEffect.Style if needed + return style + } + + if let stringValue = value as? String { + // Convert String values to UIBlurEffect.Style if needed + switch stringValue { + case "regular": return .regular + case "light": return .light + case "dark": return .dark + default: break + } + } + + return .regular + } +} + +// MARK: - CSS addresses (for caching) +typealias ThemeCSSAddress = String + +extension [ThemeCSSSelector] { + func address(with property: ThemeCSSProperty) -> ThemeCSSAddress { + var stringComponents = self.compactMap { selector in selector.rawValue } + stringComponents.append(property.rawValue) + + return stringComponents.joined(separator: ".") + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSSRecord.swift b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSSRecord.swift new file mode 100644 index 000000000..f96e02b97 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSSRecord.swift @@ -0,0 +1,87 @@ +// +// ThemeCSSRecord.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 19.03.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 ThemeCSSRecord: NSObject { + open var selectors: [ThemeCSSSelector] + open var property: ThemeCSSProperty + open var important: Bool + + public init(selectors: [ThemeCSSSelector], property: ThemeCSSProperty, value: Any?, important: Bool = false) { + self.selectors = selectors + self.property = property + self.value = value + self.important = important + } + + convenience init?(with selectorString: String, value: Any) { + var selectorsAndProperty = selectorString.components(separatedBy: ".") + + let propertyName = selectorsAndProperty.last + selectorsAndProperty.removeLast() + + guard let propertyName else { return nil } + + let property = ThemeCSSProperty(rawValue: propertyName) + + let parsedSelectors: [ThemeCSSSelector] = selectorsAndProperty.compactMap { selectorString in + return ThemeCSSSelector(rawValue: selectorString) + } + + self.init(selectors: parsedSelectors, property: property, value: value) + } + + open var value: Any? + + open func score(for inSelectors: [ThemeCSSSelector], property inProperty: ThemeCSSProperty) -> Int { + var score: Int = 0 + + if inProperty != property { + // Not applicable + return -1 + } else { + // Property match + score += 1 + } + + // Direct match of most specific (== last) selector + if let typeSelector = inSelectors.last, selectors.last == typeSelector { + score += 100 + } + + // Match selectors + for selector in selectors { + // Matches weighted by position, the farther along the higher the specificity/score, + // so that "collection.cell" does not override "segments" for "collection.cell.segments.title" + if let inSelectorIndex = inSelectors.firstIndex(of: selector) { + score += (inSelectorIndex + 1) * 10 + } else { + return -1 + } + } + + // Important + if important { + score += 1000 + } + + // Property match + return score + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/UIView+ThemeCSS.swift b/ownCloudAppShared/User Interface/Theme/CSS/UIView+ThemeCSS.swift new file mode 100644 index 000000000..c51e881a7 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/UIView+ThemeCSS.swift @@ -0,0 +1,72 @@ +// +// UIView+ThemeCSS.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 20.03.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 UIView { + func apply(css: ThemeCSS, selectors: [ThemeCSSSelector]? = nil, state stateSelectors: [ThemeCSSSelector]? = nil, properties: [ThemeCSSProperty]) { + for property in properties { + switch property { + case .fill: + if let color = css.getColor(property, selectors: selectors, state: stateSelectors, for: self) { + if let button = self as? UIButton { + var buttonConfig = button.configuration + buttonConfig?.baseBackgroundColor = color + button.configuration = buttonConfig + } else { + backgroundColor = color + } + } + + case .stroke: + if let color = css.getColor(property, selectors: selectors, state: stateSelectors, for: self) { + if let label = self as? UILabel { + label.textColor = color + } else if let textField = self as? UITextField { + textField.textColor = color + } else if let textField = self as? UITextView { + textField.textColor = color + } else if let button = self as? UIButton { + var buttonConfig = button.configuration + buttonConfig?.baseForegroundColor = color + button.configuration = buttonConfig + + button.tintColor = color + button.setTitleColor(color, for: .normal) + } else { + tintColor = color + } + } + + case .cornerRadius: + if let radius = css.getCGFloat(property, selectors: selectors, state: stateSelectors, for: self) { + layer.cornerRadius = radius + } + + case .keyboardAppearance: + let keyboardAppearance = css.getKeyboardAppearance(selectors: selectors, state: stateSelectors, for: self) + + if let textField = self as? UITextField { + textField.keyboardAppearance = keyboardAppearance + } + + default: break + } + } + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSButton.swift b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSButton.swift new file mode 100644 index 000000000..770294679 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSButton.swift @@ -0,0 +1,41 @@ +// +// ThemeCSSButton.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 27.03.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 ThemeCSSButton: UIButton, Themeable { + private var _hasRegistered = false + + convenience public init(withSelectors: [ThemeCSSSelector]?) { + self.init() + cssSelectors = withSelectors + } + + open override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_hasRegistered { + _hasRegistered = true + Theme.shared.register(client: self) + } + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + applyThemeCollection(collection) + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSLabel.swift b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSLabel.swift new file mode 100644 index 000000000..f48ffa122 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSLabel.swift @@ -0,0 +1,41 @@ +// +// ThemeCSSLabel.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 20.03.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 ThemeCSSLabel: UILabel, Themeable { + private var _hasRegistered = false + + convenience public init(withSelectors: [ThemeCSSSelector]?) { + self.init() + cssSelectors = withSelectors + } + + open override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_hasRegistered { + _hasRegistered = true + Theme.shared.register(client: self) + } + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + apply(css: collection.css, properties: [.stroke]) + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSProgressView.swift b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSProgressView.swift new file mode 100644 index 000000000..907b35d2f --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSProgressView.swift @@ -0,0 +1,37 @@ +// +// ThemeCSSProgressView.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 20.03.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 ThemeCSSProgressView: UIProgressView, Themeable { + private var _hasRegistered = false + + open override func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !_hasRegistered { + _hasRegistered = true + Theme.shared.register(client: self) + } + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + tintColor = collection.css.getColor(.stroke, for: self) + trackTintColor = collection.css.getColor(.fill, for: self) + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSTextField.swift b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSTextField.swift new file mode 100644 index 000000000..d22b6e34c --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSTextField.swift @@ -0,0 +1,44 @@ +// +// ThemeCSSTextField.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 27.03.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 ThemeCSSTextField: UITextField, Themeable { + private var hasRegistered : Bool = false + + override open func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !hasRegistered { + hasRegistered = true + Theme.shared.register(client: self, applyImmediately: true) + } + } + + override open var isEnabled: Bool { + didSet { + if hasRegistered { + applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .update) + } + } + } + + open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.applyThemeCollection(collection) + } +} diff --git a/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSView.swift b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSView.swift new file mode 100644 index 000000000..79c0afeb4 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/CSS/Views/ThemeCSSView.swift @@ -0,0 +1,64 @@ +// +// ThemeCSSView.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 20.03.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 + +open class ThemeCSSView: UIView, Themeable { + private var hasRegistered : Bool = false + + public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + convenience public init(withSelectors: [ThemeCSSSelector]) { + self.init() + cssSelectors = withSelectors + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func didMoveToWindow() { + super.didMoveToWindow() + + if window != nil, !hasRegistered { + hasRegistered = true + Theme.shared.register(client: self, applyImmediately: true) + } + } + + private var themeAppliers : [ThemeApplier] = [] + + open func addThemeApplier(_ applier: @escaping ThemeApplier) { + themeAppliers.append(applier) + } + + open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + for applier in themeAppliers { + applier(theme, collection, event) + } + + apply(css: collection.css, properties: [.fill]) + } +} diff --git a/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift b/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift index b3adf4ec3..e30e26bec 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 @@ -31,12 +32,12 @@ public enum ThemeItemStyle { case destructive case cancel - case logo + // case logo case title case message - case welcomeTitle - case welcomeMessage +// case welcomeTitle +// case welcomeMessage case bigTitle case bigMessage @@ -46,6 +47,9 @@ public enum ThemeItemStyle { case purchase case welcome + case welcomeInformal + + case content } public enum ThemeItemState { @@ -60,154 +64,99 @@ public enum ThemeItemState { self = .normal } } -} -public protocol ThemeableSectionHeader : AnyObject { - var sectionHeaderColor : UIColor? { get set } -} + var cssState: [ThemeCSSSelector] { + switch self { + case .disabled: + return [.disabled] + + case .highlighted: + return [.highlighted] -public protocol ThemeableSectionFooter : AnyObject { - var sectionFooterColor : UIColor? { get set } + default: + return [] + } + } } public extension NSObject { - func applyThemeCollection(_ collection: ThemeCollection, itemStyle: ThemeItemStyle = .defaultForItem, itemState: ThemeItemState = .normal) { - if let themeButton = self as? ThemeButton { - switch itemStyle { - case .approval: - themeButton.themeColorCollection = collection.approvalColors - - case .neutral: - themeButton.themeColorCollection = collection.neutralColors - - case .destructive: - themeButton.themeColorCollection = collection.destructiveColors - - case .bigTitle: - themeButton.themeColorCollection = collection.neutralColors - themeButton.titleLabel?.font = UIFont.systemFont(ofSize: 34) - - case .purchase: - themeButton.themeColorCollection = collection.purchaseColors - - case .welcome: - themeButton.themeColorCollection = collection.loginColors.filledColorPairCollection + func applyThemeCollection(_ collection: ThemeCollection, itemStyle: ThemeItemStyle = .defaultForItem, itemState: ThemeItemState = .normal, cellState: UICellConfigurationState? = nil) { + let css = collection.css - case .informal: - themeButton.themeColorCollection = collection.informalColors.filledColorPairCollection - - case .cancel: - themeButton.themeColorCollection = collection.cancelColors.filledColorPairCollection - - default: - themeButton.themeColorCollection = collection.lightBrandColors.filledColorPairCollection - } - } else if let button = self as? UIButton { - button.tintColor = collection.navigationBarColors.tintColor + if let button = self as? UIButton, (self as? ThemeButton) == nil { + button.apply(css: css, properties: [.stroke]) } if let navigationController = self as? UINavigationController { - navigationController.navigationBar.applyThemeCollection(collection, itemStyle: itemStyle) - navigationController.view.backgroundColor = collection.tableBackgroundColor + navigationController.navigationBar.applyThemeCollection(collection) + navigationController.view.apply(css: css, properties: [.fill]) } 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(navigationBar: navigationBar) + let navigationBarScrollEdgeAppearance = collection.navigationBarAppearance(navigationBar: navigationBar, scrollEdge: true) - let navigationBarAppearance = collection.navigationBarAppearance + navigationBar.tintColor = css.getColor(.stroke, for: navigationBar) navigationBar.standardAppearance = navigationBarAppearance navigationBar.compactAppearance = navigationBarAppearance - navigationBar.scrollEdgeAppearance = navigationBarAppearance + navigationBar.scrollEdgeAppearance = navigationBarScrollEdgeAppearance } if let toolbar = self as? UIToolbar { - toolbar.barTintColor = collection.toolbarColors.backgroundColor - toolbar.tintColor = collection.toolbarColors.tintColor - } + let backgroundColor = css.getColor(.fill, for: toolbar) + let tintColor = css.getColor(.stroke, for: toolbar) - if let tabBar = self as? UITabBar { - tabBar.barTintColor = collection.toolbarColors.backgroundColor - tabBar.tintColor = collection.toolbarColors.tintColor - tabBar.unselectedItemTintColor = collection.toolbarColors.secondaryLabelColor + let standardAppearance = UIToolbarAppearance() + standardAppearance.backgroundColor = backgroundColor + toolbar.standardAppearance = standardAppearance - let appearance = UITabBarAppearance() - appearance.backgroundColor = collection.toolbarColors.backgroundColor + let edgeAppearance = UIToolbarAppearance() + edgeAppearance.backgroundColor = backgroundColor + edgeAppearance.shadowColor = .clear + toolbar.scrollEdgeAppearance = edgeAppearance - tabBar.standardAppearance = appearance - tabBar.scrollEdgeAppearance = appearance + toolbar.barTintColor = backgroundColor + toolbar.tintColor = tintColor } if let tableView = self as? UITableView { - tableView.backgroundColor = tableView.style == .grouped ? collection.tableGroupBackgroundColor : collection.tableBackgroundColor - tableView.separatorColor = collection.tableSeparatorColor - - if let themeableSectionHeaderTableView = tableView as? ThemeableSectionHeader { - themeableSectionHeaderTableView.sectionHeaderColor = collection.tableSectionHeaderColor - } - - if let themeableSectionFooterTableView = tableView as? ThemeableSectionFooter { - themeableSectionFooterTableView.sectionFooterColor = collection.tableSectionFooterColor - } + tableView.backgroundColor = css.getColor(.fill, for: tableView) + tableView.separatorColor = css.getColor(.fill, selectors: [.separator], for: tableView) } if let collectionView = self as? UICollectionView { - collectionView.backgroundColor = collection.tableBackgroundColor + collectionView.apply(css: css, properties: [.fill]) } if let searchBar = self as? UISearchBar { - searchBar.tintColor = collection.searchBarColors.tintColor - searchBar.barStyle = collection.barStyle + searchBar.tintColor = css.getColor(.stroke, selectors: [.navigationBar], for: searchBar) + searchBar.barStyle = css.getBarStyle(for: searchBar) ?? .default - searchBar.searchTextField.textColor = collection.searchBarColors.labelColor // Ensure search bar icon color is correct - searchBar.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle - searchBar.searchTextField.backgroundColor = collection.searchBarColors.backgroundColor + searchBar.overrideUserInterfaceStyle = collection.css.getUserInterfaceStyle() - if let glassIconView = searchBar.searchTextField.leftView as? UIImageView { - glassIconView.image = glassIconView.image?.withRenderingMode(.alwaysTemplate) - 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.tintColor = collection.searchBarColors.secondaryLabelColor - } + searchBar.searchTextField.applyThemeCollection(collection) } if let label = self as? UILabel { - var normalColor : UIColor = collection.tableRowColors.labelColor - var highlightColor : UIColor = collection.tableRowHighlightColors.labelColor - let disabledColor : UIColor = collection.tableRowColors.secondaryLabelColor + var normalColor : UIColor // = collection.tableRowColors.labelColor + var highlightColor : UIColor // = collection.tableRowHighlightColors.labelColor + let disabledColor : UIColor = css.getColor(.stroke, selectors: [.secondary], for: label) ?? .secondaryLabel // = collection.tableRowColors.secondaryLabelColor switch itemStyle { - case .title, .bigTitle, .system(textStyle: _, weight: _): - normalColor = collection.tableRowColors.labelColor - highlightColor = collection.tableRowHighlightColors.labelColor - - case .welcomeTitle: - normalColor = collection.loginColors.labelColor - highlightColor = collection.loginColors.labelColor - - case .welcomeMessage: - normalColor = collection.loginColors.secondaryLabelColor - highlightColor = collection.loginColors.secondaryLabelColor - case .message, .bigMessage, .systemSecondary(textStyle: _, weight: _): - normalColor = collection.tableRowColors.secondaryLabelColor - highlightColor = collection.tableRowHighlightColors.secondaryLabelColor - - case .logo: - normalColor = collection.navigationBarColors.labelColor - highlightColor = collection.navigationBarColors.secondaryLabelColor + normalColor = css.getColor(.stroke, selectors: [.secondary], for: label) ?? .secondaryLabel + highlightColor = css.getColor(.stroke, selectors: [.secondary], state: [.highlighted], for: label) ?? .tertiaryLabel + // normalColor = collection.tableRowColors.secondaryLabelColor + // highlightColor = collection.tableRowHighlightColors.secondaryLabelColor default: - normalColor = collection.tableRowColors.labelColor - highlightColor = collection.tableRowHighlightColors.labelColor + // case .title, .bigTitle, .system(textStyle: _, weight: _): + normalColor = css.getColor(.stroke, selectors: [.primary], for: label) ?? .label + highlightColor = css.getColor(.stroke, selectors: [.primary], state: [.highlighted], for: label) ?? .label + // normalColor = collection.tableRowColors.labelColor + // highlightColor = collection.tableRowHighlightColors.labelColor } switch itemStyle { @@ -248,18 +197,39 @@ public extension NSObject { } if let textField = self as? UITextField { - textField.textColor = collection.tableRowColors.labelColor + let tintColor = css.getColor(.stroke, for: textField) + + textField.tintColor = tintColor + textField.textColor = css.getColor(.stroke, selectors: [.label], state: (textField.isEnabled ? [] : [.disabled]), for: textField) + textField.overrideUserInterfaceStyle = css.getUserInterfaceStyle(for: textField) + textField.keyboardAppearance = css.getKeyboardAppearance(for: textField) + + if let placeholderString = textField.placeholder, let placeholderTextColor = css.getColor(.stroke, selectors: [.placeholder], for: textField) { + textField.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [.foregroundColor : placeholderTextColor]) + } + + if let clearButton = textField.value(forKey: "clearButton") as? UIButton { + clearButton.setImage(OCSymbol.icon(forSymbolName: "xmark.circle.fill"), for: .normal) + clearButton.tintColor = tintColor + } + + if let searchTextField = textField as? UISearchTextField { + if let glassIconView = searchTextField.leftView as? UIImageView { + glassIconView.image = glassIconView.image?.withRenderingMode(.alwaysTemplate) + glassIconView.tintColor = tintColor + } + } } if let cell = self as? UITableViewCell { - cell.backgroundColor = collection.tableRowColors.backgroundColor - cell.tintColor = collection.lightBrandColor + cell.backgroundColor = css.getColor(.fill, for: cell) // collection.tableRowColors.backgroundColor + cell.tintColor = css.getColor(.stroke, for: cell) // collection.lightBrandColor if cell.selectionStyle != .none { - if collection.tableRowHighlightColors.backgroundColor != nil { + if let highlightedBackgroundColor = css.getColor(.fill, state: [.highlighted], for: cell) { // collection.tableRowHighlightColors.backgroundColor let backgroundView = UIView() - backgroundView.backgroundColor = collection.tableRowHighlightColors.backgroundColor + backgroundView.backgroundColor = highlightedBackgroundColor cell.selectedBackgroundView = backgroundView } else { @@ -267,38 +237,68 @@ public extension NSObject { } } - cell.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle + cell.overrideUserInterfaceStyle = css.getUserInterfaceStyle(for: cell) // collection.interfaceStyle.userInterfaceStyle } - if let cell = self as? UICollectionViewListCell { - var backgroundConfig = cell.backgroundConfiguration - backgroundConfig?.backgroundColor = collection.tableRowColors.backgroundColor - cell.backgroundConfiguration = backgroundConfig + if let cell = self as? UICollectionViewCell { + var updateColors = true - cell.tintColor = collection.lightBrandColor + if let listCell = cell as? ThemeableCollectionViewListCell { + updateColors = listCell.updateColors + } - cell.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle - } + if updateColors { + var stateSelectors: [ThemeCSSSelector] = [] + + if let cellState { + if cellState.isHighlighted { stateSelectors.append(.highlighted) } + if cellState.isSelected { stateSelectors.append(.selected) } + } + + if let fillColor = css.getColor(.fill, selectors: stateSelectors, for: cell) { + var backgroundConfig = (cellState != nil) ? cell.backgroundConfiguration?.updated(for: cellState!) : cell.backgroundConfiguration + backgroundConfig?.backgroundColor = fillColor + cell.backgroundConfiguration = backgroundConfig + } - if let progressView = self as? UIProgressView { - progressView.tintColor = collection.tintColor - progressView.trackTintColor = collection.tableSeparatorColor + if var cellListConfiguration = cell.contentConfiguration as? UIListContentConfiguration { + if let textColor = css.getColor(.stroke, selectors: stateSelectors, for: cell) { + cellListConfiguration.textProperties.color = textColor + } + + var iconSelectors = stateSelectors + iconSelectors.append(.icon) + + if let iconColor = css.getColor(.stroke, selectors: iconSelectors, for: cell) { + cellListConfiguration.imageProperties.tintColor = iconColor + } + cell.contentConfiguration = cellListConfiguration + } + + cell.tintColor = css.getColor(.stroke, selectors: stateSelectors, for: cell) + + cell.overrideUserInterfaceStyle = (css.get(.style, for: cell)?.value as? UIUserInterfaceStyle) ?? .unspecified + } } if let segmentedControl = self as? UISegmentedControl { - var tintColor = collection.tintColor + if let textColor = css.getColor(.stroke, for: segmentedControl) { + segmentedControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor : textColor], for: .normal) + } + + let tintColor = css.getColor(.fill, for: segmentedControl) + segmentedControl.tintColor = tintColor - if let navigationTintColor = collection.navigationBarColors.tintColor { - tintColor = navigationTintColor + if let selectedTextColor = css.getColor(.stroke, state: [.selected], for: segmentedControl) { + segmentedControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor : selectedTextColor], for: .selected) } - segmentedControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor : tintColor], for: .normal) - segmentedControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor : collection.navigationBarColors.backgroundColor!], for: .selected) - segmentedControl.selectedSegmentTintColor = tintColor + let selectedTintColor = css.getColor(.fill, state: [.selected], for: segmentedControl) + segmentedControl.selectedSegmentTintColor = selectedTintColor } if let visualEffectView = self as? UIVisualEffectView { - visualEffectView.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle + visualEffectView.overrideUserInterfaceStyle = collection.css.getUserInterfaceStyle() } } } diff --git a/ownCloudAppShared/User Interface/Theme/TVG/TVGImage.swift b/ownCloudAppShared/User Interface/Theme/TVG/TVGImage.swift index eec75c83b..fc063f54c 100644 --- a/ownCloudAppShared/User Interface/Theme/TVG/TVGImage.swift +++ b/ownCloudAppShared/User Interface/Theme/TVG/TVGImage.swift @@ -26,6 +26,8 @@ public class TVGImage: NSObject { var bezierPathsByIdentifier : [String:[SVGBezierPath]] = [:] var bezierPathsBoundsByIdentifier : [String:CGRect] = [:] + var imageName: String? + public init?(with data: Data) { do { let tvgObject : Any = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) @@ -69,6 +71,8 @@ public class TVGImage: NSObject { } self.init(with: data) + + imageName = name } public func svgString(with variables: [String:String]? = nil) -> String? { diff --git a/ownCloudAppShared/User Interface/Theme/TVG/VectorImageView.swift b/ownCloudAppShared/User Interface/Theme/TVG/VectorImageView.swift index b5c6e1b35..178df63bd 100644 --- a/ownCloudAppShared/User Interface/Theme/TVG/VectorImageView.swift +++ b/ownCloudAppShared/User Interface/Theme/TVG/VectorImageView.swift @@ -28,6 +28,11 @@ public class VectorImageView: UIView, Themeable { set(newVectorImage) { _vectorImage = newVectorImage + if let imageName = _vectorImage?.imageName { + self.cssSelectors = [ .vectorImage, ThemeCSSSelector(rawValue: "\(imageName)VectorImage") ] + } else { + self.cssSelectors = [ .vectorImage ] + } self.updateLayerWithRasteredImage() } } @@ -81,3 +86,7 @@ public class VectorImageView: UIView, Themeable { updateLayerWithRasteredImage(viewBounds: self.bounds, themeCollection: collection) } } + +extension ThemeCSSSelector { + static let vectorImage = ThemeCSSSelector(rawValue: "vectorImage") +} diff --git a/ownCloudAppShared/User Interface/Theme/Theme.swift b/ownCloudAppShared/User Interface/Theme/Theme.swift index 272c58f25..bfae63e58 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 } @@ -215,13 +220,9 @@ public class Theme: NSObject { } // Globally change color values for UI elements - UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).keyboardAppearance = collection.keyboardAppearance - if VendorServices.shared.isBranded { - UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = .black - } else { - UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = collection.tintColor - } - UITextField.appearance().tintColor = collection.searchBarColors.tintColor + UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).keyboardAppearance = collection.css.getKeyboardAppearance(selectors: [.all], for: nil) + UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = collection.css.getColor(.stroke, selectors: [.alert], for: nil) + UITextField.appearance().tintColor = collection.css.getColor(.stroke, selectors: [.textField], for: nil) // searchBarColors.tintColor } } diff --git a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift index 693d4d829..715d70a2e 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift @@ -18,7 +18,64 @@ import UIKit -public class ThemeColorPair : NSObject { +// MARK: - Color Sets +private struct ThemeColorSet { + var labelColor: UIColor + var secondaryLabelColor: UIColor + + var iconColor: UIColor //!< Icons (non-interactive) + var tintColor: UIColor //!< User-interactive buttons, accessories + + var backgroundColor: UIColor + + static func from(backgroundColor: UIColor, tintColor: UIColor, for style: UIUserInterfaceStyle) -> ThemeColorSet { + let preferredtTraitCollection = UITraitCollection(userInterfaceStyle: (style == .light) ? .light : .dark) + let preferredPrimaryLabelColor = UIColor.label.resolvedColor(with: preferredtTraitCollection) + let preferredSecondaryLabelColor = UIColor.secondaryLabel.resolvedColor(with: preferredtTraitCollection) + + let alternateTraitCollection = UITraitCollection(userInterfaceStyle: (style == .light) ? .dark : .light) + let alternatePrimaryLabelColor = UIColor.label.resolvedColor(with: alternateTraitCollection) + let alternateSecondaryLabelColor = UIColor.secondaryLabel.resolvedColor(with: alternateTraitCollection) + + let labelColor = backgroundColor.preferredContrastColor(from: [preferredPrimaryLabelColor, alternatePrimaryLabelColor]) ?? preferredPrimaryLabelColor + let secondaryLabelColor = backgroundColor.preferredContrastColor(from: [preferredSecondaryLabelColor, alternateSecondaryLabelColor]) ?? preferredSecondaryLabelColor + let iconColor = labelColor + + return ThemeColorSet(labelColor: labelColor, secondaryLabelColor: secondaryLabelColor, iconColor: iconColor, tintColor: tintColor, backgroundColor: backgroundColor) + } + + func disabledSet(for style: UIUserInterfaceStyle) -> ThemeColorSet { + return ThemeColorSet(labelColor: secondaryLabelColor.greyscale, secondaryLabelColor: secondaryLabelColor.greyscale, iconColor: secondaryLabelColor.greyscale, tintColor: tintColor.greyscale, backgroundColor: backgroundColor.greyscale) + } + + func highlightedSet(for style: UIUserInterfaceStyle) -> ThemeColorSet { + var highlightedColorSet = self + + switch style { + case .light: highlightedColorSet.backgroundColor = highlightedColorSet.backgroundColor.darker(0.10) + case .dark: highlightedColorSet.backgroundColor = highlightedColorSet.backgroundColor.lighter(0.15) + default: break + } + + return highlightedColorSet + } +} + +private struct ThemeColorStateSet { + var regular: ThemeColorSet + + var selected: ThemeColorSet + var highlighted: ThemeColorSet + var disabled: ThemeColorSet + + static func from(colorSet: ThemeColorSet, for style: UIUserInterfaceStyle) -> ThemeColorStateSet { + let highlightedSet = colorSet.highlightedSet(for: style) + + return ThemeColorStateSet(regular: colorSet, selected: highlightedSet, highlighted: highlightedSet, disabled: colorSet.disabledSet(for: style)) + } +} + +private class ThemeColorPair : NSObject { @objc public var foreground: UIColor @objc public var background: UIColor @@ -28,7 +85,7 @@ public class ThemeColorPair : NSObject { } } -public class ThemeColorPairCollection : NSObject { +private class ThemeColorPairCollection : NSObject { @objc public var normal : ThemeColorPair @objc public var highlighted : ThemeColorPair @objc public var disabled : ThemeColorPair @@ -46,7 +103,7 @@ public class ThemeColorPairCollection : NSObject { } } -public class ThemeColorCollection : NSObject { +private class ThemeColorCollection : NSObject { @objc public var backgroundColor : UIColor? @objc public var labelColor : UIColor @objc public var secondaryLabelColor : UIColor @@ -68,27 +125,18 @@ public class ThemeColorCollection : NSObject { public enum ThemeCollectionStyle : String, CaseIterable { case dark case light - case contrast public var name : String { switch self { case .dark: return "Dark".localized case .light: return "Light".localized - case .contrast: return "Contrast".localized } } -} - -public enum ThemeCollectionInterfaceStyle : String, CaseIterable { - case dark - case light - case unspecified public var userInterfaceStyle : UIUserInterfaceStyle { switch self { case .dark: return .dark case .light: return .light - case .unspecified: return .unspecified } } } @@ -96,71 +144,8 @@ public enum ThemeCollectionInterfaceStyle : String, CaseIterable { public class ThemeCollection : NSObject { @objc var identifier : String = UUID().uuidString - // MARK: - Interface style - public var interfaceStyle : ThemeCollectionInterfaceStyle - public var keyboardAppearance : UIKeyboardAppearance - public var backgroundBlurEffectStyle : UIBlurEffect.Style - - // MARK: - Brand colors - @objc public var darkBrandColor: UIColor - @objc public var lightBrandColor: UIColor - - // MARK: - Brand color collection - @objc public var darkBrandColors : ThemeColorCollection - @objc public var lightBrandColors : ThemeColorCollection - - // MARK: - Button / Fill color collections - @objc public var approvalColors : ThemeColorPairCollection - @objc public var neutralColors : ThemeColorPairCollection - @objc public var destructiveColors : ThemeColorPairCollection - - @objc public var purchaseColors : ThemeColorPairCollection - - // MARK: - Label colors - @objc public var informativeColor: UIColor - @objc public var successColor: UIColor - @objc public var warningColor: UIColor - @objc public var errorColor: UIColor - - @objc public var tintColor : UIColor - - // MARK: - Table views - @objc public var tableBackgroundColor : UIColor - @objc public var tableGroupBackgroundColor : UIColor - @objc public var tableSectionHeaderColor : UIColor? - @objc public var tableSectionFooterColor : UIColor? - @objc public var tableSeparatorColor : UIColor? - @objc public var tableRowColors : ThemeColorCollection - @objc public var tableRowHighlightColors : ThemeColorCollection - @objc public var tableRowBorderColor : UIColor? - - // MARK: - Bars - @objc public var navigationBarColors : ThemeColorCollection - @objc public var toolbarColors : ThemeColorCollection - @objc public var statusBarStyle : UIStatusBarStyle - @objc public var loginStatusBarStyle : UIStatusBarStyle - @objc public var barStyle : UIBarStyle - - // MARK: - SearchBar - @objc public var searchBarColors : ThemeColorCollection - - // MARK: - Progress - @objc public var progressColors : ThemeColorPair - - // MARK: - Activity View - @objc public var activityIndicatorViewStyle : UIActivityIndicatorView.Style - @objc public var searchBarActivityIndicatorViewStyle : UIActivityIndicatorView.Style - - // MARK: - Icon colors - @objc public var iconColors : [String:String] - - // MARK: - Login colors - @objc public var loginColors : ThemeColorCollection - @objc public var informalColors : ThemeColorCollection - @objc public var cancelColors : ThemeColorCollection - - @objc public var favoriteEnabledColor : UIColor? - @objc public var favoriteDisabledColor : UIColor? + // MARK: - ThemeCSS + public var css: ThemeCSS // MARK: - Default Collection static public var defaultCollection : ThemeCollection = { @@ -181,434 +166,606 @@ public class ThemeCollection : NSObject { return (collection) }() + private static func generateColorPairs(with baseSelectors:[ThemeCSSSelector], foregroundColor: UIColor, backgroundColor: UIColor) -> [ThemeCSSRecord] { + var disabledSelectors = baseSelectors + disabledSelectors.append(.disabled) + var highlightedSelectors = baseSelectors + highlightedSelectors.append(.highlighted) + + let colorPairs: [ThemeCSSRecord] = [ + ThemeCSSRecord(selectors: baseSelectors, property: .stroke, value: foregroundColor), + ThemeCSSRecord(selectors: baseSelectors, property: .fill, value: backgroundColor), + + ThemeCSSRecord(selectors: highlightedSelectors, property: .stroke, value: foregroundColor), + ThemeCSSRecord(selectors: highlightedSelectors, property: .fill, value: backgroundColor.lighter(0.25)), + + ThemeCSSRecord(selectors: disabledSelectors, property: .stroke, value: foregroundColor), + ThemeCSSRecord(selectors: disabledSelectors, property: .fill, value: backgroundColor.lighter(0.25)) + ] + + return colorPairs + } + + private static func generateColorPairs(with baseSelectors:[ThemeCSSSelector], from pairCollection: ThemeColorPairCollection) -> [ThemeCSSRecord] { + var disabledSelectors = baseSelectors + disabledSelectors.append(.disabled) + var highlightedSelectors = baseSelectors + highlightedSelectors.append(.highlighted) + + let colorPairs: [ThemeCSSRecord] = [ + ThemeCSSRecord(selectors: baseSelectors, property: .stroke, value: pairCollection.normal.foreground), + ThemeCSSRecord(selectors: baseSelectors, property: .fill, value: pairCollection.normal.background), + + ThemeCSSRecord(selectors: highlightedSelectors, property: .stroke, value: pairCollection.highlighted.foreground), + ThemeCSSRecord(selectors: highlightedSelectors, property: .fill, value: pairCollection.highlighted.background), + + ThemeCSSRecord(selectors: disabledSelectors, property: .stroke, value: pairCollection.disabled.foreground), + ThemeCSSRecord(selectors: disabledSelectors, property: .fill, value: pairCollection.disabled.background) + ] + + return colorPairs + } + init(darkBrandColor darkColor: UIColor, lightBrandColor lightColor: UIColor, style: ThemeCollectionStyle = .dark, customColors: NSDictionary? = nil, genericColors: NSDictionary? = nil, interfaceStyles: NSDictionary? = nil) { var logoFillColor : UIColor? - self.interfaceStyle = .unspecified - self.keyboardAppearance = .default - self.backgroundBlurEffectStyle = .regular + self.css = ThemeCSS() - self.darkBrandColor = darkColor - self.lightBrandColor = lightColor + var interfaceStyle : UIUserInterfaceStyle = .unspecified + var keyboardAppearance : UIKeyboardAppearance = .default + var backgroundBlurEffectStyle : UIBlurEffect.Style = .regular - let colors = ThemeColorValueResolver(colorValues: customColors, genericValues: genericColors, themeCollectionStyle: style) - let styleResolver = ThemeStyleValueResolver(styleValues: interfaceStyles) + var statusBarStyle : UIStatusBarStyle + var barStyle : UIBarStyle - self.darkBrandColors = colors.resolveThemeColorCollection("darkBrandColors", ThemeColorCollection( - backgroundColor: darkColor, - tintColor: lightColor, - labelColor: UIColor.white, - secondaryLabelColor: UIColor.lightGray, - symbolColor: UIColor.white, - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: darkColor)) - )) + let darkBrandColor = darkColor + let lightBrandColor = lightColor - self.lightBrandColors = colors.resolveThemeColorCollection("lightBrandColors", ThemeColorCollection( - backgroundColor: lightColor, - tintColor: UIColor.white, - labelColor: UIColor.white, - secondaryLabelColor: UIColor.lightGray, - symbolColor: UIColor.white, - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) - )) + /* + Cells: + - cell -> lightCell + - groupedCell -> lightGroupedCell + - sidebarCell -> darkCell + - accountCell -> darkGroupedCell + + Bars: + - topBar + - sidebarTopBar + + - bottomBar + - sidebarBottomBar + + Buttons: + - plain + - bordered + - confirm + - .. + */ + var lightBrandSet = ThemeColorSet.from(backgroundColor: lightColor, tintColor: darkColor, for: style.userInterfaceStyle) + let darkBrandSet = ThemeColorSet.from(backgroundColor: darkColor, tintColor: lightColor, for: style.userInterfaceStyle) - self.informativeColor = colors.resolveColor("Label.informativeColor", UIColor.darkGray) - self.successColor = colors.resolveColor("Label.successColor", UIColor(hex: 0x27AE60)) - self.warningColor = colors.resolveColor("Label.warningColor", UIColor(hex: 0xF2994A)) - self.errorColor = colors.resolveColor("Label.errorColor", UIColor(hex: 0xEB5757)) + let styleTraitCollection = UITraitCollection(userInterfaceStyle: interfaceStyle) - self.approvalColors = colors.resolveThemeColorPairCollection("Fill.approvalColors", ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: UIColor(hex: 0x1AC763)))) - self.neutralColors = colors.resolveThemeColorPairCollection("Fill.neutralColors", lightBrandColors.filledColorPairCollection) - 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))) + var cellSet: ThemeColorSet + var groupedCellSet: ThemeColorSet + var collectionBackgroundColor: UIColor + var groupedCollectionBackgroundColor: UIColor + var accountCellSet: ThemeColorSet + var sidebarAccountCellSet: ThemeColorSet - self.tintColor = colors.resolveColor("tintColor", self.lightBrandColor) + var navigationBarSet: ThemeColorSet + var toolbarSet: ThemeColorSet + var contentNavigationBarSet: ThemeColorSet + var contentToolbarSet: ThemeColorSet - // Table view - self.tableBackgroundColor = colors.resolveColor("Table.tableBackgroundColor", UIColor.white) + var cellStateSet: ThemeColorStateSet + var groupedCellStateSet: ThemeColorStateSet + var sidebarCellStateSet: ThemeColorStateSet - self.tableGroupBackgroundColor = colors.resolveColor("Table.tableGroupBackgroundColor", UIColor.systemGroupedBackground.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light))) - let color = colors.resolveColor("Table.tableSeparatorColor", UIColor.separator) - self.tableSeparatorColor = color - self.tableSectionHeaderColor = UIColor.gray - self.tableSectionFooterColor = UIColor.gray + var sidebarLogoIconColor: UIColor + var sidebarLogoLabel: UIColor + var iconSymbolColor: UIColor - let rowColor : UIColor? = UIColor.black.withAlphaComponent(0.1) - self.tableRowBorderColor = colors.resolveColor("Table.tableRowBorderColor", rowColor) + var inlineActionBackgroundColor: UIColor + var inlineActionBackgroundColorHighlighted: UIColor - var defaultTableRowLabelColor = darkColor - if VendorServices.shared.isBranded { - defaultTableRowLabelColor = UIColor(hex: 0x000000) - } + let tintColor: UIColor = lightBrandColor - self.tableRowColors = colors.resolveThemeColorCollection("Table.tableRowColors", ThemeColorCollection( - backgroundColor: tableBackgroundColor, - tintColor: nil, - labelColor: defaultTableRowLabelColor, - secondaryLabelColor: UIColor(hex: 0x475770), - symbolColor: UIColor(hex: 0x475770), - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) - )) - - self.tableRowHighlightColors = colors.resolveThemeColorCollection("Table.tableRowHighlightColors", ThemeColorCollection( - backgroundColor: UIColor.white.darker(0.1), - tintColor: nil, - labelColor: darkColor, - secondaryLabelColor: UIColor(hex: 0x475770), - symbolColor: UIColor(hex: 0x475770), - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) - )) + var separatorColor: UIColor = UIColor.opaqueSeparator.resolvedColor(with: styleTraitCollection) + var sectionHeaderColor: UIColor + var sectionFooterColor: UIColor + var groupedSectionHeaderColor: UIColor + var groupedSectionFooterColor: UIColor - self.favoriteEnabledColor = UIColor(hex: 0xFFCC00) - self.favoriteDisabledColor = UIColor(hex: 0x7C7C7C) + var moreHeaderBackgroundColor: UIColor - // Styles - switch style { - case .dark: - // Interface style - self.interfaceStyle = styleResolver.resolveInterfaceStyle(fallback: .dark) - self.keyboardAppearance = styleResolver.resolveKeyboardStyle(fallback: .dark) - self.backgroundBlurEffectStyle = styleResolver.resolveBlurEffectStyle(fallback: .dark) + var modalBackgroundColor: UIColor - // Bars - self.navigationBarColors = colors.resolveThemeColorCollection("NavigationBar", self.darkBrandColors) - self.toolbarColors = colors.resolveThemeColorCollection("Toolbar", self.darkBrandColors) - self.searchBarColors = colors.resolveThemeColorCollection("Searchbar", self.darkBrandColors) - self.loginColors = colors.resolveThemeColorCollection("Login", self.darkBrandColors) - - // Table view - self.tableBackgroundColor = colors.resolveColor("Table.tableBackgroundColor", navigationBarColors.backgroundColor!.darker(0.1)) - self.tableGroupBackgroundColor = colors.resolveColor("Table.tableGroupBackgroundColor", navigationBarColors.backgroundColor!.darker(0.3)) - let separatorColor : UIColor? = UIColor.darkGray - self.tableSeparatorColor = colors.resolveColor("Table.tableSeparatorColor", separatorColor) - let rowBorderColor : UIColor? = UIColor.white.withAlphaComponent(0.1) - self.tableRowBorderColor = colors.resolveColor("Table.tableRowBorderColor", rowBorderColor) - self.tableRowColors = colors.resolveThemeColorCollection("Table.tableRowColors", ThemeColorCollection( - backgroundColor: tableBackgroundColor, - tintColor: navigationBarColors.tintColor, - labelColor: navigationBarColors.labelColor, - secondaryLabelColor: navigationBarColors.secondaryLabelColor, - symbolColor: lightColor, - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) - )) - - self.tableRowHighlightColors = colors.resolveThemeColorCollection("Table.tableRowHighlightColors", ThemeColorCollection( - backgroundColor: lightColor.darker(0.2), - tintColor: UIColor.white, - labelColor: UIColor.white, - secondaryLabelColor: UIColor.white, - symbolColor: darkColor, - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) - )) - - // Bar styles - self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: .lightContent) - self.loginStatusBarStyle = styleResolver.resolveStatusBarStyle(for: "loginStatusBarStyle", fallback: self.statusBarStyle) - self.barStyle = styleResolver.resolveBarStyle(fallback: .black) + let lightBrandColors = ThemeColorCollection( + backgroundColor: lightColor, + tintColor: UIColor.white, + labelColor: UIColor.white, + secondaryLabelColor: UIColor.lightGray, + symbolColor: UIColor.white, + filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) + ) - // Progress - self.progressColors = colors.resolveThemeColorPair("Progress", ThemeColorPair(foreground: self.lightBrandColor, background: self.lightBrandColor.withAlphaComponent(0.3))) + let neutralColors = lightBrandColors.filledColorPairCollection + let purchaseColors = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: lightBrandColors.labelColor, background: lightBrandColor)) + purchaseColors.disabled.background = purchaseColors.disabled.background.greyscale - // Activity - self.activityIndicatorViewStyle = styleResolver.resolveActivityIndicatorViewStyle(for: "activityIndicatorViewStyle", fallback: .medium) - self.searchBarActivityIndicatorViewStyle = styleResolver.resolveActivityIndicatorViewStyle(for: "searchBarActivityIndicatorViewStyle", fallback: .medium) + var tokenForegroundColor = lightBrandColor + var tokenBackgroundColor = UIColor(white: 0, alpha: 0.1) - // Logo fill color - let logoColor : UIColor? = UIColor.white - logoFillColor = colors.resolveColor("Icon.logoFillColor", logoColor) + // Table view + var progressColors : ThemeColorPair - case .light: + // Styles + switch style { + case .dark: // Interface style - self.interfaceStyle = styleResolver.resolveInterfaceStyle(fallback: .light) - self.keyboardAppearance = styleResolver.resolveKeyboardStyle(fallback: .light) - self.backgroundBlurEffectStyle = styleResolver.resolveBlurEffectStyle(fallback: .light) + interfaceStyle = .dark + keyboardAppearance = .dark + backgroundBlurEffectStyle = .dark + statusBarStyle = .lightContent + barStyle = .black - // Bars - self.navigationBarColors = colors.resolveThemeColorCollection("NavigationBar", ThemeColorCollection( - backgroundColor: UIColor.white.darker(0.05), - tintColor: nil, - labelColor: darkColor, - secondaryLabelColor: UIColor.gray, - symbolColor: darkColor, - filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) - )) - - self.toolbarColors = colors.resolveThemeColorCollection("Toolbar", self.navigationBarColors) - self.searchBarColors = colors.resolveThemeColorCollection("Searchbar", self.navigationBarColors) - self.loginColors = colors.resolveThemeColorCollection("Login", self.darkBrandColors) - - // Bar styles - self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: .darkContent) - self.loginStatusBarStyle = styleResolver.resolveStatusBarStyle(for: "loginStatusBarStyle", fallback: self.statusBarStyle) - self.barStyle = styleResolver.resolveBarStyle(fallback: .default) + lightBrandSet.labelColor = .white + lightBrandSet.secondaryLabelColor = .white + lightBrandSet.iconColor = .white - // Progress - self.progressColors = colors.resolveThemeColorPair("Progress", ThemeColorPair(foreground: self.lightBrandColor, background: UIColor.lightGray.withAlphaComponent(0.3))) + accountCellSet = lightBrandSet + sidebarAccountCellSet = ThemeColorSet.from(backgroundColor: .init(hex: 0, alpha: 0.5), tintColor: lightBrandColor, for: interfaceStyle) + accountCellSet = sidebarAccountCellSet - // Activity - self.activityIndicatorViewStyle = styleResolver.resolveActivityIndicatorViewStyle(for: "activityIndicatorViewStyle", fallback: .medium) - self.searchBarActivityIndicatorViewStyle = styleResolver.resolveActivityIndicatorViewStyle(for: "searchBarActivityIndicatorViewStyle", fallback: .medium) + navigationBarSet = darkBrandSet + toolbarSet = darkBrandSet - // Logo fill color - let logoColor : UIColor? = UIColor.lightGray - logoFillColor = colors.resolveColor("Icon.logoFillColor", logoColor) + cellSet = ThemeColorSet.from(backgroundColor: UIColor(hex: 0), tintColor: lightColor, for: interfaceStyle) + cellStateSet = ThemeColorStateSet.from(colorSet: cellSet, for: interfaceStyle) + collectionBackgroundColor = darkColor.darker(0.1) - case .contrast: - // Interface style - self.interfaceStyle = styleResolver.resolveInterfaceStyle(fallback: .light) - self.keyboardAppearance = styleResolver.resolveKeyboardStyle(fallback: .light) - self.backgroundBlurEffectStyle = styleResolver.resolveBlurEffectStyle(fallback: .light) + groupedCellSet = ThemeColorSet.from(backgroundColor: darkColor, tintColor: lightColor, for: interfaceStyle) + groupedCellStateSet = ThemeColorStateSet.from(colorSet: groupedCellSet, for: interfaceStyle) + groupedCollectionBackgroundColor = navigationBarSet.backgroundColor.darker(0.3) - // Bars - self.navigationBarColors = colors.resolveThemeColorCollection("NavigationBar", self.darkBrandColors) - let tmpDarkBrandColors = self.darkBrandColors - - if VendorServices.shared.isBranded { - tmpDarkBrandColors.secondaryLabelColor = UIColor(hex: 0xF7F7F7) - } - if self.tintColor == UIColor(hex: 0xFFFFFF) { - tmpDarkBrandColors.secondaryLabelColor = .lightGray - } - self.toolbarColors = colors.resolveThemeColorCollection("Toolbar", tmpDarkBrandColors) - - let defaultSearchBarColor = self.darkBrandColors - if VendorServices.shared.isBranded { - defaultSearchBarColor.labelColor = UIColor(hex: 0x000000) - defaultSearchBarColor.secondaryLabelColor = UIColor.gray - defaultSearchBarColor.backgroundColor = UIColor(hex: 0xF7F7F7) - self.tableRowColors.symbolColor = darkColor - self.tableRowHighlightColors.symbolColor = darkColor - } - - self.searchBarColors = colors.resolveThemeColorCollection("Searchbar", defaultSearchBarColor) - - self.loginColors = colors.resolveThemeColorCollection("Login", self.darkBrandColors) - - // Bar styles - self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: .lightContent) - self.loginStatusBarStyle = styleResolver.resolveStatusBarStyle(for: "loginStatusBarStyle", fallback: self.statusBarStyle) - self.barStyle = styleResolver.resolveBarStyle(fallback: .black) + contentNavigationBarSet = cellSet + contentToolbarSet = cellSet - // Progress - self.progressColors = colors.resolveThemeColorPair("Progress", ThemeColorPair(foreground: self.lightBrandColor, background: UIColor.lightGray.withAlphaComponent(0.3))) + sidebarCellStateSet = ThemeColorStateSet.from(colorSet: darkBrandSet, for: interfaceStyle) + sidebarCellStateSet.selected.backgroundColor = sidebarCellStateSet.regular.labelColor + sidebarCellStateSet.selected.labelColor = sidebarCellStateSet.regular.backgroundColor + sidebarCellStateSet.selected.iconColor = sidebarCellStateSet.regular.backgroundColor - // Activity - self.activityIndicatorViewStyle = styleResolver.resolveActivityIndicatorViewStyle(for: "activityIndicatorViewStyle", fallback: .medium) - self.searchBarActivityIndicatorViewStyle = styleResolver.resolveActivityIndicatorViewStyle(for: "searchBarActivityIndicatorViewStyle", fallback: .medium) + sidebarLogoIconColor = .white + sidebarLogoLabel = .white + iconSymbolColor = lightColor - // Logo fill color - logoFillColor = UIColor.lightGray + separatorColor = .darkGray - if lightBrandColor.isEqual(UIColor(hex: 0xFFFFFF)) { - self.neutralColors.normal.background = self.darkBrandColor - self.lightBrandColors.filledColorPairCollection.normal.background = self.darkBrandColor - } - } + sectionHeaderColor = .white + sectionFooterColor = .lightGray + groupedSectionHeaderColor = .lightGray + groupedSectionFooterColor = .lightGray - self.informalColors = colors.resolveThemeColorCollection("Informal", self.lightBrandColors) - self.cancelColors = colors.resolveThemeColorCollection("Cancel", self.lightBrandColors) + moreHeaderBackgroundColor = darkColor.lighter(0.05) - let iconSymbolColor = self.tableRowColors.symbolColor + modalBackgroundColor = darkBrandColor - self.iconColors = [ - "folderFillColor" : colors.resolveColor("Icon.folderFillColor", iconSymbolColor).hexString(), - "fileFillColor" : colors.resolveColor("Icon.fileFillColor", iconSymbolColor).hexString(), - "logoFillColor" : colors.resolveColor("Icon.logoFillColor", logoFillColor)?.hexString() ?? "#ffffff", - "iconFillColor" : colors.resolveColor("Icon.iconFillColor", tableRowColors.tintColor)?.hexString() ?? iconSymbolColor.hexString(), - "symbolFillColor" : colors.resolveColor("Icon.symbolFillColor", iconSymbolColor).hexString() - ] - } + inlineActionBackgroundColor = UIColor(white: 1, alpha: 0.10) + inlineActionBackgroundColorHighlighted = UIColor(white: 1, alpha: 0.05) - convenience override init() { - self.init(darkBrandColor: UIColor(hex: 0x1D293B), lightBrandColor: UIColor(hex: 0x468CC8)) - } -} + // Bars + tokenForegroundColor = lightBrandColor + tokenBackgroundColor = UIColor(white: 1, alpha: 0.1) -class ThemeStyleValueResolver : NSObject { + // Progress + progressColors = ThemeColorPair(foreground: lightBrandColor, background: lightBrandColor.withAlphaComponent(0.3)) - var styles: NSDictionary? + // Logo fill color + logoFillColor = .white - init(styleValues : NSDictionary?) { - styles = styleValues - } + case .light: + // Interface style + interfaceStyle = .light + keyboardAppearance = .light + backgroundBlurEffectStyle = .light + statusBarStyle = .darkContent + barStyle = .default - func resolveStatusBarStyle(for key: String, fallback: UIStatusBarStyle) -> UIStatusBarStyle { - if let styleValue = styles?.value(forKeyPath: key) as? String { - switch styleValue { - case "default": - return .default - case "lightContent": - return .lightContent - case "darkContent": - return .darkContent - default: - return fallback - } - } - return fallback - } + sidebarAccountCellSet = ThemeColorSet.from(backgroundColor: .white, tintColor: .white, for: interfaceStyle) + accountCellSet = sidebarAccountCellSet - func resolveBarStyle(fallback: UIBarStyle) -> UIBarStyle { - if let styleValue = styles?.value(forKeyPath: "barStyle") as? String { - switch styleValue { - case "default": - return .default - case "black": - return .black - default: - return fallback - } - } - return fallback - } + navigationBarSet = ThemeColorSet.from(backgroundColor: .systemBackground.resolvedColor(with: styleTraitCollection), tintColor: lightColor, for: interfaceStyle) + toolbarSet = navigationBarSet - func resolveActivityIndicatorViewStyle(for key: String, fallback: UIActivityIndicatorView.Style) -> UIActivityIndicatorView.Style { - if let styleValue = styles?.value(forKeyPath: key) as? String { - switch styleValue { - case "medium", "white", "gray": - return .medium - case "whiteLarge", "large": - return .large - default: - return fallback - } - } - return fallback - } + cellSet = ThemeColorSet.from(backgroundColor: .systemBackground.resolvedColor(with: styleTraitCollection), tintColor: lightColor, for: interfaceStyle) + cellStateSet = ThemeColorStateSet.from(colorSet: cellSet, for: interfaceStyle) + collectionBackgroundColor = cellSet.backgroundColor - func resolveKeyboardStyle(fallback: UIKeyboardAppearance) -> UIKeyboardAppearance { - if let styleValue = styles?.value(forKeyPath: "keyboardAppearance") as? String { - switch styleValue { - case "default": - return .default - case "light": - return .light - case "dark": - return .dark - default: - return fallback - } - } - return fallback - } + groupedCellSet = cellSet + groupedCellStateSet = ThemeColorStateSet.from(colorSet: groupedCellSet, for: interfaceStyle) + groupedCollectionBackgroundColor = .systemGroupedBackground.resolvedColor(with: styleTraitCollection) - func resolveBlurEffectStyle(fallback: UIBlurEffect.Style) -> UIBlurEffect.Style { - if let styleValue = styles?.value(forKeyPath: "backgroundBlurEffectStyle") as? String { - switch styleValue { - case "regular": - return .regular - case "light": - return .light - case "dark": - return .dark - default: - return fallback - } - } - return fallback - } + contentNavigationBarSet = cellSet + contentToolbarSet = cellSet - func resolveInterfaceStyle(fallback: ThemeCollectionInterfaceStyle) -> ThemeCollectionInterfaceStyle { - if let styleValue = styles?.value(forKeyPath: "interfaceStyle") as? String { - switch styleValue { - case "unspecified": - return .unspecified - case "light": - return .light - case "dark": - return .dark - default: - return fallback - } - } - return fallback - } + sidebarCellStateSet = ThemeColorStateSet.from(colorSet: lightBrandSet, for: .light) + sidebarCellStateSet.regular.backgroundColor = .secondarySystemBackground.resolvedColor(with: styleTraitCollection) + sidebarCellStateSet.selected.labelColor = .white + sidebarCellStateSet.selected.iconColor = .white + sidebarCellStateSet.selected.backgroundColor = darkBrandColor -} + sidebarLogoIconColor = darkBrandColor + sidebarLogoLabel = darkBrandColor + iconSymbolColor = darkColor -class ThemeColorValueResolver : NSObject { + sectionHeaderColor = .label.resolvedColor(with: styleTraitCollection) + sectionFooterColor = .secondaryLabel.resolvedColor(with: styleTraitCollection) + groupedSectionHeaderColor = .secondaryLabel.resolvedColor(with: styleTraitCollection) + groupedSectionFooterColor = .secondaryLabel.resolvedColor(with: styleTraitCollection) - var generic: NSDictionary? - var colors: NSDictionary? + moreHeaderBackgroundColor = cellSet.backgroundColor - var themeCollectionStyle : ThemeCollectionStyle? + modalBackgroundColor = collectionBackgroundColor - init(colorValues : NSDictionary?, genericValues: NSDictionary?, themeCollectionStyle: ThemeCollectionStyle? = nil) { - colors = colorValues - generic = genericValues - self.themeCollectionStyle = themeCollectionStyle - } + inlineActionBackgroundColor = UIColor(white: 0, alpha: 0.05) + inlineActionBackgroundColorHighlighted = UIColor(white: 0, alpha: 0.10) - func resolveColor(_ forKeyPath: String, _ fallback : UIColor) -> UIColor { - if let rawColor = colors?.value(forKeyPath: forKeyPath) as? String { - if rawColor.contains("."), let genericRawColor = generic?.value(forKeyPath: rawColor) as? String, let decodedHexColor = genericRawColor.colorFromHex { - return decodedHexColor - } else if let decodedHexColor = rawColor.colorFromHex { - return decodedHexColor - } - } - return fallback - } + // Progress + progressColors = ThemeColorPair(foreground: lightBrandColor, background: UIColor.lightGray.withAlphaComponent(0.3)) - func resolveColor(_ forKeyPath: String, _ fallback : UIColor? = nil) -> UIColor? { - if let rawColor = colors?.value(forKeyPath: forKeyPath) as? String { - if rawColor.contains("."), let genericRawColor = generic?.value(forKeyPath: rawColor) as? String, let decodedHexColor = genericRawColor.colorFromHex { - return decodedHexColor - } else if let decodedHexColor = rawColor.colorFromHex { - if forKeyPath.hasPrefix("NavigationBar") { - } - return decodedHexColor - } + // Logo fill color + logoFillColor = .lightGray } - return fallback - } - func resolveThemeColorPair(_ forKeyPath: String, _ colorPair : ThemeColorPair) -> ThemeColorPair { - let pair = ThemeColorPair(foreground: self.resolveColor(forKeyPath.appending(".foreground"), colorPair.foreground), - background: self.resolveColor(forKeyPath.appending(".background"), colorPair.background)) + // Fixed colors + let primaryLabelColor = cellSet.labelColor // UIColor.label.resolvedColor(with: styleTraitCollection) + let secondaryLabelColor = cellSet.secondaryLabelColor // UIColor.secondaryLabel.resolvedColor(with: styleTraitCollection) + let tertiaryLabelColor = cellSet.secondaryLabelColor // UIColor.tertiaryLabel.resolvedColor(with: styleTraitCollection) + let placeholderTextColor = cellSet.secondaryLabelColor // UIColor.placeholderText.resolvedColor(with: styleTraitCollection) - return pair + let primaryBackgroundColor = UIColor.systemBackground.resolvedColor(with: styleTraitCollection) + let secondaryBackgroundColor = UIColor.secondarySystemBackground.resolvedColor(with: styleTraitCollection) + let tertiaryBackgroundColor = UIColor.tertiarySystemBackground.resolvedColor(with: styleTraitCollection) + + let progressForegroundColor = progressColors.foreground + let progressBackgroundColor = progressColors.background + + let favoriteEnabledColor = UIColor(hex: 0xFFCC00) + let favoriteDisabledColor = UIColor(hex: 0x7C7C7C) + + // CSS + css.add(records: [ + // Global styles + // - Interface Style + ThemeCSSRecord(selectors: [.all], property: .style, value: interfaceStyle), + + // - Blur Effect Style + ThemeCSSRecord(selectors: [.all], property: .blurEffectStyle, value: backgroundBlurEffectStyle), + + // - Status Bar + ThemeCSSRecord(selectors: [.all], property: .statusBarStyle, value: statusBarStyle), + + // - Bar + ThemeCSSRecord(selectors: [.all], property: .barStyle, value: barStyle), + + // - Activity Indicator + ThemeCSSRecord(selectors: [.all], property: .activityIndicatorStyle, value: UIActivityIndicatorView.Style.medium), + + // General + // - Seperator + ThemeCSSRecord(selectors: [.separator], property: .fill, value: separatorColor), + + // - Navigation Bar + ThemeCSSRecord(selectors: [.navigationBar], property: .stroke, value: navigationBarSet.tintColor), + ThemeCSSRecord(selectors: [.navigationBar, .label], property: .stroke, value: navigationBarSet.labelColor), + ThemeCSSRecord(selectors: [.navigationBar], property: .fill, value: navigationBarSet.backgroundColor), + + // - Toolbar + ThemeCSSRecord(selectors: [.toolbar], property: .stroke, value: toolbarSet.tintColor), + ThemeCSSRecord(selectors: [.toolbar], property: .fill, value: toolbarSet.backgroundColor), + + // - Progress + ThemeCSSRecord(selectors: [.progress], property: .fill, value: progressBackgroundColor), + ThemeCSSRecord(selectors: [.progress], property: .stroke,value: progressForegroundColor), + ThemeCSSRecord(selectors: [.progress, .button], property: .fill, value: tintColor), + + // - Cells + ThemeCSSRecord(selectors: [.cell, .sectionHeader], property: .stroke, value: sectionHeaderColor), + + // - Modal + ThemeCSSRecord(selectors: [.modal], property: .fill, value: modalBackgroundColor), + ThemeCSSRecord(selectors: [.modal, .issues, .table], property: .fill, value: modalBackgroundColor), + ThemeCSSRecord(selectors: [.modal, .issues, .table, .cell], property: .fill, value: modalBackgroundColor), + ThemeCSSRecord(selectors: [.modal], property: .stroke, value: cellSet.labelColor), + + // - Splitview + ThemeCSSRecord(selectors: [.splitView], property: .fill, value: cellSet.backgroundColor), + + // - Collection View + ThemeCSSRecord(selectors: [.collection], property: .fill, value: cellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.collection, .highlighted, .cell], property: .fill, value: cellStateSet.highlighted.backgroundColor), + ThemeCSSRecord(selectors: [.collection, .cell], property: .stroke, value: cellStateSet.regular.tintColor), + ThemeCSSRecord(selectors: [.collection, .cell,.title], property: .stroke, value: cellStateSet.regular.labelColor), + ThemeCSSRecord(selectors: [.collection, .cell,.segments], property: .stroke, value: cellStateSet.regular.secondaryLabelColor), + ThemeCSSRecord(selectors: [.collection, .cell,.segments], property: .fill, value: UIColor.clear), + ThemeCSSRecord(selectors: [.collection, .cell,.segments,.icon], property: .stroke, value: cellStateSet.regular.secondaryLabelColor), + ThemeCSSRecord(selectors: [.collection, .cell,.segments,.title],property: .stroke, value: cellStateSet.regular.secondaryLabelColor), + ThemeCSSRecord(selectors: [.collection, .sectionFooter], property: .stroke, value: sectionFooterColor), + ThemeCSSRecord(selectors: [.collection, .cell], property: .fill, value: cellStateSet.regular.backgroundColor), + + ThemeCSSRecord(selectors: [.collection, .selectionCheckmark], property: .fill, value: cellStateSet.regular.tintColor), + ThemeCSSRecord(selectors: [.collection, .selectionCheckmark], property: .stroke, value: cellStateSet.regular.backgroundColor), + + ThemeCSSRecord(selectors: [.collection, .selected, .selectionCheckmark], property: .stroke, value: UIColor.white), + + // - Table View + ThemeCSSRecord(selectors: [.table], property: .fill, value: cellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.grouped, .table], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.insetGrouped, .table], property: .fill, value: groupedCollectionBackgroundColor), + + ThemeCSSRecord(selectors: [.table, .cell], property: .stroke, value: cellStateSet.regular.tintColor), // tableRowColors.tintColor), + ThemeCSSRecord(selectors: [.table, .cell], property: .fill, value: cellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.table, .highlighted, .cell], property: .fill, value: cellStateSet.highlighted.backgroundColor), + + ThemeCSSRecord(selectors: [.grouped, .table, .cell], property: .stroke, value: groupedCellStateSet.regular.tintColor), // tableRowColors.tintColor), + ThemeCSSRecord(selectors: [.grouped, .table, .cell, .label], property: .stroke, value: groupedCellStateSet.regular.labelColor), + ThemeCSSRecord(selectors: [.grouped, .table, .cell], property: .fill, value: groupedCellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.grouped, .table, .highlighted, .cell], property: .fill, value: groupedCellStateSet.highlighted.backgroundColor), + + ThemeCSSRecord(selectors: [.grouped, .table, .sectionHeader], property: .stroke, value: groupedSectionHeaderColor), + ThemeCSSRecord(selectors: [.grouped, .table, .sectionFooter], property: .stroke, value: groupedSectionFooterColor), + + ThemeCSSRecord(selectors: [.insetGrouped, .table, .cell], property: .stroke, value: groupedCellStateSet.regular.tintColor), // tableRowColors.tintColor), + ThemeCSSRecord(selectors: [.insetGrouped, .table, .cell], property: .fill, value: groupedCellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.insetGrouped, .table, .highlighted, .cell], property: .fill, value: groupedCellStateSet.highlighted.backgroundColor), + + ThemeCSSRecord(selectors: [.table, .sectionHeader], property: .stroke, value: sectionHeaderColor), + ThemeCSSRecord(selectors: [.table, .sectionFooter], property: .stroke, value: sectionFooterColor), + + ThemeCSSRecord(selectors: [.table, .icon], property: .stroke, value: cellStateSet.regular.iconColor), + ThemeCSSRecord(selectors: [.table, .label, .primary], property: .stroke, value: cellStateSet.regular.labelColor), + ThemeCSSRecord(selectors: [.table, .label, .secondary], property: .stroke, value: cellStateSet.regular.secondaryLabelColor), + ThemeCSSRecord(selectors: [.table, .label, .highlighted, .primary], property: .stroke, value: cellStateSet.highlighted.labelColor), + ThemeCSSRecord(selectors: [.table, .label, .highlighted, .secondary], property: .stroke, value: cellStateSet.highlighted.secondaryLabelColor), + + // - Accessories + ThemeCSSRecord(selectors: [.accessory], property: .stroke, value: cellStateSet.regular.secondaryLabelColor), + ThemeCSSRecord(selectors: [.accessory, .accept], property: .stroke, value: UIColor.systemGreen), + ThemeCSSRecord(selectors: [.accessory, .decline], property: .stroke, value: UIColor.systemRed), + + // - Segment View + ThemeCSSRecord(selectors: [.segments], property: .fill, value: UIColor.clear), + ThemeCSSRecord(selectors: [.segments, .icon], property: .stroke, value: cellSet.iconColor), + ThemeCSSRecord(selectors: [.segments, .title], property: .stroke, value: cellSet.secondaryLabelColor), + + ThemeCSSRecord(selectors: [.segments, .token], property: .fill, value: tokenBackgroundColor), + ThemeCSSRecord(selectors: [.segments, .token, .icon], property: .stroke, value: tokenForegroundColor), + ThemeCSSRecord(selectors: [.segments, .token, .title], property: .stroke, value: tokenForegroundColor), + + ThemeCSSRecord(selectors: [.segments, .item, .separator], property: .fill, value: nil), + ThemeCSSRecord(selectors: [.segments, .item, .separator], property: .stroke, value: cellSet.secondaryLabelColor), + + // - Messages + ThemeCSSRecord(selectors: [.infoBox, .background], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.infoBox, .icon], property: .fill, value: secondaryLabelColor), + ThemeCSSRecord(selectors: [.infoBox, .subtitle], property: .fill, value: secondaryLabelColor), + + ThemeCSSRecord(selectors: [.title], property: .stroke, value: primaryLabelColor), + ThemeCSSRecord(selectors: [.subtitle], property: .stroke, value: secondaryLabelColor), + ThemeCSSRecord(selectors: [.message], property: .stroke, value: tertiaryLabelColor), + + ThemeCSSRecord(selectors: [.primary], property: .stroke, value: primaryLabelColor), + ThemeCSSRecord(selectors: [.secondary], property: .stroke, value: secondaryLabelColor), + ThemeCSSRecord(selectors: [.tertiary], property: .stroke, value: tertiaryLabelColor), + + // - Saved Search + ThemeCSSRecord(selectors: [.savedSearch], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.savedSearch, .cell], property: .fill, value: groupedCollectionBackgroundColor), + + // - Fills + ThemeCSSRecord(selectors: [.primary], property: .fill, value: primaryBackgroundColor), + ThemeCSSRecord(selectors: [.secondary], property: .fill, value: secondaryBackgroundColor), + ThemeCSSRecord(selectors: [.tertiary], property: .fill, value: tertiaryBackgroundColor), + + // - Text Field + ThemeCSSRecord(selectors: [.textField], property: .fill, value: collectionBackgroundColor), // Background color + ThemeCSSRecord(selectors: [.textField, .label], property: .stroke, value: cellSet.labelColor), // Text color + ThemeCSSRecord(selectors: [.textField, .disabled, .label], property: .stroke, value: cellSet.secondaryLabelColor), // Disabled text color + ThemeCSSRecord(selectors: [.textField, .placeholder], property: .stroke, value: placeholderTextColor), // Text field placeholder + + // - Search Field + ThemeCSSRecord(selectors: [.textField, .searchField], property: .stroke, value: lightBrandColor), // Search tint color (UI elements other than text) + ThemeCSSRecord(selectors: [.textField, .searchField, .label], property: .stroke, value: primaryLabelColor), // Search text color + + // - Slider + ThemeCSSRecord(selectors: [.slider], property: .stroke, value: lightBrandColor), + + // - Buttons + Popups + ThemeCSSRecord(selectors: [.button], property: .stroke, value: lightBrandColor), + ThemeCSSRecord(selectors: [.popupButton], property: .stroke, value: lightBrandColor), + + // - Label styles + ThemeCSSRecord(selectors: [.label, .destructive], property: .stroke, value: UIColor.red), + ThemeCSSRecord(selectors: [.label, .warning], property: .stroke, value: UIColor(hex: 0xF2994A)), + ThemeCSSRecord(selectors: [.label, .error], property: .stroke, value: UIColor(hex: 0xEB5757)), + ThemeCSSRecord(selectors: [.label, .success], property: .stroke, value: UIColor(hex: 0x27AE60)) + ]) + + // - Fill styles + css.add(records: ThemeCollection.generateColorPairs(with: [.destructive], foregroundColor: .white, backgroundColor: .red)) + css.add(records: ThemeCollection.generateColorPairs(with: [.confirm], foregroundColor: .white, backgroundColor: UIColor(hex: 0x1AC763))) + css.add(records: ThemeCollection.generateColorPairs(with: [.cancel], foregroundColor: .white, backgroundColor: lightBrandColor)) + css.add(records: ThemeCollection.generateColorPairs(with: [.proceed], foregroundColor: .white, backgroundColor: lightBrandColor)) + css.add(records: ThemeCollection.generateColorPairs(with: [.info], foregroundColor: .white, backgroundColor: lightBrandColor)) + css.add(records: ThemeCollection.generateColorPairs(with: [.warning], foregroundColor: .black, backgroundColor: .systemYellow)) + + css.add(records: ThemeCollection.generateColorPairs(with: [.purchase], from: purchaseColors)) + + // - Pass code fill style + css.add(records: ThemeCollection.generateColorPairs(with: [.digit], from: neutralColors)) + + css.add(records: [ + ThemeCSSRecord(selectors: [.passcode], property: .fill, value: collectionBackgroundColor), + ThemeCSSRecord(selectors: [.passcode, .title], property: .stroke, value: primaryLabelColor), + ThemeCSSRecord(selectors: [.passcode, .disabled, .title], property: .stroke, value: secondaryLabelColor), + ThemeCSSRecord(selectors: [.passcode, .code], property: .stroke, value: neutralColors.normal.background), + ThemeCSSRecord(selectors: [.passcode, .disabled, .code], property: .stroke, value: primaryLabelColor), + ThemeCSSRecord(selectors: [.passcode, .subtitle], property: .stroke, value: secondaryLabelColor), + ThemeCSSRecord(selectors: [.passcode, .disabled, .subtitle], property: .stroke, value: tertiaryLabelColor), + + // - Alert View Controller + ThemeCSSRecord(selectors: [.alert], property: .stroke, value: tintColor), + + // - Action / Drop target (plain) fill style + ThemeCSSRecord(selectors: [.action], property: .fill, value: inlineActionBackgroundColor), + ThemeCSSRecord(selectors: [.action, .highlighted], property: .fill, value: inlineActionBackgroundColorHighlighted), + + // - Drive Header + ThemeCSSRecord(selectors: [.header, .drive, .cover], property: .fill, value: lightBrandColor), + + // - Expandable Resource Cell + ThemeCSSRecord(selectors: [.expandable], property: .fill, value: collectionBackgroundColor), + ThemeCSSRecord(selectors: [.expandable, .button], property: .stroke, value: tintColor), + ThemeCSSRecord(selectors: [.expandable, .textView], property: .fill, value: collectionBackgroundColor), + ThemeCSSRecord(selectors: [.expandable, .textView], property: .stroke, value: secondaryLabelColor), + ThemeCSSRecord(selectors: [.expandable, .shadow], property: .fill, value: cellSet.labelColor), + + // - Location Bar + ThemeCSSRecord(selectors: [.locationBar], property: .fill, value: cellSet.backgroundColor), + + // - Keyboard + ThemeCSSRecord(selectors: [.all], property: .keyboardAppearance, value: keyboardAppearance), + + // - Account Cell + ThemeCSSRecord(selectors: [.account], property: .fill, value: accountCellSet.backgroundColor), + ThemeCSSRecord(selectors: [.account, .title], property: .stroke, value: accountCellSet.labelColor), + ThemeCSSRecord(selectors: [.account, .description], property: .stroke, value: accountCellSet.secondaryLabelColor), + ThemeCSSRecord(selectors: [.account, .disconnect], property: .stroke, value: accountCellSet.tintColor), + ThemeCSSRecord(selectors: [.account, .disconnect], property: .fill, value: accountCellSet.labelColor), + + // - Location Picker + ThemeCSSRecord(selectors: [.locationPicker, .collection, .accountList], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.locationPicker, .collection, .accountList, .cell], property: .fill, value: accountCellSet.backgroundColor), + ThemeCSSRecord(selectors: [.locationPicker, .navigationBar], property: .fill, value: groupedCollectionBackgroundColor), + + // - More card header + ThemeCSSRecord(selectors: [.more, .header], property: .fill, value: moreHeaderBackgroundColor), + ThemeCSSRecord(selectors: [.more, .collection], property: .fill, value: groupedCellStateSet), + ThemeCSSRecord(selectors: [.more, .insetGrouped, .table, .cell, .proceed], property: .stroke, value: UIColor.white), + + ThemeCSSRecord(selectors: [.more, .favorite], property: .stroke, value: favoriteEnabledColor), + ThemeCSSRecord(selectors: [.more, .favorite, .disabled], property: .stroke, value: favoriteDisabledColor), + + // - TVG icon colors + ThemeCSSRecord(selectors: [.vectorImage, .folderColor], property: .fill, value: iconSymbolColor), + ThemeCSSRecord(selectors: [.vectorImage, .fileColor], property: .fill, value: iconSymbolColor), + ThemeCSSRecord(selectors: [.vectorImage, .logoColor], property: .fill, value: logoFillColor ?? UIColor.white), + ThemeCSSRecord(selectors: [.vectorImage, .iconColor], property: .fill, value: iconSymbolColor), + ThemeCSSRecord(selectors: [.vectorImage, .symbolColor], property: .fill, value: iconSymbolColor), + + // Side Bar + // - Interface Style + ThemeCSSRecord(selectors: [.sidebar], property: .style, value: UIUserInterfaceStyle.light), + + // - Status Bar + ThemeCSSRecord(selectors: [.sidebar], property: .statusBarStyle, value: statusBarStyle), + + // - Collection View + ThemeCSSRecord(selectors: [.sidebar, .collection, .cell], property: .fill, value: sidebarCellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.sidebar, .collection], property: .fill, value: sidebarCellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.sidebar, .collection, .cell], property: .stroke, value: sidebarCellStateSet.regular.labelColor), + // ThemeCSSRecord(selectors: [.sidebar, .collection, .label], property: .stroke, value: sidebarCellStateSet.regular.labelColor), + // ThemeCSSRecord(selectors: [.sidebar, .collection, .icon], property: .stroke, value: sidebarCellStateSet.regular.labelColor), + + ThemeCSSRecord(selectors: [.sidebar, .collection, .selected, .cell], property: .stroke, value: sidebarCellStateSet.selected.labelColor), + ThemeCSSRecord(selectors: [.sidebar, .collection, .selected, .cell], property: .fill, value: sidebarCellStateSet.selected.backgroundColor), + + // - Warning + ThemeCSSRecord(selectors: [.sidebar, .warning, .icon], property: .stroke, value: UIColor.black), // "Access denied" sidebar icon + + // - Account Cell + ThemeCSSRecord(selectors: [.sidebar, .account], property: .fill, value: sidebarAccountCellSet.backgroundColor), + ThemeCSSRecord(selectors: [.sidebar, .account, .title], property: .stroke, value: sidebarAccountCellSet.labelColor), + ThemeCSSRecord(selectors: [.sidebar, .account, .description], property: .stroke, value: sidebarAccountCellSet.secondaryLabelColor), + ThemeCSSRecord(selectors: [.sidebar, .account, .disconnect], property: .stroke, value: sidebarAccountCellSet.tintColor), + ThemeCSSRecord(selectors: [.sidebar, .account, .disconnect], property: .fill, value: sidebarAccountCellSet.labelColor), + + // - Navigation Bar + ThemeCSSRecord(selectors: [.sidebar, .navigationBar], property: .stroke, value: lightColor), + ThemeCSSRecord(selectors: [.sidebar, .navigationBar], property: .fill, value: nil), + ThemeCSSRecord(selectors: [.sidebar, .navigationBar, .logo], property: .stroke, value: sidebarLogoIconColor), + ThemeCSSRecord(selectors: [.sidebar, .navigationBar, .logo, .label],property: .stroke, value: sidebarLogoLabel), + + // - Toolbar + ThemeCSSRecord(selectors: [.sidebar, .toolbar], property: .fill, value: sidebarCellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.sidebar, .toolbar], property: .stroke, value: lightColor), + + // Content Area + ThemeCSSRecord(selectors: [.content], property: .fill, value: collectionBackgroundColor), + + // - Navigation Bar + ThemeCSSRecord(selectors: [.content, .navigationBar], property: .fill, value: contentNavigationBarSet.backgroundColor), + ThemeCSSRecord(selectors: [.content, .navigationBar], property: .stroke, value: contentNavigationBarSet.tintColor), + ThemeCSSRecord(selectors: [.content, .navigationBar, .label, .title], property: .stroke, value: contentNavigationBarSet.labelColor), + + // - Toolbar + ThemeCSSRecord(selectors: [.content, .toolbar], property: .stroke, value: contentToolbarSet.tintColor), + ThemeCSSRecord(selectors: [.content, .toolbar], property: .fill, value: contentToolbarSet.backgroundColor), + + // - Location Bar + ThemeCSSRecord(selectors: [.content, .toolbar, .locationBar, .segments, .item, .plain], property: .stroke, value: contentToolbarSet.tintColor), + ThemeCSSRecord(selectors: [.content, .toolbar, .locationBar, .segments, .item, .separator], property: .stroke, value: contentToolbarSet.secondaryLabelColor), + ThemeCSSRecord(selectors: [.content, .toolbar, .locationBar], property: .fill, value: contentToolbarSet.backgroundColor) + ]) } - func resolveThemeColorCollection(_ forKeyPath: String, _ colorCollection : ThemeColorCollection) -> ThemeColorCollection { - let collection = ThemeColorCollection(backgroundColor: self.resolveColor(forKeyPath.appending(".backgroundColor"), colorCollection.backgroundColor), - tintColor: self.resolveColor(forKeyPath.appending(".tintColor"), colorCollection.tintColor), - labelColor: self.resolveColor(forKeyPath.appending(".labelColor"), colorCollection.labelColor), - secondaryLabelColor: self.resolveColor(forKeyPath.appending(".secondaryLabelColor"), colorCollection.secondaryLabelColor), - symbolColor: self.resolveColor(forKeyPath.appending(".symbolColor"), colorCollection.symbolColor), - filledColorPairCollection: self.resolveThemeColorPairCollection(forKeyPath.appending(".filledColorPairCollection"), colorCollection.filledColorPairCollection)) - - return collection + convenience override init() { + self.init(darkBrandColor: UIColor(hex: 0x1D293B), lightBrandColor: UIColor(hex: 0x468CC8)) } - func resolveThemeColorPairCollection(_ forKeyPath: String, _ colorPairCollection : ThemeColorPairCollection) -> ThemeColorPairCollection { - let newColorPairCollection = ThemeColorPairCollection(fromPairCollection: colorPairCollection) - - if let baseForegroundColor = self.resolveColor(forKeyPath.appending(".baseForegroundColor")), - let baseBackgroundColor = self.resolveColor(forKeyPath.appending(".baseBackgroundColor")) { - return ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: baseForegroundColor, background: baseBackgroundColor)) - } else { - newColorPairCollection.normal = self.resolveThemeColorPair(forKeyPath.appending(".normal"), colorPairCollection.normal) - newColorPairCollection.highlighted = self.resolveThemeColorPair(forKeyPath.appending(".highlighted"), colorPairCollection.highlighted) - newColorPairCollection.disabled = self.resolveThemeColorPair(forKeyPath.appending(".disabled"), colorPairCollection.disabled) + // MARK: - Icon colors + var _iconColors: [String:String]? + public var iconColors: [String:String] { + if _iconColors == nil { + _iconColors = [:] + + _iconColors?["folderFillColor"] = css.getColor(.fill, selectors: [.vectorImage, .folderColor], for: nil)?.hexString() + _iconColors?["fileFillColor"] = css.getColor(.fill, selectors: [.vectorImage, .fileColor], for: nil)?.hexString() + _iconColors?["logoFillColor"] = css.getColor(.fill, selectors: [.vectorImage, .logoColor], for: nil)?.hexString() + _iconColors?["iconFillColor"] = css.getColor(.fill, selectors: [.vectorImage, .iconColor], for: nil)?.hexString() + _iconColors?["symbolFillColor"] = css.getColor(.fill, selectors: [.vectorImage, .symbolColor], for: nil)?.hexString() } - return newColorPairCollection + return _iconColors ?? [:] } +} +extension ThemeCSSSelector { + static let folderColor = ThemeCSSSelector(rawValue: "folderColor") + static let fileColor = ThemeCSSSelector(rawValue: "fileColor") + static let logoColor = ThemeCSSSelector(rawValue: "logoColor") + static let iconColor = ThemeCSSSelector(rawValue: "iconColor") + static let symbolColor = ThemeCSSSelector(rawValue: "symbolColor") } -@available(iOS 13.0, *) extension ThemeCollection { - var navigationBarAppearance : UINavigationBarAppearance { + func navigationBarAppearance(navigationBar: UINavigationBar, 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 + + if let labelColor = css.getColor(.stroke, selectors: [.label], for: navigationBar) { + appearance.titleTextAttributes = [ .foregroundColor : labelColor ] + appearance.largeTitleTextAttributes = [ .foregroundColor : labelColor ] + } + + appearance.backgroundColor = css.getColor(.fill, for: navigationBar) + + 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..4e52656af 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeStyle+DefaultStyles.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeStyle+DefaultStyles.swift @@ -33,8 +33,4 @@ extension ThemeStyle { static public var ownCloudDark : ThemeStyle { return (ThemeStyle(styleIdentifier: "com.owncloud.dark", localizedName: "Dark".localized, lightColor: .ownCloudLightColor, darkColor: .ownCloudDarkColor, themeStyle: .dark)) } - - static public var ownCloudClassic : ThemeStyle { - return (ThemeStyle(styleIdentifier: "com.owncloud.classic", darkStyleIdentifier: "com.owncloud.dark", localizedName: "Classic".localized, lightColor: .ownCloudLightColor, darkColor: .ownCloudDarkColor, themeStyle: .contrast)) - } } diff --git a/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift b/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift index 7d4fb7513..a64fd1c12 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeStyle+Extensions.swift @@ -20,17 +20,6 @@ import Foundation import ownCloudSDK import ownCloudApp -@available(iOS 13.0, *) -extension UIUserInterfaceStyle { - func themeCollectionStyles() -> [ThemeCollectionStyle] { - if self == .dark { - return [.dark] - } - - return [.light, .contrast] - } -} - extension ThemeStyle { public func themeStyleExtension(isDefault: Bool = false, isBranding: Bool = false) -> OCExtension { let features : [String:Any] = [ @@ -116,7 +105,7 @@ extension ThemeStyle { applyStyle = style } } else { - if ThemeStyle.preferredStyle.themeStyle == .dark, let style = ThemeStyle.availableStyles(for: [.contrast])?.first { + if ThemeStyle.preferredStyle.themeStyle == .dark, let style = ThemeStyle.availableStyles(for: [.light])?.first { ThemeStyle.preferredStyle = style applyStyle = style } @@ -128,7 +117,7 @@ extension ThemeStyle { if let themeWindowSubviews = rootView?.subviews { for view in themeWindowSubviews { - view.overrideUserInterfaceStyle = themeCollection.interfaceStyle.userInterfaceStyle + view.overrideUserInterfaceStyle = themeCollection.css.getUserInterfaceStyle() } } @@ -196,9 +185,8 @@ 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.ownCloudDark.themeStyleExtension()) } } diff --git a/ownCloudAppShared/User Interface/Theme/ThemeStyle.swift b/ownCloudAppShared/User Interface/Theme/ThemeStyle.swift index fde6c6d94..7afd769a5 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeStyle.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeStyle.swift @@ -30,44 +30,23 @@ public class ThemeStyle : NSObject { public var darkStyleIdentifier: String? - public var customizedColorsByPath : [String:String]? public var customColors : NSDictionary? public var genericColors : NSDictionary? public var styles : NSDictionary? - public init(styleIdentifier: String, darkStyleIdentifier darkIdentifier: String? = nil, localizedName name: String, lightColor lColor: UIColor, darkColor dColor: UIColor, themeStyle style: ThemeCollectionStyle = .light, customizedColorsByPath customizations: [String:String]? = nil, customColors: NSDictionary? = nil, genericColors: NSDictionary? = nil, interfaceStyles: NSDictionary? = nil) { + public var cssRecordStrings: [String]? + + public init(styleIdentifier: String, darkStyleIdentifier darkIdentifier: String? = nil, localizedName name: String, lightColor lColor: UIColor, darkColor dColor: UIColor, themeStyle style: ThemeCollectionStyle = .light, customColors: NSDictionary? = nil, genericColors: NSDictionary? = nil, interfaceStyles: NSDictionary? = nil, cssRecordStrings: [String]? = nil) { self.identifier = styleIdentifier self.darkStyleIdentifier = darkIdentifier self.localizedName = name self.lightColor = lColor self.darkColor = dColor self.themeStyle = style - self.customizedColorsByPath = customizations self.customColors = customColors self.genericColors = genericColors self.styles = interfaceStyles - } - - public var parsedCustomizedColorsByPath : [String:UIColor]? { - if let rawColorsByPath = customizedColorsByPath { - var colorsByPath : [String:UIColor] = [:] - - for (keyPath, rawColor) in rawColorsByPath { - var color : UIColor? - - if let decodedHexColor = rawColor.colorFromHex { - color = decodedHexColor - } - - if color != nil { - colorsByPath[keyPath] = color - } - } - - return colorsByPath - } - - return nil + self.cssRecordStrings = cssRecordStrings } } @@ -89,13 +68,37 @@ public extension String { } } +public extension ThemeCSSRecord { + static func from(_ cssRecordStrings: [String]) -> [ThemeCSSRecord]? { + // Format: selector1.selector2.property: value + var records: [ThemeCSSRecord] = [] + + let whitespace = CharacterSet.whitespaces + + for recordString in cssRecordStrings { + let recordString = recordString as NSString + let separatorIndex = recordString.range(of: ":").location + + if separatorIndex != -1 { + let selectionAndPropertyPart = recordString.substring(to: separatorIndex) + let valuePart = recordString.substring(from: separatorIndex+1).trimmingCharacters(in: whitespace) + + if let record = ThemeCSSRecord(with: selectionAndPropertyPart, value: valuePart) { + records.append(record) + } + } + } + + return records.count > 0 ? records : nil + } +} + public extension ThemeCollection { convenience init(with style: ThemeStyle) { self.init(darkBrandColor: style.darkColor, lightBrandColor: style.lightColor, style: style.themeStyle, customColors: style.customColors, genericColors: style.genericColors, interfaceStyles: style.styles) - if let customizationColors = style.parsedCustomizedColorsByPath { - for (keyPath, color) in customizationColors { - self.setValue(color, forKeyPath: keyPath) - } + + if let cssRecordStrings = style.cssRecordStrings, let records = ThemeCSSRecord.from(cssRecordStrings) { + css.add(records: records) } } } diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeButton.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeButton.swift index 280957793..dee17d14b 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeButton.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeButton.swift @@ -25,26 +25,34 @@ public enum ThemeButtonCornerRadiusStyle : CGFloat { } @IBDesignable -open class ThemeButton : UIButton { - internal var _themeColorCollection : ThemeColorPairCollection? - public var themeColorCollection : ThemeColorPairCollection? { - set(colorCollection) { - _themeColorCollection = colorCollection - - if _themeColorCollection != nil { - self.setTitleColor(_themeColorCollection?.normal.foreground, for: .normal) - self.setTitleColor(_themeColorCollection?.highlighted.foreground, for: .highlighted) - self.setTitleColor(_themeColorCollection?.disabled.foreground, for: .disabled) - - self.updateBackgroundColor() - } +open class ThemeButton : UIButton, Themeable, ThemeCSSChangeObserver { + private var themeRegistered = false + open override func didMoveToWindow() { + super.didMoveToWindow() + + if !themeRegistered, window != nil { + // Postpone registration with theme until we actually need to. Makes sure self.applyThemeCollection() can take all properties into account + Theme.shared.register(client: self, applyImmediately: true) + themeRegistered = true } + } - get { - return _themeColorCollection + public func cssSelectorsChanged() { + if superview != nil { + self.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .update) } } + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + let css = collection.css + + setTitleColor(css.getColor(.stroke, for: self), for: .normal) + setTitleColor(css.getColor(.stroke, selectors: [.highlighted], for: self), for: .highlighted) + setTitleColor(css.getColor(.stroke, selectors: [.disabled], for: self), for: .highlighted) + + updateBackgroundColor() + } + public override var isHighlighted: Bool { set(newIsHighlighted) { super.isHighlighted = newIsHighlighted @@ -68,15 +76,15 @@ open class ThemeButton : UIButton { } private func updateBackgroundColor() { - if _themeColorCollection != nil { - if !self.isEnabled { - self.backgroundColor = _themeColorCollection?.disabled.background + let css = Theme.shared.activeCollection.css + + if !isEnabled { + backgroundColor = css.getColor(.fill, selectors: [.disabled], for: self) + } else { + if isHighlighted { + backgroundColor = css.getColor(.fill, selectors: [.highlighted], for: self) } else { - if self.isHighlighted { - self.backgroundColor = _themeColorCollection?.highlighted.background - } else { - self.backgroundColor = _themeColorCollection?.normal.background - } + backgroundColor = css.getColor(.fill, for: self) } } } diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift new file mode 100644 index 000000000..0d955e317 --- /dev/null +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeCertificateViewController.swift @@ -0,0 +1,54 @@ +// +// ThemeCertificateViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 23.08.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 ownCloudUI + +public class ThemeCertificateViewController: OCCertificateViewController, Themeable { + override public func viewDidLoad() { + super.viewDidLoad() + + Theme.shared.register(client: self, applyImmediately: true) + } + + deinit { + Theme.shared.unregister(client: self) + } + + var cellBackgroundColor: UIColor? + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + tableView.backgroundColor = collection.css.getColor(.fill, selectors: [.grouped], for:tableView) + tableView.separatorColor = collection.css.getColor(.fill, selectors: [.separator], for:tableView) + + self.sectionHeaderTextColor = collection.css.getColor(.stroke, selectors: [.cell, .sectionHeader], for:tableView) ?? .secondaryLabel + + self.lineTitleColor = collection.css.getColor(.stroke, selectors: [.label, .secondary], for:tableView) ?? .secondaryLabel + self.lineValueColor = collection.css.getColor(.stroke, selectors: [.label, .primary], for:tableView) ?? .label + + cellBackgroundColor = collection.css.getColor(.fill, selectors: [.grouped, .table, .cell], for:nil) + } + + override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell : UITableViewCell = super.tableView(tableView, cellForRowAt: indexPath) + + cell.backgroundColor = cellBackgroundColor + + return cell + } +} diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift index 2c5841fa2..830072b67 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeNavigationController.swift @@ -22,19 +22,24 @@ 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 { - if self.presentedViewController == nil, let loginViewController = object as? CustomStatusBarViewControllerProtocol { - return loginViewController.statusBarStyle() - } else { - return Theme.shared.activeCollection.statusBarStyle - } + if let object = self.viewControllers.last, self.presentedViewController == nil, let loginViewController = object as? CustomStatusBarViewControllerProtocol { + return loginViewController.statusBarStyle() } - return Theme.shared.activeCollection.statusBarStyle + return Theme.shared.activeCollection.css.getStatusBarStyle(for: self) ?? .default } open override var childForStatusBarStyle: UIViewController? { @@ -43,20 +48,13 @@ 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) + applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) } - deinit { - Theme.shared.remove(applierForToken: themeToken) + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + Theme.shared.register(client: self, applyImmediately: true) } open var popLastHandler : ((UIViewController?) -> Bool)? @@ -76,4 +74,14 @@ open class ThemeNavigationController: UINavigationController { return super.popViewController(animated: animated) } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.applyThemeCollection(collection) + self.toolbar.applyThemeCollection(collection) + self.view.backgroundColor = .clear + + if event == .update { + self.setNeedsStatusBarAppearanceUpdate() + } + } } diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift index 959547d77..ac09c8acd 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift @@ -43,8 +43,10 @@ open class ThemeTableViewCell: UITableViewCell, Themeable { } public typealias CellStyler = (_ cell: ThemeTableViewCell, _ styleSet: CellStyleSet) -> Bool + public typealias CellCustomizer = (_ cell: ThemeTableViewCell, _ styleSet: CellStyleSet) -> Void public var cellStyler : CellStyler? + public var cellCustomizer: CellCustomizer? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { if style == .default { @@ -53,6 +55,8 @@ open class ThemeTableViewCell: UITableViewCell, Themeable { } else { super.init(style: style, reuseIdentifier: reuseIdentifier) } + + cssSelector = .cell } public typealias CellLayouter = (_ cell: ThemeTableViewCell, _ textLabel: UILabel, _ detailLabel : UILabel?) -> Void @@ -158,13 +162,13 @@ open class ThemeTableViewCell: UITableViewCell, Themeable { } } - open override func willMove(toSuperview newSuperview: UIView?) { - super.willMove(toSuperview: newSuperview) + open override func didMoveToWindow() { + super.didMoveToWindow() - if !themeRegistered { + if !themeRegistered, window != nil { // Postpone registration with theme until we actually need to. Makes sure self.applyThemeCollection() can take all properties into account - Theme.shared.register(client: self, applyImmediately: true) themeRegistered = true + Theme.shared.register(client: self, applyImmediately: true) } } @@ -177,35 +181,35 @@ open class ThemeTableViewCell: UITableViewCell, Themeable { var doStyle = true switch messageStyle { - case .plain: - textColor = collection.tableRowColors.labelColor - backgroundColor = collection.tableRowColors.backgroundColor - - case .text: - textColor = collection.tableRowColors.labelColor - backgroundColor = collection.tableRowColors.backgroundColor + case .plain, .text: + textColor = self.getThemeCSSColor(.stroke, selectors: [.label, .primary]) // collection.tableRowColors.labelColor + backgroundColor = self.getThemeCSSColor(.fill) // collection.tableRowColors.backgroundColor case .confirmation: - textColor = collection.approvalColors.normal.foreground - backgroundColor = collection.approvalColors.normal.background + textColor = collection.css.getColor(.stroke, selectors: [.confirm], for: self) + backgroundColor = collection.css.getColor(.fill, selectors: [.confirm], for: self) case .warning: textColor = .black backgroundColor = .systemYellow case .alert: - textColor = collection.destructiveColors.normal.foreground - backgroundColor = collection.destructiveColors.normal.background + textColor = collection.css.getColor(.stroke, selectors: [.destructive], for: self) + backgroundColor = collection.css.getColor(.fill, selectors: [.destructive], for: self) case let .custom(customTextColor, customBackgroundColor, _): textColor = customTextColor backgroundColor = customBackgroundColor } - if let cellStyler = cellStyler, cellStyler(self, CellStyleSet(theme: theme, collection: collection, backgroundColor: backgroundColor, textColor: textColor)) { + if let cellStyler, cellStyler(self, CellStyleSet(theme: theme, collection: collection, backgroundColor: backgroundColor, textColor: textColor)) { doStyle = false } + if let cellCustomizer { + cellCustomizer(self, CellStyleSet(theme: theme, collection: collection, backgroundColor: backgroundColor, textColor: textColor)) + } + if doStyle { self.textLabel?.textColor = textColor self.detailTextLabel?.textColor = textColor diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift index cb07a330b..dd0c1e37e 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeView.swift @@ -18,41 +18,32 @@ import UIKit -open class ThemeView: UIView, Themeable { - private var hasRegistered : Bool = false - - public init() { - super.init(frame: .zero) - } - - deinit { - if hasRegistered { - Theme.shared.unregister(client: self) - } - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } +open class ThemeView: ThemeCSSView { + // Important implementation difference: ThemeCSSView makes the initial themeing calls only when moved to a window (=> becomes part of visible view hiearchy), ThemeView did this when moved to a subview (=> may still be in view setup) + private var hasSetupSubviews = false override open func didMoveToSuperview() { + super.didMoveToSuperview() + if self.superview != nil { - if !hasRegistered { - hasRegistered = true - Theme.shared.register(client: self, applyImmediately: true) + if !hasSetupSubviews { + hasSetupSubviews = true + setupSubviews() } } } - private var themeAppliers : [ThemeApplier] = [] + override open func didMoveToWindow() { + if window != nil, !hasSetupSubviews { + hasSetupSubviews = true + setupSubviews() + } - open func addThemeApplier(_ applier: @escaping ThemeApplier) { - themeAppliers.append(applier) + super.didMoveToWindow() } - open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - for applier in themeAppliers { - applier(theme, collection, event) - } + open func setupSubviews() { + // Override point for subclasses + // Themeing is performed at a later time, when moved into a visible view tree (window) } } diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeWindow.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeWindow.swift index ee3cf6c55..3954f045d 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeWindow.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeWindow.swift @@ -72,7 +72,6 @@ public class ThemeWindow : UIWindow { ThemeWindow.addThemeWindow(self) } - @available(iOS 13, *) override public init(windowScene: UIWindowScene) { super.init(windowScene: windowScene) diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeableColoredView.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeableColoredView.swift deleted file mode 100644 index 38491eaa9..000000000 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeableColoredView.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ThemeableColoredView.swift -// ownCloud -// -// Created by Matthias Hühne on 18.02.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 - -open class ThemeableColoredView: UIView, Themeable { - - // MARK: - Instance variables. - - var messageThemeApplierToken : ThemeApplierToken? - - override public init(frame: CGRect) { - super.init(frame: frame) - - Theme.shared.register(client: self, applyImmediately: true) - } - - required public 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 - - public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.backgroundColor = collection.navigationBarColors.backgroundColor - } -} diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemedAlertController.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemedAlertController.swift index 8f6e628a9..1aaf258ad 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemedAlertController.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemedAlertController.swift @@ -24,8 +24,7 @@ public class ThemedAlertController: UIAlertController, Themeable { override open func viewDidLoad() { super.viewDidLoad() - self.overrideUserInterfaceStyle = Theme.shared.activeCollection.interfaceStyle.userInterfaceStyle - view.tintColor = Theme.shared.activeCollection.tableRowColors.labelColor + applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) } override open func viewWillAppear(_ animated: Bool) { @@ -34,8 +33,10 @@ public class ThemedAlertController: UIAlertController, Themeable { } open func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.overrideUserInterfaceStyle = collection.interfaceStyle.userInterfaceStyle - view.tintColor = collection.tableRowColors.labelColor + let css = Theme.shared.activeCollection.css + + self.overrideUserInterfaceStyle = css.getUserInterfaceStyle(for: self) + view.tintColor = css.getColor(.stroke, for: self) } deinit { 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 78% rename from ownCloud/View Providers/OCResourceText+ViewProvider.swift rename to ownCloudAppShared/User Interface/View Providers/OCResourceText+ViewProvider.swift index 31bb14f43..963905eff 100644 --- a/ownCloud/View Providers/OCResourceText+ViewProvider.swift +++ b/ownCloudAppShared/User Interface/View Providers/OCResourceText+ViewProvider.swift @@ -18,12 +18,10 @@ import UIKit import ownCloudSDK -import ownCloudAppShared import Down class ThemeableTextView : UITextView, Themeable { init() { - registeredThemeable = false super.init(frame: .zero, textContainer: nil) } @@ -31,34 +29,19 @@ class ThemeableTextView : UITextView, Themeable { fatalError("init(coder:) has not been implemented") } - deinit { - Theme.shared.unregister(client: self) - } - - private var registeredThemeable : Bool - - func registerThemeable() { - if !registeredThemeable { - registeredThemeable = true - Theme.shared.register(client: self, applyImmediately: true) - } - } - - override var attributedText: NSAttributedString! { - didSet { - registerThemeable() - } - } + private var _themeRegistered = false + public override func didMoveToWindow() { + super.didMoveToWindow() - override var text: String! { - didSet { - registerThemeable() + if window != nil, !_themeRegistered { + _themeRegistered = true + Theme.shared.register(client: self) } } func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - backgroundColor = collection.tableBackgroundColor - textColor = collection.tableRowColors.labelColor + backgroundColor = collection.css.getColor(.fill, for: self) + textColor = collection.css.getColor(.stroke, for: self) } } 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 9d9b50b620c7e22e755a3c2ead71e592539d248f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1555 zcmXqLV&gYxV*b8>nTe5!iId@qs@k999Y2f=c-c6$+C196^D;8BvN9MX8FCwNvN4CU zun9A{xEcx=@PRlS!t73oWr;ZmtUTjoReRgqL*KkZXhSlYiMC$Y-nI)WNcty5C!B~B5~>B zh9)K@WUn)_GB7tW@iQ1SF>x_9F)=di>V5S@Y~)t0LNyQTde znjY4D{fj+x;+NOwx0!_&oSvE^cTzup2b<@tmwi*R4;%~TU#BE$7uv6Wq+r2DgYD@t@e^L?V<+Cfy6z{%VT!G>^(F=bso~ zJbo>H@9epAPfz;s-{FB`$JISKyn7VvMdPlj)dw8zlO*X92GB=_aKpYA1|uFT@cZ+Q0!rSCeaCf5~W&9dPeSHl{~B@s>0 zo9;WutT4%)ZSPpx`lU*Pv1Qk)?#cg5GDQ=4o=@&rzWN{6ebGOUQtpOdR}=~Q5Phpt z%fPBI?04Sd#lNR&Ecf#dw^z_s$l5tI`oYR0|CM|UdQ!e!U8pTq5p=3C`OtR%DQgRM z|M_=&LRRM|(~En%W=z+qTC-@yw})(j*PTiq^C)+*e4hN-qTD-bhga*hxs}J01OpP1 z61K!%KJt0Araq~3hUy0A?10#hx4nAbtP_4*_&t!zd;7!Q zpiI3to6IJb?47o2ZMXfKRwXOp@D=N4H#Sw0pq7LknqKBr1Hx+JOITj6_T*{PQwu8A}lG)@7@E3-8A7&LY^U{$j) zZAl6}l64bO#^wRJN|=S!fSHl;KXO_JW^-U#XJn`s`sn2_ePKndaE7-~h}ZEuONy`i z>Hkf=Z@=|n>jkTT&6?Q}Yu>sz#%{^j8|ypi$8)|F)sA;xuta1Dn7r&hEWU_s3$7Zf=P@?y2#kIeA2`#nApKIION`_Rlh*H#FF7qWkZ@}? z)#?g8FO)Cesou8Y>)D^ao&3yMw--bocr`_P&0&^>|7U1QE5EuGoOX?2ONA!eB=M`C zn55T!5>*UzJKn{#@b}DolP4*gdCFGvbZ@NBJ@83uq0pN3bKDC`7F-SJwmKsj?RQtL zwAb=G!bj~`MEI&k@6F7L{GMvJZ$0RgY)pPsV+ zOTk@(LkGE6bp<5L=$^Y2zG+g#Id*Q*#3P~Qmsdt#O}uFP!B9u4JUxxS=f*~zlm1r! zq%?bP2wG15P{)|wld^G|hU)Bjo2KqF`*6Lhb*q4Eb?z!R*9zybrd?tij-~xyk)g-C z%75omftE`O6<%&zt}k-=-TblJVf92Ip`Oc;zXMnq%DOUleAdoYwCUe;^3~fjM?$u* zE2`AlE;q~ib7S4Lo0;dGuXs%lRk?Cv=1xwJ>+kJ6Wt+FI`kR`xpgXzjkl;(vX#gOt BgpL3J 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 + } +} diff --git a/tools/MakeTVG/main.swift b/tools/MakeTVG/main.swift index 45b4923b1..69fce613d 100644 --- a/tools/MakeTVG/main.swift +++ b/tools/MakeTVG/main.swift @@ -99,7 +99,7 @@ func extractRootAttributes(from svgString: String) -> [String:String]? { var viewBoxRect : CGRect? if sizeWidth != nil, sizeHeight != nil { - viewBoxRect = CGRect(origin: .zero, size: CGSize(width: sizeWidth!, height: sizeHeight!)) + viewBoxRect = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: sizeWidth!, height: sizeHeight!)) if originX != nil, originY != nil { viewBoxRect?.origin = CGPoint(x: originX!, y: originY!)