Search + lastXMin Filter + dynamic text size

This commit is contained in:
relikd
2020-05-13 01:37:50 +02:00
parent 9485d7e9b5
commit 8424c161b9
25 changed files with 865 additions and 211 deletions

View File

@@ -1,8 +1,24 @@
import UIKit
class TVCDomains: UITableViewController, IncrementalDataSourceUpdate {
class TVCDomains: UITableViewController, IncrementalDataSourceUpdate, UISearchBarDelegate {
internal var dataSource: [GroupedDomain] = []
private func dataSource(at: Int) -> GroupedDomain {
dataSource[(searchActive ? searchIndices[at] : at)]
}
private var searchActive: Bool = false
private var searchIndices: [Int] = []
private var searchTerm: String?
private let searchBar: UISearchBar = {
let x = UISearchBar(frame: CGRect.init(x: 0, y: 0, width: 20, height: 10))
x.sizeToFit()
x.showsCancelButton = true
x.autocapitalizationType = .none
x.autocorrectionType = .no
return x
}()
@IBOutlet private var filterButton: UIBarButtonItem!
@IBOutlet private var filterButtonDetail: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
@@ -12,30 +28,129 @@ class TVCDomains: UITableViewController, IncrementalDataSourceUpdate {
NotifyLogHistoryReset.observe(call: #selector(reloadDataSource), on: self)
reloadDataSource()
DBWrp.dataA_delegate = self
searchBar.delegate = self
NotifyDateFilterChanged.observe(call: #selector(dateFilterChanged), on: self)
dateFilterChanged()
}
@objc func reloadDataSource() {
dataSource = DBWrp.listOfDomains()
tableView.reloadData()
if searchActive {
searchBar(searchBar, textDidChange: "")
} else {
tableView.reloadData()
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let index = tableView.indexPathForSelectedRow?.row {
(segue.destination as? TVCHosts)?.parentDomain = dataSource[index].domain
(segue.destination as? TVCHosts)?.parentDomain = dataSource(at: index).domain
}
}
// MARK: - Table View Delegate
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count }
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int {
searchActive ? searchIndices.count : dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DomainCell")!
let entry = dataSource[indexPath.row]
let entry = dataSource(at: indexPath.row)
cell.textLabel?.text = entry.domain
cell.detailTextLabel?.text = entry.detailCellText
cell.imageView?.image = entry.options?.tableRowImage()
return cell
}
// MARK: - Search
@IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) {
setSearch(hidden: searchActive)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
setSearch(hidden: true)
}
private func setSearch(hidden: Bool) {
searchActive = !hidden
searchIndices = []
searchTerm = nil
searchBar.text = nil
tableView.tableHeaderView = hidden ? nil : searchBar
if !hidden { searchBar.becomeFirstResponder() }
tableView.reloadData()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
perform(#selector(performSearch), with: nil, afterDelay: 0.3)
}
@objc private func performSearch() {
searchTerm = searchBar.text?.lowercased() ?? ""
searchIndices = dataSource.enumerated().compactMap {
if $1.domain.lowercased().contains(searchTerm!) { return $0 }
return nil
}
tableView.reloadData()
}
func shouldLiveUpdateIncrementalDataSource() -> Bool { !searchActive }
func didUpdateIncrementalDataSource(_ operation: IncrementalDataSourceUpdateOperation, row: Int, moveTo: Int) {
guard searchActive else {
return
}
switch operation {
case .ReloadTable:
DispatchQueue.main.sync { tableView.reloadData() }
case .Insert:
if dataSource[row].domain.lowercased().contains(searchTerm ?? "") {
searchIndices.insert(row, at: 0)
DispatchQueue.main.sync { tableView.safeInsertRow(0, with: .left) }
}
case .Delete:
if let idx = searchIndices.firstIndex(of: row) {
searchIndices.remove(at: idx)
DispatchQueue.main.sync { tableView.safeDeleteRow(idx) }
}
case .Update, .Move:
if let idx = searchIndices.firstIndex(of: row) {
if operation == .Move { searchIndices[idx] = moveTo }
DispatchQueue.main.sync { tableView.safeReloadRow(idx) }
}
}
}
// MARK: - Filter
@IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) {
let vc = self.storyboard!.instantiateViewController(withIdentifier: "domainFilter")
vc.modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
vc.isModalInPresentation = true
}
present(vc, animated: true)
}
@objc private func dateFilterChanged() {
switch Pref.DateFilter.Kind {
case .ABRange: // read start/end time
self.filterButtonDetail.title = "AB"
self.filterButton.image = UIImage(named: "filter-filled")
case .LastXMin: // most recent
let lastXMin = Pref.DateFilter.LastXMin
if lastXMin == 0 { fallthrough }
self.filterButtonDetail.title = TimeFormat.short(minutes: lastXMin, style: .abbreviated)
self.filterButton.image = UIImage(named: "filter-filled")
default:
self.filterButtonDetail.title = ""
self.filterButton.image = UIImage(named: "filter-clear")
}
}
}

View File

@@ -0,0 +1,117 @@
import UIKit
// TODO: (count > x) filter
class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet private var segmentControl: UISegmentedControl!
@IBOutlet private var sectionTitle: UILabel!
// entries no older than
@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 rangeView: UIView!
@IBOutlet private var buttonRangeStart: UIButton!
@IBOutlet private var buttonRangeEnd: UIButton!
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
durationSlider.tag = -1 // otherwise wont update because `tag == 0`
durationSlider.value = Float(durationTimes.firstIndex(of: Pref.DateFilter.LastXMin) ?? 0) / 9
durationSliderChanged(durationSlider)
var a = Timestamp(4).asDateTime() // TODO: load from preferences
var b = Timestamp.now().asDateTime()
a.removeLast(3) // remove seconds
b.removeLast(3)
buttonRangeStart.setTitle(a, for: .normal)
buttonRangeEnd.setTitle(b, for: .normal)
}
@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 durationSliderChanged(_ sender: UISlider) {
let i = Int((sender.value + (0.499/9)) * 9) // mid-value-switch
guard i >= 0, i <= 9 else { return }
sender.value = Float(i) / 9
if sender.tag != durationTimes[i] {
sender.tag = durationTimes[i]
durationLabel.text = (sender.tag == 0 ? "Off" : TimeFormat.short(minutes: sender.tag))
}
}
@IBAction private func didTapRangeButton(_ sender: UIButton) {
// TODO: show date picker
}
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
}
if Pref.DateFilter.Kind != newKind || Pref.DateFilter.LastXMin != newXMin {
Pref.DateFilter.Kind = newKind
Pref.DateFilter.LastXMin = newXMin
DBWrp.reloadAfterDateFilterHasChanged()
NotifyDateFilterChanged.post()
}
dismiss(animated: true)
}
return false
}
}
// MARK: White Triangle Popup Arrow
@IBDesignable
class PopupTriangle: UIView {
@IBInspectable var rotation: CGFloat = 0
@IBInspectable var color: UIColor = .black
override func draw(_ rect: CGRect) {
guard let c = UIGraphicsGetCurrentContext() else { return }
let w = rect.width, h = rect.height
switch rotation {
case 90: // right
c.lineFromTo(x1: 0, y1: 0, x2: w, y2: h/2)
c.addLine(to: CGPoint(x: 0, y: h))
case 180: // bottom
c.lineFromTo(x1: w, y1: 0, x2: w/2, y2: h)
c.addLine(to: CGPoint(x: 0, y: 0))
case 270: // left
c.lineFromTo(x1: w, y1: h, x2: 0, y2: h/2)
c.addLine(to: CGPoint(x: w, y: 0))
default: // top
c.lineFromTo(x1: 0, y1: h, x2: w/2, y2: 0)
c.addLine(to: CGPoint(x: w, y: h))
}
c.closePath()
c.setFillColor(color.cgColor)
c.fillPath()
}
}