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 @@
-
+
@@ -61,7 +61,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()
}