diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index d13494032..296a41f9a 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */; }; 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */; }; 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */; }; + 39DE75CD22F960CF0064C1E2 /* SortMethodTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DE75C722F960CF0064C1E2 /* SortMethodTableViewController.swift */; }; 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; @@ -617,6 +618,7 @@ 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUploadSettingsSection.swift; sourceTree = ""; }; 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientItemCell.swift; sourceTree = ""; }; 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; + 39DE75C722F960CF0064C1E2 /* SortMethodTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortMethodTableViewController.swift; sourceTree = ""; }; 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableHeaderView.swift; sourceTree = ""; }; 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeRoundedButton.swift; sourceTree = ""; }; 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; @@ -1320,6 +1322,7 @@ 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */, 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */, 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */, + 39DE75C722F960CF0064C1E2 /* SortMethodTableViewController.swift */, ); path = Client; sourceTree = ""; @@ -2489,6 +2492,7 @@ DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */, 392557FE2278703300E83F60 /* UISearchBar+Extension.swift in Sources */, DC7DBA2B207F71E400E7337D /* VectorImageView.swift in Sources */, + 39DE75CD22F960CF0064C1E2 /* SortMethodTableViewController.swift in Sources */, DCE974BC207EACA60069FC2B /* UIImage+Extension.swift in Sources */, DC1B2707209CF0D3004715E1 /* IssuesDismissalAnimator.swift in Sources */, 23BEF1182076667F00DD2E6F /* IssuesViewController.swift in Sources */, diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index e491bbc1a..78025ded3 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -581,6 +581,10 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none } + + func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) { + popoverPresentationController.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + } } // MARK: - Drag & Drop delegates diff --git a/ownCloud/Client/Library/LibraryFilesTableViewController.swift b/ownCloud/Client/Library/LibraryFilesTableViewController.swift index 5679c129f..626a96c1b 100644 --- a/ownCloud/Client/Library/LibraryFilesTableViewController.swift +++ b/ownCloud/Client/Library/LibraryFilesTableViewController.swift @@ -29,7 +29,7 @@ class LibraryFilesTableViewController: QueryFileListTableViewController { } override func sectionIndexTitles(for tableView: UITableView) -> [String]? { - if sortMethod == .alphabeticallyAscendant || sortMethod == .alphabeticallyDescendant { + if sortMethod == .alphabetically { return Array( Set( self.items.map { String(( $0.name?.first!.uppercased())!) })).sorted() } diff --git a/ownCloud/Client/SortBar.swift b/ownCloud/Client/SortBar.swift index e7fb9ba1c..708641865 100644 --- a/ownCloud/Client/SortBar.swift +++ b/ownCloud/Client/SortBar.swift @@ -18,15 +18,40 @@ import UIKit +class SegmentedControl: UISegmentedControl { + var oldValue : Int! + + override func touchesBegan(_ touches: Set, with event: UIEvent? ) { + self.oldValue = self.selectedSegmentIndex + super.touchesBegan(touches, with: event) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent? ) { + super.touchesEnded(touches, with: event ) + + if self.oldValue == self.selectedSegmentIndex { + sendActions(for: UIControl.Event.valueChanged) + } + } +} + protocol SortBarDelegate: class { + + var sortDirection: SortDirection { get set } + var sortMethod: SortMethod { get set } + func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) } -class SortBar: UIView, Themeable { +class SortBar: UIView, Themeable, UIPopoverPresentationControllerDelegate { - weak var delegate: SortBarDelegate? + weak var delegate: SortBarDelegate? { + didSet { + updateSortButtonTitle() + } + } // MARK: - Constants let sideButtonsSize: CGSize = CGSize(width: 30.0, height: 30.0) @@ -37,19 +62,33 @@ class SortBar: UIView, Themeable { // MARK: - Instance variables. - var sortSegmentedControl: UISegmentedControl? + var sortSegmentedControl: SegmentedControl? var sortButton: UIButton? 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) + if oldValue == sortMethod { + if delegate?.sortDirection == .ascendant { + delegate?.sortDirection = .descendant + } else { + delegate?.sortDirection = .ascendant + } + } else { + delegate?.sortDirection = .ascendant // Reset sort direction when switching sort methods + } + } + updateSortButtonTitle() - let title = NSString(format: "Sort by %@".localized as NSString, sortMethod.localizedName()) as String - sortButton?.setTitle(title, for: .normal) 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) + } if let segmentIndex = SortMethod.all.index(of: sortMethod) { sortSegmentedControl?.selectedSegmentIndex = segmentIndex + sortSegmentedControl?.setTitle(sortDirectionTitle(sortMethod.localizedName()), forSegmentAt: segmentIndex) } delegate?.sortBar(self, didUpdateSortMethod: sortMethod) @@ -59,7 +98,7 @@ class SortBar: UIView, Themeable { // MARK: - Init & Deinit init(frame: CGRect, sortMethod: SortMethod) { - sortSegmentedControl = UISegmentedControl() + sortSegmentedControl = SegmentedControl() sortButton = UIButton(type: .system) @@ -86,8 +125,19 @@ class SortBar: UIView, Themeable { sortSegmentedControl.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor, constant: -rightPadding) ]) + 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)) + if titleWidth > longestTitleWidth { + longestTitleWidth = titleWidth + } + } + + var currentIndex = 0 + for _ in SortMethod.all { + sortSegmentedControl.setWidth(longestTitleWidth, forSegmentAt: currentIndex) + currentIndex += 1 } sortSegmentedControl.selectedSegmentIndex = SortMethod.all.index(of: sortMethod)! @@ -98,6 +148,7 @@ class SortBar: UIView, Themeable { sortButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline) sortButton.titleLabel?.adjustsFontForContentSizeCategory = true sortButton.semanticContentAttribute = (sortButton.effectiveUserInterfaceLayoutDirection == .leftToRight) ? .forceRightToLeft : .forceLeftToRight + sortButton.setImage(UIImage(named: "chevron-small-light"), for: .normal) sortButton.setContentHuggingPriority(.required, for: .horizontal) @@ -110,7 +161,7 @@ class SortBar: UIView, Themeable { ]) sortButton.isHidden = true - sortButton.addTarget(self, action: #selector(presentSortButtonOptions), for: .touchUpInside) + sortButton.addTarget(self, action: #selector(presentSortButtonOptions(_:)), for: .touchUpInside) } // Finalize view setup @@ -147,20 +198,35 @@ class SortBar: UIView, Themeable { } } - // MARK: - Actions - @objc private func presentSortButtonOptions() { - let controller = UIAlertController(title: "Sort by".localized, message: nil, preferredStyle: .actionSheet) - - for method in SortMethod.all { - let action = UIAlertAction(title: method.localizedName(), style: .default, handler: {(_) in - self.sortMethod = method - }) - controller.addAction(action) + // MARK: - Sort Direction Title + + func updateSortButtonTitle() { + let title = NSString(format: "Sort by %@".localized as NSString, sortMethod.localizedName()) as String + sortButton?.setTitle(sortDirectionTitle(title), for: .normal) + } + + func sortDirectionTitle(_ title: String) -> String { + if delegate?.sortDirection == .descendant { + return String(format: "%@ ↓", title) + } else { + return String(format: "%@ ↑", title) } + } - let cancel = UIAlertAction(title: "Cancel".localized, style: .cancel) - controller.addAction(cancel) - delegate?.sortBar(self, presentViewController: controller, animated: true, completionHandler: nil) + // MARK: - Actions + @objc private func presentSortButtonOptions(_ sender : UIButton) { + let tableViewController = SortMethodTableViewController() + tableViewController.modalPresentationStyle = UIModalPresentationStyle.popover + tableViewController.sortBarDelegate = self.delegate + tableViewController.sortBar = self + + let popoverPresentationController = tableViewController.popoverPresentationController + popoverPresentationController?.sourceView = sender + popoverPresentationController?.delegate = self + popoverPresentationController?.sourceRect = CGRect(x: 0, y: 0, width: sender.frame.size.width, height: sender.frame.size.height) + popoverPresentationController?.permittedArrowDirections = .up + + delegate?.sortBar(self, presentViewController: tableViewController, animated: true, completionHandler: nil) } @objc private func sortSegmentedControllerValueChanged() { @@ -169,4 +235,13 @@ class SortBar: UIView, Themeable { delegate?.sortBar(self, didUpdateSortMethod: self.sortMethod) } } + + // MARK: - UIPopoverPresentationControllerDelegate + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } + + func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) { + popoverPresentationController.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + } } diff --git a/ownCloud/Client/SortMethod.swift b/ownCloud/Client/SortMethod.swift index 84784ae40..570a460ac 100644 --- a/ownCloud/Client/SortMethod.swift +++ b/ownCloud/Client/SortMethod.swift @@ -21,25 +21,27 @@ import ownCloudSDK typealias OCSort = Comparator +public enum SortDirection: Int { + case ascendant = 0 + case descendant = 1 +} + public enum SortMethod: Int { - case alphabeticallyAscendant = 0 - case alphabeticallyDescendant = 1 - case type = 2 - case size = 3 - case date = 4 - case shared = 5 + case alphabetically = 0 + case type = 1 + case size = 2 + case date = 3 + case shared = 4 - static var all: [SortMethod] = [alphabeticallyAscendant, alphabeticallyDescendant, type, size, date, shared] + static var all: [SortMethod] = [alphabetically, type, size, date, shared] func localizedName() -> String { var name = "" switch self { - case .alphabeticallyAscendant: - name = "name (A-Z)".localized - case .alphabeticallyDescendant: - name = "name (Z-A)".localized + case .alphabetically: + name = "name".localized case .type: name = "type".localized case .size: @@ -53,7 +55,7 @@ public enum SortMethod: Int { return name } - func comparator() -> OCSort { + func comparator(direction: SortDirection) -> OCSort { var comparator: OCSort switch self { @@ -64,28 +66,23 @@ public enum SortMethod: Int { let leftSize = leftItem!.size as NSNumber let rightSize = rightItem!.size as NSNumber + if direction == .descendant { + return (leftSize.compare(rightSize)) + } return (rightSize.compare(leftSize)) } - - case .alphabeticallyAscendant: + case .alphabetically: comparator = { (left, right) in guard let leftName = (left as? OCItem)?.name, let rightName = (right as? OCItem)?.name else { return .orderedSame } - - return (leftName.caseInsensitiveCompare(rightName)) - } - - case .alphabeticallyDescendant: - comparator = { (left, right) in - guard let leftName = (left as? OCItem)?.name, let rightName = (right as? OCItem)?.name else { - return .orderedSame + if direction == .descendant { + return (rightName.caseInsensitiveCompare(leftName)) } - return (rightName.caseInsensitiveCompare(leftName)) + return (leftName.caseInsensitiveCompare(rightName)) } - case .type: comparator = { (left, right) in let leftItem = left as? OCItem @@ -109,6 +106,9 @@ public enum SortMethod: Int { if rightMimeType == nil { rightMimeType = "various" } + if direction == .descendant { + return rightMimeType!.compare(leftMimeType!) + } return leftMimeType!.compare(rightMimeType!) } @@ -122,10 +122,21 @@ public enum SortMethod: Int { if leftShared == rightShared { return .orderedSame - } else if leftShared { - return .orderedAscending } - return .orderedDescending + + if direction == .descendant { + if rightShared { + return .orderedAscending + } + + return .orderedDescending + } else { + if leftShared { + return .orderedAscending + } + + return .orderedDescending + } } case .date: comparator = { (left, right) in @@ -133,6 +144,9 @@ public enum SortMethod: Int { guard let leftLastModified = (left as? OCItem)?.lastModified, let rightLastModified = (right as? OCItem)?.lastModified else { return .orderedSame } + if direction == .descendant { + return (leftLastModified.compare(rightLastModified)) + } return (rightLastModified.compare(leftLastModified)) } diff --git a/ownCloud/Client/SortMethodTableViewController.swift b/ownCloud/Client/SortMethodTableViewController.swift new file mode 100644 index 000000000..21c5863d4 --- /dev/null +++ b/ownCloud/Client/SortMethodTableViewController.swift @@ -0,0 +1,66 @@ +// +// SortMethodTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 06.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit + +class SortMethodTableViewController: StaticTableViewController { + + // MARK: - Constants + private let maxContentWidth : CGFloat = 150 + private let rowHeight : CGFloat = 44 + + // MARK: - Instance Variables + weak var sortBarDelegate: SortBarDelegate? + weak var sortBar: SortBar? + + override func viewDidLoad() { + super.viewDidLoad() + + self.tableView.isScrollEnabled = false + self.tableView.rowHeight = rowHeight + + var rows : [StaticTableViewRow] = [] + let contentHeight : CGFloat = rowHeight * CGFloat(SortMethod.all.count) - 1 + let contentWidth : CGFloat = (view.frame.size.width < maxContentWidth) ? view.frame.size.width : maxContentWidth + self.preferredContentSize = CGSize(width: contentWidth, height: contentHeight) + + for method in SortMethod.all { + var title = method.localizedName() + + if sortBarDelegate?.sortMethod == method { + if sortBarDelegate?.sortDirection == .ascendant { // Show arrows opposite to the current sort direction to show what choosing them will lead to + title = String(format: "%@ ↓", method.localizedName()) + } else { + title = String(format: "%@ ↑", method.localizedName()) + } + } + + let aRow = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + guard let self = self else { return } + + self.sortBar?.sortMethod = method + + self.dismiss(animated: false, completion: nil) + }, title: title) + rows.append(aRow) + } + + let section : StaticTableViewSection = StaticTableViewSection(headerTitle: nil, footerTitle: nil, rows: rows) + self.addSection(section) + } +} diff --git a/ownCloud/FileLists/QueryFileListTableViewController.swift b/ownCloud/FileLists/QueryFileListTableViewController.swift index ebb152812..6e9e81897 100644 --- a/ownCloud/FileLists/QueryFileListTableViewController.swift +++ b/ownCloud/FileLists/QueryFileListTableViewController.swift @@ -42,7 +42,7 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele query.delegate = self if query.sortComparator == nil { - query.sortComparator = self.sortMethod.comparator() + query.sortComparator = self.sortMethod.comparator(direction: sortDirection) } core?.start(query) @@ -72,13 +72,22 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele // MARK: - Sorting var sortBar: SortBar? var sortMethod: SortMethod { - set { UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-method") } get { - let sort = SortMethod(rawValue: UserDefaults.standard.integer(forKey: "sort-method")) ?? SortMethod.alphabeticallyDescendant + let sort = SortMethod(rawValue: UserDefaults.standard.integer(forKey: "sort-method")) ?? SortMethod.alphabetically + return sort + } + } + 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 } } @@ -184,7 +193,7 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { sortMethod = didUpdateSortMethod - query.sortComparator = sortMethod.comparator() + query.sortComparator = sortMethod.comparator(direction: sortDirection) } func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { diff --git a/ownCloud/Resources/Assets.xcassets/Image.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 000000000..f8f827e40 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index ed7bd3482..aef5fd3c7 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -82,8 +82,7 @@ "Sort by %@" = "Sort by %@"; "Sort by" = "Sort by"; -"name (A-Z)" = "name (A-Z)"; -"name (Z-A)" = "name (Z-A)"; +"name" = "name"; "type" = "type"; "size" = "size"; "date" = "date"; diff --git a/ownCloud/UIKit Extensions/String+Extension.swift b/ownCloud/UIKit Extensions/String+Extension.swift index bfaf75b2f..1492056b6 100644 --- a/ownCloud/UIKit Extensions/String+Extension.swift +++ b/ownCloud/UIKit Extensions/String+Extension.swift @@ -17,6 +17,7 @@ */ import Foundation +import UIKit extension String { @@ -54,4 +55,11 @@ extension String { } return self } + + func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { + let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) + let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) + + return ceil(boundingBox.width) + } }