- Disable cell animations for huge changes
- Updating a cell keeps the old position whenever possible
- Async `didChangeDateFilter`
- Fixes bug where saving a recording would persist entries again
- Small changes to `TimeFormat`, `AlertDeleteLogs` and `binTreeIndex()`
This commit is contained in:
relikd
2020-06-04 17:07:37 +02:00
parent b17fb3c354
commit 7d6b071d8a
12 changed files with 142 additions and 83 deletions

View File

@@ -43,7 +43,7 @@ func AskAlert(title: String?, text: String?, buttonText: String = "Continue", bu
/// - buttons: Default: `[]`
/// - lastIsDestructive: Default: `false`
/// - cancelButtonText: Default: `"Dismiss"`
func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDestructive: Bool = false, cancelButtonText: String = "Dismiss", callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDestructive: Bool = false, cancelButtonText: String = "Cancel", callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
let alert = UIAlertController(title: title, message: text, preferredStyle: .actionSheet)
for (i, btn) in buttons.enumerated() {
let dangerous = (lastIsDestructive && i + 1 == buttons.count)
@@ -54,21 +54,13 @@ func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDe
}
func AlertDeleteLogs(_ domain: String, latest: Timestamp, success: @escaping (_ tsMin: Timestamp) -> Void) -> UIAlertController {
let sinceNow = Timestamp.now() - latest
var buttons = ["Last 5 minutes", "Last 15 minutes", "Last hour", "Last 24 hours", "Delete everything"]
var times: [Timestamp] = [300, 900, 3600, 86400]
while times.count > 0, times[0] < sinceNow {
buttons.removeFirst()
times.removeFirst()
}
return BottomAlert(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: buttons, lastIsDestructive: true, cancelButtonText: "Cancel") {
guard let idx = $0 else {
return
}
if idx >= times.count {
success(0)
} else {
success(Timestamp.now() - times[idx])
let minutesPassed = (Timestamp.now() - latest) / 60
let times: [Int] = [5, 15, 60, 1440].compactMap { minutesPassed < $0 ? $0 : nil }
let fmt = TimeFormat(.full, allowed: [.hour, .minute])
let labels = times.map { "Last " + (fmt.from(minutes: $0) ?? "?") }
return BottomAlert(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: labels + ["Delete everything"], lastIsDestructive: true) {
if let i = $0 {
success(i < times.count ? Timestamp.past(minutes: times[i]) : 0)
}
}
}

View File

@@ -19,10 +19,13 @@ extension Array {
/// Binary tree search operation.
/// - Warning: Array must be sorted already.
/// - Parameter mustExist: Determine whether to return low index or `nil` if element is missing.
/// - Parameters:
/// - mustExist: Determine whether to return low index or `nil` if element is missing.
/// - first: If `true`, keep searching for first matching element.
/// - Returns: Index or `nil` (only if `mustExist = true` and element does not exist).
/// - Complexity: O(log *n*), where *n* is the length of the array.
func binTreeIndex(of element: Element, compare fn: CompareFn, mustExist: Bool = false) -> Int? {
func binTreeIndex(of element: Element, compare fn: CompareFn, mustExist: Bool = false, findFirst: Bool = false) -> Int? {
var found = false
var lo = 0, hi = self.count - 1
while lo <= hi {
let mid = (lo + hi)/2
@@ -31,10 +34,17 @@ extension Array {
} else if fn(element, self[mid]) {
hi = mid - 1
} else {
return mid
if !findFirst { return mid } // exit early if we dont care about first index
hi = mid - 1
found = true
}
}
return mustExist ? nil : lo // not found, would be inserted at position lo
return (mustExist && !found) ? nil : lo // not found, would be inserted at position lo
}
/// Binary tree lookup whether element exists. Performs `binTreeIndex(of:compare:mustExist:)` internally.
func binTreeExists(_ element: Element, compare fn: CompareFn) -> Bool {
binTreeIndex(of: element, compare: fn, mustExist: true) != nil
}
/// Binary tree insert operation

View File

@@ -39,7 +39,27 @@ extension Timer {
}
}
// MARK: - TimeFormat
struct TimeFormat {
private var formatter: DateComponentsFormatter
/// Init new formatter with exactly 1 unit count. E.g., `61 min -> 1 hr`
/// - Parameter allowed: Default: `[.day, .hour, .minute, .second]`
init(_ style: DateComponentsFormatter.UnitsStyle, allowed: NSCalendar.Unit = [.day, .hour, .minute, .second]) {
formatter = DateComponentsFormatter()
formatter.maximumUnitCount = 1
formatter.allowedUnits = allowed
formatter.unitsStyle = style
}
/// Formatted duration string, e.g., `20 min` or `7 days`
func from(days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0) -> String? {
formatter.string(from: DateComponents(day: days, hour: hours, minute: minutes, second: seconds))
}
// MARK: static
/// Time string with format `HH:mm`
static func from(_ duration: Timestamp) -> String {
String(format: "%02d:%02d", duration / 60, duration % 60)
@@ -59,16 +79,4 @@ 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))
}
}