DatePickerAlert + DateFormat

This commit is contained in:
relikd
2020-06-11 01:32:50 +02:00
parent 946acc2460
commit 0a53898797
14 changed files with 195 additions and 72 deletions

View File

@@ -135,6 +135,8 @@
54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* TestDataSource.swift */; };
54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; };
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.swift */; };
54EFA4E6248EEE240022D618 /* DatePickerAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */; };
54EFA4E82491A16A0022D618 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E72491A16A0022D618 /* Font.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -295,6 +297,8 @@
54E540F3247D3F2600F7C34A /* TestDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataSource.swift; sourceTree = "<group>"; };
54E540F7247DB90F00F7C34A /* RecordingsDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsDB.swift; sourceTree = "<group>"; };
54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.swift; sourceTree = "<group>"; };
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerAlert.swift; sourceTree = "<group>"; };
54EFA4E72491A16A0022D618 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -418,6 +422,7 @@
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
54448A3124899A4000771C96 /* SearchBarManager.swift */,
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */,
);
path = "Common Classes";
sourceTree = "<group>";
@@ -442,6 +447,7 @@
54B345AA241BBA5B004C53CC /* AlertSheet.swift */,
54448A2F248647D900771C96 /* Time.swift */,
54751E502423955000168273 /* URL.swift */,
54EFA4E72491A16A0022D618 /* Font.swift */,
54448A2D2486464F00771C96 /* Array.swift */,
54D8B97B2471A7E000EB2414 /* String.swift */,
54B34595240F0513004C53CC /* TableView.swift */,
@@ -837,6 +843,7 @@
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */,
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
54D8B97C2471A7E000EB2414 /* String.swift in Sources */,
54EFA4E6248EEE240022D618 /* DatePickerAlert.swift in Sources */,
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */,
@@ -845,6 +852,7 @@
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */,
54EFA4E82491A16A0022D618 /* Font.swift in Sources */,
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */,
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */,
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */,

View File

@@ -0,0 +1,91 @@
import UIKit
class DatePickerAlert: UIViewController {
private var callback: (Date) -> Void
private let picker: UIDatePicker = {
let x = UIDatePicker()
let h = x.sizeThatFits(.zero).height
x.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: h)
return x
}()
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@discardableResult required init(presentIn viewController: UIViewController, configure: ((UIDatePicker) -> Void)? = nil, onSuccess: @escaping (Date) -> Void) {
callback = onSuccess
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
presentIn(viewController, configure)
}
internal override func loadView() {
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
save.titleLabel?.font = save.titleLabel?.font.bold()
//cancel.setTitleColor(.systemRed, for: .normal)
let buttons = UIStackView(arrangedSubviews: [cancel, save])
buttons.axis = .horizontal
buttons.distribution = .fillEqually
let bg = UIView(frame: picker.frame)
bg.frame.size.height += buttons.frame.height + 15
bg.frame.origin.y = UIScreen.main.bounds.height - bg.frame.height - 15
bg.backgroundColor = .sysBg
bg.addSubview(picker)
bg.addSubview(buttons)
let clearBg = UIView()
clearBg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
clearBg.addSubview(bg)
picker.anchor([.leading, .trailing, .top], to: bg)
picker.bottomAnchor =&= buttons.topAnchor
buttons.anchor([.leading, .trailing], to: bg)
buttons.bottomAnchor =&= bg.bottomAnchor - 15
bg.anchor([.leading, .trailing, .bottom], to: clearBg)
view = clearBg
view.isHidden = true // otherwise picker will flash on present
}
@objc private func didTapSave() {
dismiss(animated: true) {
self.callback(self.picker.date)
}
}
@objc private func didTapCancel() {
dismiss(animated: true)
}
private func presentIn(_ viewController: UIViewController, _ configure: ((UIDatePicker) -> Void)? = nil) {
viewController.present(self, animated: false) {
let control = self.view.subviews.first!
let prev = control.frame.origin.y
control.frame.origin.y += control.frame.height
self.view.isHidden = false
configure?(self.picker)
UIView.animate(withDuration: 0.3) {
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
control.frame.origin.y = prev
}
}
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.3, animations: {
let control = self.view.subviews.first!
self.view.backgroundColor = .clear
control.frame.origin.y += control.frame.height
}) { _ in
super.dismiss(animated: false, completion: completion)
}
}
}

View File

@@ -39,32 +39,3 @@ struct QuickUI {
return txt
}
}
extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body)
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
func h1(_ str: String) -> Self { normal(str, .title1) }
func h2(_ str: String) -> Self { normal(str, .title2) }
func h3(_ str: String) -> Self { normal(str, .title3) }
private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [
.font : withFont,
.foregroundColor : UIColor.sysFg
]))
return self
}
}
extension UIFont {
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: 0) // keep size as is
}
func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) }
}

