From 946acc246067d991a8082a3f2facbdbb57b7e865 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 8 Jun 2020 23:38:09 +0200 Subject: [PATCH] Sort order --- main/Base.lproj/Main.storyboard | 87 ++++++++++++++----- main/Common Classes/FilterPipeline.swift | 20 ++++- .../Data Source/GroupedDomainDataSource.swift | 38 +++++++- main/Extensions/Generic.swift | 13 +++ main/Extensions/Notifications.swift | 1 + main/Extensions/SharedState.swift | 35 ++++++-- main/Requests/VCDateFilter.swift | 62 ++++++++----- 7 files changed, 197 insertions(+), 59 deletions(-) diff --git a/main/Base.lproj/Main.storyboard b/main/Base.lproj/Main.storyboard index f2209cc..fcee5bc 100644 --- a/main/Base.lproj/Main.storyboard +++ b/main/Base.lproj/Main.storyboard @@ -49,7 +49,7 @@ - + - + @@ -70,20 +70,20 @@ - + - - - + + + - + @@ -111,8 +111,14 @@ + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - - + + @@ -213,16 +252,18 @@ - + + + + + - - @@ -232,7 +273,7 @@ - + diff --git a/main/Common Classes/FilterPipeline.swift b/main/Common Classes/FilterPipeline.swift index e2eab87..ff1b2ff 100644 --- a/main/Common Classes/FilterPipeline.swift +++ b/main/Common Classes/FilterPipeline.swift @@ -105,7 +105,7 @@ class FilterPipeline { pipeline.remove(at: i) if i == pipeline.count { // only if we don't reset other layers we can assure `toLessRestrictive` - display?.reset(toLessRestrictive: lastLayerIndices()) + display?.apply(lessRestrictive: lastLayerIndices()) } else { resetFilters(startingAt: i) } @@ -128,6 +128,15 @@ class FilterPipeline { reloadTableCells() } + /// Will reverse the current display order without resorting. This is faster than setting a new sorting `predicate`. + /// However, the `predicate` must be dynamic and support a sort order flag. + /// - Warning: Make sure `predicate` does reflect the change or it will lead to data inconsistency! + func reverseSorting() { + // TODO: use semaphore to prevent concurrent edits + display?.reverseOrder() + reloadTableCells() + } + /// Re-built filter and display sorting order. /// - Parameter index: Must be: `index <= pipeline.count` private func resetFilters(startingAt index: Int = 0) { @@ -344,6 +353,7 @@ class PipelineSorting { /// Create a fresh, already sorted, display order projection. /// - Parameter predicate: Return `true` if first element should be sorted before second element. + /// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`. required init(_ predicate: @escaping Predicate, pipe: FilterPipeline) { comperator = { [unowned pipe] in predicate(pipe.dataSource[$0], pipe.dataSource[$1]) @@ -351,6 +361,12 @@ class PipelineSorting { reset(to: pipe.lastLayerIndices()) } + /// - Warning: Make sure `predicate` does reflect the change. Or it will lead to data inconsistency. + /// - Complexity: O(*n*), where *n* is the length of the `filter`. + fileprivate func reverseOrder() { + projection.reverse() + } + /// Replace current `projection` with new filter indices and apply sorting. /// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`. fileprivate func reset(to filterIndices: [Int]) { @@ -367,7 +383,7 @@ class PipelineSorting { /// After removing a layer of filtering the previous layers are less restrictive and thus contain more indices. /// Therefore, the difference between both index sets will be inserted into the projection. /// - Complexity: O(*m* log *n*), where *m* is the difference to the previous layer and *n* is the length of the `projection`. - fileprivate func reset(toLessRestrictive filterIndices: [Int]) { + fileprivate func apply(lessRestrictive filterIndices: [Int]) { for x in filterIndices.difference(toSubset: projection.sorted(), compare: (<)) { insertNew(x) } diff --git a/main/Data Source/GroupedDomainDataSource.swift b/main/Data Source/GroupedDomainDataSource.swift index 6999beb..61fc7c2 100644 --- a/main/Data Source/GroupedDomainDataSource.swift +++ b/main/Data Source/GroupedDomainDataSource.swift @@ -13,19 +13,20 @@ class GroupedDomainDataSource { private let parent: String? let pipeline: FilterPipeline private lazy var search = SearchBarManager(on: pipeline.delegate!.tableView) + private var currentOrder: DateFilterOrderBy = .Date + private var orderAsc = false init(withDelegate tvc: FilterPipelineDelegate, parent p: String?) { parent = p pipeline = .init(withDelegate: tvc) pipeline.setDataSource { [unowned self] in self.dataSourceCallback() } - pipeline.setSorting { - $0.lastModified > $1.lastModified - } + resetSortingOrder(force: true) if #available(iOS 10.0, *) { tvc.tableView.refreshControl = UIRefreshControl(call: #selector(reloadFromSource), on: self) } NotifyLogHistoryReset.observe(call: #selector(reloadFromSource), on: self) NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self) + NotifySortOrderChanged.observe(call: #selector(didChangeSortOrder), on: self) NotifySyncInsert.observe(call: #selector(syncInsert), on: self) NotifySyncRemove.observe(call: #selector(syncRemove), on: self) } @@ -43,6 +44,30 @@ class GroupedDomainDataSource { return log } + /// Read user defaults and apply new sorting order. Either by setting a new or reversing the current. + /// - Parameter force: If `true` set new sorting even if the type does not differ. + private func resetSortingOrder(force: Bool = false) { + let orderDidChange = (orderAsc =? Pref.DateFilter.OrderAsc) + if currentOrder =? Pref.DateFilter.OrderBy || force { + switch currentOrder { + case .Date: + pipeline.setSorting { [unowned self] in + self.orderAsc ? $0.lastModified < $1.lastModified : $0.lastModified > $1.lastModified + } + case .Name: + pipeline.setSorting { [unowned self] in + self.orderAsc ? $0.domain < $1.domain : $0.domain > $1.domain + } + case .Count: + pipeline.setSorting { [unowned self] in + self.orderAsc ? $0.total < $1.total : $0.total > $1.total + } + } + } else if orderDidChange { + pipeline.reverseSorting() + } + } + /// Pause recurring background updates to force reload `dataSource`. /// Callback fired on user action `pull-to-refresh`, or another background task triggered `NotifyLogHistoryReset`. /// - Parameter sender: May be either `UIRefreshControl` or `Notification` @@ -63,7 +88,7 @@ class GroupedDomainDataSource { } } - /// Callback fired when user editslist of `blocked` or `ignored` domains in settings. (`NotifyDNSFilterChanged` notification) + /// Callback fired when user edits list of `blocked` or `ignored` domains in settings. (`NotifyDNSFilterChanged` notification) @objc private func didChangeDomainFilter(_ notification: Notification) { guard let domain = notification.object as? String else { reloadFromSource() @@ -76,6 +101,11 @@ class GroupedDomainDataSource { } } + /// Callback fired when user changes date filter settings. (`NotifySortOrderChanged` notification) + @objc private func didChangeSortOrder(_ notification: Notification) { + resetSortingOrder() + } + // MARK: Table View Data Source diff --git a/main/Extensions/Generic.swift b/main/Extensions/Generic.swift index 14ae5c5..1ef7146 100644 --- a/main/Extensions/Generic.swift +++ b/main/Extensions/Generic.swift @@ -26,3 +26,16 @@ extension UIEdgeInsets { self.init(top: top ?? all, left: left ?? all, bottom: bottom ?? all, right: right ?? all) } } + +infix operator =? : ComparisonPrecedence +extension Equatable { + /// Assign a new value to `lhs` if the `newValue` differs from the previous value. Return whether the new value was set. + /// - Returns: `true` if `lhs` was overwritten with another value + static func =?(lhs: inout Self, newValue: Self) -> Bool { + if lhs != newValue { + lhs = newValue + return true + } + return false + } +} diff --git a/main/Extensions/Notifications.swift b/main/Extensions/Notifications.swift index 7a9d43c..f09721b 100644 --- a/main/Extensions/Notifications.swift +++ b/main/Extensions/Notifications.swift @@ -3,6 +3,7 @@ import Foundation let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState! let NotifyDNSFilterChanged = NSNotification.Name("PSIDNSFilterSettingsChanged") // domain: String? let NotifyDateFilterChanged = NSNotification.Name("PSIDateFilterSettingsChanged") // nil! +let NotifySortOrderChanged = NSNotification.Name("PSIDateFilterSortOrderChanged") // nil! let NotifyLogHistoryReset = NSNotification.Name("PSILogHistoryReset") // domain: String? let NotifySyncInsert = NSNotification.Name("PSISyncInsert") // SQLiteRowRange! let NotifySyncRemove = NSNotification.Name("PSISyncRemove") // SQLiteRowRange! diff --git a/main/Extensions/SharedState.swift b/main/Extensions/SharedState.swift index 70d70ec..265bad1 100644 --- a/main/Extensions/SharedState.swift +++ b/main/Extensions/SharedState.swift @@ -8,24 +8,40 @@ public enum VPNState : Int { } struct Pref { + static func Int(_ key: String) -> Int { UserDefaults.standard.integer(forKey: key) } + static func Int(_ val: Int, _ key: String) { UserDefaults.standard.set(val, forKey: key) } + static func Bool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) } + static func Bool(_ val: Bool, _ key: String) { UserDefaults.standard.set(val, forKey: key) } + struct DidShowTutorial { static var Welcome: Bool { - get { UserDefaults.standard.bool(forKey: "didShowTutorialAppWelcome") } - set { UserDefaults.standard.set(newValue, forKey: "didShowTutorialAppWelcome") } + get { Pref.Bool("didShowTutorialAppWelcome") } + set { Pref.Bool(newValue, "didShowTutorialAppWelcome") } } static var Recordings: Bool { - get { UserDefaults.standard.bool(forKey: "didShowTutorialRecordings") } - set { UserDefaults.standard.set(newValue, forKey: "didShowTutorialRecordings") } + get { Pref.Bool("didShowTutorialRecordings") } + set { Pref.Bool(newValue, "didShowTutorialRecordings") } } } struct DateFilter { static var Kind: DateFilterKind { - get { DateFilterKind(rawValue: UserDefaults.standard.integer(forKey: "dateFilterType"))! } - set { UserDefaults.standard.set(newValue.rawValue, forKey: "dateFilterType") } + get { DateFilterKind(rawValue: Pref.Int("dateFilterType"))! } + set { Pref.Int(newValue.rawValue, "dateFilterType") } } + /// Default: `0` (disabled) static var LastXMin: Int { - get { UserDefaults.standard.integer(forKey: "dateFilterLastXMin") } - set { UserDefaults.standard.set(newValue, forKey: "dateFilterLastXMin") } + get { Pref.Int("dateFilterLastXMin") } + set { Pref.Int(newValue, "dateFilterLastXMin") } + } + /// default: `.Date` + static var OrderBy: DateFilterOrderBy { + get { DateFilterOrderBy(rawValue: Pref.Int("dateFilterOderType"))! } + set { Pref.Int(newValue.rawValue, "dateFilterOderType") } + } + /// default: `false` (Desc) + static var OrderAsc: Bool { + get { Pref.Bool("dateFilterOderAsc") } + set { Pref.Bool(newValue, "dateFilterOderAsc") } } /// Return selected timestamp filter or `nil` if filtering is disabled. @@ -39,3 +55,6 @@ struct Pref { enum DateFilterKind: Int { case Off = 0, LastXMin = 1, ABRange = 2; } +enum DateFilterOrderBy: Int { + case Date = 0, Name = 1, Count = 2; +} diff --git a/main/Requests/VCDateFilter.swift b/main/Requests/VCDateFilter.swift index 3fa200b..b4e957f 100644 --- a/main/Requests/VCDateFilter.swift +++ b/main/Requests/VCDateFilter.swift @@ -4,27 +4,32 @@ import UIKit class VCDateFilter: UIViewController, UIGestureRecognizerDelegate { - @IBOutlet private var segmentControl: UISegmentedControl! - @IBOutlet private var sectionTitle: UILabel! + @IBOutlet private var filterBy: UISegmentedControl! // entries no older than + @IBOutlet private var durationTitle: UILabel! @IBOutlet private var durationView: UIView! @IBOutlet private var durationSlider: UISlider! @IBOutlet private var durationLabel: UILabel! private let durationTimes = [0, 1, 20, 60, 360, 720, 1440, 2880, 4320, 10080] // entries within range + @IBOutlet private var rangeTitle: UILabel! @IBOutlet private var rangeView: UIView! @IBOutlet private var buttonRangeStart: UIButton! @IBOutlet private var buttonRangeEnd: UIButton! + // order by + @IBOutlet private var orderbyType: UISegmentedControl! + @IBOutlet private var orderbyAsc: UISegmentedControl! + override func viewDidLoad() { super.viewDidLoad() - segmentControl.selectedSegmentIndex = (Pref.DateFilter.Kind == .ABRange ? 1 : 0) - didChangeSegment(segmentControl) - segmentControl.setEnabled(false, forSegmentAt: 1) // TODO: until range filter is ready + filterBy.selectedSegmentIndex = (Pref.DateFilter.Kind == .ABRange ? 1 : 0) + didChangeFilterBy(filterBy) + filterBy.setEnabled(false, forSegmentAt: 1) // TODO: until range filter is ready durationSlider.tag = -1 // otherwise wont update because `tag == 0` durationSlider.value = Float(durationTimes.firstIndex(of: Pref.DateFilter.LastXMin) ?? 0) / 9 @@ -36,16 +41,17 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate { b.removeLast(3) buttonRangeStart.setTitle(a, for: .normal) buttonRangeEnd.setTitle(b, for: .normal) + + orderbyType.selectedSegmentIndex = Pref.DateFilter.OrderBy.rawValue + orderbyAsc.selectedSegmentIndex = (Pref.DateFilter.OrderAsc ? 0 : 1) } - @IBAction private func didChangeSegment(_ sender: UISegmentedControl) { - durationView.isHidden = (sender.selectedSegmentIndex != 0) - rangeView.isHidden = (sender.selectedSegmentIndex != 1) - switch sender.selectedSegmentIndex { - case 0: sectionTitle.text = "Show entries no older than" - case 1: sectionTitle.text = "Show entries within range" - default: break - } + @IBAction private func didChangeFilterBy(_ sender: UISegmentedControl) { + let firstSelected = (sender.selectedSegmentIndex == 0) + durationTitle.isHidden = !firstSelected + durationView.isHidden = !firstSelected + rangeTitle.isHidden = firstSelected + rangeView.isHidden = firstSelected } @IBAction private func durationSliderChanged(_ sender: UISlider) { @@ -65,16 +71,28 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if gestureRecognizer.view == touch.view { let newXMin = durationSlider.tag - let newKind: DateFilterKind - if segmentControl.selectedSegmentIndex == 1 { - newKind = .ABRange - } else if newXMin > 0 { - newKind = .LastXMin - } else { - newKind = .Off + let filterType: DateFilterKind + let orderType: DateFilterOrderBy + + switch filterBy.selectedSegmentIndex { + case 0: filterType = (newXMin > 0) ? .LastXMin : .Off + case 1: filterType = .ABRange + default: preconditionFailure() } - if Pref.DateFilter.Kind != newKind || Pref.DateFilter.LastXMin != newXMin { - Pref.DateFilter.Kind = newKind + switch orderbyType.selectedSegmentIndex { + case 0: orderType = .Date + case 1: orderType = .Name + case 2: orderType = .Count + default: preconditionFailure() + } + let orderAsc = (orderbyAsc.selectedSegmentIndex == 0) + if Pref.DateFilter.OrderBy != orderType || Pref.DateFilter.OrderAsc != orderAsc { + Pref.DateFilter.OrderBy = orderType + Pref.DateFilter.OrderAsc = orderAsc + NotifySortOrderChanged.post() + } + if Pref.DateFilter.Kind != filterType || Pref.DateFilter.LastXMin != newXMin { + Pref.DateFilter.Kind = filterType Pref.DateFilter.LastXMin = newXMin NotifyDateFilterChanged.post() }