Search + lastXMin Filter + dynamic text size
This commit is contained in:
@@ -2,7 +2,7 @@ import UIKit
|
||||
|
||||
extension UIAlertController {
|
||||
func presentIn(_ viewController: UIViewController?) {
|
||||
viewController?.present(self, animated: true, completion: nil)
|
||||
viewController?.present(self, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func AlertDeleteLogs(_ domain: String, latest: Timestamp, success: @escaping (_
|
||||
if idx >= times.count {
|
||||
success(0)
|
||||
} else {
|
||||
success(Timestamp(Date().timeIntervalSince1970) - times[idx])
|
||||
success(Timestamp.now() - times[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
extension GroupedDomain {
|
||||
/// Return new `GroupedDomain` by adding `total` and `blocked` counts. Set `lastModified` to the maximum of the two.
|
||||
static func +(a: GroupedDomain, b: GroupedDomain) -> Self {
|
||||
GroupedDomain(domain: a.domain, total: a.total + b.total, blocked: a.blocked + b.blocked,
|
||||
lastModified: max(a.lastModified, b.lastModified), options: a.options ?? b.options )
|
||||
}
|
||||
/// Return new `GroupedDomain` by subtracting `total` and `blocked` counts.
|
||||
static func -(a: GroupedDomain, b: GroupedDomain) -> Self {
|
||||
GroupedDomain(domain: a.domain, total: a.total - b.total, blocked: a.blocked - b.blocked,
|
||||
lastModified: a.lastModified, options: a.options )
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == GroupedDomain {
|
||||
@@ -26,7 +32,9 @@ extension Recording {
|
||||
}
|
||||
|
||||
extension Timestamp {
|
||||
/// - Returns: Time string with format `yyyy-MM-dd HH:mm:ss`
|
||||
func asDateTime() -> String { dateTimeFormat.string(from: self) }
|
||||
func toDate() -> Date { Date(timeIntervalSince1970: Double(self)) }
|
||||
static func now() -> Timestamp { Timestamp(Date().timeIntervalSince1970) }
|
||||
static func past(minutes: Int) -> Timestamp { now() - Timestamp(minutes * 60) }
|
||||
}
|
||||
|
||||
@@ -99,6 +99,17 @@ struct TimeFormat {
|
||||
static func since(_ date: Date, millis: Bool = false) -> String {
|
||||
from(Date().timeIntervalSince(date), millis: millis)
|
||||
}
|
||||
/// Formatted duration string, e.g., `20 min` or `7 days`
|
||||
/// - Parameters:
|
||||
/// - minutes: Duration in minutes
|
||||
/// - style: Default: `.short`
|
||||
static func short(minutes: Int, style: DateComponentsFormatter.UnitsStyle = .short) -> String? {
|
||||
let dcf = DateComponentsFormatter()
|
||||
dcf.maximumUnitCount = 1
|
||||
dcf.allowedUnits = [.day, .hour, .minute]
|
||||
dcf.unitsStyle = style
|
||||
return dcf.string(from: DateComponents(minute: minutes))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState!
|
||||
let NotifyFilterChanged = NSNotification.Name("PSIFilterSettingsChanged") // nil!
|
||||
let NotifyDNSFilterChanged = NSNotification.Name("PSIDNSFilterSettingsChanged") // nil!
|
||||
let NotifyDateFilterChanged = NSNotification.Name("PSIDateFilterSettingsChanged") // nil!
|
||||
let NotifyLogHistoryReset = NSNotification.Name("PSILogHistoryReset") // nil!
|
||||
let NotifyRecordingChanged = NSNotification.Name("PSIRecordingChanged") // (Recording, deleted: Bool)!
|
||||
|
||||
|
||||
@@ -6,3 +6,36 @@ var currentVPNState: VPNState = .off
|
||||
public enum VPNState : Int {
|
||||
case on = 1, inbetween, off
|
||||
}
|
||||
|
||||
struct Pref {
|
||||
struct DidShowTutorial {
|
||||
static var Welcome: Bool {
|
||||
get { UserDefaults.standard.bool(forKey: "didShowTutorialAppWelcome") }
|
||||
set { UserDefaults.standard.set(newValue, forKey: "didShowTutorialAppWelcome") }
|
||||
}
|
||||
static var Recordings: Bool {
|
||||
get { UserDefaults.standard.bool(forKey: "didShowTutorialRecordings") }
|
||||
set { UserDefaults.standard.set(newValue, forKey: "didShowTutorialRecordings") }
|
||||
}
|
||||
}
|
||||
struct DateFilter {
|
||||
static var Kind: DateFilterKind {
|
||||
get { DateFilterKind(rawValue: UserDefaults.standard.integer(forKey: "dateFilterType"))! }
|
||||
set { UserDefaults.standard.set(newValue.rawValue, forKey: "dateFilterType") }
|
||||
}
|
||||
static var LastXMin: Int {
|
||||
get { UserDefaults.standard.integer(forKey: "dateFilterLastXMin") }
|
||||
set { UserDefaults.standard.set(newValue, forKey: "dateFilterLastXMin") }
|
||||
}
|
||||
|
||||
/// Return selected timestamp filter or `nil` if filtering is disabled.
|
||||
/// - Returns: `Timestamp.now() - LastXMin * 60`
|
||||
static func lastXMinTimestamp() -> Timestamp? {
|
||||
if Kind != .LastXMin { return nil }
|
||||
return Timestamp.past(minutes: Pref.DateFilter.LastXMin)
|
||||
}
|
||||
}
|
||||
}
|
||||
enum DateFilterKind: Int {
|
||||
case Off = 0, LastXMin = 1, ABRange = 2;
|
||||
}
|
||||
|
||||
@@ -39,60 +39,96 @@ extension UIRefreshControl {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Incremental Update Delegate
|
||||
|
||||
protocol IncrementalDataSourceUpdate : UITableViewController {
|
||||
var dataSource: [GroupedDomain] { get set }
|
||||
}
|
||||
|
||||
extension IncrementalDataSourceUpdate {
|
||||
func ifDisplayed(_ block: () -> Void) {
|
||||
DispatchQueue.main.sync {
|
||||
if self.tableView.window?.isKeyWindow ?? false {
|
||||
block()
|
||||
// TODO: custom handling if cell is being edited
|
||||
} else {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
func insertRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource.insert(obj, at: index)
|
||||
ifDisplayed {
|
||||
self.tableView.insertRows(at: [IndexPath(row: index)], with: .left)
|
||||
}
|
||||
}
|
||||
func moveRow(_ obj: GroupedDomain, from: Int, to: Int) {
|
||||
dataSource.remove(at: from)
|
||||
dataSource.insert(obj, at: to)
|
||||
ifDisplayed {
|
||||
let source = IndexPath(row: from)
|
||||
let cell = self.tableView.cellForRow(at: source)
|
||||
cell?.detailTextLabel?.text = obj.detailCellText
|
||||
self.tableView.moveRow(at: source, to: IndexPath(row: to))
|
||||
}
|
||||
}
|
||||
func replaceRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource[index] = obj
|
||||
ifDisplayed {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: index)], with: .automatic)
|
||||
}
|
||||
}
|
||||
func deleteRow(at index: Int) {
|
||||
dataSource.remove(at: index)
|
||||
ifDisplayed {
|
||||
self.tableView.deleteRows(at: [IndexPath(row: index)], with: .automatic)
|
||||
}
|
||||
}
|
||||
func replaceData(with newData: [GroupedDomain]) {
|
||||
dataSource = newData
|
||||
ifDisplayed {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: TableView extensions
|
||||
|
||||
extension IndexPath {
|
||||
/// Convenience init with `section: 0`
|
||||
public init(row: Int) { self.init(row: row, section: 0) }
|
||||
}
|
||||
|
||||
extension UITableView {
|
||||
/// Returns `true` if this `tableView` is the currently frontmost visible
|
||||
var isFrontmost: Bool { window?.isKeyWindow ?? false }
|
||||
|
||||
/// If frontmost window, perform `deleteRows()`; If not, perform `reloadData()`
|
||||
func safeDeleteRow(_ index: Int, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? deleteRows(at: [IndexPath(row: index)], with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `reloadRows()`; If not, perform `reloadData()`
|
||||
func safeReloadRow(_ index: Int, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? reloadRows(at: [IndexPath(row: index)], with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `insertRows()`; If not, perform `reloadData()`
|
||||
func safeInsertRow(_ index: Int, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? insertRows(at: [IndexPath(row: index)], with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `moveRow()`; If not, perform `reloadData()`
|
||||
func safeMoveRow(_ from: Int, to: Int) {
|
||||
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Incremental Update Delegate
|
||||
|
||||
enum IncrementalDataSourceUpdateOperation {
|
||||
case ReloadTable, Update, Insert, Delete, Move
|
||||
}
|
||||
|
||||
protocol IncrementalDataSourceUpdate : UITableViewController {
|
||||
var dataSource: [GroupedDomain] { get set }
|
||||
func shouldLiveUpdateIncrementalDataSource() -> Bool
|
||||
/// - Warning: Called on a background thread!
|
||||
/// - Parameters:
|
||||
/// - operation: Row update action
|
||||
/// - row: Which row index is affected? `IndexPath(row: row)`
|
||||
/// - moveTo: Only set for `Move` operation, otherwise `-1`
|
||||
func didUpdateIncrementalDataSource(_ operation: IncrementalDataSourceUpdateOperation, row: Int, moveTo: Int)
|
||||
}
|
||||
|
||||
extension IncrementalDataSourceUpdate {
|
||||
func shouldLiveUpdateIncrementalDataSource() -> Bool { true }
|
||||
func didUpdateIncrementalDataSource(_: IncrementalDataSourceUpdateOperation, row: Int, moveTo: Int) {}
|
||||
// TODO: custom handling if cell is being edited
|
||||
|
||||
func insertRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource.insert(obj, at: index)
|
||||
if shouldLiveUpdateIncrementalDataSource() {
|
||||
DispatchQueue.main.sync { tableView.safeInsertRow(index, with: .left) }
|
||||
}
|
||||
didUpdateIncrementalDataSource(.Insert, row: index, moveTo: -1)
|
||||
}
|
||||
func moveRow(_ obj: GroupedDomain, from: Int, to: Int) {
|
||||
dataSource.remove(at: from)
|
||||
dataSource.insert(obj, at: to)
|
||||
if shouldLiveUpdateIncrementalDataSource() {
|
||||
DispatchQueue.main.sync {
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: from))
|
||||
cell?.detailTextLabel?.text = obj.detailCellText
|
||||
tableView.safeMoveRow(from, to: to)
|
||||
}
|
||||
}
|
||||
didUpdateIncrementalDataSource(.Move, row: from, moveTo: to)
|
||||
}
|
||||
func replaceRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource[index] = obj
|
||||
if shouldLiveUpdateIncrementalDataSource() {
|
||||
DispatchQueue.main.sync { tableView.safeReloadRow(index) }
|
||||
}
|
||||
didUpdateIncrementalDataSource(.Update, row: index, moveTo: -1)
|
||||
}
|
||||
func deleteRow(at index: Int) {
|
||||
dataSource.remove(at: index)
|
||||
if shouldLiveUpdateIncrementalDataSource() {
|
||||
DispatchQueue.main.sync { tableView.safeDeleteRow(index) }
|
||||
}
|
||||
didUpdateIncrementalDataSource(.Delete, row: index, moveTo: -1)
|
||||
}
|
||||
func replaceData(with newData: [GroupedDomain]) {
|
||||
dataSource = newData
|
||||
if shouldLiveUpdateIncrementalDataSource() {
|
||||
DispatchQueue.main.sync { tableView.reloadData() }
|
||||
}
|
||||
didUpdateIncrementalDataSource(.ReloadTable, row: -1, moveTo: -1)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user