View File

@@ -47,7 +47,6 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
let content = UIView()
x.addSubview(content)
content.translatesAutoresizingMaskIntoConstraints = false
content.anchor([.left, .right, .top, .bottom], to: x)
content.anchor([.width, .height], to: x) | .defaultLow
return x
@@ -62,7 +61,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
// MARK: Init
required init?(coder: NSCoder) { super.init(coder: coder) }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
required init() {
super.init(nibName: nil, bundle: nil)
@@ -98,7 +97,6 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
pager.numberOfPages += 1
updateButtonTitle()
let x = UIStackView(frame: pageScroll.bounds)
x.translatesAutoresizingMaskIntoConstraints = false
x.axis = .vertical
x.backgroundColor = UIColor.black
x.isOpaque = true
@@ -125,8 +123,6 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
sheetBg.addSubview(pageScroll)
sheetBg.addSubview(button)
for x in sheetBg.subviews { x.translatesAutoresizingMaskIntoConstraints = false }
pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: cornerRadius/2) | .defaultHigh

View File

@@ -130,6 +130,13 @@ extension SQLiteDatabase {
// MARK: read
func dnsLogsMinDate() -> Timestamp? {
try? run(sql:"SELECT min(ts) FROM heap") {
try ifStep($0, SQLITE_ROW)
return sqlite3_column_int64($0, 0)
}
}
/// Select min and max row id with given condition `ts >= ? AND ts < ?`
/// - Returns: `nil` in case no rows are matching the condition
func dnsLogsRowRange(between ts: Timestamp, and ts2: Timestamp) -> SQLiteRowRange? {

View File

@@ -16,8 +16,8 @@ extension GroupedDomain {
extension GroupedDomain {
var detailCellText: String { get {
return blocked > 0
? "\(lastModified.asDateTime())\(blocked)/\(total) blocked"
: "\(lastModified.asDateTime())\(total)"
? "\(DateFormat.seconds(lastModified))\(blocked)/\(total) blocked"
: "\(DateFormat.seconds(lastModified))\(total)"
}
}
}

View File

@@ -59,6 +59,7 @@ extension UIView {
private static let inverseItem: [NSLayoutConstraint.Attribute] = [.right, .bottom, .trailing, .lastBaseline, .rightMargin, .bottomMargin, .trailingMargin]
/// Create and active constraints for provided edges. Constraints will anchor the same edge on both `self` and `other`.
/// - Note: Will set `translatesAutoresizingMaskIntoConstraints = false`
/// - Parameters:
/// - edges: List of constraint attributes, e.g. `[.top, .bottom, .left, .right]`
/// - other: Instance to bind to, e.g. `UIView` or `UILayoutGuide`
@@ -66,7 +67,8 @@ extension UIView {
/// - rel: Constraint relation. (Default: `.equal`)
/// - Returns: List of created and active constraints
@discardableResult func anchor(_ edges: [NSLayoutConstraint.Attribute], to other: Any, margin: CGFloat = 0, if rel: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
edges.map {
translatesAutoresizingMaskIntoConstraints = false
return edges.map {
let (A, B) = UIView.inverseItem.contains($0) ? (other, self) : (self, other)
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on()
}

View File

@@ -0,0 +1,34 @@
import UIKit
extension UIFont {
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: 0) // keep size as is
}
func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) }
func monoSpace() -> UIFont {
let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue
return .monospacedDigitSystemFont(ofSize: pointSize, weight: .init(rawValue: weight))
}
}
extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body)
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
func h1(_ str: String) -> Self { normal(str, .title1) }
func h2(_ str: String) -> Self { normal(str, .title2) }
func h3(_ str: String) -> Self { normal(str, .title3) }
private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [
.font : withFont,
.foregroundColor : UIColor.sysFg
]))
return self
}
}

View File

