Refactoring II.

- Filter by date range
- SyncUpdate tasks run fully asynchronous in background
- Move tableView manipulations into FilterPipelineDelegate
- Move SyncUpdate notification into SyncUpdateDelegate
- Fix: sync cache before persisting a recording
- Restructuring GroupedDomainDataSource
- Performance: db logs queries use rowids instead of timestamps
- Add 'now' button to DatePickerAlert
This commit is contained in:
relikd
2020-06-17 00:27:22 +02:00
parent 0a53898797
commit e947ad6d4d
20 changed files with 644 additions and 386 deletions

View File

@@ -1,8 +1,8 @@
import UIKit
class TVCDomains: UITableViewController, UISearchBarDelegate, FilterPipelineDelegate {
class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataSourceDelegate {
lazy var source = GroupedDomainDataSource(withDelegate: self, parent: nil)
lazy var source = GroupedDomainDataSource(withParent: nil)
@IBOutlet private var filterButton: UIBarButtonItem!
@IBOutlet private var filterButtonDetail: UIBarButtonItem!
@@ -11,14 +11,7 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, FilterPipelineDele
super.viewDidLoad()
NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self)
didChangeDateFilter()
}
private var didLoadAlready = false
override func viewDidAppear(_ animated: Bool) {
if !didLoadAlready {
didLoadAlready = true
source.reloadFromSource()
}
source.delegate = self // init lazy var, ready for tableView data source
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
@@ -76,7 +69,7 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, FilterPipelineDele
return cell
}
func rowNeedsUpdate(_ row: Int) {
func groupedDomainDataSource(needsUpdate row: Int) {
let entry = source[row]
let cell = tableView.cellForRow(at: IndexPath(row: row))
cell?.detailTextLabel?.text = entry.detailCellText

View File

@@ -1,62 +1,16 @@
import UIKit
class TVCHostDetails: UITableViewController {
class TVCHostDetails: UITableViewController, SyncUpdateDelegate {
public var fullDomain: String!
private var dataSource: [GroupedTsOccurrence] = []
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.prompt = fullDomain
super.viewDidLoad()
sync.addObserver(self) // calls `syncUpdate(reset:)`
if #available(iOS 10.0, *) {
tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self)
}
NotifyLogHistoryReset.observe(call: #selector(reloadDataSource), on: self)
NotifySyncInsert.observe(call: #selector(syncInsert), on: self)
NotifySyncRemove.observe(call: #selector(syncRemove), on: self)
reloadDataSource()
}
@objc func reloadDataSource(sender: Any? = nil) {
let refreshControl = sender as? UIRefreshControl
let notification = sender as? Notification
if let affectedDomain = notification?.object as? String {
guard fullDomain.isSubdomain(of: affectedDomain) else { return }
}
DispatchQueue.global().async { [weak self] in
self?.dataSource = AppDB?.timesForDomain(self?.fullDomain ?? "", since: sync.tsEarliest) ?? []
DispatchQueue.main.sync {
self?.tableView.reloadData()
sync.syncNow() // sync outstanding entries in cache
refreshControl?.endRefreshing()
}
}
}
@objc private func syncInsert(_ notification: Notification) {
let range = notification.object as! SQLiteRowRange
if let latest = AppDB?.timesForDomain(fullDomain, range: range), latest.count > 0 {
dataSource.insert(contentsOf: latest, at: 0)
if tableView.isFrontmost {
let indices = (0..<latest.count).map { IndexPath(row: $0) }
tableView.insertRows(at: indices, with: .left)
} else {
tableView.reloadData()
}
}
}
@objc private func syncRemove(_ notification: Notification) {
let earliest = sync.tsEarliest
if let i = dataSource.firstIndex(where: { $0.ts < earliest }) {
// since they are ordered, we can optimize
let indices = (i..<dataSource.endIndex).map { IndexPath(row: $0) }
dataSource.removeLast(dataSource.count - i)
if tableView.isFrontmost {
tableView.deleteRows(at: indices, with: .automatic)
} else {
tableView.reloadData()
}
sync.allowPullToRefresh(onTVC: self, forObserver: self)
}
}
@@ -73,3 +27,55 @@ class TVCHostDetails: UITableViewController {
return cell
}
}
// ################################
// #
// # MARK: - Partial Update
// #
// ################################
extension TVCHostDetails {
func syncUpdate(_ _: SyncUpdate, reset rows: SQLiteRowRange) {
dataSource = AppDB?.timesForDomain(fullDomain, range: rows) ?? []
DispatchQueue.main.sync { tableView.reloadData() }
}
func syncUpdate(_ _: SyncUpdate, insert rows: SQLiteRowRange) {
guard let latest = AppDB?.timesForDomain(fullDomain, range: rows), latest.count > 0 else {
return
}
// TODO: if filter will be ever editable at this point, we cannot insert at 0
dataSource.insert(contentsOf: latest, at: 0)
DispatchQueue.main.sync {
if tableView.isFrontmost {
let indices = (0..<latest.count).map { IndexPath(row: $0) }
tableView.insertRows(at: indices, with: .left)
} else {
tableView.reloadData()
}
}
}
func syncUpdate(_ sender: SyncUpdate, remove _: SQLiteRowRange) {
let earliest = sender.tsEarliest
let latest = sender.tsLatest
// Assuming they are ordered by ts and in descending order
if let i = dataSource.lastIndex(where: { $0.ts >= earliest }), (i+1) < dataSource.count {
let indices = ((i+1)..<dataSource.endIndex).map{ $0 }
dataSource.removeLast(dataSource.count - (i+1))
DispatchQueue.main.sync { tableView.safeDeleteRows(indices) }
}
if let i = dataSource.firstIndex(where: { $0.ts <= latest }), i > 0 {
let indices = (dataSource.startIndex..<i).map{ $0 }
dataSource.removeFirst(i)
DispatchQueue.main.sync { tableView.safeDeleteRows(indices) }
}
}
func syncUpdate(_ sender: SyncUpdate, partialRemove affectedDomain: String) {
if fullDomain.isSubdomain(of: affectedDomain) {
syncUpdate(sender, reset: sender.rows)
}
}
}

View File

@@ -1,17 +1,17 @@
import UIKit
class TVCHosts: UITableViewController, FilterPipelineDelegate {
class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate {
lazy var source = GroupedDomainDataSource(withDelegate: self, parent: parentDomain)
lazy var source = GroupedDomainDataSource(withParent: parentDomain)
public var parentDomain: String!
private var isSpecial: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.prompt = parentDomain
super.viewDidLoad()
isSpecial = (parentDomain.first == "#") // aka: "# IP address"
source.reloadFromSource() // init lazy var
source.delegate = self // init lazy var, ready for tableView data source
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
@@ -45,7 +45,7 @@ class TVCHosts: UITableViewController, FilterPipelineDelegate {
return cell
}
func rowNeedsUpdate(_ row: Int) {
func groupedDomainDataSource(needsUpdate row: Int) {
let entry = source[row]
let cell = tableView.cellForRow(at: IndexPath(row: row))
cell?.detailTextLabel?.text = entry.detailCellText

View File

@@ -18,8 +18,8 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet private var rangeView: UIView!
@IBOutlet private var buttonRangeStart: UIButton!
@IBOutlet private var buttonRangeEnd: UIButton!
private lazy var tsRangeA: Timestamp = AppDB?.dnsLogsMinDate() ?? 0
private lazy var tsRangeB: Timestamp = .now()
private lazy var tsRangeA: Timestamp = Pref.DateFilter.RangeA ?? AppDB?.dnsLogsMinDate() ?? .now()
private lazy var tsRangeB: Timestamp = Pref.DateFilter.RangeB ?? .now()
// order by
@IBOutlet private var orderbyType: UISegmentedControl!
@@ -31,15 +31,11 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
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
durationSliderChanged(durationSlider)
// Force set seconds to 00 and 59 respectively. Its retained during change.
tsRangeA = tsRangeA - tsRangeA % 60 + 00
tsRangeB = tsRangeB - tsRangeB % 60 + 59
buttonRangeStart.setTitle(DateFormat.minutes(tsRangeA), for: .normal)
buttonRangeEnd.setTitle(DateFormat.minutes(tsRangeB), for: .normal)
@@ -70,45 +66,57 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
DatePickerAlert(presentIn: self, configure: {
$0.setDate(Date(flag ? self.tsRangeA : self.tsRangeB), animated: false)
}, onSuccess: {
flag ? (self.tsRangeA = $0.timestamp) : (self.tsRangeB = $0.timestamp)
sender.setTitle(DateFormat.minutes($0), for: .normal)
var ts = $0.timestamp
ts -= ts % 60 // remove seconds
// if one of these is greater than the other, adjust the latter too.
if flag || self.tsRangeA > ts {
self.tsRangeA = ts // lower end of minute
self.buttonRangeStart.setTitle(DateFormat.minutes(ts), for: .normal)
}
if !flag || ts > self.tsRangeB {
self.tsRangeB = ts + 59 // upper end of minute
self.buttonRangeEnd.setTitle(DateFormat.minutes(ts + 59), for: .normal)
}
})
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if gestureRecognizer.view == touch.view {
let newXMin = durationSlider.tag
let filterType: DateFilterKind
let orderType: DateFilterOrderBy
switch filterBy.selectedSegmentIndex {
case 0: filterType = (newXMin > 0) ? .LastXMin : .Off
case 1: filterType = .ABRange
default: preconditionFailure()
}
switch orderbyType.selectedSegmentIndex {
case 0: orderType = .Date
case 1: orderType = .Name
case 2: orderType = .Count
default: preconditionFailure()
}
sync.pause()
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()
}
sync.continue()
if gestureRecognizer.view === touch.view {
saveSettings()
dismiss(animated: true)
}
return false
}
private func saveSettings() {
let newXMin = durationSlider.tag
let filterType: DateFilterKind
let orderType: DateFilterOrderBy
switch filterBy.selectedSegmentIndex {
case 0: filterType = (newXMin > 0) ? .LastXMin : .Off
case 1: filterType = .ABRange
default: preconditionFailure()
}
switch orderbyType.selectedSegmentIndex {
case 0: orderType = .Date
case 1: orderType = .Name
case 2: orderType = .Count
default: preconditionFailure()
}
let a = Pref.DateFilter.OrderBy <-? orderType
let b = Pref.DateFilter.OrderAsc <-? (orderbyAsc.selectedSegmentIndex == 0)
if a || b {
NotifySortOrderChanged.post()
}
let c = Pref.DateFilter.Kind <-? filterType
let d = Pref.DateFilter.LastXMin <-? newXMin
let e = Pref.DateFilter.RangeA <-? (filterType == .ABRange ? tsRangeA : nil)
let f = Pref.DateFilter.RangeB <-? (filterType == .ABRange ? tsRangeB : nil)
if c || d || e || f {
NotifyDateFilterChanged.post()
}
}
}