308 lines
10 KiB
Swift
308 lines
10 KiB
Swift
import UIKit
|
|
|
|
protocol GroupedDomainDataSourceDelegate: UITableViewController {
|
|
/// Currently only called when a row is moved and the `tableView` is frontmost.
|
|
func groupedDomainDataSource(needsUpdate row: Int)
|
|
}
|
|
|
|
// ##########################
|
|
// #
|
|
// # MARK: DataSource
|
|
// #
|
|
// ##########################
|
|
|
|
class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
|
|
|
|
let parent: String?
|
|
private let pipeline = FilterPipeline<GroupedDomain>()
|
|
private var currentOrder: DateFilterOrderBy = .Date
|
|
private var orderAsc = false
|
|
|
|
private(set) lazy var search = SearchBarManager { [unowned self] _ in
|
|
self.pipeline.reloadFilter(withId: "search")
|
|
}
|
|
|
|
/// Will init `sync.allowPullToRefresh()` on `tableView.refreshControl` as well.
|
|
weak var delegate: GroupedDomainDataSourceDelegate? {
|
|
willSet { if #available(iOS 10.0, *), newValue !== delegate {
|
|
sync.allowPullToRefresh(onTVC: newValue, forObserver: self)
|
|
}}}
|
|
|
|
/// - Note: Will call `tableview.reloadData()`
|
|
init(withParent: String?) {
|
|
parent = withParent
|
|
let len: Int
|
|
if let p = withParent, p.first != "#" { len = p.count } else { len = 0 }
|
|
|
|
pipeline.addFilter("search") { [unowned self] in
|
|
!self.search.isActive ||
|
|
$0.domain.prefix($0.domain.count - len).lowercased().contains(self.search.term)
|
|
}
|
|
pipeline.delegate = self
|
|
resetSortingOrder(force: true)
|
|
|
|
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
|
|
NotifySortOrderChanged.observe(call: #selector(didChangeSortOrder), on: self)
|
|
|
|
sync.addObserver(self) // calls syncUpdate(reset:)
|
|
}
|
|
|
|
/// Callback fired when user changes date filter settings. (`NotifySortOrderChanged` notification)
|
|
@objc private func didChangeSortOrder(_ notification: Notification) {
|
|
resetSortingOrder()
|
|
}
|
|
|
|
/// 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 orderAscChanged = (orderAsc <-? Pref.DateFilter.OrderAsc)
|
|
let orderTypChanged = (currentOrder <-? Pref.DateFilter.OrderBy)
|
|
if orderTypChanged || 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 orderAscChanged {
|
|
pipeline.reverseSorting()
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
preconditionFailure("Domain independent filter reset not implemented") // `syncUpdate(reset:)` async!
|
|
}
|
|
if let x = pipeline.dataSourceGet(where: { $0.domain == domain }) {
|
|
var obj = x.object
|
|
obj.options = DomainFilter[domain]
|
|
pipeline.update(obj, at: x.index)
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: Table View Data Source
|
|
|
|
@inline(__always) var numberOfRows: Int { get { pipeline.displayObjectCount() } }
|
|
|
|
@inline(__always) subscript(_ row: Int) -> GroupedDomain { pipeline.displayObject(at: row) }
|
|
}
|
|
|
|
|
|
// ################################
|
|
// #
|
|
// # MARK: - Partial Update
|
|
// #
|
|
// ################################
|
|
|
|
extension GroupedDomainDataSource {
|
|
|
|
func syncUpdate(_: SyncUpdate, reset rows: SQLiteRowRange) {
|
|
var logs = AppDB?.dnsLogsGrouped(range: rows, parentDomain: parent) ?? []
|
|
for (i, val) in logs.enumerated() {
|
|
logs[i].options = DomainFilter[val.domain]
|
|
}
|
|
DispatchQueue.main.sync {
|
|
pipeline.reset(dataSource: logs)
|
|
}
|
|
}
|
|
|
|
func syncUpdate(_: SyncUpdate, insert rows: SQLiteRowRange, affects: SyncUpdateEnd) {
|
|
guard let latest = AppDB?.dnsLogsGrouped(range: rows, parentDomain: parent) else {
|
|
assertionFailure("NotifySyncInsert fired with empty range")
|
|
return
|
|
}
|
|
DispatchQueue.main.sync {
|
|
cellAnimationsGroup(if: latest.count > 14)
|
|
for x in latest {
|
|
if let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) {
|
|
pipeline.update(obj + x, at: i)
|
|
} else {
|
|
var y = x
|
|
y.options = DomainFilter[x.domain]
|
|
pipeline.addNew(y)
|
|
}
|
|
}
|
|
cellAnimationsCommit()
|
|
}
|
|
}
|
|
|
|
func syncUpdate(_ sender: SyncUpdate, remove rows: SQLiteRowRange, affects: SyncUpdateEnd) {
|
|
if affects == .Latest {
|
|
// TODO: alternatively query last modified from db (last entry _before_ range)
|
|
syncUpdate(sender, reset: sender.rows)
|
|
return
|
|
}
|
|
guard let outdated = AppDB?.dnsLogsGrouped(range: rows, parentDomain: parent),
|
|
outdated.count > 0 else {
|
|
return
|
|
}
|
|
DispatchQueue.main.sync {
|
|
cellAnimationsGroup(if: outdated.count > 14)
|
|
var listOfDeletes: [Int] = []
|
|
for x in outdated {
|
|
guard let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) else {
|
|
assertionFailure("Try to remove non-existent element")
|
|
continue // should never happen
|
|
}
|
|
if obj.total > x.total {
|
|
pipeline.update(obj - x, at: i)
|
|
} else {
|
|
listOfDeletes.append(i)
|
|
}
|
|
}
|
|
pipeline.remove(indices: listOfDeletes.sorted())
|
|
cellAnimationsCommit()
|
|
}
|
|
}
|
|
|
|
func syncUpdate(_ sender: SyncUpdate, partialRemove affectedFQDN: String) {
|
|
let affectedParent = affectedFQDN.extractDomain()
|
|
guard parent == nil || parent == affectedParent else {
|
|
return // does not affect current table
|
|
}
|
|
let affected = (parent == nil ? affectedParent : affectedFQDN)
|
|
let updated = AppDB?.dnsLogsGrouped(range: sender.rows, matchingDomain: affected, parentDomain: parent)?.first
|
|
DispatchQueue.main.sync {
|
|
guard let old = pipeline.dataSourceGet(where: { $0.domain == affected }) else {
|
|
// can only happen if delete sheet is open while background sync removed the element
|
|
return
|
|
}
|
|
if var updated = updated {
|
|
assert(old.object.domain == updated.domain)
|
|
updated.options = DomainFilter[updated.domain]
|
|
pipeline.update(updated, at: old.index)
|
|
} else {
|
|
pipeline.remove(indices: [old.index])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// #################################
|
|
// #
|
|
// # MARK: - Cell Animations
|
|
// #
|
|
// #################################
|
|
|
|
extension GroupedDomainDataSource {
|
|
/// Sets `pipeline.delegate = nil` to disable individual cell animations (update, insert, delete & move).
|
|
private func cellAnimationsGroup(if condition: Bool = true) {
|
|
if condition || delegate?.tableView.isFrontmost == false {
|
|
pipeline.delegate = nil
|
|
}
|
|
}
|
|
/// No-Op if cell animations are enabled already.
|
|
/// Else, set `pipeline.delegate = self` and perform `reloadData()`.
|
|
private func cellAnimationsCommit() {
|
|
if pipeline.delegate == nil {
|
|
pipeline.delegate = self
|
|
delegate?.tableView.reloadData()
|
|
}
|
|
}
|
|
|
|
// TODO: Collect animations and post them in a single animations block.
|
|
// This will require enormous work to translate them into a final set.
|
|
func filterPipelineDidReset() { delegate?.tableView.reloadData() }
|
|
func filterPipeline(delete rows: [Int]) { delegate?.tableView.safeDeleteRows(rows) }
|
|
func filterPipeline(insert row: Int) { delegate?.tableView.safeInsertRow(row, with: .left) }
|
|
func filterPipeline(update row: Int) {
|
|
guard let tv = delegate?.tableView else { return }
|
|
if !tv.isEditing { tv.safeReloadRow(row) }
|
|
else if tv.isFrontmost == true {
|
|
delegate?.groupedDomainDataSource(needsUpdate: row)
|
|
}
|
|
}
|
|
func filterPipeline(move oldRow: Int, to newRow: Int) {
|
|
delegate?.tableView.safeMoveRow(oldRow, to: newRow)
|
|
if delegate?.tableView.isFrontmost == true {
|
|
delegate?.groupedDomainDataSource(needsUpdate: newRow)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ##########################
|
|
// #
|
|
// # MARK: - Edit Row
|
|
// #
|
|
// ##########################
|
|
|
|
protocol GroupedDomainEditRow : UIViewController, EditableRows {
|
|
var source: GroupedDomainDataSource { get }
|
|
}
|
|
|
|
extension GroupedDomainEditRow {
|
|
|
|
func editableRowActions(_ index: IndexPath) -> [(RowAction, String)] {
|
|
let x = source[index.row]
|
|
if x.domain.starts(with: "#") {
|
|
return [(.delete, "Delete")]
|
|
}
|
|
let b = x.options?.contains(.blocked) ?? false
|
|
let i = x.options?.contains(.ignored) ?? false
|
|
return [(.delete, "Delete"), (.block, b ? "Unblock" : "Block"), (.ignore, i ? "Unignore" : "Ignore")]
|
|
}
|
|
|
|
func editableRowActionColor(_: IndexPath, _ action: RowAction) -> UIColor? {
|
|
action == .block ? .systemOrange : nil
|
|
}
|
|
|
|
func editableRowUserInfo(_ index: IndexPath) -> Any? { source[index.row] }
|
|
|
|
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
|
|
let entry = userInfo as! GroupedDomain
|
|
switch action {
|
|
case .ignore: showFilterSheet(entry, .ignored)
|
|
case .block: showFilterSheet(entry, .blocked)
|
|
case .delete:
|
|
let name = entry.domain
|
|
let flag = (source.parent != nil)
|
|
AlertDeleteLogs(name, latest: entry.lastModified) {
|
|
TheGreatDestroyer.deleteLogs(domain: name, since: $0, strict: flag)
|
|
}.presentIn(self)
|
|
}
|
|
return true
|
|
}
|
|
|
|
private func showFilterSheet(_ entry: GroupedDomain, _ filter: FilterOptions) {
|
|
if entry.options?.contains(filter) ?? false {
|
|
DomainFilter.update(entry.domain, remove: filter)
|
|
} else {
|
|
// TODO: alert sheet
|
|
DomainFilter.update(entry.domain, add: filter)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Extensions
|
|
extension TVCDomains : GroupedDomainEditRow {
|
|
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
|
getRowActionsIOS9(indexPath, tableView)
|
|
}
|
|
@available(iOS 11.0, *)
|
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
getRowActionsIOS11(indexPath)
|
|
}
|
|
}
|
|
|
|
extension TVCHosts : GroupedDomainEditRow {
|
|
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
|
getRowActionsIOS9(indexPath, tableView)
|
|
}
|
|
@available(iOS 11.0, *)
|
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
getRowActionsIOS11(indexPath)
|
|
}
|
|
}
|