From 98413f70811de3e7031346f8927a30f349e70efd Mon Sep 17 00:00:00 2001 From: Michael Neuwert Date: Fri, 1 Feb 2019 15:33:55 +0100 Subject: [PATCH] Multi selection on the file list (#234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added UIViewController extension allowing to swap tabBar for toolbar * Hiding ‘more’ button when cell is configured for edit mode * Laid down the foundation for multi-selection and related actions * Multi-selection works with “Move” and “Delete” actions * Re-added upload button * Exchanging tab-bar with toolbar not animated * Fixed margins for devices with safe area guides (iPhoneX etc.) * Avoiding visual glitches by adding a toolbar in a proper position in the view hierarchy * Showing upload button only when appropriate * exclude app ownCloudTests from lint rules (#186) * Add notification to update version numbers on oc.org/download (#201) * [feature/fp-series-2] File Provider improvements (#167) * - Minor memory optimizations - Updated to latest SDK * - Save changes prior to merge with master * - Convert to Swift 4.2 * - Further Swift 4.2 changes * - Update to latest SDK develop commit * - Adapting APP to latest SDK changes - maintenance mode support - replacing uses of reachability monitor with OCCore.connectionStatus * - Added back ownCloudUI.framework to copy phase of ownCloud target - Updated to latest SDK * - Add support for OCLogToggle in Settings - App now logs current log settings at launch * - Fix issues in ClientQueryViewController related to UITableView not reloading when its view controller is not "visible": - no longer uses items from table view cells to initiate actions - detects if UITableView.reloadData is set to do nothing and repeats the call in viewWillAppear - The fixed issue was causing https://github.com/owncloud/ios-app/issues/178 and possibly others * - Remove last traces of OCMocking.framework from ownCloud app target, make sure it's only built, linked to and copied in the ownCloudTests target * - Fix EarlGrey/CocoaPods workspace / build errors * - Adopt new OCLogger tags APIs in Log.swift and FileProviderExtension * - Update SDK * - Remove superfluous [FP] from log message * - Make sure DisplayViewController uses the updated version of an item after downloading it instead of the (now) outdated original version * - Update to latest SDK * - Fixing thumbnail aspect ratio and removing duplicate code in NamingViewController (https://github.com/owncloud/ios-app/issues/141) * - Update SDK * - Exit editing mode in the server list when the user selects an existing or adds a new bookmark (as reported by @mneuwert) * - Adapt app code base to change in OCError Swift conversion (following OCErrorAuthorizationCancelled typo correction) * - Fix https://github.com/owncloud/ios-app/issues/152: if the user cancels OAuth2, an error is no longer shown (and errors are better to read with recent SDK updates) * - Update to latest SDK * - Make ProgressSummarizer only consider Progress objects with descriptions, resolving "ghost" progress bars without any information in them * - Adapt to change of name of OCConnectionIssue to OCIssue * - Update to latest SDK - ConnectionIssueViewController - added option to provide a block to be called when dismissal has finished - normalize code formatting - UIAlertController+OCIssue - added option to provide a completionHandler that's called when the user made a choice - ClientRootViewController - switched issue and alert presentation to use a AsyncSequentialQueue to present them in order and not on top of each other - fixed a bug where a UIViewController that's in the process of being dismissed led to a failure to present alerts and issues * - Make DisplayViewController use OCCoreOptionReturnImmediatelyIfOfflineOrUnavailable and fix a typo (#179) * - Added priority summaries to ProgressSummarizer - Fixed a crash bug in ImageDisplayViewController - Removed core connection status interpretation from ClientQueryViewController - Added core connection status tracking to ClientRootViewController and utilize priority summaries to display offline or server in maintenance mode status messages * - Fix an issue where "Offline." was shortly shown when logging in - Switch to utilizing short connection status descriptions coming straight from the SDK's connection signal providers - Adapt Localizable.strings accordingly * - Add *.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist to .gitignore * - Updated ios-sdk * - Made FileProviderEnumerator use an OCQuery that includes the root item (fixes https://github.com/owncloud/ios-app/issues/203) - Made FileProviderExtension observe the domain property and trigger an initial query of the root directory when none has even been done before (fixes https://github.com/owncloud/ios-app/issues/205) * - Adapted to latest round of nullability additions in the SDK - Fixing potential crash bugs in DisplayHostViewController, ConnectionIssueViewController, SortMethod and others - Update actions - make use of .completed() instead of calling the completionHandler directly - fixing multi-item support for several actions (completionHandler would previously get executed once for every item instead of once) - ClientDirectoryPickerViewController now calls the completinHandler even on cancelation - Cleaned up OCIssue+Extension and OCItem+Extension - Replacing AsyncSequentialQueue with OCAsyncSequentialQueue * - Update SDK * - Updated ios-sdk * - Update ios-sdk with IPC fixes * - Adapt tests to SDK changes (all tests pass) - Stop CocoaPods from sending statistics by adding a line to the Podfile turning this off * - Present cache contents while waiting for a reply from the server - Updated SDK fixing a lot of issues * - Replacing DispatchQueue.main.async* with OnMainThread and a newly added OnMainThread(after: timeInterval) * - Add verbose logging to FileProviderExtension to track the commands received by iOS and the results that were returned * - Temporary workaround: make importFileFromURL: not return directly with the placeholder, but only when upload has completed - and then return the final item * - Switching ios-sdk to updated master branch * Version Bump to 99 * [tx] updated from transifex * Added several languages (#231) * Added the languages cs_CZ de de_DE en_GB ko mk nb_NO nn_NO pt_BR pt_PT ru sq th_TH zh_CN * Fixed a compilation issue because CZ file * Updated the commit of the SDK * [tx] updated from transifex * Version Bump to 100 * Copy action (#207) * - Made the Copy action. - Fix some UI Color scheme inconsistencies. * - Made the 'Copy here' button respect the original title * - SDK Update * - Fix for a lint warning * Offline behaviour in Open In Action (#227) * -Add proper message when no connection and user tries to make the open in action * - Localize the error message. * - Made the name of the app to be the app's name in the bundle * - Changed the three dots bar button in the file list to a '+'. (#206) - Made UISegmentedControll to be Themeable. * Version Bump to 101 * [tx] updated from transifex * [tx] updated from transifex * Laid down the foundation for multi-selection and related actions * Multi-selection works with “Move” and “Delete” actions * [feature/fp-series-2] File Provider improvements (#167) * - Minor memory optimizations - Updated to latest SDK * - Save changes prior to merge with master * - Convert to Swift 4.2 * - Further Swift 4.2 changes * - Update to latest SDK develop commit * - Adapting APP to latest SDK changes - maintenance mode support - replacing uses of reachability monitor with OCCore.connectionStatus * - Added back ownCloudUI.framework to copy phase of ownCloud target - Updated to latest SDK * - Add support for OCLogToggle in Settings - App now logs current log settings at launch * - Fix issues in ClientQueryViewController related to UITableView not reloading when its view controller is not "visible": - no longer uses items from table view cells to initiate actions - detects if UITableView.reloadData is set to do nothing and repeats the call in viewWillAppear - The fixed issue was causing https://github.com/owncloud/ios-app/issues/178 and possibly others * - Remove last traces of OCMocking.framework from ownCloud app target, make sure it's only built, linked to and copied in the ownCloudTests target * - Fix EarlGrey/CocoaPods workspace / build errors * - Adopt new OCLogger tags APIs in Log.swift and FileProviderExtension * - Update SDK * - Remove superfluous [FP] from log message * - Make sure DisplayViewController uses the updated version of an item after downloading it instead of the (now) outdated original version * - Update to latest SDK * - Fixing thumbnail aspect ratio and removing duplicate code in NamingViewController (https://github.com/owncloud/ios-app/issues/141) * - Update SDK * - Exit editing mode in the server list when the user selects an existing or adds a new bookmark (as reported by @mneuwert) * - Adapt app code base to change in OCError Swift conversion (following OCErrorAuthorizationCancelled typo correction) * - Fix https://github.com/owncloud/ios-app/issues/152: if the user cancels OAuth2, an error is no longer shown (and errors are better to read with recent SDK updates) * - Update to latest SDK * - Make ProgressSummarizer only consider Progress objects with descriptions, resolving "ghost" progress bars without any information in them * - Adapt to change of name of OCConnectionIssue to OCIssue * - Update to latest SDK - ConnectionIssueViewController - added option to provide a block to be called when dismissal has finished - normalize code formatting - UIAlertController+OCIssue - added option to provide a completionHandler that's called when the user made a choice - ClientRootViewController - switched issue and alert presentation to use a AsyncSequentialQueue to present them in order and not on top of each other - fixed a bug where a UIViewController that's in the process of being dismissed led to a failure to present alerts and issues * - Make DisplayViewController use OCCoreOptionReturnImmediatelyIfOfflineOrUnavailable and fix a typo (#179) * - Added priority summaries to ProgressSummarizer - Fixed a crash bug in ImageDisplayViewController - Removed core connection status interpretation from ClientQueryViewController - Added core connection status tracking to ClientRootViewController and utilize priority summaries to display offline or server in maintenance mode status messages * - Fix an issue where "Offline." was shortly shown when logging in - Switch to utilizing short connection status descriptions coming straight from the SDK's connection signal providers - Adapt Localizable.strings accordingly * - Add *.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist to .gitignore * - Updated ios-sdk * - Made FileProviderEnumerator use an OCQuery that includes the root item (fixes https://github.com/owncloud/ios-app/issues/203) - Made FileProviderExtension observe the domain property and trigger an initial query of the root directory when none has even been done before (fixes https://github.com/owncloud/ios-app/issues/205) * - Adapted to latest round of nullability additions in the SDK - Fixing potential crash bugs in DisplayHostViewController, ConnectionIssueViewController, SortMethod and others - Update actions - make use of .completed() instead of calling the completionHandler directly - fixing multi-item support for several actions (completionHandler would previously get executed once for every item instead of once) - ClientDirectoryPickerViewController now calls the completinHandler even on cancelation - Cleaned up OCIssue+Extension and OCItem+Extension - Replacing AsyncSequentialQueue with OCAsyncSequentialQueue * - Update SDK * - Updated ios-sdk * - Update ios-sdk with IPC fixes * - Adapt tests to SDK changes (all tests pass) - Stop CocoaPods from sending statistics by adding a line to the Podfile turning this off * - Present cache contents while waiting for a reply from the server - Updated SDK fixing a lot of issues * - Replacing DispatchQueue.main.async* with OnMainThread and a newly added OnMainThread(after: timeInterval) * - Add verbose logging to FileProviderExtension to track the commands received by iOS and the results that were returned * - Temporary workaround: make importFileFromURL: not return directly with the placeholder, but only when upload has completed - and then return the final item * - Switching ios-sdk to updated master branch * - Added th-TH to the .tx config (#241) - Updated the th translations pushed recently to the right folder - Updated the commit of the library * Added the tx link to contribute on trnslations (#245) * Version Bump to 102 * Laid down the foundation for multi-selection and related actions * Multi-selection works with “Move” and “Delete” actions --- ownCloud.xcodeproj/project.pbxproj | 8 + .../Actions+Extensions/DeleteAction.swift | 2 +- .../Actions+Extensions/MoveAction.swift | 2 +- .../ClientDirectoryPickerViewController.swift | 4 +- ownCloud/Client/ClientItemCell.swift | 67 +++++-- .../Client/ClientQueryViewController.swift | 167 +++++++++++++++--- .../Client/ClientRootViewController.swift | 17 +- .../UIBarButtonItem+Extension.swift | 31 ++++ .../UIViewController+Extension.swift | 27 +++ 9 files changed, 286 insertions(+), 39 deletions(-) create mode 100644 ownCloud/UIKit Extensions/UIBarButtonItem+Extension.swift create mode 100644 ownCloud/UIKit Extensions/UIViewController+Extension.swift diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 23d1a4790..6b7fb6dbe 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */; }; 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F6238020B587EF004FDE8B /* SortMethod.swift */; }; 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; + 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */; }; 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE12187AF1400D30602 /* PDFThumbnailCollectionViewCell.swift */; }; 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE82187AF1400D30602 /* PDFTocTableViewController.swift */; }; 4C464BF12187AF1500D30602 /* PDFTocTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BE92187AF1400D30602 /* PDFTocTableViewCell.swift */; }; @@ -43,6 +44,7 @@ 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BEC2187AF1500D30602 /* PDFSearchTableViewCell.swift */; }; 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BED2187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift */; }; 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BEE2187AF1500D30602 /* PDFTocItem.swift */; }; + 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */; }; 5917244E20D3DC2100809B38 /* BiometricalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917244D20D3DC2100809B38 /* BiometricalTests.swift */; }; 593A821120C7D4C5000E2A90 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; 593BAB46209AE1BC00023634 /* PasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 593BAB44209AE1BC00023634 /* PasscodeViewController.swift */; }; @@ -417,6 +419,7 @@ 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDisplayViewController.swift; sourceTree = ""; }; 23F6238020B587EF004FDE8B /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 23FA23E520BFD3D8009A6D73 /* SortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBar.swift; sourceTree = ""; }; + 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; 4C464BE12187AF1400D30602 /* PDFThumbnailCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFThumbnailCollectionViewCell.swift; sourceTree = ""; }; 4C464BE82187AF1400D30602 /* PDFTocTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocTableViewController.swift; sourceTree = ""; }; 4C464BE92187AF1400D30602 /* PDFTocTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocTableViewCell.swift; sourceTree = ""; }; @@ -425,6 +428,7 @@ 4C464BEC2187AF1500D30602 /* PDFSearchTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFSearchTableViewCell.swift; sourceTree = ""; }; 4C464BED2187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFThumbnailsCollectionViewController.swift; sourceTree = ""; }; 4C464BEE2187AF1500D30602 /* PDFTocItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocItem.swift; sourceTree = ""; }; + 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Extension.swift"; sourceTree = ""; }; 5917244D20D3DC2100809B38 /* BiometricalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricalTests.swift; sourceTree = ""; }; 593A821220C7D4C5000E2A90 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 593A821820C7D4DC000E2A90 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; @@ -782,6 +786,8 @@ DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */, DC248C66213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift */, 6EADE9362192E235006821B3 /* UIImagePickerController+Extension.swift */, + 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */, + 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -1642,6 +1648,7 @@ 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */, 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */, DC018F8320A0F56300135198 /* UIView+Animation.swift in Sources */, + 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */, DC42244A207CAFAA0006A2A6 /* Theme.swift in Sources */, 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */, 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */, @@ -1685,6 +1692,7 @@ DC85493421831B0B00782BA8 /* Tools.swift in Sources */, DCFED972208095E200A2D984 /* ClientItemCell.swift in Sources */, 23E22BB720C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift in Sources */, + 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */, 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, diff --git a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift index d6db181be..cb3de4dae 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift @@ -22,7 +22,7 @@ class DeleteAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.delete") } override class var category : ActionCategory? { return .destructive } override class var name : String? { return "Delete".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .tableRow, .moreFolder] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .tableRow, .moreFolder, .toolbar] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift index 2b0c4485a..bbeef1153 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift @@ -22,7 +22,7 @@ class MoveAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.move") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Move".localized } - override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder] } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift index 534940109..88b39c39a 100644 --- a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift +++ b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift @@ -65,7 +65,7 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { // Cancel button creation cancelBarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelBarButtonPressed)) - navigationItem.rightBarButtonItem = cancelBarButton + navigationItem.rightBarButtonItems = [cancelBarButton] } override func viewWillAppear(_ animated: Bool) { @@ -84,7 +84,7 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { guard item.type == OCItemType.collection, let core = self.core, let path = item.path else { return } - + self.navigationController?.pushViewController(ClientDirectoryPickerViewController(core: core, path: path, selectButtonTitle: selectButtonTitle, completion: completion), animated: true) } diff --git a/ownCloud/Client/ClientItemCell.swift b/ownCloud/Client/ClientItemCell.swift index b474f1691..803df62a0 100644 --- a/ownCloud/Client/ClientItemCell.swift +++ b/ownCloud/Client/ClientItemCell.swift @@ -27,6 +27,10 @@ protocol ClientItemCellDelegate: class { class ClientItemCell: ThemeTableViewCell { + let horizontalMargin : CGFloat = 20.0 + let spacing : CGFloat = 15.0 + let moreButtonWidth : CGFloat = 60.0 + weak var delegate: ClientItemCellDelegate? var titleLabel : UILabel = UILabel() @@ -34,6 +38,8 @@ class ClientItemCell: ThemeTableViewCell { var iconView : UIImageView = UIImageView() var moreButton: UIButton = UIButton() + var moreButtonWidthConstraint : NSLayoutConstraint? + var activeThumbnailRequestProgress : Progress? weak var core : OCCore? @@ -41,6 +47,12 @@ class ClientItemCell: ThemeTableViewCell { override 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 + }() } required init?(coder aDecoder: NSCoder) { @@ -67,9 +79,9 @@ class ClientItemCell: ThemeTableViewCell { self.contentView.addSubview(iconView) self.contentView.addSubview(moreButton) - iconView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 20).isActive = true - iconView.rightAnchor.constraint(equalTo: titleLabel.leftAnchor, constant: -15).isActive = true - iconView.rightAnchor.constraint(equalTo: detailLabel.leftAnchor, constant: -15).isActive = true + iconView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: horizontalMargin).isActive = true + iconView.rightAnchor.constraint(equalTo: titleLabel.leftAnchor, constant: -spacing).isActive = true + iconView.rightAnchor.constraint(equalTo: detailLabel.leftAnchor, constant: -spacing).isActive = true moreButton.setAttributedTitle(NSAttributedString(string: "● ● ●", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]), for: .normal) @@ -80,24 +92,25 @@ class ClientItemCell: ThemeTableViewCell { moreButton.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true moreButton.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true moreButton.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true - moreButton.widthAnchor.constraint(equalToConstant: 60).isActive = true + moreButtonWidthConstraint = moreButton.widthAnchor.constraint(equalToConstant: 60) + moreButtonWidthConstraint?.isActive = true moreButton.rightAnchor.constraint(equalTo: self.contentView.rightAnchor).isActive = true moreButton.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside) - moreButton.contentEdgeInsets.left = -20 + moreButton.contentEdgeInsets.left = -horizontalMargin moreButton.titleEdgeInsets.right = 10 - moreButton.titleEdgeInsets.left = 15 - moreButton.contentEdgeInsets.right = -15 + moreButton.titleEdgeInsets.left = spacing + moreButton.contentEdgeInsets.right = -spacing - titleLabel.rightAnchor.constraint(equalTo: moreButton.leftAnchor, constant: -20).isActive = true - detailLabel.rightAnchor.constraint(equalTo: moreButton.leftAnchor, constant: -20).isActive = true + titleLabel.rightAnchor.constraint(equalTo: moreButton.leftAnchor, constant: -horizontalMargin).isActive = true + detailLabel.rightAnchor.constraint(equalTo: moreButton.leftAnchor, constant: -horizontalMargin).isActive = true - iconView.widthAnchor.constraint(equalToConstant: 60).isActive = true + iconView.widthAnchor.constraint(equalToConstant: moreButtonWidth).isActive = true iconView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true - titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 20).isActive = true + titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: horizontalMargin).isActive = true titleLabel.bottomAnchor.constraint(equalTo: detailLabel.topAnchor, constant: -5).isActive = true - detailLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -20).isActive = true + detailLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -horizontalMargin).isActive = true iconView.setContentHuggingPriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.vertical) titleLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.vertical) @@ -187,6 +200,36 @@ class ClientItemCell: ThemeTableViewCell { self.moreButton.setAttributedTitle(moreTitle, for: .normal) } + // MARK: - Editing mode + + func setMoreButton(hidden:Bool, animated: Bool = false) { + if hidden { + moreButtonWidthConstraint?.constant = 0 + } else { + moreButtonWidthConstraint?.constant = moreButtonWidth + } + moreButton.isHidden = hidden + if animated { + UIView.animate(withDuration: 0.25) { + self.contentView.layoutIfNeeded() + } + } else { + self.contentView.layoutIfNeeded() + } + } + + override func setEditing(_ editing: Bool, animated: Bool) { + super.setEditing(editing, animated: animated) + + if editing { + setMoreButton(hidden: true, animated: animated) + } else { + if let item = self.item { + setMoreButton(hidden: item.isPlaceholder ? true : false, animated: animated) + } + } + } + // MARK: - Actions @objc func moreButtonTapped() { self.delegate?.moreButtonTapped(cell: self) diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index d2615cbbb..7f23fbf4a 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -29,8 +29,9 @@ class ClientQueryViewController: UITableViewController, Themeable { var query : OCQuery var items : [OCItem] = [] + var actions : [Action]? - var selectedItem: OCItem? + var actions : [Action]? var queryProgressSummary : ProgressSummary? { willSet { @@ -48,6 +49,14 @@ class ClientQueryViewController: UITableViewController, Themeable { var progressSummarizer : ProgressSummarizer? var refreshController: UIRefreshControl? + let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + + var deleteMultipleBarButtonItem: UIBarButtonItem? + var moveMultipleBarButtonItem: UIBarButtonItem? + + var selectBarButton: UIBarButtonItem? + var uploadBarButton: UIBarButtonItem? + // MARK: - Init & Deinit public init(core inCore: OCCore, query inQuery: OCQuery) { @@ -146,9 +155,20 @@ class ClientQueryViewController: UITableViewController, Themeable { self.tableView.dropDelegate = self self.tableView.dragInteractionEnabled = true - let actionsBarButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(uploadsBarButtonPressed)) - actionsBarButton.accessibilityLabel = "Upload files".localized - self.navigationItem.rightBarButtonItem = actionsBarButton + self.tableView.allowsMultipleSelectionDuringEditing = true + + uploadBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(uploadsBarButtonPressed)) + selectBarButton = UIBarButtonItem(title: "Select".localized, style: .done, target: self, action: #selector(multipleSelectionButtonPressed)) + self.navigationItem.rightBarButtonItems = [selectBarButton!, uploadBarButton!] + + // Create bar button items for the toolbar + deleteMultipleBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(actOnMultipleItems)) + deleteMultipleBarButtonItem?.actionIdentifier = DeleteAction.identifier + deleteMultipleBarButtonItem?.isEnabled = false + + moveMultipleBarButtonItem = UIBarButtonItem(barButtonSystemItem: .organize, target: self, action: #selector(actOnMultipleItems)) + moveMultipleBarButtonItem?.actionIdentifier = MoveAction.identifier + moveMultipleBarButtonItem?.isEnabled = false } private var viewControllerVisible : Bool = false @@ -205,7 +225,7 @@ class ClientQueryViewController: UITableViewController, Themeable { switch query.state { case .idle: - OnMainThread { + OnMainThread{ if !self.refreshController!.isRefreshing { self.refreshController?.beginRefreshing() } @@ -267,23 +287,41 @@ class ClientQueryViewController: UITableViewController, Themeable { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let rowItem : OCItem = itemAtIndexPath(indexPath) - - if let core = self.core { - switch rowItem.type { - case .collection: - if let path = rowItem.path { - self.navigationController?.pushViewController(ClientQueryViewController(core: core, query: OCQuery(forPath: path)), animated: true) - } - - case .file: - let itemViewController = DisplayHostViewController(for: rowItem, with: core, root: query.rootItem!) - self.navigationController?.pushViewController(itemViewController, animated: true) - } - } - - tableView.deselectRow(at: indexPath, animated: true) - } + // If not in multiple-selection mode, just navigate to the file or folder (collection) + if !self.tableView.isEditing { + let rowItem : OCItem = itemAtIndexPath(indexPath) + + if let core = self.core { + switch rowItem.type { + case .collection: + if let path = rowItem.path { + self.navigationController?.pushViewController(ClientQueryViewController(core: core, query: OCQuery(forPath: path)), animated: true) + } + case .file: + let itemViewController = DisplayHostViewController(for: rowItem, with: core, root: query.rootItem!) + self.navigationController?.pushViewController(itemViewController, animated: true) + } + } + + tableView.deselectRow(at: indexPath, animated: true) + } else { + updateToolbarItems() + } + } + + override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + if tableView.isEditing { + updateToolbarItems() + } + } + + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if tableView.isEditing { + return true + } else { + return true + } + } func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { return true @@ -530,7 +568,92 @@ class ClientQueryViewController: UITableViewController, Themeable { } } + // MARK: - Toolbar actions handling multiply selected items + + func updateToolbarItems() { + guard let tabBarController = self.tabBarController as? ClientRootViewController else { return } + + guard let toolbarItems = tabBarController.toolbar?.items else { return } + + // Do we have selected items? + if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { + if selectedIndexPaths.count > 0 { + + if let core = self.core { + // Get array of OCItems from selected table view index paths + var selectedItems = [OCItem]() + for indexPath in selectedIndexPaths { + selectedItems.append(itemAtIndexPath(indexPath)) + } + + // Get possible associated actions + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .toolbar) + let actionContext = ActionContext(viewController: self, core: core, items: selectedItems, 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 + } + } + } + } + } else { + self.actions = nil + for item in toolbarItems { + item.isEnabled = false + } + } + } + + func leaveMultipleSelection() { + self.tableView.setEditing(false, animated: true) + selectBarButton?.title = "Select".localized + self.navigationItem.rightBarButtonItems = [selectBarButton!, uploadBarButton!] + removeToolbar() + } + + @objc func actOnMultipleItems(_ sender: UIBarButtonItem) { + + // Find associated action + if let action = self.actions?.first(where: {type(of:$0).identifier == sender.actionIdentifier}) { + // Configure progress handler + action.progressHandler = { [weak self] progress in + self?.progressSummarizer?.startTracking(progress: progress) + } + + action.completionHandler = { [weak self] _ in + DispatchQueue.main.async { + self?.leaveMultipleSelection() + } + } + + // Execute the action + action.willRun() + action.run() + } + } + // MARK: - Navigation Bar Actions + + @objc func multipleSelectionButtonPressed(_ sender: UIBarButtonItem) { + + if !self.tableView.isEditing { + updateToolbarItems() + self.tableView.setEditing(true, animated: true) + selectBarButton?.title = "Done".localized + self.populateToolbar(with: [moveMultipleBarButtonItem!, flexibleSpaceBarButton, deleteMultipleBarButtonItem!]) + self.navigationItem.rightBarButtonItems = [selectBarButton!] + + } else { + leaveMultipleSelection() + } + } + @objc func uploadsBarButtonPressed(_ sender: UIBarButtonItem) { let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 5e03d7e7c..4560d92d4 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -25,7 +25,8 @@ class ClientRootViewController: UITabBarController { var filesNavigationController : ThemeNavigationController? var progressBar : CollapsibleProgressBar? var progressSummarizer : ProgressSummarizer? - + var toolbar:UIToolbar? + var connectionStatusObservation : NSKeyValueObservation? var connectionStatusSummary : ProgressSummary? { willSet { @@ -155,6 +156,20 @@ class ClientRootViewController: UITabBarController { self.tabBar.applyThemeCollection(Theme.shared.activeCollection) + toolbar = UIToolbar(frame: CGRect.zero) + toolbar?.translatesAutoresizingMaskIntoConstraints = false + toolbar?.insetsLayoutMarginsFromSafeArea = true + + self.view.addSubview(toolbar!) + + toolbar?.applyThemeCollection(Theme.shared.activeCollection) + toolbar?.leftAnchor.constraint(equalTo: self.tabBar.leftAnchor).isActive = true + toolbar?.rightAnchor.constraint(equalTo: self.tabBar.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 + self.viewControllers = [filesNavigationController] as? [UIViewController] } diff --git a/ownCloud/UIKit Extensions/UIBarButtonItem+Extension.swift b/ownCloud/UIKit Extensions/UIBarButtonItem+Extension.swift new file mode 100644 index 000000000..3fa25a331 --- /dev/null +++ b/ownCloud/UIKit Extensions/UIBarButtonItem+Extension.swift @@ -0,0 +1,31 @@ +// +// UIBarButtonItem+Extension.swift +// ownCloud +// +// Created by Michael Neuwert on 24.01.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +import Foundation +import UIKit +import ownCloudSDK + +public extension UIBarButtonItem { + + + private struct AssociatedKeys { + static var actionKey = "actionKey" + } + + public 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) + } + } + } +} diff --git a/ownCloud/UIKit Extensions/UIViewController+Extension.swift b/ownCloud/UIKit Extensions/UIViewController+Extension.swift new file mode 100644 index 000000000..237f81810 --- /dev/null +++ b/ownCloud/UIKit Extensions/UIViewController+Extension.swift @@ -0,0 +1,27 @@ +// +// UIViewController+Extension.swift +// ownCloud +// +// Created by Michael Neuwert on 23.01.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +import UIKit + +extension UIViewController { + func populateToolbar(with items:[UIBarButtonItem]) { + if let tabBarController = self.tabBarController as? ClientRootViewController { + tabBarController.toolbar?.isHidden = false + tabBarController.tabBar.isHidden = true + tabBarController.toolbar?.setItems(items, animated: true) + } + } + + func removeToolbar() { + if let tabBarController = self.tabBarController as? ClientRootViewController { + tabBarController.toolbar?.isHidden = true + tabBarController.tabBar.isHidden = false + tabBarController.toolbar?.setItems(nil, animated: true) + } + } +}