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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user