From ea6c868de1de9edd3b7c24c60166a5e92b29bedd Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 17 Mar 2021 22:38:36 +0100 Subject: [PATCH 01/37] Port changes from feature/globalsearch to milestone/11.6 --- .../ClientQueryViewController.swift | 131 +++++++++++-- .../QueryFileListTableViewController.swift | 176 +++++++++++------- .../Client/User Interface/SortBar.swift | 76 +++++++- 3 files changed, 293 insertions(+), 90 deletions(-) diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 2a200404b..276fe79f3 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -29,7 +29,7 @@ public struct OCItemDraggingValue { var bookmarkUUID : String } -open class ClientQueryViewController: QueryFileListTableViewController, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate { +open class ClientQueryViewController: QueryFileListTableViewController, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate, UISearchControllerDelegate { public var folderActionBarButton: UIBarButtonItem? public var plusBarButton: UIBarButtonItem? @@ -43,6 +43,47 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn private let ItemDataUTI = "com.owncloud.ios-app.item-data" + private var _query : OCQuery + public override var query : OCQuery { + set { + _query = newValue + } + + get { + if let customSearchQuery = customSearchQuery { + return customSearchQuery + } else { + return _query + } + } + } + var customSearchQuery : OCQuery? { + willSet { + if customSearchQuery != newValue, let query = customSearchQuery { + core?.stop(query) + query.delegate = nil + } + } + + didSet { + if customSearchQuery != nil, let query = customSearchQuery { + query.delegate = self + query.sortComparator = sortMethod.comparator(direction: sortDirection) + core?.start(query) + } + } + } + open override var searchScope: SearchScope { + set { + UserDefaults.standard.setValue(newValue.rawValue, forKey: "search-scope") + } + + get { + let scope = SearchScope(rawValue: UserDefaults.standard.integer(forKey: "search-scope")) ?? SearchScope.local + return scope + } + } + // MARK: - Init & Deinit public override convenience init(core inCore: OCCore, query inQuery: OCQuery) { self.init(core: inCore, query: inQuery, rootViewController: nil) @@ -50,6 +91,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn public init(core inCore: OCCore, query inQuery: OCQuery, rootViewController: UIViewController?) { clientRootViewController = rootViewController + _query = inQuery super.init(core: inCore, query: inQuery) updateTitleView() @@ -106,6 +148,49 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } } + // MARK: - Search events + open func willPresentSearchController(_ searchController: UISearchController) { + self.sortBar?.showSearchScope = true + } + + open func willDismissSearchController(_ searchController: UISearchController) { + self.sortBar?.showSearchScope = false + } + + // MARK: - Search scope support + private var searchText: String? + + open override func applySearchFilter(for searchText: String?, to query: OCQuery) { + self.searchText = searchText + + updateCustomSearchQuery() + } + + open override func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SearchScope) { + updateCustomSearchQuery() + } + + open override func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { + sortMethod = didUpdateSortMethod + + let comparator = sortMethod.comparator(direction: sortDirection) + + _query.sortComparator = comparator + customSearchQuery?.sortComparator = comparator + } + + func updateCustomSearchQuery() { + if let searchText = searchText, let searchScope = sortBar?.searchScope, searchScope == .global { + self.customSearchQuery = OCQuery(condition: .where(.name, contains: searchText), inputFilter: nil) + } else { + self.customSearchQuery = nil + } + + super.applySearchFilter(for: searchText, to: _query) + + self.queryHasChangesAvailable(query) + } + // MARK: - View controller events open override func viewDidLoad() { super.viewDidLoad() @@ -136,6 +221,12 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn 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) @@ -364,23 +455,29 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn // MARK: - Updates open override func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { - if let rootItem = self.query.rootItem { - if query.queryPath != "/" { - var totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) - if self.items.count == 1 { - totalSize = String(format: "%@ item | ", "\(self.items.count)") + totalSize - } else if self.items.count > 1 { - totalSize = String(format: "%@ items | ", "\(self.items.count)") + totalSize - } - self.updateFooter(text: totalSize) - } - - if #available(iOS 13.0, *) { - 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 + if query == self.query { + super.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) + + if let rootItem = self.query.rootItem, searchText == nil { + if query.queryPath != "/" { + var totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) + if self.items.count == 1 { + totalSize = String(format: "%@ item | ", "\(self.items.count)") + totalSize + } else if self.items.count > 1 { + totalSize = String(format: "%@ items | ", "\(self.items.count)") + totalSize + } + self.updateFooter(text: totalSize) + } + + if #available(iOS 13.0, *) { + 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) } } } diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 02b5c8172..4fd4f915d 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -72,6 +72,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa public init(core inCore: OCCore, query inQuery: OCQuery) { query = inQuery + searchScope = .global super.init(core: inCore) @@ -113,14 +114,15 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa return sort } } + open var searchScope: SearchScope = .local open var sortDirection: SortDirection { set { UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-direction") } get { - let sort = SortDirection(rawValue: UserDefaults.standard.integer(forKey: "sort-direction")) ?? SortDirection.ascendant - return sort + let direction = SortDirection(rawValue: UserDefaults.standard.integer(forKey: "sort-direction")) ?? SortDirection.ascendant + return direction } } @@ -131,27 +133,31 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa open func updateSearchResults(for searchController: UISearchController) { let searchText = searchController.searchBar.text! - let filterHandler: OCQueryFilterHandler = { (_, _, item) -> Bool in - if let itemName = item?.name { - return itemName.localizedCaseInsensitiveContains(searchText) - } - return false - } - - if searchText == "" { - if let filter = query.filter(withIdentifier: "text-search") { - query.removeFilter(filter) - } - } else { - 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") - } - } - } + applySearchFilter(for: (searchText == "") ? nil : searchText, to: query) + } + + open func applySearchFilter(for searchText: String?, to query: OCQuery) { + if let searchText = searchText { + let filterHandler: OCQueryFilterHandler = { (_, _, item) -> Bool in + if let itemName = item?.name { + return itemName.localizedCaseInsensitiveContains(searchText) + } + 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 @@ -241,48 +247,48 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa queryRefreshRateLimiter.runRateLimitedBlock { query.requestChangeSet(withFlags: .onlyResults) { (query, changeSet) in OnMainThread { - 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: .toolbar) - 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, 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.tableView.reloadData() - case .targetRemoved: - self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) - self.tableView.reloadData() - - default: - self.messageView?.message(show: false) - } +// 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: .toolbar) +// 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, 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.tableView.reloadData() +// case .targetRemoved: +// self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) +// self.tableView.reloadData() +// +// default: +// self.messageView?.message(show: false) +// } self.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) } @@ -291,6 +297,42 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa } open func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { + if query.state.isFinal { + OnMainThread { + if self.pullToRefreshControl?.isRefreshing == true { + self.pullToRefreshControl?.endRefreshing() + } + } + } + + let previousItemCount = self.items.count + + self.items = changeSet?.queryResult ?? [] + + 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, 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.tableView.reloadData() + case .targetRemoved: + self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) + self.tableView.reloadData() + + default: + self.messageView?.message(show: false) + } } // MARK: - Themeable @@ -322,6 +364,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa 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?.updateForCurrentTraitCollection() sortBar?.showSelectButton = showSelectButton @@ -449,6 +492,9 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa return 0 } + open func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SearchScope) { + } + open func toggleSelectMode() { if let multiSelectionSupport = self as? MultiSelectSupport { if !tableView.isEditing { diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index 2de0f9817..7f4ab1756 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -35,13 +35,32 @@ public class SegmentedControl: UISegmentedControl { } } +public enum SearchScope : Int, CaseIterable { + case global + case local + + var label : String { + var name : String! + + switch self { + case .global: name = "all".localized + case .local: name = "folder".localized + } + + return name + } +} + public protocol SortBarDelegate: class { var sortDirection: SortDirection { get set } var sortMethod: SortMethod { get set } + var searchScope: SearchScope { get set } func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) + func sortBar(_ sortBar: SortBar, didUpdateSearchScope: SearchScope) + func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) func toggleSelectMode() @@ -56,7 +75,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } // MARK: - Constants - let sideButtonsSize: CGSize = CGSize(width: 44.0, height: 44.0) + let sideButtonsSize: CGSize = CGSize(width: 22.0, height: 22.0) let leftPadding: CGFloat = 20.0 let rightPadding: CGFloat = 20.0 let rightSelectButtonPadding: CGFloat = 8.0 @@ -67,6 +86,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate public var sortSegmentedControl: SegmentedControl? public var sortButton: UIButton? + public var searchScopeSegmentedControl : SegmentedControl? public var selectButton: UIButton? public var showSelectButton: Bool = false { didSet { @@ -78,6 +98,19 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } } + var showSearchScope: Bool = false { + didSet { + self.searchScopeSegmentedControl?.isHidden = false + self.searchScopeSegmentedControl?.alpha = oldValue ? 1.0 : 0.0 + + 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) @@ -108,36 +141,57 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } } + public var searchScope : SearchScope { + didSet { + delegate?.searchScope = searchScope + searchScopeSegmentedControl?.selectedSegmentIndex = searchScope.rawValue + } + } + // MARK: - Init & Deinit - public init(frame: CGRect, sortMethod: SortMethod) { + public init(frame: CGRect, sortMethod: SortMethod, searchScope: SearchScope = .local) { sortSegmentedControl = SegmentedControl() selectButton = UIButton() sortButton = UIButton(type: .system) + searchScopeSegmentedControl = SegmentedControl() self.sortMethod = sortMethod + self.searchScope = searchScope super.init(frame: frame) - if let sortButton = sortButton, let sortSegmentedControl = sortSegmentedControl, let selectButton = selectButton { + if let sortButton = sortButton, let sortSegmentedControl = sortSegmentedControl, let searchScopeSegmentedControl = searchScopeSegmentedControl, let selectButton = selectButton { sortButton.translatesAutoresizingMaskIntoConstraints = false sortSegmentedControl.translatesAutoresizingMaskIntoConstraints = false selectButton.translatesAutoresizingMaskIntoConstraints = false + searchScopeSegmentedControl.translatesAutoresizingMaskIntoConstraints = false sortButton.accessibilityIdentifier = "sort-bar.sortButton" sortSegmentedControl.accessibilityIdentifier = "sort-bar.segmentedControl" + searchScopeSegmentedControl.accessibilityIdentifier = "sort-bar.searchScopeSegmentedControl" + + for scope in SearchScope.allCases { + searchScopeSegmentedControl.insertSegment(withTitle: scope.label, at: scope.rawValue, animated:false) + } + searchScopeSegmentedControl.selectedSegmentIndex = searchScope.rawValue + searchScopeSegmentedControl.isHidden = !self.showSearchScope + searchScopeSegmentedControl.addTarget(self, action: #selector(searchScopeValueChanged), for: .valueChanged) self.addSubview(sortSegmentedControl) self.addSubview(sortButton) + self.addSubview(searchScopeSegmentedControl) self.addSubview(selectButton) // Sort segmented control NSLayoutConstraint.activate([ sortSegmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: topPadding), sortSegmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -bottomPadding), - sortSegmentedControl.centerXAnchor.constraint(equalTo: self.centerXAnchor), - sortSegmentedControl.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor, constant: leftPadding), - sortSegmentedControl.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -rightPadding) + sortSegmentedControl.leftAnchor.constraint(equalTo: self.leftAnchor, constant: leftPadding), + + searchScopeSegmentedControl.trailingAnchor.constraint(equalTo: selectButton.leadingAnchor, constant: -10), + searchScopeSegmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: topPadding), + searchScopeSegmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -bottomPadding) ]) var longestTitleWidth : CGFloat = 0.0 @@ -203,8 +257,6 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate Theme.shared.register(client: self) selectButton?.isHidden = !showSelectButton - selectButton?.accessibilityElementsHidden = !showSelectButton - selectButton?.isEnabled = showSelectButton updateForCurrentTraitCollection() } @@ -222,6 +274,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate self.sortButton?.applyThemeCollection(collection) self.selectButton?.applyThemeCollection(collection) self.sortSegmentedControl?.applyThemeCollection(collection) + self.searchScopeSegmentedControl?.applyThemeCollection(collection) self.backgroundColor = collection.navigationBarColors.backgroundColor } @@ -306,6 +359,13 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } } + @objc private func searchScopeValueChanged() { + if let selectedIndex = searchScopeSegmentedControl?.selectedSegmentIndex { + self.searchScope = SearchScope(rawValue: selectedIndex)! + delegate?.sortBar(self, didUpdateSearchScope: self.searchScope) + } + } + @objc private func toggleSelectMode() { delegate?.toggleSelectMode() } From e059a8d84fc3f5a400a04173f575426c69be4fe8 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 17 Mar 2021 23:05:26 +0100 Subject: [PATCH 02/37] - clean up QueryFileListTableViewController.swift - replace all/folder text in scope selection with icons on iOS 13+ --- .../QueryFileListTableViewController.swift | 50 +++---------------- .../Client/User Interface/SortBar.swift | 19 ++++++- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 4fd4f915d..aac9f3de5 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -247,49 +247,6 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa queryRefreshRateLimiter.runRateLimitedBlock { query.requestChangeSet(withFlags: .onlyResults) { (query, changeSet) in OnMainThread { -// 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: .toolbar) -// 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, 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.tableView.reloadData() -// case .targetRemoved: -// self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) -// self.tableView.reloadData() -// -// default: -// self.messageView?.message(show: false) -// } - self.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) } } @@ -309,6 +266,12 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa self.items = changeSet?.queryResult ?? [] + // Setup new action context + if let core = self.core { + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .toolbar) + 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 { @@ -326,6 +289,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa } self.tableView.reloadData() + case .targetRemoved: self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) self.tableView.reloadData() diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index 7f4ab1756..ecfb1bcf7 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -49,6 +49,19 @@ public enum SearchScope : Int, CaseIterable { return name } + + var image : UIImage? { + var image : UIImage? + + if #available(iOS 13, *) { + switch self { + case .global: image = UIImage(systemName: "globe") + case .local: image = UIImage(systemName: "folder") + } + } + + return image + } } public protocol SortBarDelegate: class { @@ -172,7 +185,11 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate searchScopeSegmentedControl.accessibilityIdentifier = "sort-bar.searchScopeSegmentedControl" for scope in SearchScope.allCases { - searchScopeSegmentedControl.insertSegment(withTitle: scope.label, at: scope.rawValue, animated:false) + if let image = scope.image { + searchScopeSegmentedControl.insertSegment(with: image, at: scope.rawValue, animated: false) + } else { + searchScopeSegmentedControl.insertSegment(withTitle: scope.label, at: scope.rawValue, animated: false) + } } searchScopeSegmentedControl.selectedSegmentIndex = searchScope.rawValue searchScopeSegmentedControl.isHidden = !self.showSearchScope From 457204037a3b382dba005e5b9dae89fecfb8b9ee Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 18 Mar 2021 00:19:09 +0100 Subject: [PATCH 03/37] - SortMethod - turn localizedName from a function into a property - add sortPropertyName to simplify construction of custom OCQuerys - ClientQueryViewController - add sort property, sort order and result limits to OCQueryCondition - refresh search query when changing sort order and peak results --- ios-sdk | 2 +- ownCloud/Key Commands/KeyCommands.swift | 4 +- .../ClientQueryViewController.swift | 16 +++++++- .../Client/User Interface/SortBar.swift | 12 +++--- .../Client/User Interface/SortMethod.swift | 40 ++++++++++++++----- .../SortMethodTableViewController.swift | 2 +- 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/ios-sdk b/ios-sdk index 29c2eb835..fc927e0f9 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 29c2eb835bbd017f0d417e9c77359d909215fe50 +Subproject commit fc927e0f9a4f60dd19a6dfada30ff3267bc9841b diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index f01dcaf12..b3c0240db 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -694,7 +694,7 @@ extension QueryFileListTableViewController { shortcuts.append(toggleSortCommand) for (index, method) in SortMethod.all.enumerated() { - let sortTitle = String(format: "Sort by %@".localized, method.localizedName()) + let sortTitle = String(format: "Sort by %@".localized, method.localizedName) let sortCommand = UIKeyCommand(input: String(index + 1), modifierFlags: [.command, .alternate], action: #selector(changeSortMethod), discoverabilityTitle: sortTitle) shortcuts.append(sortCommand) } @@ -761,7 +761,7 @@ extension QueryFileListTableViewController { @objc func changeSortMethod(_ command : UIKeyCommand) { for method in SortMethod.all { - let sortTitle = String(format: "Sort by %@".localized, method.localizedName()) + let sortTitle = String(format: "Sort by %@".localized, method.localizedName) if command.discoverabilityTitle == sortTitle { self.sortBar?.sortMethod = method break diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 276fe79f3..de176521d 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -159,6 +159,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn // MARK: - Search scope support private var searchText: String? + private let maxResultCount = 3 open override func applySearchFilter(for searchText: String?, to query: OCQuery) { self.searchText = searchText @@ -177,11 +178,24 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn _query.sortComparator = comparator customSearchQuery?.sortComparator = comparator + + if (customSearchQuery?.queryResults?.count ?? 0) >= maxResultCount { + updateCustomSearchQuery() + } } func updateCustomSearchQuery() { if let searchText = searchText, let searchScope = sortBar?.searchScope, searchScope == .global { - self.customSearchQuery = OCQuery(condition: .where(.name, contains: searchText), inputFilter: nil) + let condition : OCQueryCondition = .where(.name, contains: 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 } diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index ecfb1bcf7..20f3ff8cf 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -139,15 +139,15 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } updateSortButtonTitle() - sortButton?.accessibilityLabel = NSString(format: "Sort by %@".localized as NSString, sortMethod.localizedName()) as String + sortButton?.accessibilityLabel = NSString(format: "Sort by %@".localized as NSString, sortMethod.localizedName) as String sortButton?.sizeToFit() if let oldSementIndex = SortMethod.all.index(of: oldValue) { - sortSegmentedControl?.setTitle(oldValue.localizedName(), forSegmentAt: oldSementIndex) + sortSegmentedControl?.setTitle(oldValue.localizedName, forSegmentAt: oldSementIndex) } if let segmentIndex = SortMethod.all.index(of: sortMethod) { sortSegmentedControl?.selectedSegmentIndex = segmentIndex - sortSegmentedControl?.setTitle(sortDirectionTitle(sortMethod.localizedName()), forSegmentAt: segmentIndex) + sortSegmentedControl?.setTitle(sortDirectionTitle(sortMethod.localizedName), forSegmentAt: segmentIndex) } delegate?.sortBar(self, didUpdateSortMethod: sortMethod) @@ -213,8 +213,8 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate var longestTitleWidth : CGFloat = 0.0 for method in SortMethod.all { - sortSegmentedControl.insertSegment(withTitle: method.localizedName(), at: SortMethod.all.index(of: method)!, animated: false) - let titleWidth = method.localizedName().appending(" ↓").width(withConstrainedHeight: sortSegmentedControl.frame.size.height, font: UIFont.systemFont(ofSize: 16.0)) + sortSegmentedControl.insertSegment(withTitle: method.localizedName, at: SortMethod.all.index(of: method)!, animated: false) + let titleWidth = method.localizedName.appending(" ↓").width(withConstrainedHeight: sortSegmentedControl.frame.size.height, font: UIFont.systemFont(ofSize: 16.0)) if titleWidth > longestTitleWidth { longestTitleWidth = titleWidth } @@ -326,7 +326,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate // MARK: - Sort Direction Title func updateSortButtonTitle() { - let title = NSString(format: "Sort by %@".localized as NSString, sortMethod.localizedName()) as String + let title = NSString(format: "Sort by %@".localized as NSString, sortMethod.localizedName) as String sortButton?.setTitle(sortDirectionTitle(title), for: .normal) } diff --git a/ownCloudAppShared/Client/User Interface/SortMethod.swift b/ownCloudAppShared/Client/User Interface/SortMethod.swift index d02ee2c9c..075eedfd1 100644 --- a/ownCloudAppShared/Client/User Interface/SortMethod.swift +++ b/ownCloudAppShared/Client/User Interface/SortMethod.swift @@ -37,25 +37,43 @@ public enum SortMethod: Int { public static var all: [SortMethod] = [alphabetically, kind, size, date, shared] - public func localizedName() -> String { + public var localizedName : String { var name = "" switch self { - case .alphabetically: - name = "name".localized - case .kind: - name = "kind".localized - case .size: - name = "size".localized - case .date: - name = "date".localized - case .shared: - name = "shared".localized + case .alphabetically: + name = "name".localized + case .kind: + name = "kind".localized + case .size: + name = "size".localized + case .date: + name = "date".localized + case .shared: + name = "shared".localized } return name } + public var sortPropertyName : OCItemPropertyName? { + var propertyName : OCItemPropertyName? + + switch self { + case .alphabetically: + propertyName = .name + case .kind: + propertyName = .mimeType + case .size: + propertyName = .size + case .date: + propertyName = .lastModified + case .shared: break + } + + return propertyName + } + public func comparator(direction: SortDirection) -> OCSort { var comparator: OCSort var combinedComparator: OCSort? diff --git a/ownCloudAppShared/Client/User Interface/SortMethodTableViewController.swift b/ownCloudAppShared/Client/User Interface/SortMethodTableViewController.swift index 8ffab04e8..5d051b1d3 100644 --- a/ownCloudAppShared/Client/User Interface/SortMethodTableViewController.swift +++ b/ownCloudAppShared/Client/User Interface/SortMethodTableViewController.swift @@ -40,7 +40,7 @@ class SortMethodTableViewController: StaticTableViewController { self.preferredContentSize = CGSize(width: contentWidth, height: contentHeight) for method in SortMethod.all { - let title = method.localizedName() + let title = method.localizedName var sortDirectionTitle = "" if sortBarDelegate?.sortMethod == method { From e53e51adf5f2e044785f6307e32793149eb846b2 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 23 Mar 2021 17:44:56 +0100 Subject: [PATCH 04/37] =?UTF-8?q?-=20UIView+Extension:=20add=20simply=20me?= =?UTF-8?q?thods=20to=20start/stop=20a=20pulsing=20effect=20on=20a=20view?= =?UTF-8?q?=20-=20ServerListTableViewController=20=09-=20add=20user=20acti?= =?UTF-8?q?vity=20to=20allow=20state=20restoring=20to=20server=20list=20(p?= =?UTF-8?q?reviously=20only=20*always*=20to=20the=20last=20opened=20bookma?= =?UTF-8?q?rk)=20=09-=20remove=20auto=20login=20code=20for=20pre-iOS=2013?= =?UTF-8?q?=20(in=20iOS=2013=20that's=20handled=20by=20scene/state=20resto?= =?UTF-8?q?ration)=20=09-=20add=20inline=20status=20reporting=20for=20data?= =?UTF-8?q?base=20migrations=20-=20SceneDelegate:=20add=20support=20to=20s?= =?UTF-8?q?tate=20restore=20to=20server=20list=20-=20ClientQueryViewContro?= =?UTF-8?q?ller=20=09-=20switch=20to=20search=20tokenizer=20to=20construct?= =?UTF-8?q?=20OCQueryCondition=20=09-=20make=20sure=20sort=20/=20search=20?= =?UTF-8?q?options=20are=20no=20longer=20hidden=20when=20showing=20large?= =?UTF-8?q?=20"No=20results"=20message=20-=20ClientRootViewController=20?= =?UTF-8?q?=09-=20add=20support=20for=20migration=20progress=20reporting?= =?UTF-8?q?=20/=20OCCoreBusyStatusHandler=20passing=20=09-=20fix=20OCFileP?= =?UTF-8?q?roviderServiceStandby=20leak=20if=20OCCoreManager=20returns=20a?= =?UTF-8?q?=20request=20with=20an=20error=20-=20FileProvider=20extension?= =?UTF-8?q?=20=09-=20return=20an=20error=20when=20trying=20to=20access=20a?= =?UTF-8?q?n=20account=20whose=20database=20needs=20migration=20=09-=20han?= =?UTF-8?q?dle=20case=20where=20no=20OCCore=20is=20available=20-=20Message?= =?UTF-8?q?View:=20add=20option=20to=20show=20messages=20with=20insets=20a?= =?UTF-8?q?t=20the=20edges=20-=20NSDate+ComputedTimes:=20=09-=20simplified?= =?UTF-8?q?=20computation=20of=20beginning=20of=20days,=20weeks,=20months?= =?UTF-8?q?=20and=20years,=20with=20support=20for=20offsets=20=09-=20add?= =?UTF-8?q?=20unit=20test=20-=20OCQueryCondition+SearchSegmenter:=20=09-?= =?UTF-8?q?=20segmentation=20of=20search=20queries=20into=20terms=20and=20?= =?UTF-8?q?"keywords"=20=09-=20supports=20placing=20terms=20in=20""=20as?= =?UTF-8?q?=20well=20as=20unclosed=20"=20=09-=20keyword=20support=20to=20f?= =?UTF-8?q?ilter=20for:=20=09=09-=20files=20(:file),=20folders=20(:folder)?= =?UTF-8?q?,=20=09=09-=20images=20(:image),=20video=20(:video)=20=09=09-?= =?UTF-8?q?=20time=20frames=20(:today,=20:week,=20:month,=20:year)=20=09?= =?UTF-8?q?=09-=20dynamic=20time=20frames=20(:7d,=20:2w,=20:1m,=20:1y,=20d?= =?UTF-8?q?ays:7,=20weeks:2,=20month:1,=20year:1,=20=E2=80=A6)=20=09=09-?= =?UTF-8?q?=20file=20types/suffixes=20(type:jpg)=20=09-=20add=20unit=20tes?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios-sdk | 2 +- .../FileProviderExtension.m | 69 ++++- ownCloud.xcodeproj/project.pbxproj | 20 ++ .../xcshareddata/xcschemes/MakeTVG.xcscheme | 2 +- .../xcschemes/ownCloud File Provider.xcscheme | 14 +- .../ownCloud File ProviderUI.xcscheme | 2 +- .../xcschemes/ownCloud Intents.xcscheme | 2 +- .../ownCloud Share Extension.xcscheme | 2 +- .../xcshareddata/xcschemes/ownCloud.xcscheme | 2 +- .../xcschemes/ownCloudApp.xcscheme | 2 +- .../ownCloudScreenshotsTests.xcscheme | 2 +- .../xcschemes/ownCloudTests.xcscheme | 2 +- .../Client/ClientRootViewController.swift | 24 +- ownCloud/SceneDelegate.swift | 33 ++- .../ServerListTableViewController.swift | 68 ++++- .../NSDate+ComputedTimes.h | 32 +++ .../NSDate+ComputedTimes.m | 101 +++++++ .../OCQueryCondition+SearchSegmenter.h | 36 +++ .../OCQueryCondition+SearchSegmenter.m | 265 ++++++++++++++++++ ownCloudAppFramework/ownCloudApp.h | 2 + .../SearchSegmentationTests.m | 79 ++++++ .../ClientQueryViewController.swift | 17 +- .../QueryFileListTableViewController.swift | 2 +- .../Client/User Interface/MessageView.swift | 8 +- .../UIKit Extension/UIView+Extension.swift | 16 ++ 25 files changed, 736 insertions(+), 68 deletions(-) create mode 100644 ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h create mode 100644 ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m create mode 100644 ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h create mode 100644 ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m create mode 100644 ownCloudAppFrameworkTests/SearchSegmentationTests.m diff --git a/ios-sdk b/ios-sdk index fc927e0f9..ffa4e97c5 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit fc927e0f9a4f60dd19a6dfada30ff3267bc9841b +Subproject commit ffa4e97c5bdc8ab6ec7751b66940d7f8e51af519 diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index 6043049dc..289c6b451 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -175,25 +175,44 @@ - (NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier OCSyncExec(itemRetrieval, { // Resolve the given identifier to a record in the model - if ([identifier isEqual:NSFileProviderRootContainerItemIdentifier]) + NSError *coreError = nil; + OCCore *core = [self coreWithError:&coreError]; + + if (core != nil) { - // Root item - [self.core.vault.database retrieveCacheItemsAtPath:@"/" itemOnly:YES completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, NSArray *items) { - item = items.firstObject; - returnError = error; + if (coreError != nil) + { + returnError = coreError; + } + else + { + if ([identifier isEqual:NSFileProviderRootContainerItemIdentifier]) + { + // Root item + [self.core.vault.database retrieveCacheItemsAtPath:@"/" itemOnly:YES completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, NSArray *items) { + item = items.firstObject; + returnError = error; - OCSyncExecDone(itemRetrieval); - }]; + OCSyncExecDone(itemRetrieval); + }]; + } + else + { + // Other item + [self.core retrieveItemFromDatabaseForLocalID:(OCLocalID)identifier completionHandler:^(NSError *error, OCSyncAnchor syncAnchor, OCItem *itemFromDatabase) { + item = itemFromDatabase; + returnError = error; + + OCSyncExecDone(itemRetrieval); + }]; + } + } } else { - // Other item - [self.core retrieveItemFromDatabaseForLocalID:(OCLocalID)identifier completionHandler:^(NSError *error, OCSyncAnchor syncAnchor, OCItem *itemFromDatabase) { - item = itemFromDatabase; - returnError = error; + returnError = coreError; - OCSyncExecDone(itemRetrieval); - }]; + OCSyncExecDone(itemRetrieval); } }); @@ -1001,11 +1020,17 @@ - (OCBookmark *)bookmark } - (OCCore *)core +{ + return ([self coreWithError:nil]); +} + +- (OCCore *)coreWithError:(NSError **)outError { OCLogDebug(@"FileProviderExtension[%p].core[enter]: _core=%p, bookmark=%@", self, _core, self.bookmark); OCBookmark *bookmark = self.bookmark; __block OCCore *retCore = nil; + __block NSError *retError = nil; @synchronized(self) { @@ -1037,6 +1062,11 @@ - (OCCore *)core retCore = self->_core; } } + + if (error != nil) + { + retError = error; + } } completionHandler:^(OCCore *core, NSError *error) { if (!hasCore) { @@ -1052,6 +1082,12 @@ - (OCCore *)core retCore = self->_core; } + if (error != nil) + { + retError = error; + retCore = nil; + } + OCSyncExecDone(waitForCore); if (hasCore) @@ -1066,7 +1102,12 @@ - (OCCore *)core if (retCore == nil) { - OCLogError(@"Error getting core for domain %@ (UUID %@)", OCLogPrivate(self.domain.displayName), OCLogPrivate(self.domain.identifier)); + OCLogError(@"Error getting core for domain %@ (UUID %@): %@", OCLogPrivate(self.domain.displayName), OCLogPrivate(self.domain.identifier), OCLogPrivate(retError)); + } + + if (outError != NULL) + { + *outError = retError; } OCLogDebug(@"FileProviderExtension[%p].core[leave]: _core=%p, bookmark=%@", self, retCore, bookmark); diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index fcef57932..c1e34b0b8 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -362,6 +362,9 @@ DCAEB06121F9FC510067E147 /* EarlGrey+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB06021F9FC510067E147 /* EarlGrey+Tools.swift */; }; DCB2C05F250C1F9E001083CA /* BrandingClassSettingsSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB2C05D250C1F9E001083CA /* BrandingClassSettingsSource.h */; }; DCB2C061250C253C001083CA /* BrandingClassSettingsSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB2C05E250C1F9E001083CA /* BrandingClassSettingsSource.m */; }; + DCB458ED2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB458EB2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCB458EE2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB458EC2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m */; }; + DCB459052604AD2A006A02AB /* SearchSegmentationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB459042604AD2A006A02AB /* SearchSegmentationTests.m */; }; DCB6C4D72453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6C4D62453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift */; }; DCB6C4DE24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6C4DD24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift */; }; DCBD8EA824B3751900D92E1F /* OCItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754E123279EED00119FCB /* OCItem+Extension.swift */; }; @@ -392,6 +395,8 @@ 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, ); }; }; + DCCD778C2604C91B00098573 /* NSDate+ComputedTimes.m in Sources */ = {isa = PBXBuildFile; fileRef = DCCD776B2604C81B00098573 /* NSDate+ComputedTimes.m */; }; 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 */; }; @@ -1315,6 +1320,9 @@ 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 = ""; }; DCB44D86218718BA00DAA4CC /* VendorServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VendorServices.swift; sourceTree = ""; }; + DCB458EB2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCQueryCondition+SearchSegmenter.h"; sourceTree = ""; }; + DCB458EC2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCQueryCondition+SearchSegmenter.m"; sourceTree = ""; }; + DCB459042604AD2A006A02AB /* SearchSegmentationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SearchSegmentationTests.m; sourceTree = ""; }; DCB504D7221EF07E007638BE /* status-flash.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "status-flash.tvg"; path = "img/filetypes-tvg/status-flash.tvg"; sourceTree = SOURCE_ROOT; }; DCB6C4D62453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientAuthenticationUpdater.swift; sourceTree = ""; }; DCB6C4DD24559B1600C1EAE1 /* ClientAuthenticationUpdaterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientAuthenticationUpdaterViewController.swift; sourceTree = ""; }; @@ -1350,6 +1358,8 @@ DCC83303242CF3AC00153F8C /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; DCC8535723CE1236007BA3EB /* LicenseInAppProductListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseInAppProductListViewController.swift; sourceTree = ""; }; DCC8535F23CE1AF8007BA3EB /* PurchasesSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesSettingsSection.swift; sourceTree = ""; }; + DCCD776A2604C81B00098573 /* NSDate+ComputedTimes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDate+ComputedTimes.h"; sourceTree = ""; }; + DCCD776B2604C81B00098573 /* NSDate+ComputedTimes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDate+ComputedTimes.m"; sourceTree = ""; }; 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 = ""; }; @@ -2357,6 +2367,8 @@ DC774E6122F44E6D000B11A1 /* OCCore+BundleImport.h */, DC7C100F24B5F81E00227085 /* OCBookmark+AppExtensions.m */, DC7C100E24B5F81E00227085 /* OCBookmark+AppExtensions.h */, + DCB458EC2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m */, + DCB458EB2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h */, ); path = "SDK Extensions"; sourceTree = ""; @@ -2608,6 +2620,7 @@ isa = PBXGroup; children = ( DCC0856B2293F1FD008CC05C /* LicensingTests.m */, + DCB459042604AD2A006A02AB /* SearchSegmentationTests.m */, DCC0856D2293F1FD008CC05C /* Info.plist */, ); path = ownCloudAppFrameworkTests; @@ -2618,6 +2631,8 @@ children = ( DCC5E445232654DE002E5B84 /* NSObject+AnnotatedProperties.m */, DCC5E444232654DE002E5B84 /* NSObject+AnnotatedProperties.h */, + DCCD776B2604C81B00098573 /* NSDate+ComputedTimes.m */, + DCCD776A2604C81B00098573 /* NSDate+ComputedTimes.h */, ); path = "Foundation Extensions"; sourceTree = ""; @@ -3117,6 +3132,8 @@ DCF2DA8124C836240026D790 /* OCBookmark+FPServices.h in Headers */, DC66F3A523965A1400CF4812 /* NSDate+RFC3339.h in Headers */, DC0030C22350B1CE00BB8570 /* NSData+Encoding.h in Headers */, + DCCD77792604C91600098573 /* NSDate+ComputedTimes.h in Headers */, + DCB458ED2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.h in Headers */, DCC5E4472326564F002E5B84 /* NSObject+AnnotatedProperties.h in Headers */, DC66F3AB23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h in Headers */, DC049156258C00C400DEDC27 /* OCFileProviderServiceStandby.h in Headers */, @@ -4201,6 +4218,7 @@ files = ( DCFEFE9D2368D7FA009A142F /* OCLicenseObserver.m in Sources */, DC66F39D239659C000CF4812 /* OCASN1.m in Sources */, + DCCD778C2604C91B00098573 /* NSDate+ComputedTimes.m in Sources */, DC66F3A623965A1400CF4812 /* NSDate+RFC3339.m in Sources */, DCF2DA8724C87A330026D790 /* OCCore+FPServices.m in Sources */, DC7C101224B5FD6500227085 /* OCBookmark+AppExtensions.m in Sources */, @@ -4229,6 +4247,7 @@ DCFEFE4A23687C83009A142F /* OCLicenseEntitlement.m in Sources */, DC66F3AC23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m in Sources */, DCD8109B23984AF6003B0053 /* OCLicenseDuration.m in Sources */, + DCB458EE2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m in Sources */, DC080CF3238C92480044C5D2 /* OCLicenseAppStoreItem.m in Sources */, DCFEFE982368D099009A142F /* OCLicenseEnvironment.m in Sources */, DC049157258C00C400DEDC27 /* OCFileProviderServiceStandby.m in Sources */, @@ -4240,6 +4259,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DCB459052604AD2A006A02AB /* SearchSegmentationTests.m in Sources */, DCE442CE2387452000940A6D /* LicensingTests.m in Sources */, 39057AA7233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, ); diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme index 29e114fe2..dc35afe39 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme @@ -1,6 +1,6 @@ + + + + + + Void)) { + 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) @@ -189,14 +196,14 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa // Remove skip available offline when user opens the bookmark core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) - - // Set up FP standby - if let core = core { - self.fpServiceStandby = OCFileProviderServiceStandby(core: core) - self.fpServiceStandby?.start() - } }, completionHandler: { (core, error) in if error == nil { + // Set up FP standby + if let core = core { + self.fpServiceStandby = OCFileProviderServiceStandby(core: core) + self.fpServiceStandby?.start() + } + // Core is ready self.coreReady(lastVisibleItemId) @@ -207,6 +214,9 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa }) } } else { + self.core = nil + self.coreRequested = false + Log.error("Error requesting/starting core: \(String(describing: error))") } diff --git a/ownCloud/SceneDelegate.swift b/ownCloud/SceneDelegate.swift index b5349e7a2..b1c6391b2 100644 --- a/ownCloud/SceneDelegate.swift +++ b/ownCloud/SceneDelegate.swift @@ -86,22 +86,29 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } @discardableResult func configure(window: ThemeWindow?, with activity: NSUserActivity) -> Bool { - guard let bookmarkUUIDString = activity.userInfo?[OCBookmark.ownCloudOpenAccountAccountUuidKey] as? String, let bookmarkUUID = UUID(uuidString: bookmarkUUIDString), let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID), let navigationController = window?.rootViewController as? ThemeNavigationController, let serverListController = navigationController.topViewController as? ServerListTableViewController else { - return false - } + if let bookmarkUUIDString = activity.userInfo?[OCBookmark.ownCloudOpenAccountAccountUuidKey] as? String, + let bookmarkUUID = UUID(uuidString: bookmarkUUIDString), + let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID), + let navigationController = window?.rootViewController as? ThemeNavigationController, + let serverListController = navigationController.topViewController as? ServerListTableViewController { + if activity.title == OCBookmark.ownCloudOpenAccountPath { + serverListController.connect(to: bookmark, lastVisibleItemId: nil, animated: false) + window?.windowScene?.userActivity = bookmark.openAccountUserActivity + + return true + } else if activity.title == OpenItemUserActivity.ownCloudOpenItemPath { + guard let itemLocalID = activity.userInfo?[OpenItemUserActivity.ownCloudOpenItemUuidKey] as? String else { + return false + } - if activity.title == OCBookmark.ownCloudOpenAccountPath { - serverListController.connect(to: bookmark, lastVisibleItemId: nil, animated: false) - window?.windowScene?.userActivity = bookmark.openAccountUserActivity + // At first connect to the bookmark for the item + serverListController.connect(to: bookmark, lastVisibleItemId: itemLocalID, animated: false) + window?.windowScene?.userActivity = activity - return true - } else if activity.title == OpenItemUserActivity.ownCloudOpenItemPath { - guard let itemLocalID = activity.userInfo?[OpenItemUserActivity.ownCloudOpenItemUuidKey] as? String else { - return false + return true } - - // At first connect to the bookmark for the item - serverListController.connect(to: bookmark, lastVisibleItemId: itemLocalID, animated: false) + } else if activity.activityType == ServerListTableViewController.showServerListActivityType { + // Show server list window?.windowScene?.userActivity = activity return true diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift index 225e8472b..443f28562 100644 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ b/ownCloud/Server List/ServerListTableViewController.swift @@ -32,6 +32,19 @@ class ServerListTableViewController: UITableViewController, Themeable { @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 @@ -105,7 +118,10 @@ class ServerListTableViewController: UITableViewController, Themeable { self.navigationItem.title = VendorServices.shared.appName - NotificationCenter.default.addObserver(self, selector: #selector(considerAutoLogin), name: UIApplication.didBecomeActiveNotification, object: nil) + if #available(iOS 13, *) { } else { + // Log in automatically on iOS 12 (handled by scene restoration in iOS 13+) + NotificationCenter.default.addObserver(self, selector: #selector(considerAutoLogin), name: UIApplication.didBecomeActiveNotification, object: nil) + } if ReleaseNotesDatasource().shouldShowReleaseNotes { let releaseNotesHostController = ReleaseNotesHostViewController() @@ -194,8 +210,6 @@ class ServerListTableViewController: UITableViewController, Themeable { } override func viewDidAppear(_ animated: Bool) { - var showBetaWarning = VendorServices.shared.showBetaWarning - super.viewDidAppear(animated) ClientSessionManager.shared.add(delegate: self) @@ -214,17 +228,17 @@ class ServerListTableViewController: UITableViewController, Themeable { PasscodeSetupCoordinator(parentViewController: self, action: .setup).start() } - if showBetaWarning, shownFirstTime { - showBetaWarning = !considerAutoLogin() - } - - if showBetaWarning { + if VendorServices.shared.showBetaWarning, shownFirstTime { considerBetaWarning() } if !shownFirstTime { VendorServices.shared.considerReviewPrompt() } + + if #available(iOS 13, *) { + view.window?.windowScene?.userActivity = ServerListTableViewController.showServerListActivity + } } @objc func considerAutoLogin() -> Bool { @@ -532,10 +546,11 @@ class ServerListTableViewController: UITableViewController, Themeable { let clientRootViewController = ClientSessionManager.shared.startSession(for: bookmark)! - let bookmarkRow = self.tableView.cellForRow(at: indexPath) + 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 @@ -552,7 +567,32 @@ class ServerListTableViewController: UITableViewController, Themeable { clientRootViewController.authDelegate = self clientRootViewController.modalPresentationStyle = .overFullScreen - clientRootViewController.afterCoreStart(lastVisibleItemId, completionHandler: { (error) in + 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 { @@ -569,7 +609,13 @@ class ServerListTableViewController: UITableViewController, Themeable { self.activeClientRootViewController = clientRootViewController // save this ClientRootViewController as the active one (only weakly referenced) // Set up custom push transition for presentation - let transitionDelegate = PushTransitionDelegate() + let transitionDelegate = PushTransitionDelegate(with: { (toViewController, window) in + window.addSubview(toViewController.view) + + if #available(iOS 13, *) { + window.windowScene?.userActivity = ServerListTableViewController.showServerListActivity + } + }) clientRootViewController.pushTransition = transitionDelegate // Keep a reference, so it's still around on dismissal clientRootViewController.transitioningDelegate = transitionDelegate diff --git a/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h new file mode 100644 index 000000000..173251d97 --- /dev/null +++ b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h @@ -0,0 +1,32 @@ +// +// NSDate+ComputedTimes.h +// ownCloud +// +// Created by Felix Schwarz on 19.03.21. +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface NSDate (ComputedTimes) + ++ (instancetype)startOfDay:(NSInteger)dayOffset; ++ (instancetype)startOfWeek:(NSInteger)weekOffset; ++ (instancetype)startOfMonth:(NSInteger)monthOffset; ++ (instancetype)startOfYear:(NSInteger)yearOffset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m new file mode 100644 index 000000000..ca05550bf --- /dev/null +++ b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m @@ -0,0 +1,101 @@ +// +// NSDate+ComputedTimes.m +// ownCloud +// +// Created by Felix Schwarz on 19.03.21. +// 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 "NSDate+ComputedTimes.h" + +@implementation NSDate (ComputedTimes) + +- (instancetype)recomputeWithUnits:(NSCalendarUnit)units modifier:(void(^)(NSDateComponents *components))componentModifier +{ + NSCalendar *calendar = NSCalendar.autoupdatingCurrentCalendar; + NSDateComponents *components = [calendar components:units fromDate:self]; + + if (componentModifier != nil) + { + componentModifier(components); + } + + return ([calendar dateFromComponents:components]); +} + ++ (instancetype)startOfDay:(NSInteger)dayOffset +{ + return ([[NSDate dateWithTimeIntervalSinceNow:(NSTimeInterval)(dayOffset * 24 * 60 * 60)] recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:nil]); +} + ++ (instancetype)startOfWeek:(NSInteger)weekOffset +{ + return ([[NSDate dateWithTimeIntervalSinceNow:(NSTimeInterval)(weekOffset * 7 * 24 * 60 * 60)] recomputeWithUnits:NSCalendarUnitWeekday|NSCalendarUnitWeekOfMonth|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { + components.weekday = 2; // Monday, 1 = Sunday + }]); +} + ++ (instancetype)startOfMonth:(NSInteger)monthOffset +{ + return ([NSDate.date recomputeWithUnits:NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { + if (monthOffset < 0) + { + NSInteger remainingMonths = -monthOffset; + + while (remainingMonths > 0) + { + if (components.month > 1) + { + components.month -= 1; + } + else + { + components.year -= 1; + components.month = 12; + } + + remainingMonths--; + }; + } + else + { + NSInteger remainingMonths = monthOffset; + + while (remainingMonths > 0) + { + if (components.month > 11) + { + components.year += 1; + components.month = 1; + } + else + { + components.month += 1; + } + + remainingMonths--; + }; + } + }]); +} + ++ (instancetype)startOfYear:(NSInteger)yearOffset +{ + return ([NSDate.date recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { + components.day = 1; + components.month = 1; + components.year += yearOffset; + }]); +} + +@end diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h new file mode 100644 index 000000000..a74eb3daa --- /dev/null +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h @@ -0,0 +1,36 @@ +// +// OCQueryCondition+SearchSegmenter.h +// ownCloudApp +// +// Created by Felix Schwarz on 19.03.21. +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (SearchSegmenter) + +- (NSArray *)segmentedForSearch; + +@end + +@interface OCQueryCondition (SearchSegmenter) + ++ (nullable instancetype)forSearchSegment:(NSString *)segmentString; ++ (nullable instancetype)fromSearchTerm:(NSString *)searchTerm; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m new file mode 100644 index 000000000..9149421c2 --- /dev/null +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -0,0 +1,265 @@ +// +// OCQueryCondition+SearchSegmenter.m +// ownCloudApp +// +// Created by Felix Schwarz on 19.03.21. +// 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 "OCQueryCondition+SearchSegmenter.h" +#import "NSDate+ComputedTimes.h" + +@implementation NSString (SearchSegmenter) + +- (NSArray *)segmentedForSearch +{ + NSMutableArray *segments = [NSMutableArray new]; + NSArray *terms; + + if ((terms = [self componentsSeparatedByString:@" "]) != nil) + { + __block NSString *segmentString = nil; + __block BOOL segmentOpen = NO; + + void (^SubmitSegment)(void) = ^{ + if (segmentString.length > 0) + { + [segments addObject:segmentString]; + } + + segmentString = nil; + }; + + for (NSString *inTerm in terms) + { + NSString *term = inTerm; + BOOL closingSegment = NO; + + if ([term hasPrefix:@"\""]) + { + // Submit any open segment + SubmitSegment(); + + // Start new segment + term = [term substringFromIndex:1]; + segmentOpen = YES; + } + + if ([term hasSuffix:@"\""]) + { + // End segment + term = [term substringToIndex:term.length-1]; + closingSegment = YES; + } + + // Append term to current segment + if (segmentString.length == 0) + { + segmentString = term; + + if (!segmentOpen) + { + // Submit standalone segment + SubmitSegment(); + } + } + else + { + // Append to segment string + segmentString = [segmentString stringByAppendingFormat:@" %@", term]; + } + + // Submit closed segments + if (closingSegment) + { + segmentOpen = NO; + SubmitSegment(); + } + } + + SubmitSegment(); + } + + return (segments); +} + +@end + +@implementation OCQueryCondition (SearchSegmenter) + ++ (instancetype)forSearchSegment:(NSString *)segmentString +{ + NSString *segmentStringLowercase = segmentString.lowercaseString; + + if ([segmentStringLowercase isEqual:@":folder"]) + { + return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeCollection)]); + } + else if ([segmentStringLowercase isEqual:@":file"]) + { + return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeFile)]); + } + else if ([segmentStringLowercase isEqual:@":image"]) + { + return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"image/"]); + } + else if ([segmentStringLowercase isEqual:@":video"]) + { + return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"video/"]); + } + else if ([segmentStringLowercase isEqual:@":today"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfDay:0]]); + } + else if ([segmentStringLowercase isEqual:@":week"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfWeek:0]]); + } + else if ([segmentStringLowercase isEqual:@":month"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfMonth:0]]); + } + else if ([segmentStringLowercase isEqual:@":year"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfYear:0]]); + } + else if ([segmentStringLowercase containsString:@":"]) + { + NSArray *parts = [segmentString componentsSeparatedByString:@":"]; + NSString *modifier; + + if ((modifier = parts.firstObject.lowercaseString) != nil) + { + NSArray *parameters = [[segmentString substringFromIndex:modifier.length+1] componentsSeparatedByString:@","]; + NSMutableArray *orConditions = [NSMutableArray new]; + + for (NSString *parameter in parameters) + { + if (parameter.length > 0) + { + OCQueryCondition *condition = nil; + + if ([modifier isEqual:@"type"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameName endsWith:parameter]; + } + else if ([modifier isEqual:@"days"] || [modifier isEqual:@"day"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfDay:-parameter.integerValue]]; + } + else if ([modifier isEqual:@"weeks"] || [modifier isEqual:@"week"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfWeek:-parameter.integerValue]]; + } + else if ([modifier isEqual:@"months"] || [modifier isEqual:@"month"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfMonth:-parameter.integerValue]]; + } + else if ([modifier isEqual:@"years"] || [modifier isEqual:@"year"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfYear:-parameter.integerValue]]; + } + else if ([modifier isEqual:@""]) + { + // Parse time formats, f.ex.: 7d, 2w, 1m, 2y + NSString *numString = nil; + + if ((parameter.length == 1) || // :d :w :m :y + ((parameter.length > 1) && // :7d :2w :1m :2y + ((numString = [parameter substringToIndex:parameter.length-1]) != nil) && + [@([numString integerValue]).stringValue isEqual:numString] + ) + ) + { + NSInteger numParam = numString.integerValue; + NSString *timeLabel = [parameter substringFromIndex:parameter.length-1].lowercaseString; + + if ([timeLabel isEqual:@"d"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfDay:-numParam]]; + } + else if ([timeLabel isEqual:@"w"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfWeek:-numParam]]; + } + else if ([timeLabel isEqual:@"m"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfMonth:-numParam]]; + } + else if ([timeLabel isEqual:@"y"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfYear:-numParam]]; + } + } + } + + if (condition != nil) + { + [orConditions addObject:condition]; + } + } + } + + if (orConditions.count == 1) + { + return (orConditions.firstObject); + } + else if (orConditions.count > 0) + { + return ([OCQueryCondition anyOf:orConditions]); + } + else if ([modifier isEqual:@"type"] || + [modifier isEqual:@"days"] || [modifier isEqual:@"day"] || + [modifier isEqual:@"weeks"] || [modifier isEqual:@"week"] || + [modifier isEqual:@"months"] || [modifier isEqual:@"month"] || + [modifier isEqual:@"years"] || [modifier isEqual:@"year"] + ) + { + // Modifiers without parameters + return (nil); + } + } + } + + return ([OCQueryCondition where:OCItemPropertyNameName contains:segmentString]); +} + ++ (instancetype)fromSearchTerm:(NSString *)searchTerm +{ + NSArray *segments = [searchTerm segmentedForSearch]; + NSMutableArray *conditions = [NSMutableArray new]; + OCQueryCondition *queryCondition = nil; + + for (NSString *segment in segments) + { + OCQueryCondition *condition; + + if ((condition = [self forSearchSegment:segment]) != nil) + { + [conditions addObject:condition]; + } + } + + if (conditions.count == 1) + { + queryCondition = conditions.firstObject; + } + else if (conditions.count > 0) + { + queryCondition = [OCQueryCondition require:conditions]; + } + + return (queryCondition); +} + +@end diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 1bd986e3c..39c5b50a2 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -30,8 +30,10 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import #import #import +#import #import #import +#import #import #import diff --git a/ownCloudAppFrameworkTests/SearchSegmentationTests.m b/ownCloudAppFrameworkTests/SearchSegmentationTests.m new file mode 100644 index 000000000..fe6ac4f4c --- /dev/null +++ b/ownCloudAppFrameworkTests/SearchSegmentationTests.m @@ -0,0 +1,79 @@ +// +// SearchSegmentationTests.m +// ownCloudAppTests +// +// Created by Felix Schwarz on 19.03.21. +// Copyright © 2021 ownCloud GmbH. All rights reserved. +// + +#import +#import + +@interface SearchSegmentationTests : XCTestCase + +@end + +@implementation SearchSegmentationTests + +- (void)testStringSegmentation +{ + NSDictionary *> *expectedSegmentsByStrings = @{ + @"\"Hello world\" term2" : @[ + @"Hello world", + @"term2" + ], + + @"\"Hello" : @[ + @"Hello" + ], + + @"Hello\"" : @[ + @"Hello" + ], + + @"Hello\" \"World" : @[ + @"Hello", @"World" + ], + + @"\"Hello World \"hello world\" term3" : @[ + @"Hello World", + @"hello world", + @"term3" + ], + + @"\"Hello World \"term2" : @[ + @"Hello World", + @"term2" + ], + + @"\"Hello World \"term2 \"term3" : @[ + @"Hello World", + @"term2", + @"term3" + ], + }; + + [expectedSegmentsByStrings enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull term, NSArray * _Nonnull expectedSegments, BOOL * _Nonnull stop) { + NSArray *segments = [term segmentedForSearch]; + + XCTAssert([segments isEqual:expectedSegments], @"segments %@ doesn't match expectation %@", segments, expectedSegments); + }]; +} + +- (void)testDateComputations +{ + NSLog(@"Start of day(-2): %@", [NSDate startOfDay:-2]); + NSLog(@"Start of day( 0): %@", [NSDate startOfDay:0]); + NSLog(@"Start of day(+2): %@", [NSDate startOfDay:2]); + NSLog(@"Start of week(-1): %@", [NSDate startOfWeek:-1]); + NSLog(@"Start of week( 0): %@", [NSDate startOfWeek:0]); + NSLog(@"Start of week(+1): %@", [NSDate startOfWeek:1]); + NSLog(@"Start of month(-1): %@", [NSDate startOfMonth:-1]); + NSLog(@"Start of month( 0): %@", [NSDate startOfMonth:0]); + NSLog(@"Start of month(+1): %@", [NSDate startOfMonth:1]); + NSLog(@"Start of year(-1): %@", [NSDate startOfYear:-1]); + NSLog(@"Start of year( 0): %@", [NSDate startOfYear: 0]); + NSLog(@"Start of year(+1): %@", [NSDate startOfYear:+2]); +} + +@end diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index de176521d..dd8db9f61 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -159,7 +159,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn // MARK: - Search scope support private var searchText: String? - private let maxResultCount = 3 + private let maxResultCount = 100 // Maximum number of results to return from database open override func applySearchFilter(for searchText: String?, to query: OCQuery) { self.searchText = searchText @@ -185,17 +185,18 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } func updateCustomSearchQuery() { - if let searchText = searchText, let searchScope = sortBar?.searchScope, searchScope == .global { - let condition : OCQueryCondition = .where(.name, contains: searchText) - - if let sortPropertyName = sortBar?.sortMethod.sortPropertyName { - condition.sortBy = sortPropertyName - condition.sortAscending = (sortDirection != .ascendant) + 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) + self.customSearchQuery = OCQuery(condition:condition, inputFilter: nil) } else { self.customSearchQuery = nil } diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index aac9f3de5..828f5a1cd 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -280,7 +280,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa if self.items.count == 0 { if self.searchController?.searchBar.text != "" { - self.messageView?.message(show: true, imageName: "icon-search", title: "No matches".localized, message: "There is no results for this search".localized) + 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) } diff --git a/ownCloudAppShared/Client/User Interface/MessageView.swift b/ownCloudAppShared/Client/User Interface/MessageView.swift index 802168e8f..13778d943 100644 --- a/ownCloudAppShared/Client/User Interface/MessageView.swift +++ b/ownCloudAppShared/Client/User Interface/MessageView.swift @@ -56,7 +56,7 @@ open class MessageView: UIView { } } - open func message(show: Bool, imageName : String? = nil, title : String? = nil, message : String? = nil) { + open func message(show: Bool, with insets: UIEdgeInsets? = nil, imageName : String? = nil, title : String? = nil, message : String? = nil) { if !show { if messageView?.superview != nil { messageView?.removeFromSuperview() @@ -174,9 +174,9 @@ open class MessageView: UIView { } NSLayoutConstraint.activate([ - rootView.leftAnchor.constraint(equalTo: self.mainView.leftAnchor), - rootView.widthAnchor.constraint(equalTo: self.mainView.widthAnchor), - rootView.topAnchor.constraint(equalTo: self.mainView.safeAreaLayoutGuide.topAnchor), + rootView.leftAnchor.constraint(equalTo: self.mainView.leftAnchor, constant: insets?.left ?? 0), + rootView.widthAnchor.constraint(equalTo: self.mainView.widthAnchor, constant: -(insets?.right ?? 0)), + rootView.topAnchor.constraint(equalTo: self.mainView.safeAreaLayoutGuide.topAnchor, constant: insets?.top ?? 0), self.composeViewBottomConstraint ]) diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index 960f1a5a1..b2d6f6b3d 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -30,6 +30,22 @@ public extension UIView { self.layer.add(animation, forKey: "shakeHorizontally") } + func beginPulsing(duration: TimeInterval = 1) { + let pulseAnimation = CABasicAnimation(keyPath: "opacity") + + pulseAnimation.fromValue = 1 + pulseAnimation.toValue = 0.3 + pulseAnimation.repeatCount = .infinity + pulseAnimation.autoreverses = true + pulseAnimation.duration = duration + + layer.add(pulseAnimation, forKey: "opacity") + } + + func endPulsing() { + layer.removeAnimation(forKey: "opacity") + } + // MARK: - View hierarchy func findSubviewInTree(where filter: (UIView) -> Bool) -> UIView? { for subview in subviews { From b18df1d30a5337d088b6f0cef18367965569d256 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 23 Mar 2021 21:43:05 +0100 Subject: [PATCH 05/37] - add "Show more results" row at the end of the search result list if there could be more results --- .../Resources/en.lproj/Localizable.strings | 2 + .../ClientQueryViewController.swift | 56 ++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index ab9e7015d..ac8441049 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -104,6 +104,8 @@ "%@ item | " = "%@ item | "; "%@ items | " = "%@ items | "; +"Show more results" = "Show more results"; + /* Static Login Setup */ "Server error" = "Server error"; "The server doesn't support any allowed authentication method." = "The server doesn't support any allowed authentication method."; diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index dd8db9f61..c5deefea8 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -148,6 +148,12 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } } + open override func registerCellClasses() { + super.registerCellClasses() + + self.tableView.register(ThemeTableViewCell.self, forCellReuseIdentifier: "moreCell") + } + // MARK: - Search events open func willPresentSearchController(_ searchController: UISearchController) { self.sortBar?.showSearchScope = true @@ -159,7 +165,8 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn // MARK: - Search scope support private var searchText: String? - private let maxResultCount = 100 // Maximum number of results to return from database + 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 @@ -184,7 +191,15 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } } + private var lastSearchText : String? + func updateCustomSearchQuery() { + if lastSearchText != searchText { + // Reset max result count when search text changes + maxResultCount = maxResultCountDefault + lastSearchText = searchText + } + if let searchText = searchText, let searchScope = sortBar?.searchScope, searchScope == .global, @@ -270,6 +285,45 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn 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) + } else { + let moreCell = tableView.dequeueReusableCell(withIdentifier: "moreCell", for: indexPath) as? ThemeTableViewCell + + moreCell?.accessibilityIdentifier = "more-results" + moreCell?.primaryTextLabel?.text = "Show more results".localized + moreCell?.primaryTextLabel?.textAlignment = .center + + cell = moreCell + } + + return cell! + } + + public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath), cell.accessibilityIdentifier == "more-results" { + maxResultCount += maxResultCountDefault + updateCustomSearchQuery() + } else { + super.tableView(tableView, didSelectRowAt: indexPath) + } + } + // MARK: - Table view delegate open override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { From 75cc69e1d2b41cb8cba728a28e97843fb1ecd965 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 23 Mar 2021 21:55:48 +0100 Subject: [PATCH 06/37] - Cleanup more cell identifiers --- .../File Lists/ClientQueryViewController.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index c5deefea8..135e288a1 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -42,6 +42,8 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn private var _actionProgressHandler : ActionProgressHandler? private let ItemDataUTI = "com.owncloud.ios-app.item-data" + private let moreCellIdentifier = "moreCell" + private let moreCellAccessibilityIdentifier = "more-results" private var _query : OCQuery public override var query : OCQuery { @@ -151,7 +153,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn open override func registerCellClasses() { super.registerCellClasses() - self.tableView.register(ThemeTableViewCell.self, forCellReuseIdentifier: "moreCell") + self.tableView.register(ThemeTableViewCell.self, forCellReuseIdentifier: moreCellIdentifier) } // MARK: - Search events @@ -303,11 +305,10 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn if indexPath.row < numberOfRows { cell = super.tableView(tableView, cellForRowAt: indexPath) } else { - let moreCell = tableView.dequeueReusableCell(withIdentifier: "moreCell", for: indexPath) as? ThemeTableViewCell + let moreCell = tableView.dequeueReusableCell(withIdentifier: moreCellIdentifier, for: indexPath) as? ThemeTableViewCell - moreCell?.accessibilityIdentifier = "more-results" - moreCell?.primaryTextLabel?.text = "Show more results".localized - moreCell?.primaryTextLabel?.textAlignment = .center + moreCell?.accessibilityIdentifier = moreCellAccessibilityIdentifier + moreCell?.textLabel?.text = "Show more results".localized cell = moreCell } @@ -316,7 +317,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let cell = tableView.cellForRow(at: indexPath), cell.accessibilityIdentifier == "more-results" { + if let cell = tableView.cellForRow(at: indexPath), cell.accessibilityIdentifier == moreCellAccessibilityIdentifier { maxResultCount += maxResultCountDefault updateCustomSearchQuery() } else { From 4fbcf1bbd4141801857380800be21a03b648842e Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 24 Mar 2021 16:07:11 +0100 Subject: [PATCH 07/37] - QueryFileListTableViewController: search remains active after choosing an item and then navigating back to search - ClientQueryViewController: adapt to QueryFileListTableViewController changes --- .../Client/File Lists/ClientQueryViewController.swift | 2 ++ .../Client/File Lists/QueryFileListTableViewController.swift | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 135e288a1..42f40a4fd 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -141,6 +141,8 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } deinit { + customSearchQuery = nil + queryStateObservation = nil quotaObservation = nil diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 828f5a1cd..663044fc7 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -131,7 +131,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa // MARK: - Search: UISearchResultsUpdating Delegate open func updateSearchResults(for searchController: UISearchController) { - let searchText = searchController.searchBar.text! + let searchText = searchController.searchBar.text ?? "" applySearchFilter(for: (searchText == "") ? nil : searchText, to: query) } @@ -377,9 +377,6 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa core?.stop(query) queryProgressSummary = nil - - searchController?.searchBar.text = "" - searchController?.dismiss(animated: true, completion: nil) } // MARK: - Item retrieval From 9a39982e03e02cae4e593966996eab42dc962ef2 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 24 Mar 2021 16:48:47 +0100 Subject: [PATCH 08/37] SDK update to fix OCCore stop bug --- ios-sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios-sdk b/ios-sdk index ffa4e97c5..7235790ba 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit ffa4e97c5bdc8ab6ec7751b66940d7f8e51af519 +Subproject commit 7235790ba7814ab69c961168f3566143538eaa57 From 469db006b0766b0d35c97adba60db7441b349390 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 24 Mar 2021 23:51:19 +0100 Subject: [PATCH 09/37] - BreadCrumbTableViewController - add navigationHandler property to allow custom navigation actions to get triggered by the breadcrumb view - ClientItemCell - add reveal button support - ClientQueryViewController / QueryFileListTableViewController - allow providing an item to reveal and highlight - allow specifying if bread crumbs should push new view controllers - show reveal arrows for custom queries --- .../ClientQueryViewController.swift | 41 ++++++++++- .../FileListTableViewController.swift | 15 ++++ .../QueryFileListTableViewController.swift | 27 ++++++- .../BreadCrumbTableViewController.swift | 15 +++- .../User Interface/ClientItemCell.swift | 72 ++++++++++++++++++- 5 files changed, 162 insertions(+), 8 deletions(-) diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 42f40a4fd..d29082ba4 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -37,9 +37,13 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn public var quotaObservation : NSKeyValueObservation? public var titleButtonThemeApplierToken : ThemeApplierToken? + public var breadCrumbsPush : Bool = false + weak public var clientRootViewController : UIViewController? private var _actionProgressHandler : ActionProgressHandler? + private var revealItemLocalID : String? + private var revealItemFound : Bool = false private let ItemDataUTI = "com.owncloud.ios-app.item-data" private let moreCellIdentifier = "moreCell" @@ -91,8 +95,10 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn self.init(core: inCore, query: inQuery, rootViewController: nil) } - public init(core inCore: OCCore, query inQuery: OCQuery, rootViewController: UIViewController?) { + public init(core inCore: OCCore, query inQuery: OCQuery, reveal inItem: OCItem? = nil, rootViewController: UIViewController?) { clientRootViewController = rootViewController + revealItemLocalID = inItem?.localID + breadCrumbsPush = revealItemLocalID != nil _query = inQuery super.init(core: inCore, query: inQuery) @@ -306,6 +312,10 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn 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 @@ -327,6 +337,10 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } } + 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 { @@ -497,6 +511,16 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn if let shortName = core?.bookmark.shortName { tableViewController.bookmarkShortName = shortName } + if breadCrumbsPush { + tableViewController.navigationHandler = { [weak self] (path) in + if let self = self, let core = self.core { + let queryViewController = ClientQueryViewController(core: core, query: OCQuery(forPath: path)) + queryViewController.breadCrumbsPush = true + + self.navigationController?.pushViewController(queryViewController, animated: true) + } + } + } if #available(iOS 13, *) { // On iOS 13.0/13.1, the table view's content needs to be inset by the height of the arrow @@ -530,6 +554,21 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn if query == self.query { 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 query.queryPath != "/" { var totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) diff --git a/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift index b38fc9c4e..615e3d4c3 100644 --- a/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift @@ -27,6 +27,11 @@ public protocol MoreItemHandling { @discardableResult func moreOptions(for item: OCItem, at location: OCExtensionLocationIdentifier, core: OCCore, query: OCQuery?, sender: AnyObject?) -> Bool } +public protocol RevealItemHandling { + @discardableResult func reveal(item: OCItem, core: OCCore, sender: AnyObject?) -> Bool + func showReveal(at path: IndexPath) -> Bool +} + public protocol InlineMessageSupport { func hasInlineMessage(for item: OCItem) -> Bool func showInlineMessageFor(item: OCItem) @@ -89,6 +94,16 @@ open class FileListTableViewController: UITableViewController, ClientItemCellDel } } + open func revealButtonTapped(cell: ClientItemCell) { + guard let item = self.item(for: cell), 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? InlineMessageSupport { diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 663044fc7..91297f86b 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -39,7 +39,7 @@ public protocol MultiSelectSupport { func populateToolbar() } -open class QueryFileListTableViewController: FileListTableViewController, SortBarDelegate, OCQueryDelegate, UISearchResultsUpdating { +open class QueryFileListTableViewController: FileListTableViewController, SortBarDelegate, RevealItemHandling, OCQueryDelegate, UISearchResultsUpdating { public var query : OCQuery @@ -405,6 +405,8 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa 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) { @@ -423,6 +425,29 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa return cell! } + public func reveal(item: OCItem, core: OCCore, sender: AnyObject?) -> Bool { + if let parentPath = item.path?.parentPath { + let revealQueryViewController = ClientQueryViewController(core: core, query: OCQuery(forPath: parentPath), reveal: 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]? { diff --git a/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift b/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift index 658bc1760..ad6321a7c 100644 --- a/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift +++ b/ownCloudAppShared/Client/User Interface/BreadCrumbTableViewController.swift @@ -30,6 +30,7 @@ open class BreadCrumbTableViewController: StaticTableViewController { open var parentNavigationController : UINavigationController? open var queryPath : NSString = "" open var bookmarkShortName : String? + open var navigationHandler : ((_ path: String) -> Void)? open override func viewDidLoad() { super.viewDidLoad() @@ -52,7 +53,7 @@ open class BreadCrumbTableViewController: StaticTableViewController { let contentWidth : CGFloat = (view.frame.size.width < maxContentWidth) ? view.frame.size.width : maxContentWidth self.preferredContentSize = CGSize(width: contentWidth, height: contentHeight) - for (_, currentPath) in pathComp.enumerated().reversed() { + for (idx, currentPath) in pathComp.enumerated().reversed() { var stackIndex = stackViewControllers.count - currentViewContollerIndex if stackIndex < 0 { stackIndex = 0 @@ -61,10 +62,18 @@ open class BreadCrumbTableViewController: StaticTableViewController { 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 stackViewControllers.indices.contains(stackIndex) { - self.parentNavigationController?.popToViewController((stackViewControllers[stackIndex]), animated: true) + if let navigationHandler = self.navigationHandler { + navigationHandler(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))) diff --git a/ownCloudAppShared/Client/User Interface/ClientItemCell.swift b/ownCloudAppShared/Client/User Interface/ClientItemCell.swift index 346090205..04379e47b 100644 --- a/ownCloudAppShared/Client/User Interface/ClientItemCell.swift +++ b/ownCloudAppShared/Client/User Interface/ClientItemCell.swift @@ -23,6 +23,7 @@ public protocol ClientItemCellDelegate: class { func moreButtonTapped(cell: ClientItemCell) func messageButtonTapped(cell: ClientItemCell) + func revealButtonTapped(cell: ClientItemCell) func hasMessage(for item: OCItem) -> Bool } @@ -36,7 +37,8 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { private let smallSpacing : CGFloat = 2 private let iconViewWidth : CGFloat = 40 private let detailIconViewHeight : CGFloat = 15 - private let moreButtonWidth : CGFloat = 60 + private let moreButtonWidth : CGFloat = 45 + 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) @@ -56,9 +58,11 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { 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? @@ -136,6 +140,8 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { moreButton.translatesAutoresizingMaskIntoConstraints = false + revealButton.translatesAutoresizingMaskIntoConstraints = false + messageButton.translatesAutoresizingMaskIntoConstraints = false cloudStatusIconView.translatesAutoresizingMaskIntoConstraints = false @@ -164,6 +170,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { 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) @@ -172,6 +179,15 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { moreButton.isPointerInteractionEnabled = true } + if #available(iOS 13.4, *) { + revealButton.setImage(UIImage(systemName: "arrow.right.circle.fill"), for: .normal) + revealButton.isPointerInteractionEnabled = true + } else { + revealButton.setTitle("→", for: .normal) + } + revealButton.contentMode = .center + revealButton.isHidden = !showRevealButton + messageButton.setTitle("⚠️", for: .normal) messageButton.contentMode = .center if #available(iOS 13.4, *) { @@ -180,6 +196,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { 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) sharedStatusIconView.setContentHuggingPriority(.required, for: .vertical) @@ -203,6 +220,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) moreButtonWidthConstraint = moreButton.widthAnchor.constraint(equalToConstant: moreButtonWidth) + revealButtonWidthConstraint = revealButton.widthAnchor.constraint(equalToConstant: showRevealButton ? revealButtonWidth : 0) cloudStatusIconViewZeroWidthConstraint = cloudStatusIconView.widthAnchor.constraint(equalToConstant: 0) sharedStatusIconViewZeroWidthConstraint = sharedStatusIconView.widthAnchor.constraint(equalToConstant: 0) @@ -244,11 +262,15 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { sharedStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), publicLinkStatusIconView.heightAnchor.constraint(equalToConstant: detailIconViewHeight), - moreButton.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor), moreButton.topAnchor.constraint(equalTo: self.contentView.topAnchor), moreButton.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), moreButtonWidthConstraint!, - moreButton.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor), + 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), @@ -489,6 +511,16 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { } // 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) @@ -505,6 +537,12 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { if showingIcon, let item = item { iconView.image = item.icon(fitInSize: iconSize) } + + if revealHighlight { + backgroundColor = collection.tableRowHighlightColors.backgroundColor?.withAlphaComponent(0.5) + } else { + backgroundColor = .clear + } } // MARK: - Editing mode @@ -524,10 +562,35 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { } } + 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 @@ -537,6 +600,9 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { @objc open func messageButtonTapped() { self.delegate?.messageButtonTapped(cell: self) } + @objc open func revealButtonTapped() { + self.delegate?.revealButtonTapped(cell: self) + } } public extension NSNotification.Name { From 53e8f19ec18ff7c4a2931ba012a00f30ca16e6e7 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 25 Mar 2021 00:27:07 +0100 Subject: [PATCH 10/37] - fix issue of stopped custom query after revealing an item and returning - use search term segmentation also for folder search (via OCQueryCondition+Item) --- ios-sdk | 2 +- .../ClientQueryViewController.swift | 8 ++++++++ .../QueryFileListTableViewController.swift | 19 ++++++++++++++----- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ios-sdk b/ios-sdk index 7235790ba..37382d8b7 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 7235790ba7814ab69c961168f3566143538eaa57 +Subproject commit 37382d8b7159899e117c78805a6c54fbfa7ca3f2 diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index d29082ba4..124680492 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -267,6 +267,14 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn searchController?.delegate = self } + open override func startQuery(_ theQuery: OCQuery) { + if theQuery == customSearchQuery { + self.updateCustomSearchQuery() + } else { + super.startQuery(theQuery) + } + } + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 91297f86b..4f3cfb42c 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -138,10 +138,11 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa 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 itemName = item?.name { - return itemName.localizedCaseInsensitiveContains(searchText) - } + if let item = item, let queryCondition = queryCondition { + return queryCondition.fulfilled(by: item) + } return false } @@ -356,10 +357,18 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa } } + open func startQuery(_ theQuery: OCQuery) { + core?.start(theQuery) + } + + open func stopQuery(_ theQuery: OCQuery) { + core?.stop(theQuery) + } + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - core?.start(query) + startQuery(query) queryStateObservation = query.observe(\OCQuery.state, options: .initial, changeHandler: { [weak self] (_, _) in self?.updateQueryProgressSummary() @@ -374,7 +383,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa queryStateObservation?.invalidate() queryStateObservation = nil - core?.stop(query) + stopQuery(query) queryProgressSummary = nil } From 0f8650303caad560c45aa2deb57fe36da8d9a0b9 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 25 Mar 2021 09:46:48 +0100 Subject: [PATCH 11/37] - OCBookmark+AppExtensions: check if displayName and userName have at least one character before using them --- .../SDK Extensions/OCBookmark+AppExtensions.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ownCloudAppFramework/SDK Extensions/OCBookmark+AppExtensions.m b/ownCloudAppFramework/SDK Extensions/OCBookmark+AppExtensions.m index 2f2e30619..01305ee30 100644 --- a/ownCloudAppFramework/SDK Extensions/OCBookmark+AppExtensions.m +++ b/ownCloudAppFramework/SDK Extensions/OCBookmark+AppExtensions.m @@ -41,12 +41,12 @@ - (NSString *)shortName NSString *userNamePrefix = @""; NSString *displayName = nil, *userName = nil; - if ((displayName = self.displayName) != nil) + if (((displayName = self.displayName) != nil) && (displayName.length > 0)) { userNamePrefix = [displayName stringByAppendingString:@"@"]; } - if ((userNamePrefix.length == 0) && ((userName = self.userName) != nil)) + if ((userNamePrefix.length == 0) && ((userName = self.userName) != nil) && (userName.length > 0)) { userNamePrefix = [userName stringByAppendingString:@"@"]; } From a78f0f99cb54a213d4d3f705c9547f6240f32fb8 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 25 Mar 2021 12:06:10 +0100 Subject: [PATCH 12/37] - update SDK - add concept and property of activeQuery to QueryFileListTableViewController - adopt activeQuery in ClientQueryViewController to prevent QueryFileListTableViewController from controlling the customSearchQuery / run into conflicts - resolve issue where a "Stopped" status was displayed sometimes when returning from a revealed search result --- ios-sdk | 2 +- .../ClientQueryViewController.swift | 126 +++++++++--------- .../QueryFileListTableViewController.swift | 37 ++--- 3 files changed, 85 insertions(+), 80 deletions(-) diff --git a/ios-sdk b/ios-sdk index 37382d8b7..0589b44a7 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 37382d8b7159899e117c78805a6c54fbfa7ca3f2 +Subproject commit 0589b44a72f14765c92d4085d071dc1d3cd8a8c4 diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 124680492..06e1811fa 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -49,33 +49,27 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn private let moreCellIdentifier = "moreCell" private let moreCellAccessibilityIdentifier = "more-results" - private var _query : OCQuery - public override var query : OCQuery { - set { - _query = newValue - } + open override var activeQuery : OCQuery { + if let customSearchQuery = customSearchQuery { + return customSearchQuery + } else { + return query + } + } - get { - if let customSearchQuery = customSearchQuery { - return customSearchQuery - } else { - return _query - } - } - } var customSearchQuery : OCQuery? { willSet { - if customSearchQuery != newValue, let query = customSearchQuery { - core?.stop(query) - query.delegate = nil + if customSearchQuery != newValue, let customQuery = customSearchQuery { + core?.stop(customQuery) + customQuery.delegate = nil } } didSet { - if customSearchQuery != nil, let query = customSearchQuery { - query.delegate = self - query.sortComparator = sortMethod.comparator(direction: sortDirection) - core?.start(query) + if customSearchQuery != nil, let customQuery = customSearchQuery { + customQuery.delegate = self + customQuery.sortComparator = sortMethod.comparator(direction: sortDirection) + core?.start(customQuery) } } } @@ -99,7 +93,6 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn clientRootViewController = rootViewController revealItemLocalID = inItem?.localID breadCrumbsPush = revealItemLocalID != nil - _query = inQuery super.init(core: inCore, query: inQuery) updateTitleView() @@ -193,7 +186,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn let comparator = sortMethod.comparator(direction: sortDirection) - _query.sortComparator = comparator + query.sortComparator = comparator customSearchQuery?.sortComparator = comparator if (customSearchQuery?.queryResults?.count ?? 0) >= maxResultCount { @@ -226,9 +219,9 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn self.customSearchQuery = nil } - super.applySearchFilter(for: searchText, to: _query) + super.applySearchFilter(for: searchText, to: query) - self.queryHasChangesAvailable(query) + self.queryHasChangesAvailable(activeQuery) } // MARK: - View controller events @@ -266,14 +259,14 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn searchController?.delegate = self } - - open override func startQuery(_ theQuery: OCQuery) { - if theQuery == customSearchQuery { - self.updateCustomSearchQuery() - } else { - super.startQuery(theQuery) - } - } +// +// open override func startQuery(_ theQuery: OCQuery) { +// if theQuery == customSearchQuery { +// self.updateCustomSearchQuery() +// } else { +// super.startQuery(theQuery) +// } +// } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -411,8 +404,11 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } // 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 } @@ -559,45 +555,47 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn // MARK: - Updates open override func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { - if query == self.query { - super.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) + guard query == activeQuery else { + return + } - if let revealItemLocalID = revealItemLocalID, !revealItemFound { - var rowIdx : Int = 0 + super.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) - 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 + 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) } - rowIdx += 1 + revealItemFound = true + break } + rowIdx += 1 } + } - if let rootItem = self.query.rootItem, searchText == nil { - if query.queryPath != "/" { - var totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) - if self.items.count == 1 { - totalSize = String(format: "%@ item | ", "\(self.items.count)") + totalSize - } else if self.items.count > 1 { - totalSize = String(format: "%@ items | ", "\(self.items.count)") + totalSize - } - self.updateFooter(text: totalSize) - } - - if #available(iOS 13.0, *) { - 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 - } + if let rootItem = self.query.rootItem, searchText == nil { + if query.queryPath != "/" { + var totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) + if self.items.count == 1 { + totalSize = String(format: "%@ item | ", "\(self.items.count)") + totalSize + } else if self.items.count > 1 { + totalSize = String(format: "%@ items | ", "\(self.items.count)") + totalSize + } + self.updateFooter(text: totalSize) + } + + if #available(iOS 13.0, *) { + 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) } + } else { + self.updateFooter(text: nil) } } diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 4f3cfb42c..d163ee6b6 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -42,6 +42,9 @@ public protocol MultiSelectSupport { open class QueryFileListTableViewController: FileListTableViewController, SortBarDelegate, RevealItemHandling, OCQueryDelegate, UISearchResultsUpdating { public var query : OCQuery + open var activeQuery : OCQuery { + return query + } public var queryRefreshRateLimiter : OCRateLimiter = OCRateLimiter(minimumTime: 0.2) @@ -186,7 +189,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa override open func performPullToRefreshAction() { super.performPullToRefreshAction() - core?.reload(query) + core?.reload(activeQuery) } open func updateQueryProgressSummary() { @@ -216,6 +219,8 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa 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() @@ -245,16 +250,22 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa } open func queryHasChangesAvailable(_ query: OCQuery) { - queryRefreshRateLimiter.runRateLimitedBlock { - query.requestChangeSet(withFlags: .onlyResults) { (query, changeSet) in - OnMainThread { - self.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) + 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 { @@ -357,18 +368,12 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa } } - open func startQuery(_ theQuery: OCQuery) { - core?.start(theQuery) - } - - open func stopQuery(_ theQuery: OCQuery) { - core?.stop(theQuery) - } - open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - startQuery(query) + 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() @@ -380,10 +385,12 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + Log.debug("Query(p=\(unsafeBitCast(query, to: Int.self)), stop/viewWillDisappear") + queryStateObservation?.invalidate() queryStateObservation = nil - stopQuery(query) + core?.stop(query) queryProgressSummary = nil } From 67810915aeb9279daa51bcdd9020a1c8fc4267fa Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 25 Mar 2021 22:18:11 +0100 Subject: [PATCH 13/37] - more differentiated status 503 handling (https://github.com/owncloud/enterprise/issues/4476) via SDK update --- ios-sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios-sdk b/ios-sdk index 0589b44a7..672ad2835 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 0589b44a72f14765c92d4085d071dc1d3cd8a8c4 +Subproject commit 672ad2835500e2684b53c67d535d70a41685206a From 18f1ef834bebc02e3465ea460702277d2b62c192 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 30 Mar 2021 01:07:02 +0200 Subject: [PATCH 14/37] - NSDate+ComputedTimes - add date parsing for strings following [year]-[[month][-[day]]] syntax - clarify all other method names - NSString+ByteCountParser: parse strings into byte counts, support TB, TiB, GB, GiB, MB, MiB, B and bytes-without-B - OCQueryCondition+SearchSegmenter - add support for localized keywords in local search - clean up keywords, removing kind-of-duplicates - add new keywords: - after: return items last modified after the given date - before: return items last modified before the given date - on: return items last modified on the given date - smaller: return items less than this size - greater: return items greater than this size --- ownCloud.xcodeproj/project.pbxproj | 8 ++ .../NSDate+ComputedTimes.h | 10 +- .../NSDate+ComputedTimes.m | 47 +++++++++- .../NSString+ByteCountParser.h | 29 ++++++ .../NSString+ByteCountParser.m | 88 ++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 11 +++ .../OCQueryCondition+SearchSegmenter.m | 93 +++++++++++++------ .../SearchSegmentationTests.m | 24 ++--- 8 files changed, 263 insertions(+), 47 deletions(-) create mode 100644 ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.h create mode 100644 ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.m diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index a93db1753..8d6840717 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -339,6 +339,8 @@ DC68057A212EAB5E006C3B1F /* ThemeCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */; }; DC6C68362574FD0400E46BD4 /* PLCrashReporter.LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = DC6C68352574FD0400E46BD4 /* PLCrashReporter.LICENSE */; }; DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6CF7FA219446050013B9F9 /* LogSettingsViewController.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 */; }; DC774E6022F44E57000B11A1 /* ZIPArchive.h in Headers */ = {isa = PBXBuildFile; fileRef = DC774E5C22F44E4A000B11A1 /* ZIPArchive.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC774E6322F44E6D000B11A1 /* OCCore+BundleImport.h in Headers */ = {isa = PBXBuildFile; fileRef = DC774E6122F44E6D000B11A1 /* OCCore+BundleImport.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1289,6 +1291,8 @@ DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCertificateViewController.swift; sourceTree = ""; }; DC6C68352574FD0400E46BD4 /* PLCrashReporter.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = PLCrashReporter.LICENSE; sourceTree = ""; }; DC6CF7FA219446050013B9F9 /* LogSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogSettingsViewController.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 = ""; }; DC774E5D22F44E4A000B11A1 /* ZIPArchive.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ZIPArchive.m; sourceTree = ""; }; DC774E6122F44E6D000B11A1 /* OCCore+BundleImport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCCore+BundleImport.h"; sourceTree = ""; }; @@ -2669,6 +2673,8 @@ DCC5E444232654DE002E5B84 /* NSObject+AnnotatedProperties.h */, DCCD776B2604C81B00098573 /* NSDate+ComputedTimes.m */, DCCD776A2604C81B00098573 /* NSDate+ComputedTimes.h */, + DC70398426128B89009F2DC1 /* NSString+ByteCountParser.m */, + DC70398326128B89009F2DC1 /* NSString+ByteCountParser.h */, ); path = "Foundation Extensions"; sourceTree = ""; @@ -3141,6 +3147,7 @@ DC4332002472E1B4002DC0E5 /* OCLicenseEMMProvider.h in Headers */, DCFEFE39236877A7009A142F /* OCLicenseFeature.h in Headers */, DC23D1DA238F391200423F62 /* OCLicenseAppStoreReceipt.h in Headers */, + DC70398526128B89009F2DC1 /* NSString+ByteCountParser.h in Headers */, DCF2DA8324C83BFB0026D790 /* OCFileProviderService.h in Headers */, DCF2DA8624C87A330026D790 /* OCCore+FPServices.h in Headers */, DC774E6022F44E57000B11A1 /* ZIPArchive.h in Headers */, @@ -4270,6 +4277,7 @@ DCFEFE2B236876BD009A142F /* OCLicenseManager.m in Sources */, DCDC20A22399A715003CFF5B /* OCCore+LicenseEnvironment.m in Sources */, DCDC20AC2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m in Sources */, + DC70398626128B89009F2DC1 /* NSString+ByteCountParser.m in Sources */, DCFEFE3A236877A7009A142F /* OCLicenseFeature.m in Sources */, DCFEFE50236880B5009A142F /* OCLicenseOffer.m in Sources */, DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */, diff --git a/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h index 173251d97..7abd30217 100644 --- a/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h +++ b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.h @@ -22,10 +22,12 @@ NS_ASSUME_NONNULL_BEGIN @interface NSDate (ComputedTimes) -+ (instancetype)startOfDay:(NSInteger)dayOffset; -+ (instancetype)startOfWeek:(NSInteger)weekOffset; -+ (instancetype)startOfMonth:(NSInteger)monthOffset; -+ (instancetype)startOfYear:(NSInteger)yearOffset; ++ (instancetype)startOfRelativeDay:(NSInteger)dayOffset; ++ (instancetype)startOfRelativeWeek:(NSInteger)weekOffset; ++ (instancetype)startOfRelativeMonth:(NSInteger)monthOffset; ++ (instancetype)startOfRelativeYear:(NSInteger)yearOffset; + ++ (nullable instancetype)dateFromKeywordString:(NSString *)dateString; @end diff --git a/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m index ca05550bf..8d89d7ded 100644 --- a/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m +++ b/ownCloudAppFramework/Foundation Extensions/NSDate+ComputedTimes.m @@ -33,19 +33,19 @@ - (instancetype)recomputeWithUnits:(NSCalendarUnit)units modifier:(void(^)(NSDat return ([calendar dateFromComponents:components]); } -+ (instancetype)startOfDay:(NSInteger)dayOffset ++ (instancetype)startOfRelativeDay:(NSInteger)dayOffset { return ([[NSDate dateWithTimeIntervalSinceNow:(NSTimeInterval)(dayOffset * 24 * 60 * 60)] recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:nil]); } -+ (instancetype)startOfWeek:(NSInteger)weekOffset ++ (instancetype)startOfRelativeWeek:(NSInteger)weekOffset { return ([[NSDate dateWithTimeIntervalSinceNow:(NSTimeInterval)(weekOffset * 7 * 24 * 60 * 60)] recomputeWithUnits:NSCalendarUnitWeekday|NSCalendarUnitWeekOfMonth|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { components.weekday = 2; // Monday, 1 = Sunday }]); } -+ (instancetype)startOfMonth:(NSInteger)monthOffset ++ (instancetype)startOfRelativeMonth:(NSInteger)monthOffset { return ([NSDate.date recomputeWithUnits:NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { if (monthOffset < 0) @@ -89,7 +89,7 @@ + (instancetype)startOfMonth:(NSInteger)monthOffset }]); } -+ (instancetype)startOfYear:(NSInteger)yearOffset ++ (instancetype)startOfRelativeYear:(NSInteger)yearOffset { return ([NSDate.date recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { components.day = 1; @@ -98,4 +98,43 @@ + (instancetype)startOfYear:(NSInteger)yearOffset }]); } ++ (nullable instancetype)dateFromKeywordString:(NSString *)dateString +{ + NSArray *components = [dateString componentsSeparatedByString:@"-"]; + NSString *yearString = ((components.firstObject != nil) && (components.firstObject.length == 4)) ? components.firstObject : nil; + NSString *monthString = ((components.count >= 2) && (components[1].length > 0)) ? components[1] : nil; + NSString *dayString = ((components.count == 3) && (components[2].length > 0)) ? components[2] : nil; + + if (yearString != nil) + { + if (components.count == 1) + { + return ([NSDate.date recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { + components.year = yearString.integerValue; + components.month = 1; + components.day = 1; + }]); + } + else if ((components.count == 2) && (monthString != nil)) + { + return ([NSDate.date recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { + components.year = yearString.integerValue; + components.month = monthString.integerValue; + components.day = 1; + }]); + } + else if ((components.count == 3) && (monthString != nil) && (dayString != nil)) + { + return ([NSDate.date recomputeWithUnits:NSCalendarUnitDay|NSCalendarUnitMonth|NSCalendarUnitYear modifier:^(NSDateComponents *components) { + components.year = yearString.integerValue; + components.month = monthString.integerValue; + components.day = dayString.integerValue; + }]); + } + } + + return (nil); +} + + @end diff --git a/ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.h b/ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.h new file mode 100644 index 000000000..ff50c9c93 --- /dev/null +++ b/ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.h @@ -0,0 +1,29 @@ +// +// NSString+ByteCountParser.h +// ownCloudApp +// +// Created by Felix Schwarz on 30.03.21. +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (ByteCountParser) + +- (nullable NSNumber *)byteCountNumber; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.m b/ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.m new file mode 100644 index 000000000..d3d17847f --- /dev/null +++ b/ownCloudAppFramework/Foundation Extensions/NSString+ByteCountParser.m @@ -0,0 +1,88 @@ +// +// NSString+ByteCountParser.m +// ownCloudApp +// +// Created by Felix Schwarz on 30.03.21. +// 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 "NSString+ByteCountParser.h" + +@implementation NSString (ByteCountParser) + +- (nullable NSNumber *)byteCountNumber +{ + NSNumber *byteCountNumber = nil; + NSString *lcString = self.lowercaseString, *bcString = nil; + NSUInteger multiplier = 0; + + if ([lcString hasSuffix:@"tb"]) + { + bcString = [lcString substringToIndex:self.length-2]; + multiplier = 1000000000000; + } + else if ([lcString hasSuffix:@"tib"]) + { + bcString = [lcString substringToIndex:self.length-3]; + multiplier = 1099511627776; + } + else if ([lcString hasSuffix:@"gb"]) + { + bcString = [lcString substringToIndex:self.length-2]; + multiplier = 1000000000; + } + else if ([lcString hasSuffix:@"gib"]) + { + bcString = [lcString substringToIndex:self.length-3]; + multiplier = 1073741824; + } + else if ([lcString hasSuffix:@"mb"]) + { + bcString = [lcString substringToIndex:self.length-2]; + multiplier = 1000000; + } + else if ([lcString hasSuffix:@"mib"]) + { + bcString = [lcString substringToIndex:self.length-3]; + multiplier = 1048576; + } + else if ([lcString hasSuffix:@"kb"]) + { + bcString = [lcString substringToIndex:self.length-2]; + multiplier = 1000; + } + else if ([lcString hasSuffix:@"kib"]) + { + bcString = [lcString substringToIndex:self.length-3]; + multiplier = 1024; + } + else if ([lcString hasSuffix:@"b"]) + { + bcString = [lcString substringToIndex:self.length-1]; + multiplier = 1; + } + else if (lcString.length > 0) + { + bcString = lcString; + multiplier = 1; + } + + if (multiplier != 0) + { + byteCountNumber = @(((NSUInteger)bcString.integerValue) * multiplier); + } + + return (byteCountNumber); +} + +@end diff --git a/ownCloudAppFramework/Resources/en.lproj/Localizable.strings b/ownCloudAppFramework/Resources/en.lproj/Localizable.strings index b9c25f1fe..47df91a3f 100644 --- a/ownCloudAppFramework/Resources/en.lproj/Localizable.strings +++ b/ownCloudAppFramework/Resources/en.lproj/Localizable.strings @@ -43,3 +43,14 @@ "%lu years" = "%lu years"; "%@ already unlocked for %@." = "%@ already unlocked for %@."; + +/* Search keywords */ +"type" = "type"; +"before" = "before"; +"after" = "after"; +"on" = "on"; +"d" = "d"; /* short-form for "day" */ +"w" = "w"; /* short-form for "week" */ +"m" = "m"; /* short-form for "month" */ +"y" = "y"; /* short-form for "year" */ + diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index 9149421c2..b83d315b9 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -18,6 +18,7 @@ #import "OCQueryCondition+SearchSegmenter.h" #import "NSDate+ComputedTimes.h" +#import "NSString+ByteCountParser.h" @implementation NSString (SearchSegmenter) @@ -119,19 +120,19 @@ + (instancetype)forSearchSegment:(NSString *)segmentString } else if ([segmentStringLowercase isEqual:@":today"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfDay:0]]); + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:0]]); } else if ([segmentStringLowercase isEqual:@":week"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfWeek:0]]); + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:0]]); } else if ([segmentStringLowercase isEqual:@":month"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfMonth:0]]); + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:0]]); } else if ([segmentStringLowercase isEqual:@":year"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfYear:0]]); + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:0]]); } else if ([segmentStringLowercase containsString:@":"]) { @@ -149,25 +150,62 @@ + (instancetype)forSearchSegment:(NSString *)segmentString { OCQueryCondition *condition = nil; - if ([modifier isEqual:@"type"]) + #define OCLocalizedKeyword(x) OCLocalized(x).lowercaseString + + if ([modifier isEqual:@"type"] || [modifier isEqual:OCLocalizedKeyword(@"type")]) { - condition = [OCQueryCondition where:OCItemPropertyNameName endsWith:parameter]; + condition = [OCQueryCondition where:OCItemPropertyNameName endsWith:[@"." stringByAppendingString:parameter]]; } - else if ([modifier isEqual:@"days"] || [modifier isEqual:@"day"]) + else if ([modifier isEqual:@"after"] || [modifier isEqual:OCLocalizedKeyword(@"after")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfDay:-parameter.integerValue]]; + NSDate *afterDate; + + if ((afterDate = [NSDate dateFromKeywordString:parameter]) != nil) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:afterDate]; + } } - else if ([modifier isEqual:@"weeks"] || [modifier isEqual:@"week"]) + else if ([modifier isEqual:@"before"] || [modifier isEqual:OCLocalizedKeyword(@"before")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfWeek:-parameter.integerValue]]; + NSDate *beforeDate; + + if ((beforeDate = [NSDate dateFromKeywordString:parameter]) != nil) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isLessThan:beforeDate]; + } } - else if ([modifier isEqual:@"months"] || [modifier isEqual:@"month"]) + else if ([modifier isEqual:@"on"] || [modifier isEqual:OCLocalizedKeyword(@"on")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfMonth:-parameter.integerValue]]; + NSDate *onStartDate = nil, *onEndDate = nil; + + if ((onStartDate = [NSDate dateFromKeywordString:parameter]) != nil) + { + onStartDate = [onStartDate dateByAddingTimeInterval:-1]; + onEndDate = [onStartDate dateByAddingTimeInterval:60*60*24+2]; + + condition = [OCQueryCondition require:@[ + [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:onStartDate], + [OCQueryCondition where:OCItemPropertyNameLastModified isLessThan:onEndDate] + ]]; + } } - else if ([modifier isEqual:@"years"] || [modifier isEqual:@"year"]) + else if ([modifier isEqual:@"smaller"] || [modifier isEqual:OCLocalizedKeyword(@"smaller")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfYear:-parameter.integerValue]]; + NSNumber *byteCount = [parameter byteCountNumber]; + + if (byteCount != nil) + { + condition = [OCQueryCondition where:OCItemPropertyNameSize isLessThan:byteCount]; + } + } + else if ([modifier isEqual:@"greater"] || [modifier isEqual:OCLocalizedKeyword(@"greater")]) + { + NSNumber *byteCount = [parameter byteCountNumber]; + + if (byteCount != nil) + { + condition = [OCQueryCondition where:OCItemPropertyNameSize isGreaterThan:byteCount]; + } } else if ([modifier isEqual:@""]) { @@ -184,21 +222,21 @@ + (instancetype)forSearchSegment:(NSString *)segmentString NSInteger numParam = numString.integerValue; NSString *timeLabel = [parameter substringFromIndex:parameter.length-1].lowercaseString; - if ([timeLabel isEqual:@"d"]) + if ([timeLabel isEqual:@"d"] || [timeLabel isEqual:OCLocalizedKeyword(@"d")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfDay:-numParam]]; + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:-numParam]]; } - else if ([timeLabel isEqual:@"w"]) + else if ([timeLabel isEqual:@"w"] || [timeLabel isEqual:OCLocalizedKeyword(@"w")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfWeek:-numParam]]; + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:-numParam]]; } - else if ([timeLabel isEqual:@"m"]) + else if ([timeLabel isEqual:@"m"] || [timeLabel isEqual:OCLocalizedKeyword(@"m")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfMonth:-numParam]]; + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:-numParam]]; } - else if ([timeLabel isEqual:@"y"]) + else if ([timeLabel isEqual:@"y"] || [timeLabel isEqual:OCLocalizedKeyword(@"y")]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfYear:-numParam]]; + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:-numParam]]; } } } @@ -218,11 +256,12 @@ + (instancetype)forSearchSegment:(NSString *)segmentString { return ([OCQueryCondition anyOf:orConditions]); } - else if ([modifier isEqual:@"type"] || - [modifier isEqual:@"days"] || [modifier isEqual:@"day"] || - [modifier isEqual:@"weeks"] || [modifier isEqual:@"week"] || - [modifier isEqual:@"months"] || [modifier isEqual:@"month"] || - [modifier isEqual:@"years"] || [modifier isEqual:@"year"] + else if ([modifier isEqual:@"type"] || [modifier isEqual:OCLocalized(@"type")] || + [modifier isEqual:@"after"] || [modifier isEqual:OCLocalized(@"after")] || + [modifier isEqual:@"before"] || [modifier isEqual:OCLocalized(@"before")] || + [modifier isEqual:@"on"] || [modifier isEqual:OCLocalized(@"on")] || + [modifier isEqual:@"greater"] || [modifier isEqual:OCLocalized(@"greater")] || + [modifier isEqual:@"smaller"] || [modifier isEqual:OCLocalized(@"smaller")] ) { // Modifiers without parameters diff --git a/ownCloudAppFrameworkTests/SearchSegmentationTests.m b/ownCloudAppFrameworkTests/SearchSegmentationTests.m index fe6ac4f4c..31633aef8 100644 --- a/ownCloudAppFrameworkTests/SearchSegmentationTests.m +++ b/ownCloudAppFrameworkTests/SearchSegmentationTests.m @@ -62,18 +62,18 @@ - (void)testStringSegmentation - (void)testDateComputations { - NSLog(@"Start of day(-2): %@", [NSDate startOfDay:-2]); - NSLog(@"Start of day( 0): %@", [NSDate startOfDay:0]); - NSLog(@"Start of day(+2): %@", [NSDate startOfDay:2]); - NSLog(@"Start of week(-1): %@", [NSDate startOfWeek:-1]); - NSLog(@"Start of week( 0): %@", [NSDate startOfWeek:0]); - NSLog(@"Start of week(+1): %@", [NSDate startOfWeek:1]); - NSLog(@"Start of month(-1): %@", [NSDate startOfMonth:-1]); - NSLog(@"Start of month( 0): %@", [NSDate startOfMonth:0]); - NSLog(@"Start of month(+1): %@", [NSDate startOfMonth:1]); - NSLog(@"Start of year(-1): %@", [NSDate startOfYear:-1]); - NSLog(@"Start of year( 0): %@", [NSDate startOfYear: 0]); - NSLog(@"Start of year(+1): %@", [NSDate startOfYear:+2]); + NSLog(@"Start of day(-2): %@", [NSDate startOfRelativeDay:-2]); + NSLog(@"Start of day( 0): %@", [NSDate startOfRelativeDay:0]); + NSLog(@"Start of day(+2): %@", [NSDate startOfRelativeDay:2]); + NSLog(@"Start of week(-1): %@", [NSDate startOfRelativeWeek:-1]); + NSLog(@"Start of week( 0): %@", [NSDate startOfRelativeWeek:0]); + NSLog(@"Start of week(+1): %@", [NSDate startOfRelativeWeek:1]); + NSLog(@"Start of month(-1): %@", [NSDate startOfRelativeMonth:-1]); + NSLog(@"Start of month( 0): %@", [NSDate startOfRelativeMonth:0]); + NSLog(@"Start of month(+1): %@", [NSDate startOfRelativeMonth:1]); + NSLog(@"Start of year(-1): %@", [NSDate startOfRelativeYear:-1]); + NSLog(@"Start of year( 0): %@", [NSDate startOfRelativeYear: 0]); + NSLog(@"Start of year(+1): %@", [NSDate startOfRelativeYear:+2]); } @end From 81f3e70dda1eef5b6b50909d63b164c105cf5ea8 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 30 Mar 2021 11:18:46 +0200 Subject: [PATCH 15/37] - Update SDK to fix Service Unavailable error handling --- ios-sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios-sdk b/ios-sdk index 672ad2835..9a2ac27f5 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 672ad2835500e2684b53c67d535d70a41685206a +Subproject commit 9a2ac27f53dd3039b5fe8ebb9360077f4959be50 From d9fdee2de8669a80aaef216d32a5b23a950110d8 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 30 Mar 2021 23:17:48 +0200 Subject: [PATCH 16/37] - update SDK to add item sync info scrubbing capabilities - address Xcode / linter warnings --- ios-sdk | 2 +- ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift | 2 +- .../Client/Sharing/GroupSharingTableViewController.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios-sdk b/ios-sdk index 9a2ac27f5..e3b043fbc 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 9a2ac27f53dd3039b5fe8ebb9360077f4959be50 +Subproject commit e3b043fbcc234e7610dc7cb22b7020760ce15615 diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift index 3e70cde65..803eae522 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift @@ -109,7 +109,7 @@ class OpenInAction: Action { self.interactionController = UIDocumentInteractionController(url: fileURL) self.interactionController?.delegate = self - if let _ = self.context.sender as? UIKeyCommand, let hostViewController = hostViewController { + if self.context.sender as? UIKeyCommand != nil, let hostViewController = hostViewController { var sourceRect = hostViewController.view.frame sourceRect.origin.x = viewController.view.center.x sourceRect.origin.y = viewController.navigationController?.navigationBar.frame.size.height ?? 0.0 diff --git a/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift b/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift index 67956469e..77b41483b 100644 --- a/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift +++ b/ownCloudAppShared/Client/Sharing/GroupSharingTableViewController.swift @@ -376,7 +376,7 @@ open class GroupSharingTableViewController: SharingTableViewController, UISearch } } - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { self.resetTable(showShares: true) self.messageView?.message(show: false) From e397d7cdd2a6a4368b3cf2818c804777815bcdb2 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 8 Apr 2021 22:38:25 +0200 Subject: [PATCH 17/37] OCQueryCondition+SearchSegmenter: - add full localization + normalization of keywords - add English and German localization --- .../Resources/de.lproj/Localizable.strings | 20 ++ .../Resources/en.lproj/Localizable.strings | 27 +- .../OCQueryCondition+SearchSegmenter.m | 328 +++++++++++------- 3 files changed, 242 insertions(+), 133 deletions(-) diff --git a/ownCloudAppFramework/Resources/de.lproj/Localizable.strings b/ownCloudAppFramework/Resources/de.lproj/Localizable.strings index 556aa51d6..d1ebd1000 100644 --- a/ownCloudAppFramework/Resources/de.lproj/Localizable.strings +++ b/ownCloudAppFramework/Resources/de.lproj/Localizable.strings @@ -41,3 +41,23 @@ "%lu years" = "%lu Jahre"; "%@ already unlocked for %@." = "%@ bereits freigeschaltet für %@."; + +/* Search keywords */ +"keyword_type" = "typ"; +"keyword_before" = "vor"; +"keyword_after" = "nach"; +"keyword_on" = "am"; +"keyword_smaller" = "kleiner"; +"keyword_greater" = "größer"; +"keyword_file" = "datei"; +"keyword_folder" = "ordner"; +"keyword_image" = "bild"; +"keyword_video" = "video"; +"keyword_today" = "heute"; +"keyword_week" = "woche"; +"keyword_month" = "monat"; +"keyword_year" = "jahr"; +"keyword_d" = "t"; /* short-form for "day" */ +"keyword_w" = "w"; /* short-form for "week" */ +"keyword_m" = "m"; /* short-form for "month" */ +"keyword_y" = "j"; /* short-form for "year" */ diff --git a/ownCloudAppFramework/Resources/en.lproj/Localizable.strings b/ownCloudAppFramework/Resources/en.lproj/Localizable.strings index 47df91a3f..9114c7246 100644 --- a/ownCloudAppFramework/Resources/en.lproj/Localizable.strings +++ b/ownCloudAppFramework/Resources/en.lproj/Localizable.strings @@ -45,12 +45,21 @@ "%@ already unlocked for %@." = "%@ already unlocked for %@."; /* Search keywords */ -"type" = "type"; -"before" = "before"; -"after" = "after"; -"on" = "on"; -"d" = "d"; /* short-form for "day" */ -"w" = "w"; /* short-form for "week" */ -"m" = "m"; /* short-form for "month" */ -"y" = "y"; /* short-form for "year" */ - +"keyword_type" = "type"; +"keyword_before" = "before"; +"keyword_after" = "after"; +"keyword_on" = "on"; +"keyword_smaller" = "smaller"; +"keyword_greater" = "greater"; +"keyword_file" = "file"; +"keyword_folder" = "folder"; +"keyword_image" = "image"; +"keyword_video" = "video"; +"keyword_today" = "today"; +"keyword_week" = "week"; +"keyword_month" = "month"; +"keyword_year" = "year"; +"keyword_d" = "d"; /* short-form for "day" */ +"keyword_w" = "w"; /* short-form for "week" */ +"keyword_m" = "m"; /* short-form for "month" */ +"keyword_y" = "y"; /* short-form for "year" */ diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index b83d315b9..a0ce54dcd 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -19,6 +19,7 @@ #import "OCQueryCondition+SearchSegmenter.h" #import "NSDate+ComputedTimes.h" #import "NSString+ByteCountParser.h" +#import "OCLicenseManager.h" // needed as localization "anchor" @implementation NSString (SearchSegmenter) @@ -98,175 +99,254 @@ @implementation NSString (SearchSegmenter) @implementation OCQueryCondition (SearchSegmenter) -+ (instancetype)forSearchSegment:(NSString *)segmentString ++ (nullable NSString *)normalizeKeyword:(NSString *)keyword { - NSString *segmentStringLowercase = segmentString.lowercaseString; + static dispatch_once_t onceToken; + static NSArray *keywords; + static NSDictionary *keywordByLocalizedKeyword; + + dispatch_once(&onceToken, ^{ + NSBundle *localizationBundle = [NSBundle bundleForClass:OCLicenseManager.class]; + + #define TranslateKeyword(keyword) [[localizationBundle localizedStringForKey:@"keyword_" keyword value:keyword table:@"Localizable"] lowercaseString] : keyword + + keywordByLocalizedKeyword = @{ + // Standalone keywords + TranslateKeyword(@"file"), + TranslateKeyword(@"folder"), + TranslateKeyword(@"image"), + TranslateKeyword(@"video"), + TranslateKeyword(@"today"), + TranslateKeyword(@"week"), + TranslateKeyword(@"month"), + TranslateKeyword(@"year"), + + // Modifier keywords + TranslateKeyword(@"type"), + TranslateKeyword(@"after"), + TranslateKeyword(@"before"), + TranslateKeyword(@"on"), + TranslateKeyword(@"smaller"), + TranslateKeyword(@"greater"), + + // Suffix keywords + TranslateKeyword(@"d"), + TranslateKeyword(@"w"), + TranslateKeyword(@"m"), + TranslateKeyword(@"y") + }; - if ([segmentStringLowercase isEqual:@":folder"]) - { - return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeCollection)]); - } - else if ([segmentStringLowercase isEqual:@":file"]) - { - return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeFile)]); - } - else if ([segmentStringLowercase isEqual:@":image"]) - { - return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"image/"]); - } - else if ([segmentStringLowercase isEqual:@":video"]) - { - return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"video/"]); - } - else if ([segmentStringLowercase isEqual:@":today"]) - { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:0]]); - } - else if ([segmentStringLowercase isEqual:@":week"]) + keywords = [keywordByLocalizedKeyword allValues]; + }); + + NSString *normalizedKeyword = nil; + + if (keyword != nil) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:0]]); + keyword = [keyword lowercaseString]; + + if ((normalizedKeyword = keywordByLocalizedKeyword[keyword]) == nil) + { + if ([keywords containsObject:keyword]) + { + normalizedKeyword = keyword; + } + } } - else if ([segmentStringLowercase isEqual:@":month"]) + + if ((normalizedKeyword == nil) && (keyword.length == 0)) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:0]]); + normalizedKeyword = keyword; } - else if ([segmentStringLowercase isEqual:@":year"]) + + return (normalizedKeyword); +} + ++ (instancetype)forSearchSegment:(NSString *)segmentString +{ + NSString *segmentStringLowercase = segmentString.lowercaseString; + + if ([segmentStringLowercase hasPrefix:@":"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:0]]); + NSString *keyword = [segmentStringLowercase substringFromIndex:1]; + + if ((keyword = [OCQueryCondition normalizeKeyword:keyword]) != nil) + { + if ([keyword isEqual:@"folder"]) + { + return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeCollection)]); + } + else if ([keyword isEqual:@"file"]) + { + return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeFile)]); + } + else if ([keyword isEqual:@"image"]) + { + return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"image/"]); + } + else if ([keyword isEqual:@"video"]) + { + return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"video/"]); + } + else if ([keyword isEqual:@"today"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:0]]); + } + else if ([keyword isEqual:@"week"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:0]]); + } + else if ([keyword isEqual:@"month"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:0]]); + } + else if ([keyword isEqual:@"year"]) + { + return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:0]]); + } + } } - else if ([segmentStringLowercase containsString:@":"]) + + if ([segmentStringLowercase containsString:@":"]) { NSArray *parts = [segmentString componentsSeparatedByString:@":"]; - NSString *modifier; + NSString *modifier = nil; if ((modifier = parts.firstObject.lowercaseString) != nil) { NSArray *parameters = [[segmentString substringFromIndex:modifier.length+1] componentsSeparatedByString:@","]; NSMutableArray *orConditions = [NSMutableArray new]; + NSString *modifierKeyword; - for (NSString *parameter in parameters) + if ((modifierKeyword = [OCQueryCondition normalizeKeyword:modifier]) != nil) { - if (parameter.length > 0) + for (NSString *parameter in parameters) { - OCQueryCondition *condition = nil; - - #define OCLocalizedKeyword(x) OCLocalized(x).lowercaseString - - if ([modifier isEqual:@"type"] || [modifier isEqual:OCLocalizedKeyword(@"type")]) - { - condition = [OCQueryCondition where:OCItemPropertyNameName endsWith:[@"." stringByAppendingString:parameter]]; - } - else if ([modifier isEqual:@"after"] || [modifier isEqual:OCLocalizedKeyword(@"after")]) + if (parameter.length > 0) { - NSDate *afterDate; + OCQueryCondition *condition = nil; - if ((afterDate = [NSDate dateFromKeywordString:parameter]) != nil) + if ([modifierKeyword isEqual:@"type"]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:afterDate]; + condition = [OCQueryCondition where:OCItemPropertyNameName endsWith:[@"." stringByAppendingString:parameter]]; } - } - else if ([modifier isEqual:@"before"] || [modifier isEqual:OCLocalizedKeyword(@"before")]) - { - NSDate *beforeDate; - - if ((beforeDate = [NSDate dateFromKeywordString:parameter]) != nil) + else if ([modifierKeyword isEqual:@"after"]) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isLessThan:beforeDate]; - } - } - else if ([modifier isEqual:@"on"] || [modifier isEqual:OCLocalizedKeyword(@"on")]) - { - NSDate *onStartDate = nil, *onEndDate = nil; - - if ((onStartDate = [NSDate dateFromKeywordString:parameter]) != nil) - { - onStartDate = [onStartDate dateByAddingTimeInterval:-1]; - onEndDate = [onStartDate dateByAddingTimeInterval:60*60*24+2]; + NSDate *afterDate; - condition = [OCQueryCondition require:@[ - [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:onStartDate], - [OCQueryCondition where:OCItemPropertyNameLastModified isLessThan:onEndDate] - ]]; + if ((afterDate = [NSDate dateFromKeywordString:parameter]) != nil) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:afterDate]; + } } - } - else if ([modifier isEqual:@"smaller"] || [modifier isEqual:OCLocalizedKeyword(@"smaller")]) - { - NSNumber *byteCount = [parameter byteCountNumber]; - - if (byteCount != nil) + else if ([modifierKeyword isEqual:@"before"]) { - condition = [OCQueryCondition where:OCItemPropertyNameSize isLessThan:byteCount]; - } - } - else if ([modifier isEqual:@"greater"] || [modifier isEqual:OCLocalizedKeyword(@"greater")]) - { - NSNumber *byteCount = [parameter byteCountNumber]; + NSDate *beforeDate; - if (byteCount != nil) - { - condition = [OCQueryCondition where:OCItemPropertyNameSize isGreaterThan:byteCount]; + if ((beforeDate = [NSDate dateFromKeywordString:parameter]) != nil) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isLessThan:beforeDate]; + } } - } - else if ([modifier isEqual:@""]) - { - // Parse time formats, f.ex.: 7d, 2w, 1m, 2y - NSString *numString = nil; - - if ((parameter.length == 1) || // :d :w :m :y - ((parameter.length > 1) && // :7d :2w :1m :2y - ((numString = [parameter substringToIndex:parameter.length-1]) != nil) && - [@([numString integerValue]).stringValue isEqual:numString] - ) - ) + else if ([modifierKeyword isEqual:@"on"]) { - NSInteger numParam = numString.integerValue; - NSString *timeLabel = [parameter substringFromIndex:parameter.length-1].lowercaseString; + NSDate *onStartDate = nil, *onEndDate = nil; - if ([timeLabel isEqual:@"d"] || [timeLabel isEqual:OCLocalizedKeyword(@"d")]) + if ((onStartDate = [NSDate dateFromKeywordString:parameter]) != nil) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:-numParam]]; + onStartDate = [onStartDate dateByAddingTimeInterval:-1]; + onEndDate = [onStartDate dateByAddingTimeInterval:60*60*24+2]; + + condition = [OCQueryCondition require:@[ + [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:onStartDate], + [OCQueryCondition where:OCItemPropertyNameLastModified isLessThan:onEndDate] + ]]; } - else if ([timeLabel isEqual:@"w"] || [timeLabel isEqual:OCLocalizedKeyword(@"w")]) + } + else if ([modifierKeyword isEqual:@"smaller"]) + { + NSNumber *byteCount = [parameter byteCountNumber]; + + if (byteCount != nil) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:-numParam]]; + condition = [OCQueryCondition where:OCItemPropertyNameSize isLessThan:byteCount]; } - else if ([timeLabel isEqual:@"m"] || [timeLabel isEqual:OCLocalizedKeyword(@"m")]) + } + else if ([modifierKeyword isEqual:@"greater"]) + { + NSNumber *byteCount = [parameter byteCountNumber]; + + if (byteCount != nil) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:-numParam]]; + condition = [OCQueryCondition where:OCItemPropertyNameSize isGreaterThan:byteCount]; } - else if ([timeLabel isEqual:@"y"] || [timeLabel isEqual:OCLocalizedKeyword(@"y")]) + } + else if ([modifier isEqual:@""]) + { + // Parse time formats, f.ex.: 7d, 2w, 1m, 2y + NSString *numString = nil; + + if ((parameter.length == 1) || // :d :w :m :y + ((parameter.length > 1) && // :7d :2w :1m :2y + ((numString = [parameter substringToIndex:parameter.length-1]) != nil) && + [@([numString integerValue]).stringValue isEqual:numString] + ) + ) { - condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:-numParam]]; + NSInteger numParam = numString.integerValue; + NSString *timeLabel = [parameter substringFromIndex:parameter.length-1].lowercaseString; + + timeLabel = [OCQueryCondition normalizeKeyword:timeLabel]; + + if ([timeLabel isEqual:@"d"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:-numParam]]; + } + else if ([timeLabel isEqual:@"w"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:-numParam]]; + } + else if ([timeLabel isEqual:@"m"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:-numParam]]; + } + else if ([timeLabel isEqual:@"y"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:-numParam]]; + } } } + + if (condition != nil) + { + [orConditions addObject:condition]; + } } + } - if (condition != nil) + if (orConditions.count == 1) + { + return (orConditions.firstObject); + } + else if (orConditions.count > 0) + { + return ([OCQueryCondition anyOf:orConditions]); + } + else + { + if ([modifierKeyword isEqual:@"type"] || + [modifierKeyword isEqual:@"after"] || + [modifierKeyword isEqual:@"before"] || + [modifierKeyword isEqual:@"on"] || + [modifierKeyword isEqual:@"greater"] || + [modifierKeyword isEqual:@"smaller"] + ) { - [orConditions addObject:condition]; + // Modifiers without parameters + return (nil); } } } - - if (orConditions.count == 1) - { - return (orConditions.firstObject); - } - else if (orConditions.count > 0) - { - return ([OCQueryCondition anyOf:orConditions]); - } - else if ([modifier isEqual:@"type"] || [modifier isEqual:OCLocalized(@"type")] || - [modifier isEqual:@"after"] || [modifier isEqual:OCLocalized(@"after")] || - [modifier isEqual:@"before"] || [modifier isEqual:OCLocalized(@"before")] || - [modifier isEqual:@"on"] || [modifier isEqual:OCLocalized(@"on")] || - [modifier isEqual:@"greater"] || [modifier isEqual:OCLocalized(@"greater")] || - [modifier isEqual:@"smaller"] || [modifier isEqual:OCLocalized(@"smaller")] - ) - { - // Modifiers without parameters - return (nil); - } } } From 4e4416db56c62351841f30fe501db19c4ca45a32 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 8 Apr 2021 22:45:26 +0200 Subject: [PATCH 18/37] - update SDK - remove leftover code --- ios-sdk | 2 +- .../Client/File Lists/ClientQueryViewController.swift | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/ios-sdk b/ios-sdk index e3b043fbc..44425b3ad 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit e3b043fbcc234e7610dc7cb22b7020760ce15615 +Subproject commit 44425b3ad9bd405713eb16dcae60de010dc150bc diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 06e1811fa..e16a738f8 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -259,14 +259,6 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn searchController?.delegate = self } -// -// open override func startQuery(_ theQuery: OCQuery) { -// if theQuery == customSearchQuery { -// self.updateCustomSearchQuery() -// } else { -// super.startQuery(theQuery) -// } -// } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) From 0f5b88692456af96b6b9bd8a0f2882216c3604e5 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 8 Apr 2021 22:51:39 +0200 Subject: [PATCH 19/37] - QueryFileListTableViewController: remove unneeded/conflicting searchScope initialization - added missing localizations --- ownCloud/Resources/en.lproj/Localizable.strings | 4 ++++ .../Client/File Lists/QueryFileListTableViewController.swift | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 4fd1da046..5a165d0fb 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -106,6 +106,10 @@ "Show more results" = "Show more results"; +/* Search scope */ +"all" = "all"; +"folder" = "folder"; + /* Static Login Setup */ "Server error" = "Server error"; "The server doesn't support any allowed authentication method." = "The server doesn't support any allowed authentication method."; diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index d163ee6b6..528db2fcd 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -75,7 +75,6 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa public init(core inCore: OCCore, query inQuery: OCQuery) { query = inQuery - searchScope = .global super.init(core: inCore) From 356d42b396b8a3510d7b1793744b21321be3e58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Fri, 9 Apr 2021 14:09:38 +0200 Subject: [PATCH 20/37] Code review suggestion for PR #933: - hide multiselect button, when search is active - use labels instead of images for search scope segmented control - changed more button width for better UI alignment - changed multiselect button width for better touch experience --- .../Resources/en.lproj/Localizable.strings | 3 +- .../User Interface/ClientItemCell.swift | 8 ++--- .../Client/User Interface/SortBar.swift | 30 +++++-------------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 146ed1dcc..89f5d1ccc 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -107,8 +107,7 @@ "Show more results" = "Show more results"; /* Search scope */ -"all" = "all"; -"folder" = "folder"; +"Search scope" = "Search scope"; /* Static Login Setup */ "Server error" = "Server error"; diff --git a/ownCloudAppShared/Client/User Interface/ClientItemCell.swift b/ownCloudAppShared/Client/User Interface/ClientItemCell.swift index 04379e47b..378ac5bd8 100644 --- a/ownCloudAppShared/Client/User Interface/ClientItemCell.swift +++ b/ownCloudAppShared/Client/User Interface/ClientItemCell.swift @@ -37,7 +37,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { private let smallSpacing : CGFloat = 2 private let iconViewWidth : CGFloat = 40 private let detailIconViewHeight : CGFloat = 15 - private let moreButtonWidth : CGFloat = 45 + private let moreButtonWidth : CGFloat = 60 private let revealButtonWidth : CGFloat = 35 private let verticalLabelMarginFromCenter : CGFloat = 2 private let iconSize : CGSize = CGSize(width: 40, height: 40) @@ -81,7 +81,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { if isMoreButtonPermanentlyHidden { moreButtonWidthConstraint?.constant = 0 } else { - moreButtonWidthConstraint?.constant = moreButtonWidth + moreButtonWidthConstraint?.constant = showRevealButton ? revealButtonWidth : moreButtonWidth } } } @@ -219,7 +219,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) - moreButtonWidthConstraint = moreButton.widthAnchor.constraint(equalToConstant: moreButtonWidth) + moreButtonWidthConstraint = moreButton.widthAnchor.constraint(equalToConstant: showRevealButton ? revealButtonWidth : moreButtonWidth) revealButtonWidthConstraint = revealButton.widthAnchor.constraint(equalToConstant: showRevealButton ? revealButtonWidth : 0) cloudStatusIconViewZeroWidthConstraint = cloudStatusIconView.widthAnchor.constraint(equalToConstant: 0) @@ -550,7 +550,7 @@ open class ClientItemCell: ThemeTableViewCell, ItemContainer { if hidden || isMoreButtonPermanentlyHidden { moreButtonWidthConstraint?.constant = 0 } else { - moreButtonWidthConstraint?.constant = moreButtonWidth + moreButtonWidthConstraint?.constant = showRevealButton ? revealButtonWidth : moreButtonWidth } moreButton.isHidden = ((item?.isPlaceholder == true) || (progressView != nil)) ? true : hidden if animated { diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index 20f3ff8cf..522264407 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -43,25 +43,12 @@ public enum SearchScope : Int, CaseIterable { var name : String! switch self { - case .global: name = "all".localized - case .local: name = "folder".localized + case .global: name = "Account".localized + case .local: name = "Folder".localized } return name } - - var image : UIImage? { - var image : UIImage? - - if #available(iOS 13, *) { - switch self { - case .global: image = UIImage(systemName: "globe") - case .local: image = UIImage(systemName: "folder") - } - } - - return image - } } public protocol SortBarDelegate: class { @@ -88,10 +75,11 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate } // MARK: - Constants - let sideButtonsSize: CGSize = CGSize(width: 22.0, height: 22.0) + let sideButtonsSize: CGSize = CGSize(width: 44.0, height: 44.0) let leftPadding: CGFloat = 20.0 let rightPadding: CGFloat = 20.0 let rightSelectButtonPadding: CGFloat = 8.0 + let rightSearchScopePadding: CGFloat = 15.0 let topPadding: CGFloat = 10.0 let bottomPadding: CGFloat = 10.0 @@ -113,6 +101,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate var showSearchScope: Bool = false { didSet { + showSelectButton = !self.showSearchScope self.searchScopeSegmentedControl?.isHidden = false self.searchScopeSegmentedControl?.alpha = oldValue ? 1.0 : 0.0 @@ -183,13 +172,10 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate sortButton.accessibilityIdentifier = "sort-bar.sortButton" sortSegmentedControl.accessibilityIdentifier = "sort-bar.segmentedControl" searchScopeSegmentedControl.accessibilityIdentifier = "sort-bar.searchScopeSegmentedControl" + searchScopeSegmentedControl.accessibilityLabel = "Search scope".localized for scope in SearchScope.allCases { - if let image = scope.image { - searchScopeSegmentedControl.insertSegment(with: image, at: scope.rawValue, animated: false) - } else { - searchScopeSegmentedControl.insertSegment(withTitle: scope.label, at: scope.rawValue, animated: false) - } + searchScopeSegmentedControl.insertSegment(withTitle: scope.label, at: scope.rawValue, animated: false) } searchScopeSegmentedControl.selectedSegmentIndex = searchScope.rawValue searchScopeSegmentedControl.isHidden = !self.showSearchScope @@ -206,7 +192,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate sortSegmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -bottomPadding), sortSegmentedControl.leftAnchor.constraint(equalTo: self.leftAnchor, constant: leftPadding), - searchScopeSegmentedControl.trailingAnchor.constraint(equalTo: selectButton.leadingAnchor, constant: -10), + 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) ]) From 76547c9f646f514fd9fd38bde7b463348aed2623 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 9 Apr 2021 15:21:07 +0200 Subject: [PATCH 21/37] - change search field placeholder depending on selected scope --- ownCloud/Resources/en.lproj/Localizable.strings | 3 ++- .../QueryFileListTableViewController.swift | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 146ed1dcc..a3e67b883 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -95,7 +95,8 @@ "kind" = "kind"; "size" = "size"; "date" = "date"; -"Search this folder" = "Search this folder"; +"Search folder" = "Search folder"; +"Search account" = "Search account"; "Pending" = "Pending"; "Show parent paths" = "Show parent paths"; diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 528db2fcd..b958db110 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -116,7 +116,11 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa return sort } } - open var searchScope: SearchScope = .local + open var searchScope: SearchScope = .local { + didSet { + updateSearchPlaceholder() + } + } open var sortDirection: SortDirection { set { UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-direction") @@ -356,14 +360,20 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa 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 + if #available(iOS 13.0, *) { let attributedStringColor = [NSAttributedString.Key.foregroundColor : Theme.shared.activeCollection.searchBarColors.secondaryLabelColor] - let attributedString = NSAttributedString(string: "Search this folder".localized, attributes: attributedStringColor) + let attributedString = NSAttributedString(string: placeholderString, attributes: attributedStringColor) searchController?.searchBar.searchTextField.attributedPlaceholder = attributedString } else { // Fallback on earlier versions - searchController?.searchBar.placeholder = "Search this folder".localized + searchController?.searchBar.placeholder = placeholderString } } From ce81cac8bbc4bff12b1926774525437a4394aa0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Mon, 12 Apr 2021 15:37:32 +0200 Subject: [PATCH 22/37] - fixed showing and executing keyboard shortcuts when search is active in file list (was not shown, because action was not available in performing class) - added "Toggle Search Scope" keyboard shortcut - scroll file list to top, when search will be activated --- ownCloud/Key Commands/KeyCommands.swift | 57 ++++++++++++++++--- .../Resources/en.lproj/Localizable.strings | 1 + .../ClientQueryViewController.swift | 3 +- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index 6e0733648..ca629ee92 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -260,11 +260,27 @@ extension ClientRootViewController { if excludeViewControllers.contains(where: {$0 == type(of: visibleController)}) { return shortcuts } else if let controller = visibleController as? PDFSearchViewController { - return controller.keyCommands } } + if let navigationController = self.selectedViewController as? ThemeNavigationController, navigationController.visibleViewController?.navigationItem.searchController?.isActive ?? false { + let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) + shortcuts.append(cancelCommand) + + if let visibleViewController = navigationController.visibleViewController, let keyCommands = visibleViewController.keyCommands { + let newKeyCommands = keyCommands.map { (keyCommand) -> UIKeyCommand in + if let input = keyCommand.input, let discoverabilityTitle = keyCommand.discoverabilityTitle { + return UIKeyCommand(input: input, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController), discoverabilityTitle: discoverabilityTitle) + } + + return UIKeyCommand(input: keyCommand.input!, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController)) + } + + shortcuts.append(contentsOf: newKeyCommands) + } + } + if let navigationController = self.selectedViewController as? ThemeNavigationController, !((navigationController.visibleViewController as? UIAlertController) != nil) { let keyCommands = self.tabBar.items?.enumerated().map { (index, item) -> UIKeyCommand in let tabIndex = String(index + 1) @@ -275,14 +291,24 @@ extension ClientRootViewController { } } - if let navigationController = self.selectedViewController as? ThemeNavigationController, navigationController.visibleViewController?.navigationItem.searchController?.isActive ?? false { - let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) - shortcuts.append(cancelCommand) - } - return shortcuts } + @objc func performActionOnVisibleViewController(sender: UIKeyCommand) { + if let navigationController = self.selectedViewController as? ThemeNavigationController, let visibleController = navigationController.visibleViewController, let keyCommands = visibleController.keyCommands { + let commands = keyCommands.filter { (keyCommand) -> Bool in + if keyCommand.discoverabilityTitle == sender.discoverabilityTitle { + return true + } + return false + } + + if let command = commands.first { + visibleController.perform(command.action, with: sender) + } + } + } + @objc func dismissSearch(sender: UIKeyCommand) { if let navigationController = self.selectedViewController as? ThemeNavigationController { if let searchController = navigationController.visibleViewController?.navigationItem.searchController { @@ -627,6 +653,12 @@ extension ClientQueryViewController { open override var keyCommands: [UIKeyCommand]? { var shortcuts = [UIKeyCommand]() + + let scopeCommand = UIKeyCommand(input: "F", modifierFlags: [.command], action: #selector(changeSearchScope(_:)), discoverabilityTitle: "Toggle Search Scope".localized) + if let searchController = searchController, searchController.isActive { + shortcuts.append(scopeCommand) + } + if let superKeyCommands = super.keyCommands { shortcuts.append(contentsOf: superKeyCommands) } @@ -667,6 +699,15 @@ extension ClientQueryViewController { return shortcuts } + @objc func changeSearchScope(_ command : UIKeyCommand) { + if self.sortBar?.searchScope == .global { + self.sortBar?.searchScope = .local + } else { + self.sortBar?.searchScope = .global + } + updateCustomSearchQuery() + } + @objc func performFolderAction(_ command : UIKeyCommand) { if let core = core, let rootItem = query.rootItem { var item = rootItem @@ -763,7 +804,9 @@ extension QueryFileListTableViewController { }) } - shortcuts.append(searchCommand) + if let searchController = searchController, !searchController.isActive { + shortcuts.append(searchCommand) + } shortcuts.append(toggleSortCommand) for (index, method) in SortMethod.all.enumerated() { diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 5cbd6f48c..1f8097345 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -109,6 +109,7 @@ /* Search scope */ "Search scope" = "Search scope"; +"Toggle Search Scope" = "Toggle Search Scope"; /* Static Login Setup */ "Server error" = "Server error"; diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 26c282958..1912c2402 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -160,6 +160,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn // MARK: - Search events open func willPresentSearchController(_ searchController: UISearchController) { self.sortBar?.showSearchScope = true + self.tableView.setContentOffset(.zero, animated: false) } open func willDismissSearchController(_ searchController: UISearchController) { @@ -196,7 +197,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn private var lastSearchText : String? - func updateCustomSearchQuery() { + public func updateCustomSearchQuery() { if lastSearchText != searchText { // Reset max result count when search text changes maxResultCount = maxResultCountDefault From 77979e1203b2a7f517cebf61a45a4369d29d20ad Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Mon, 12 Apr 2021 22:17:39 +0200 Subject: [PATCH 23/37] - comment out parts of KeyCommands that seem to interfere with entry of search text, jumping to entries with the letters instead - scroll to top when results change - add support for negating search terms by prefixing them with "-", i.e. "-word" to exclude results with "word" in it --- ownCloud/Key Commands/KeyCommands.swift | 60 +++++++++---------- .../OCQueryCondition+SearchSegmenter.m | 38 ++++++++---- .../ClientQueryViewController.swift | 16 +++++ .../FileListTableViewController.swift | 6 +- .../QueryFileListTableViewController.swift | 4 +- 5 files changed, 79 insertions(+), 45 deletions(-) diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index ca629ee92..7ade4151f 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -264,22 +264,22 @@ extension ClientRootViewController { } } - if let navigationController = self.selectedViewController as? ThemeNavigationController, navigationController.visibleViewController?.navigationItem.searchController?.isActive ?? false { - let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) - shortcuts.append(cancelCommand) - - if let visibleViewController = navigationController.visibleViewController, let keyCommands = visibleViewController.keyCommands { - let newKeyCommands = keyCommands.map { (keyCommand) -> UIKeyCommand in - if let input = keyCommand.input, let discoverabilityTitle = keyCommand.discoverabilityTitle { - return UIKeyCommand(input: input, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController), discoverabilityTitle: discoverabilityTitle) - } - - return UIKeyCommand(input: keyCommand.input!, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController)) - } - - shortcuts.append(contentsOf: newKeyCommands) - } - } +// if let navigationController = self.selectedViewController as? ThemeNavigationController, navigationController.visibleViewController?.navigationItem.searchController?.isActive ?? false { +// let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) +// shortcuts.append(cancelCommand) +// +// if let visibleViewController = navigationController.visibleViewController, let keyCommands = visibleViewController.keyCommands { +// let newKeyCommands = keyCommands.map { (keyCommand) -> UIKeyCommand in +// if let input = keyCommand.input, let discoverabilityTitle = keyCommand.discoverabilityTitle { +// return UIKeyCommand(input: input, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController), discoverabilityTitle: discoverabilityTitle) +// } +// +// return UIKeyCommand(input: keyCommand.input!, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController)) +// } +// +// shortcuts.append(contentsOf: newKeyCommands) +// } +// } if let navigationController = self.selectedViewController as? ThemeNavigationController, !((navigationController.visibleViewController as? UIAlertController) != nil) { let keyCommands = self.tabBar.items?.enumerated().map { (index, item) -> UIKeyCommand in @@ -294,20 +294,20 @@ extension ClientRootViewController { return shortcuts } - @objc func performActionOnVisibleViewController(sender: UIKeyCommand) { - if let navigationController = self.selectedViewController as? ThemeNavigationController, let visibleController = navigationController.visibleViewController, let keyCommands = visibleController.keyCommands { - let commands = keyCommands.filter { (keyCommand) -> Bool in - if keyCommand.discoverabilityTitle == sender.discoverabilityTitle { - return true - } - return false - } - - if let command = commands.first { - visibleController.perform(command.action, with: sender) - } - } - } +// @objc func performActionOnVisibleViewController(sender: UIKeyCommand) { +// if let navigationController = self.selectedViewController as? ThemeNavigationController, let visibleController = navigationController.visibleViewController, let keyCommands = visibleController.keyCommands { +// let commands = keyCommands.filter { (keyCommand) -> Bool in +// if keyCommand.discoverabilityTitle == sender.discoverabilityTitle { +// return true +// } +// return false +// } +// +// if let command = commands.first { +// visibleController.perform(command.action, with: sender) +// } +// } +// } @objc func dismissSearch(sender: UIKeyCommand) { if let navigationController = self.selectedViewController as? ThemeNavigationController { diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index a0ce54dcd..245a454a0 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -164,7 +164,21 @@ + (nullable NSString *)normalizeKeyword:(NSString *)keyword + (instancetype)forSearchSegment:(NSString *)segmentString { - NSString *segmentStringLowercase = segmentString.lowercaseString; + NSString *segmentStringLowercase = nil; + BOOL negateCondition = NO; + + if ([segmentString hasPrefix:@"-"]) + { + negateCondition = YES; + segmentString = [segmentString substringFromIndex:1]; + } + + if (segmentString.length == 0) + { + return (nil); + } + + segmentStringLowercase = segmentString.lowercaseString; if ([segmentStringLowercase hasPrefix:@":"]) { @@ -174,35 +188,35 @@ + (instancetype)forSearchSegment:(NSString *)segmentString { if ([keyword isEqual:@"folder"]) { - return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeCollection)]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeCollection)]]); } else if ([keyword isEqual:@"file"]) { - return ([OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeFile)]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameType isEqualTo:@(OCItemTypeFile)]]); } else if ([keyword isEqual:@"image"]) { - return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"image/"]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"image/"]]); } else if ([keyword isEqual:@"video"]) { - return ([OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"video/"]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameMIMEType startsWith:@"video/"]]); } else if ([keyword isEqual:@"today"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:0]]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeDay:0]]]); } else if ([keyword isEqual:@"week"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:0]]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeWeek:0]]]); } else if ([keyword isEqual:@"month"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:0]]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeMonth:0]]]); } else if ([keyword isEqual:@"year"]) { - return ([OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:0]]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameLastModified isGreaterThan:[NSDate startOfRelativeYear:0]]]); } } } @@ -326,11 +340,11 @@ + (instancetype)forSearchSegment:(NSString *)segmentString if (orConditions.count == 1) { - return (orConditions.firstObject); + return ([OCQueryCondition negating:negateCondition condition:orConditions.firstObject]); } else if (orConditions.count > 0) { - return ([OCQueryCondition anyOf:orConditions]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition anyOf:orConditions]]); } else { @@ -350,7 +364,7 @@ + (instancetype)forSearchSegment:(NSString *)segmentString } } - return ([OCQueryCondition where:OCItemPropertyNameName contains:segmentString]); + return ([OCQueryCondition negating:negateCondition condition:[OCQueryCondition where:OCItemPropertyNameName contains:segmentString]]); } + (instancetype)fromSearchTerm:(NSString *)searchTerm diff --git a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift index 1912c2402..fa6798314 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -196,12 +196,16 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } 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, @@ -592,6 +596,18 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn } } + 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 diff --git a/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift index 615e3d4c3..052a8c720 100644 --- a/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/FileListTableViewController.swift @@ -228,7 +228,7 @@ open class FileListTableViewController: UITableViewController, ClientItemCellDel } if !ifNeeded || (ifNeeded && tableReloadNeeded) { - self.tableView.reloadData() + self.delegatedTableViewDataReload() if viewControllerVisible { tableReloadNeeded = false @@ -238,6 +238,10 @@ open class FileListTableViewController: UITableViewController, ClientItemCellDel } } + open func delegatedTableViewDataReload() { + self.tableView.reloadData() + } + open func restoreSelectionAfterTableReload() { } diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index b958db110..8140023f0 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -303,11 +303,11 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa self.messageView?.message(show: false) } - self.tableView.reloadData() + 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.tableView.reloadData() + self.reloadTableData() default: self.messageView?.message(show: false) From 527ab4dceaddea95908883f3e68ccfd46fe9dd39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Tue, 13 Apr 2021 13:54:41 +0200 Subject: [PATCH 24/37] - disable keyboard shortcuts for letters, if search is active - make search field first responder, when toggle search scope --- ownCloud/Key Commands/KeyCommands.swift | 64 +++++++++++++------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index 7ade4151f..81591b237 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -264,22 +264,22 @@ extension ClientRootViewController { } } -// if let navigationController = self.selectedViewController as? ThemeNavigationController, navigationController.visibleViewController?.navigationItem.searchController?.isActive ?? false { -// let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) -// shortcuts.append(cancelCommand) -// -// if let visibleViewController = navigationController.visibleViewController, let keyCommands = visibleViewController.keyCommands { -// let newKeyCommands = keyCommands.map { (keyCommand) -> UIKeyCommand in -// if let input = keyCommand.input, let discoverabilityTitle = keyCommand.discoverabilityTitle { -// return UIKeyCommand(input: input, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController), discoverabilityTitle: discoverabilityTitle) -// } -// -// return UIKeyCommand(input: keyCommand.input!, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController)) -// } -// -// shortcuts.append(contentsOf: newKeyCommands) -// } -// } + if let navigationController = self.selectedViewController as? ThemeNavigationController, navigationController.visibleViewController?.navigationItem.searchController?.isActive ?? false { + let cancelCommand = UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(dismissSearch), discoverabilityTitle: "Cancel".localized) + shortcuts.append(cancelCommand) + + if let visibleViewController = navigationController.visibleViewController, let keyCommands = visibleViewController.keyCommands { + let newKeyCommands = keyCommands.map { (keyCommand) -> UIKeyCommand in + if let input = keyCommand.input, let discoverabilityTitle = keyCommand.discoverabilityTitle { + return UIKeyCommand(input: input, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController), discoverabilityTitle: discoverabilityTitle) + } + + return UIKeyCommand(input: keyCommand.input!, modifierFlags: keyCommand.modifierFlags, action: #selector(performActionOnVisibleViewController)) + } + + shortcuts.append(contentsOf: newKeyCommands) + } + } if let navigationController = self.selectedViewController as? ThemeNavigationController, !((navigationController.visibleViewController as? UIAlertController) != nil) { let keyCommands = self.tabBar.items?.enumerated().map { (index, item) -> UIKeyCommand in @@ -294,20 +294,20 @@ extension ClientRootViewController { return shortcuts } -// @objc func performActionOnVisibleViewController(sender: UIKeyCommand) { -// if let navigationController = self.selectedViewController as? ThemeNavigationController, let visibleController = navigationController.visibleViewController, let keyCommands = visibleController.keyCommands { -// let commands = keyCommands.filter { (keyCommand) -> Bool in -// if keyCommand.discoverabilityTitle == sender.discoverabilityTitle { -// return true -// } -// return false -// } -// -// if let command = commands.first { -// visibleController.perform(command.action, with: sender) -// } -// } -// } + @objc func performActionOnVisibleViewController(sender: UIKeyCommand) { + if let navigationController = self.selectedViewController as? ThemeNavigationController, let visibleController = navigationController.visibleViewController, let keyCommands = visibleController.keyCommands { + let commands = keyCommands.filter { (keyCommand) -> Bool in + if keyCommand.discoverabilityTitle == sender.discoverabilityTitle { + return true + } + return false + } + + if let command = commands.first { + visibleController.perform(command.action, with: sender) + } + } + } @objc func dismissSearch(sender: UIKeyCommand) { if let navigationController = self.selectedViewController as? ThemeNavigationController { @@ -706,6 +706,8 @@ extension ClientQueryViewController { self.sortBar?.searchScope = .global } updateCustomSearchQuery() + self.searchController?.isActive = true + self.searchController?.searchBar.becomeFirstResponder() } @objc func performFolderAction(_ command : UIKeyCommand) { @@ -779,7 +781,7 @@ extension QueryFileListTableViewController { let toggleSortCommand = UIKeyCommand(input: "S", modifierFlags: [.command, .shift], action: #selector(toggleSortOrder), discoverabilityTitle: "Change Sort Order".localized) let searchCommand = UIKeyCommand(input: "F", modifierFlags: [.command], action: #selector(enableSearch), discoverabilityTitle: "Search".localized) // Add key commands for file name letters - if sortMethod == .alphabetically { + if sortMethod == .alphabetically, let searchController = searchController, !searchController.isActive { let indexTitles = Array( Set( self.items.map { String(( $0.name?.first!.uppercased())!) })).sorted() for title in indexTitles { let letterCommand = UIKeyCommand(input: title, modifierFlags: [], action: #selector(selectLetter)) From b89987a22c7dbe56493496d6436d1e2ab3e6003f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Tue, 13 Apr 2021 14:24:14 +0200 Subject: [PATCH 25/37] fixed search field cursor tint color --- ownCloudAppShared/User Interface/Theme/Theme.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ownCloudAppShared/User Interface/Theme/Theme.swift b/ownCloudAppShared/User Interface/Theme/Theme.swift index 27a2efe8e..dd2524a5e 100644 --- a/ownCloudAppShared/User Interface/Theme/Theme.swift +++ b/ownCloudAppShared/User Interface/Theme/Theme.swift @@ -237,7 +237,7 @@ public class Theme: NSObject { } UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).keyboardAppearance = collection.keyboardAppearance UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = collection.tintColor - UITextField.appearance().tintColor = collection.tableRowColors.labelColor + UITextField.appearance().tintColor = collection.searchBarColors.tintColor } } From e09822ecdf36e8596cb0ec069cc54f234ab07c9c Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 13 Apr 2021 16:59:28 +0200 Subject: [PATCH 26/37] - logging improvements: use OCFileOpLog to log file operation in OCCore+BundleImport.m - update SDK --- ios-sdk | 2 +- .../SDK Extensions/OCCore+BundleImport.m | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ios-sdk b/ios-sdk index 22f793a1b..29fa010dc 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 22f793a1bd9213b0de064272a53dc709604a5458 +Subproject commit 29fa010dcaad711697588008743b44c30c20d123 diff --git a/ownCloudAppFramework/SDK Extensions/OCCore+BundleImport.m b/ownCloudAppFramework/SDK Extensions/OCCore+BundleImport.m index 18b3c856c..d949f906e 100644 --- a/ownCloudAppFramework/SDK Extensions/OCCore+BundleImport.m +++ b/ownCloudAppFramework/SDK Extensions/OCCore+BundleImport.m @@ -62,9 +62,17 @@ - (nullable NSProgress *)importItemNamed:(nullable NSString *)newFileName at:(OC if ((error = [ZIPArchive compressContentsOf:sourceURL asZipFile:zipURL]) == nil) { - if ([[NSFileManager defaultManager] removeItemAtURL:sourceURL error:&error]) + BOOL success = [[NSFileManager defaultManager] removeItemAtURL:sourceURL error:&error]; + + OCFileOpLog(@"rm", error, @"Removed ZIP source at %@", sourceURL.path); + + if (success) { - if (![[NSFileManager defaultManager] moveItemAtURL:zipURL toURL:sourceURL error:&error]) + success = [[NSFileManager defaultManager] moveItemAtURL:zipURL toURL:sourceURL error:&error]; + + OCFileOpLog(@"mv", error, @"Renamed from ZIPped %@ to %@", zipURL.path, sourceURL.path); + + if (!success) { OCLogDebug(@"Moving %@ to %@ failed with error=%@", OCLogPrivate(zipURL), OCLogPrivate(sourceURL), OCLogPrivate(error)); } From 372d798a35c0560c8e12b98caa2db5c744247056 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 15 Apr 2021 15:17:13 +0200 Subject: [PATCH 27/37] - fix finding (1) in #933 via SDK update --- ios-sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios-sdk b/ios-sdk index 29fa010dc..05b109d21 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 29fa010dcaad711697588008743b44c30c20d123 +Subproject commit 05b109d21b2b38a4647dc99a6e1ffcb24862ad4b From 6ae824201b22e9c8f9efcca95759b7005a0d14f9 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 15 Apr 2021 15:22:22 +0200 Subject: [PATCH 28/37] - fix misplaced sortbar (finding (3) in #933) --- ownCloudAppShared/Client/User Interface/SortBar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index 522264407..e61aa7179 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -190,7 +190,7 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate NSLayoutConstraint.activate([ sortSegmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: topPadding), sortSegmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -bottomPadding), - sortSegmentedControl.leftAnchor.constraint(equalTo: self.leftAnchor, constant: leftPadding), + sortSegmentedControl.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: leftPadding), searchScopeSegmentedControl.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: -rightSearchScopePadding), searchScopeSegmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: topPadding), From 38ad27d1dddbd27ac38e9dd52f78fa8b00460902 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 15 Apr 2021 15:37:55 +0200 Subject: [PATCH 29/37] - OCQueryCondition+SearchSegmenter: accept more possible quotation marks when segmenting searches --- .../OCQueryCondition+SearchSegmenter.m | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index 245a454a0..37ced568c 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -23,6 +23,31 @@ @implementation NSString (SearchSegmenter) +- (BOOL)isQuotationMark +{ + return ([@"“”‘‛‟„‚'^\"′″´˝❛❜❝❞" containsString:self]); +} + +- (BOOL)hasQuotationMarkSuffix +{ + if (self.length > 0) + { + return ([[self substringWithRange:NSMakeRange(self.length-1, 1)] isQuotationMark]); + } + + return (NO); +} + +- (BOOL)hasQuotationMarkPrefix +{ + if (self.length > 0) + { + return ([[self substringWithRange:NSMakeRange(0, 1)] isQuotationMark]); + } + + return (NO); +} + - (NSArray *)segmentedForSearch { NSMutableArray *segments = [NSMutableArray new]; @@ -47,7 +72,7 @@ @implementation NSString (SearchSegmenter) NSString *term = inTerm; BOOL closingSegment = NO; - if ([term hasPrefix:@"\""]) + if ([term hasQuotationMarkPrefix]) { // Submit any open segment SubmitSegment(); @@ -57,7 +82,7 @@ @implementation NSString (SearchSegmenter) segmentOpen = YES; } - if ([term hasSuffix:@"\""]) + if ([term hasQuotationMarkSuffix]) { // End segment term = [term substringToIndex:term.length-1]; From 2e67485ef35bf1bb4cfe299bb3a6d5076d9675bd Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 15 Apr 2021 15:59:34 +0200 Subject: [PATCH 30/37] OCQueryCondition+SearchSegmenter: allow inactivation of keywords through quotation, make it possible to still negate such searches --- .../OCQueryCondition+SearchSegmenter.h | 2 +- .../OCQueryCondition+SearchSegmenter.m | 41 +++++++++++++++---- .../SearchSegmentationTests.m | 2 +- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h index a74eb3daa..03a5d35c0 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NSString (SearchSegmenter) -- (NSArray *)segmentedForSearch; +- (NSArray *)segmentedForSearchWithQuotationMarks:(BOOL)withQuotationMarks; @end diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index 37ced568c..ce28bf54d 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -25,7 +25,7 @@ @implementation NSString (SearchSegmenter) - (BOOL)isQuotationMark { - return ([@"“”‘‛‟„‚'^\"′″´˝❛❜❝❞" containsString:self]); + return ([@"“”‘‛‟„‚'\"′″´˝❛❜❝❞" containsString:self]); } - (BOOL)hasQuotationMarkSuffix @@ -48,7 +48,7 @@ - (BOOL)hasQuotationMarkPrefix return (NO); } -- (NSArray *)segmentedForSearch +- (NSArray *)segmentedForSearchWithQuotationMarks:(BOOL)withQuotationMarks { NSMutableArray *segments = [NSMutableArray new]; NSArray *terms; @@ -57,11 +57,19 @@ - (BOOL)hasQuotationMarkPrefix { __block NSString *segmentString = nil; __block BOOL segmentOpen = NO; + __block BOOL isNegated = NO; void (^SubmitSegment)(void) = ^{ if (segmentString.length > 0) { - [segments addObject:segmentString]; + if (segmentOpen && withQuotationMarks) + { + [segments addObject:[NSString stringWithFormat:@"%@\"%@\"", (isNegated ? @"-" : @""), segmentString]]; + } + else + { + [segments addObject:(isNegated ? [@"-" stringByAppendingString:segmentString] : segmentString)]; + } } segmentString = nil; @@ -72,6 +80,18 @@ - (BOOL)hasQuotationMarkPrefix NSString *term = inTerm; BOOL closingSegment = NO; + if (!segmentOpen) + { + isNegated = NO; + } + + if ([term hasPrefix:@"-"]): + { + // Negate segment + isNegated = YES; + term = [term substringFromIndex:1]; + } + if ([term hasQuotationMarkPrefix]) { // Submit any open segment @@ -109,8 +129,8 @@ - (BOOL)hasQuotationMarkPrefix // Submit closed segments if (closingSegment) { - segmentOpen = NO; SubmitSegment(); + segmentOpen = NO; } } @@ -191,6 +211,7 @@ + (instancetype)forSearchSegment:(NSString *)segmentString { NSString *segmentStringLowercase = nil; BOOL negateCondition = NO; + BOOL literalSearch = NO; if ([segmentString hasPrefix:@"-"]) { @@ -198,6 +219,12 @@ + (instancetype)forSearchSegment:(NSString *)segmentString segmentString = [segmentString substringFromIndex:1]; } + if ([segmentString hasPrefix:@"\""] && [segmentString hasSuffix:@"\""] && (segmentString.length >= 2)) + { + literalSearch = YES; + segmentString = [segmentString substringWithRange:NSMakeRange(1, segmentString.length-2)]; + } + if (segmentString.length == 0) { return (nil); @@ -205,7 +232,7 @@ + (instancetype)forSearchSegment:(NSString *)segmentString segmentStringLowercase = segmentString.lowercaseString; - if ([segmentStringLowercase hasPrefix:@":"]) + if ([segmentStringLowercase hasPrefix:@":"] && !literalSearch) { NSString *keyword = [segmentStringLowercase substringFromIndex:1]; @@ -246,7 +273,7 @@ + (instancetype)forSearchSegment:(NSString *)segmentString } } - if ([segmentStringLowercase containsString:@":"]) + if ([segmentStringLowercase containsString:@":"] && !literalSearch) { NSArray *parts = [segmentString componentsSeparatedByString:@":"]; NSString *modifier = nil; @@ -394,7 +421,7 @@ + (instancetype)forSearchSegment:(NSString *)segmentString + (instancetype)fromSearchTerm:(NSString *)searchTerm { - NSArray *segments = [searchTerm segmentedForSearch]; + NSArray *segments = [searchTerm segmentedForSearchWithQuotationMarks:YES]; NSMutableArray *conditions = [NSMutableArray new]; OCQueryCondition *queryCondition = nil; diff --git a/ownCloudAppFrameworkTests/SearchSegmentationTests.m b/ownCloudAppFrameworkTests/SearchSegmentationTests.m index 31633aef8..56400f7da 100644 --- a/ownCloudAppFrameworkTests/SearchSegmentationTests.m +++ b/ownCloudAppFrameworkTests/SearchSegmentationTests.m @@ -54,7 +54,7 @@ - (void)testStringSegmentation }; [expectedSegmentsByStrings enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull term, NSArray * _Nonnull expectedSegments, BOOL * _Nonnull stop) { - NSArray *segments = [term segmentedForSearch]; + NSArray *segments = [term segmentedForSearchWithQuotationMarks:NO]; XCTAssert([segments isEqual:expectedSegments], @"segments %@ doesn't match expectation %@", segments, expectedSegments); }]; From 0ea4bb9080ea5a1ab47e558df191617d45f88248 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 15 Apr 2021 17:37:23 +0200 Subject: [PATCH 31/37] - remove accidentally entered ":" that prevented compilation --- .../SDK Extensions/OCQueryCondition+SearchSegmenter.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index ce28bf54d..da67cb258 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -85,7 +85,7 @@ - (BOOL)hasQuotationMarkPrefix isNegated = NO; } - if ([term hasPrefix:@"-"]): + if ([term hasPrefix:@"-"]) { // Negate segment isNegated = YES; From 002ee67fd10cd4c91b4faf700dc3765e953a8167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Fri, 16 Apr 2021 10:25:52 +0200 Subject: [PATCH 32/37] fixed QA finding (8): keyboard commands for search and sort order where missing in directory picker --- ownCloud/Key Commands/KeyCommands.swift | 22 ++++--------------- .../ClientDirectoryPickerViewController.swift | 3 ++- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index 81591b237..d24d700f6 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -679,7 +679,7 @@ extension ClientQueryViewController { shortcuts.append(nextObjectCommand) } - if let core = core, let rootItem = query.rootItem { + if let core = core, let rootItem = query.rootItem, !isMoreButtonPermanentlyHidden { var item = rootItem if let indexPath = self.tableView?.indexPathForSelectedRow, let selectedItem = itemAt(indexPath: indexPath) { item = selectedItem @@ -789,7 +789,7 @@ extension QueryFileListTableViewController { } } - if let core = core, let rootItem = query.rootItem { + if let core = core, let rootItem = query.rootItem, !isMoreButtonPermanentlyHidden { var item = rootItem if let indexPath = self.tableView?.indexPathForSelectedRow, let selectedItem = itemAt(indexPath: indexPath) { item = selectedItem @@ -914,22 +914,8 @@ extension ClientDirectoryPickerViewController { open override var keyCommands: [UIKeyCommand]? { var shortcuts = [UIKeyCommand]() - let nextObjectCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectNext), discoverabilityTitle: "Select Next".localized) - let previousObjectCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectPrevious), discoverabilityTitle: "Select Previous".localized) - let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) - - if let selectedIndexPath = self.tableView?.indexPathForSelectedRow { - if selectedIndexPath.row < self.items.count - 1 { - shortcuts.append(nextObjectCommand) - } - if selectedIndexPath.row > 0 || selectedIndexPath.section > 0 { - shortcuts.append(previousObjectCommand) - } - if let item : OCItem = self.itemAt(indexPath: selectedIndexPath), item.type == OCItemType.collection { - shortcuts.append(selectObjectCommand) - } - } else { - shortcuts.append(nextObjectCommand) + if let superKeyCommands = super.keyCommands { + shortcuts.append(contentsOf: superKeyCommands) } if let selectButtonTitle = selectButton?.title, let selector = selectButton?.action { diff --git a/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift b/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift index 4c6b0e3e7..3f3459814 100644 --- a/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift @@ -36,7 +36,6 @@ open class ClientDirectoryPickerViewController: ClientQueryViewController { open var selectButton: UIBarButtonItem? private var selectButtonTitle: String? private var cancelBarButton: UIBarButtonItem? - open var directoryPath : String? open var choiceHandler: ClientDirectoryPickerChoiceHandler? @@ -115,6 +114,8 @@ open class ClientDirectoryPickerViewController: ClientQueryViewController { // Disable pull to refresh allowPullToRefresh = false + + isMoreButtonPermanentlyHidden = true } required public init?(coder aDecoder: NSCoder) { From 4fddedfdebd13fe48d8b1b48d9d00682c575e512 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 16 Apr 2021 13:01:33 +0200 Subject: [PATCH 33/37] - SortBar: new allowMultiSelect property to control whether multi select should be available, defaulting to true - QueryFileListTableViewController, ClientQueryViewController: allow efficient subclassing of relevant parts of the reveal feature - ClientDirectoryPickerViewController: provide implementations of relevant reveal subclassing points, disable multi-select (fixing finding (7) and (9) in #933) --- .../ClientDirectoryPickerViewController.swift | 23 +++++++++++++------ .../ClientQueryViewController.swift | 2 +- .../QueryFileListTableViewController.swift | 8 +++++-- .../Client/User Interface/SortBar.swift | 21 +++++++++++++---- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift b/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift index 3f3459814..b97f32780 100644 --- a/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientDirectoryPickerViewController.swift @@ -143,7 +143,7 @@ open class ClientDirectoryPickerViewController: ClientQueryViewController { // Cancel button creation cancelBarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelBarButtonPressed)) - sortBar?.showSelectButton = false + sortBar?.allowMultiSelect = false tableView.dragInteractionEnabled = false } @@ -170,12 +170,6 @@ open class ClientDirectoryPickerViewController: ClientQueryViewController { } } - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - sortBar?.showSelectButton = false - } - private func allowNavigationFor(item: OCItem?) -> Bool { guard let item = item else { return false } @@ -264,6 +258,7 @@ open class ClientDirectoryPickerViewController: ClientQueryViewController { let pickerController = ClientDirectoryPickerViewController(core: core, path: path, selectButtonTitle: selectButtonTitle, allowedPathFilter: allowedPathFilter, navigationPathFilter: navigationPathFilter, choiceHandler: choiceHandler) pickerController.cancelAction = cancelAction + pickerController.breadCrumbsPush = self.breadCrumbsPush self.navigationController?.pushViewController(pickerController, animated: true) } @@ -370,4 +365,18 @@ open class ClientDirectoryPickerViewController: ClientQueryViewController { super.queryHasChangesAvailable(query) } } + + public override func revealViewController(core: OCCore, path: String, item: OCItem, rootViewController: UIViewController?) -> UIViewController? { + guard let selectButtonTitle = selectButtonTitle, let choiceHandler = choiceHandler else { + return nil + } + + let pickerController = ClientDirectoryPickerViewController(core: core, path: path, selectButtonTitle: selectButtonTitle, allowedPathFilter: allowedPathFilter, navigationPathFilter: navigationPathFilter, 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 index fa6798314..8700ef437 100644 --- a/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift +++ b/ownCloudAppShared/Client/File Lists/ClientQueryViewController.swift @@ -42,7 +42,7 @@ open class ClientQueryViewController: QueryFileListTableViewController, UIDropIn weak public var clientRootViewController : UIViewController? private var _actionProgressHandler : ActionProgressHandler? - private var revealItemLocalID : String? + public var revealItemLocalID : String? private var revealItemFound : Bool = false private let ItemDataUTI = "com.owncloud.ios-app.item-data" diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 8140023f0..8769f6092 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -450,9 +450,13 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa return cell! } + public func revealViewController(core: OCCore, path: String, item: OCItem, rootViewController: UIViewController?) -> UIViewController? { + return ClientQueryViewController(core: core, query: OCQuery(forPath: path), reveal: item, rootViewController: nil) + } + public func reveal(item: OCItem, core: OCCore, sender: AnyObject?) -> Bool { - if let parentPath = item.path?.parentPath { - let revealQueryViewController = ClientQueryViewController(core: core, query: OCQuery(forPath: parentPath), reveal: item, rootViewController: nil) + if let parentPath = item.path?.parentPath, + let revealQueryViewController = revealViewController(core: core, path: parentPath, item: item, rootViewController: nil) { self.navigationController?.pushViewController(revealQueryViewController, animated: true) diff --git a/ownCloudAppShared/Client/User Interface/SortBar.swift b/ownCloudAppShared/Client/User Interface/SortBar.swift index e61aa7179..51855784c 100644 --- a/ownCloudAppShared/Client/User Interface/SortBar.swift +++ b/ownCloudAppShared/Client/User Interface/SortBar.swift @@ -89,16 +89,27 @@ public class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate public var sortButton: UIButton? public var searchScopeSegmentedControl : SegmentedControl? public var selectButton: UIButton? + public var allowMultiSelect: Bool = true { + didSet { + updateSelectButtonVisibility() + } + } public var showSelectButton: Bool = false { didSet { - selectButton?.isHidden = !showSelectButton - selectButton?.accessibilityElementsHidden = !showSelectButton - selectButton?.isEnabled = showSelectButton - - UIAccessibility.post(notification: .layoutChanged, argument: nil) + updateSelectButtonVisibility() } } + private func updateSelectButtonVisibility() { + let showButton = showSelectButton && allowMultiSelect + + selectButton?.isHidden = !showButton + selectButton?.accessibilityElementsHidden = !showButton + selectButton?.isEnabled = showButton + + UIAccessibility.post(notification: .layoutChanged, argument: nil) + } + var showSearchScope: Bool = false { didSet { showSelectButton = !self.showSearchScope From 399ed6d5ed41a882b529cb79b507755a5722e0bc Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 16 Apr 2021 23:33:12 +0200 Subject: [PATCH 34/37] - update SDK to add database support for ownerUserName - search segmenter: add support for owner keyword, incl. localization --- ios-sdk | 2 +- .../Resources/de.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + .../SDK Extensions/OCQueryCondition+SearchSegmenter.m | 8 +++++++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ios-sdk b/ios-sdk index 05b109d21..c45571bf7 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 05b109d21b2b38a4647dc99a6e1ffcb24862ad4b +Subproject commit c45571bf7cde74dee991b26088910192ee0d530d diff --git a/ownCloudAppFramework/Resources/de.lproj/Localizable.strings b/ownCloudAppFramework/Resources/de.lproj/Localizable.strings index d1ebd1000..bd8c0df0f 100644 --- a/ownCloudAppFramework/Resources/de.lproj/Localizable.strings +++ b/ownCloudAppFramework/Resources/de.lproj/Localizable.strings @@ -49,6 +49,7 @@ "keyword_on" = "am"; "keyword_smaller" = "kleiner"; "keyword_greater" = "größer"; +"keyword_owner" = "besitzer"; "keyword_file" = "datei"; "keyword_folder" = "ordner"; "keyword_image" = "bild"; diff --git a/ownCloudAppFramework/Resources/en.lproj/Localizable.strings b/ownCloudAppFramework/Resources/en.lproj/Localizable.strings index 9114c7246..df22ef621 100644 --- a/ownCloudAppFramework/Resources/en.lproj/Localizable.strings +++ b/ownCloudAppFramework/Resources/en.lproj/Localizable.strings @@ -51,6 +51,7 @@ "keyword_on" = "on"; "keyword_smaller" = "smaller"; "keyword_greater" = "greater"; +"keyword_owner" = "owner"; "keyword_file" = "file"; "keyword_folder" = "folder"; "keyword_image" = "image"; diff --git a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m index da67cb258..dd40d0aa0 100644 --- a/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m +++ b/ownCloudAppFramework/SDK Extensions/OCQueryCondition+SearchSegmenter.m @@ -173,6 +173,7 @@ + (nullable NSString *)normalizeKeyword:(NSString *)keyword TranslateKeyword(@"on"), TranslateKeyword(@"smaller"), TranslateKeyword(@"greater"), + TranslateKeyword(@"owner"), // Suffix keywords TranslateKeyword(@"d"), @@ -347,6 +348,10 @@ + (instancetype)forSearchSegment:(NSString *)segmentString condition = [OCQueryCondition where:OCItemPropertyNameSize isGreaterThan:byteCount]; } } + else if ([modifierKeyword isEqual:@"owner"]) + { + condition = [OCQueryCondition where:OCItemPropertyNameOwnerUserName startsWith:parameter]; + } else if ([modifier isEqual:@""]) { // Parse time formats, f.ex.: 7d, 2w, 1m, 2y @@ -405,7 +410,8 @@ + (instancetype)forSearchSegment:(NSString *)segmentString [modifierKeyword isEqual:@"before"] || [modifierKeyword isEqual:@"on"] || [modifierKeyword isEqual:@"greater"] || - [modifierKeyword isEqual:@"smaller"] + [modifierKeyword isEqual:@"smaller"] || + [modifierKeyword isEqual:@"owner"] ) { // Modifiers without parameters From 89200c3a88b6528684644c4c5628a9faf6c2ea0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Mon, 19 Apr 2021 14:08:08 +0200 Subject: [PATCH 35/37] fixed duplicated keyboard commands: changed keyboard command for toggle search order and share item --- .../Client/Actions/Actions+Extensions/CollaborateAction.swift | 2 +- ownCloud/Key Commands/KeyCommands.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift index 8127c871d..9512cc342 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CollaborateAction.swift @@ -26,7 +26,7 @@ class CollaborateAction: Action { override class var name : String { return "Sharing".localized } override class var locations : [OCExtensionLocationIdentifier]? { return [.keyboardShortcut, .contextMenuSharingItem] } override class var keyCommand : String? { return "S" } - override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .control] } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index d24d700f6..e1323a45b 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -475,7 +475,7 @@ extension PublicLinkEditTableViewController { let showInfoObjectCommand = UIKeyCommand(input: "H", modifierFlags: [.command, .alternate], action: #selector(showInfoSubtitles), discoverabilityTitle: "Help".localized) shortcuts.append(showInfoObjectCommand) } else { - let shareObjectCommand = UIKeyCommand(input: "S", modifierFlags: [.command, .alternate], action: #selector(shareLinkURL), discoverabilityTitle: "Share".localized) + let shareObjectCommand = UIKeyCommand(input: "S", modifierFlags: [.command], action: #selector(shareLinkURL), discoverabilityTitle: "Share".localized) shortcuts.append(shareObjectCommand) } @@ -778,7 +778,7 @@ extension QueryFileListTableViewController { let selectObjectCommand = UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(selectCurrent), discoverabilityTitle: "Open Selected".localized) let scrollTopCommand = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [.command, .shift], action: #selector(scrollToFirstRow), discoverabilityTitle: "Scroll to Top".localized) let scrollBottomCommand = UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [.command, .shift], action: #selector(scrollToLastRow), discoverabilityTitle: "Scroll to Bottom".localized) - let toggleSortCommand = UIKeyCommand(input: "S", modifierFlags: [.command, .shift], action: #selector(toggleSortOrder), discoverabilityTitle: "Change Sort Order".localized) + let toggleSortCommand = UIKeyCommand(input: "S", modifierFlags: [.alternate], action: #selector(toggleSortOrder), discoverabilityTitle: "Change Sort Order".localized) let searchCommand = UIKeyCommand(input: "F", modifierFlags: [.command], action: #selector(enableSearch), discoverabilityTitle: "Search".localized) // Add key commands for file name letters if sortMethod == .alphabetically, let searchController = searchController, !searchController.isActive { From 69dc227cc591e0fa2f9b5e1178a23f9822dd7c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Tue, 20 Apr 2021 14:04:57 +0200 Subject: [PATCH 36/37] added changelog entry --- changelog/unreleased/53 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/unreleased/53 diff --git a/changelog/unreleased/53 b/changelog/unreleased/53 new file mode 100644 index 000000000..7c74204b6 --- /dev/null +++ b/changelog/unreleased/53 @@ -0,0 +1,6 @@ +Change: Local account-wide search using custom queries + +User can switch between local folder or local account-wide search. +Search terms and filter keywords can be combined inside the search field to get granular search results. + +https://github.com/owncloud/ios-app/issues/53 From 9c8de1591aaaea26f818678fdae417cdfaaf5ec3 Mon Sep 17 00:00:00 2001 From: hosy Date: Tue, 20 Apr 2021 12:05:36 +0000 Subject: [PATCH 37/37] Calens changelog updated --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af004aad..97fe0ca98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Summary * Bugfix - Disable Markup Action for Mime-Type Gif: [#952](https://github.com/owncloud/ios-app/issues/952) * 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) +* Change - Local account-wide search using custom queries: [#53](https://github.com/owncloud/ios-app/issues/53) * Change - Presentation Mode: [#704](https://github.com/owncloud/ios-app/issues/704) * Change - Shortcut uploads and error handling improvements: [#858](https://github.com/owncloud/ios-app/issues/858) * Change - Added Actions to File Provider: Sharing & Public Links: [#910](https://github.com/owncloud/ios-app/pull/910) @@ -76,6 +77,13 @@ Details https://github.com/owncloud/enterprise/issues/4450 +* Change - Local account-wide search using custom queries: [#53](https://github.com/owncloud/ios-app/issues/53) + + User can switch between local folder or local account-wide search. Search terms and filter + keywords can be combined inside the search field to get granular search results. + + https://github.com/owncloud/ios-app/issues/53 + * Change - Presentation Mode: [#704](https://github.com/owncloud/ios-app/issues/704) Added an action in detail view menu which enables presentation mode. Presentation mode