@@ -1,7 +1,5 @@
import Foundation
private let dateTimeFormat = DateFormatter(withFormat: "yyyy-MM-dd HH:mm:ss")
extension DateFormatter {
convenience init(withFormat: String) {
self.init()
@@ -9,26 +7,18 @@ extension DateFormatter {
}
}
extension Timestamp {
/// Time string with format `yyyy-MM-dd HH:mm:ss`
func asDateTime() -> String {
dateTimeFormat.string(from: Date.init(timeIntervalSince1970: Double(self)))
}
extension Date {
/// Convert `Timestamp` to `Date`
func toDate() -> Date {
Date(timeIntervalSince1970: Double(self))
}
init(_ ts: Timestamp) { self.init(timeIntervalSince1970: Double(ts)) }
/// Convert `Date` to `Timestamp`
var timestamp: Timestamp { get { Timestamp(self.timeIntervalSince1970) } }
}
extension Timestamp {
/// Current time as `Timestamp` (second accuracy)
static func now() -> Timestamp {
Timestamp(Date().timeIntervalSince1970)
}
static func now() -> Timestamp { Date().timestamp }
/// Create `Timestamp` with `now() - minutes * 60`
static func past(minutes: Int) -> Timestamp {
now() - Timestamp(minutes * 60)
}
static func past(minutes: Int) -> Timestamp { now() - Timestamp(minutes * 60) }
}
extension Timer {
@@ -39,6 +29,24 @@ extension Timer {
}
}
// MARK: - DateFormat
enum DateFormat {
private static let _hms = DateFormatter(withFormat: "yyyy-MM-dd HH:mm:ss")
private static let _hm = DateFormatter(withFormat: "yyyy-MM-dd HH:mm")
/// Format: `yyyy-MM-dd HH:mm:ss`
static func seconds(_ date: Date) -> String { _hms.string(from: date) }
/// Format: `yyyy-MM-dd HH:mm:ss`
static func seconds(_ ts: Timestamp) -> String { _hms.string(from: Date(ts)) }
/// Format: `yyyy-MM-dd HH:mm`
static func minutes(_ date: Date) -> String { _hm.string(from: date) }
/// Format: `yyyy-MM-dd HH:mm`
static func minutes(_ ts: Timestamp) -> String { _hm.string(from: Date(ts)) }
}
// MARK: - TimeFormat
struct TimeFormat {

View File

@@ -69,7 +69,7 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove {
let x = dataSource[indexPath.row]
cell.textLabel?.text = x.title ?? x.fallbackTitle
cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil
cell.detailTextLabel?.text = "at \(x.start.asDateTime()), duration: \(x.durationString ?? "?")"
cell.detailTextLabel?.text = "at \(DateFormat.seconds(x.start)), duration: \(x.durationString ?? "?")"
return cell
}

View File

@@ -16,8 +16,8 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
inputTitle.text = record.title
inputNotes.text = record.notes
inputDetails.text = """
Start: \(record.start.asDateTime())
End: \(record.stop?.asDateTime() ?? "?")
Start: \(DateFormat.seconds(record.start))
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!))
Duration: \(record.durationString ?? "?")
"""
validateSaveButton()

View File

@@ -12,10 +12,7 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
prevRecController = (children.first as! UINavigationController)
prevRecController.delegate = self
// Duplicate font attributes but set monospace
let traits = timeLabel.font.fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
let weight = traits[.weight] as? CGFloat ?? UIFont.Weight.regular.rawValue
timeLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeLabel.font.pointSize, weight: UIFont.Weight(rawValue: weight))
timeLabel.font = timeLabel.font.monoSpace()
// hide timer if not running
updateUI(setRecording: false, animated: false)
currentRecording = RecordingsDB.getCurrent()
@@ -71,7 +68,7 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
guard let r = currentRecording, r.stop == nil else {
return
}
recordingTimer = Timer.repeating(0.086, call: #selector(timerCallback(_:)), on: self, userInfo: r.start.toDate())
recordingTimer = Timer.repeating(0.086, call: #selector(timerCallback(_:)), on: self, userInfo: Date(r.start))
updateUI(setRecording: true, animated: animate)
}

View File

@@ -67,7 +67,7 @@ class TVCHostDetails: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")!
let src = dataSource[indexPath.row]
cell.textLabel?.text = src.ts.asDateTime()
cell.textLabel?.text = DateFormat.seconds(src.ts)
cell.detailTextLabel?.text = (src.total > 1) ? "\(src.total)x" : nil
cell.imageView?.image = (src.blocked > 0 ? UIImage(named: "shield-x") : nil)
return cell

View File

@@ -18,6 +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()
// order by
@IBOutlet private var orderbyType: UISegmentedControl!
@@ -35,12 +37,11 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
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)
// 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)
orderbyType.selectedSegmentIndex = Pref.DateFilter.OrderBy.rawValue
orderbyAsc.selectedSegmentIndex = (Pref.DateFilter.OrderAsc ? 0 : 1)
@@ -65,7 +66,13 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
}
@IBAction private func didTapRangeButton(_ sender: UIButton) {
// TODO: show date picker
let flag = (sender == buttonRangeStart)
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)
})
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
@@ -85,6 +92,7 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
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
@@ -96,6 +104,7 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
Pref.DateFilter.LastXMin = newXMin
NotifyDateFilterChanged.post()
}
sync.continue()
dismiss(animated: true)
}
return false