Show "no results" in recordings + mark recording as shared

This commit is contained in:
relikd
2020-08-28 22:05:49 +02:00
parent 42aa7cf926
commit 448d69c6d8
8 changed files with 103 additions and 27 deletions

View File

@@ -33,3 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// This is a known issue and tolerated. // This is a known issue and tolerated.
} }
} }
extension URL {
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
}

View File

@@ -22,9 +22,12 @@ extension SQLiteDatabase {
} }
if version != 2 { if version != 2 {
// version 0 -> 1: req(domain) -> heap(fqdn, domain) // version 0 -> 1: req(domain) -> heap(fqdn, domain)
// version 1 -> 2: rec(+subtitle) // version 1 -> 2: rec(+subtitle, +opt)
if version == 1 { if version == 1 {
try run(sql: "ALTER TABLE rec ADD COLUMN subtitle TEXT;") transaction("""
ALTER TABLE rec ADD COLUMN subtitle TEXT;
ALTER TABLE rec ADD COLUMN opt INTEGER;
""")
} }
try run(sql: "PRAGMA user_version = 2;") try run(sql: "PRAGMA user_version = 2;")
} }
@@ -294,11 +297,13 @@ extension CreateTable {
appid TEXT, appid TEXT,
title TEXT, title TEXT,
subtitle TEXT, subtitle TEXT,
notes TEXT notes TEXT,
opt INTEGER
); );
"""} """}
} }
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, opt"
struct Recording { struct Recording {
let id: sqlite3_int64 let id: sqlite3_int64
let start: Timestamp let start: Timestamp
@@ -307,6 +312,7 @@ struct Recording {
var title: String? = nil var title: String? = nil
var subtitle: String? = nil var subtitle: String? = nil
var notes: String? = nil var notes: String? = nil
var shared: Bool = false
} }
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?) typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
@@ -335,8 +341,9 @@ extension SQLiteDatabase {
/// Update given recording by replacing `title`, `appid`, and `notes` with new values. /// Update given recording by replacing `title`, `appid`, and `notes` with new values.
func recordingUpdate(_ r: Recording) { func recordingUpdate(_ r: Recording) {
try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ? WHERE id = ? LIMIT 1;", try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ?, opt = ? WHERE id = ? LIMIT 1;",
bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle),
BindTextOrNil(r.notes), r.shared ? BindInt32(1) : BindNull(), BindInt64(r.id)]) { stmt -> Void in
sqlite3_step(stmt) sqlite3_step(stmt)
} }
} }
@@ -355,18 +362,20 @@ extension SQLiteDatabase {
private func readRecording(_ stmt: OpaquePointer) -> Recording { private func readRecording(_ stmt: OpaquePointer) -> Recording {
let end = col_ts(stmt, 2) let end = col_ts(stmt, 2)
let opt = sqlite3_column_int(stmt, 7)
return Recording(id: sqlite3_column_int64(stmt, 0), return Recording(id: sqlite3_column_int64(stmt, 0),
start: col_ts(stmt, 1), start: col_ts(stmt, 1),
stop: end == 0 ? nil : end, stop: end == 0 ? nil : end,
appId: col_text(stmt, 3), appId: col_text(stmt, 3),
title: col_text(stmt, 4), title: col_text(stmt, 4),
subtitle: col_text(stmt, 5), subtitle: col_text(stmt, 5),
notes: col_text(stmt, 6)) notes: col_text(stmt, 6),
shared: opt > 0)
} }
/// `WHERE stop IS NULL` /// `WHERE stop IS NULL`
func recordingGetOngoing() -> Recording? { func recordingGetOngoing() -> Recording? {
try? run(sql: "SELECT id, start, stop, appid, title, subtitle, notes FROM rec WHERE stop IS NULL LIMIT 1;") { try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NULL LIMIT 1;") {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return readRecording($0) return readRecording($0)
} }
@@ -382,14 +391,14 @@ extension SQLiteDatabase {
/// `WHERE stop IS NOT NULL` /// `WHERE stop IS NOT NULL`
func recordingGetAll() -> [Recording]? { func recordingGetAll() -> [Recording]? {
try? run(sql: "SELECT id, start, stop, appid, title, subtitle, notes FROM rec WHERE stop IS NOT NULL;") { try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NOT NULL;") {
allRows($0) { readRecording($0) } allRows($0) { readRecording($0) }
} }
} }
/// `WHERE id = ?` /// `WHERE id = ?`
private func recordingGet(withID: sqlite3_int64) throws -> Recording { private func recordingGet(withID: sqlite3_int64) throws -> Recording {
try run(sql: "SELECT id, start, stop, appid, title, subtitle, notes FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) { try run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return readRecording($0) return readRecording($0)
} }

View File

@@ -169,6 +169,10 @@ protocol DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32
} }
struct BindNull : DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_null(stmt, col) }
}
struct BindInt32 : DBBinding { struct BindInt32 : DBBinding {
let raw: Int32 let raw: Int32
init(_ value: Int32) { raw = value } init(_ value: Int32) { raw = value }

View File

@@ -31,8 +31,8 @@ func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> U
/// - Parameters: /// - Parameters:
/// - buttonText: Default: `"Continue"` /// - buttonText: Default: `"Continue"`
/// - buttonStyle: Default: `.default` /// - buttonStyle: Default: `.default`
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController { func AskAlert(title: String?, text: String?, buttonText: String = "Continue", cancelButton: String = "Cancel", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: "Cancel") let alert = Alert(title: title, text: text, buttonText: cancelButton)
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) }) alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
return alert return alert
} }
@@ -42,9 +42,7 @@ func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
AskAlert(title: "Notifications Disabled", AskAlert(title: "Notifications Disabled",
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.", text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.",
buttonText: "Open settings") { _ in buttonText: "Open settings") { _ in
if let url = URL(string: UIApplication.openSettingsURLString) { URL(string: UIApplication.openSettingsURLString)?.open()
UIApplication.shared.openURL(url)
}
}.presentIn(viewController) }.presentIn(viewController)
} }

View File

@@ -25,6 +25,13 @@ extension String {
let parts = components(separatedBy: ".") let parts = components(separatedBy: ".")
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
} }
func isValidBundleId() -> Bool {
let regex = try! NSRegularExpression(pattern: #"^[A-Za-z0-9\.\-]{1,155}$"#, options: .anchorsMatchLines)
let range = NSRange(location: 0, length: self.utf16.count)
let matches = regex.matches(in: self, options: .anchored, range: range)
return matches.count == 1
}
} }
private var listOfSLDs: [String : [String : Bool]] = { private var listOfSLDs: [String : [String : Bool]] = {

View File

@@ -212,6 +212,23 @@
</subviews> </subviews>
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="RecordNoResultsCell" textLabel="bmQ-Cn-BOm" style="IBUITableViewCellStyleDefault" id="JZ4-vZ-MnG">
<rect key="frame" x="0.0" y="172.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JZ4-vZ-MnG" id="TWb-p9-EMM">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text=" no results " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000001192092896" adjustsFontSizeToFit="NO" id="bmQ-Cn-BOm">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes> </prototypes>
<connections> <connections>
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/> <outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
@@ -222,7 +239,7 @@
<rightBarButtonItems> <rightBarButtonItems>
<barButtonItem systemItem="action" id="UkE-Wi-JjW"> <barButtonItem systemItem="action" id="UkE-Wi-JjW">
<connections> <connections>
<segue destination="P0a-ZP-uEV" kind="modal" id="s3J-zL-4zK"/> <segue destination="P0a-ZP-uEV" kind="modal" identifier="openContributeSegue" id="s3J-zL-4zK"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
<barButtonItem image="line-expand" id="xLc-O7-KVB"> <barButtonItem image="line-expand" id="xLc-O7-KVB">
@@ -274,8 +291,8 @@
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </textView>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eWC-xB-CJe"> <activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="eWC-xB-CJe">
<rect key="frame" x="292" y="12" width="20" height="20"/> <rect key="frame" x="275" y="3.5" width="37" height="37"/>
</activityIndicatorView> </activityIndicatorView>
</subviews> </subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
@@ -294,6 +311,7 @@
</view> </view>
<connections> <connections>
<outlet property="sendActivity" destination="eWC-xB-CJe" id="rx3-Jz-ppT"/> <outlet property="sendActivity" destination="eWC-xB-CJe" id="rx3-Jz-ppT"/>
<outlet property="sendButton" destination="PWY-06-ykI" id="eEf-hW-VIs"/>
<outlet property="text" destination="fFm-v5-DGy" id="fwm-RE-gHu"/> <outlet property="text" destination="fFm-v5-DGy" id="fwm-RE-gHu"/>
</connections> </connections>
</viewController> </viewController>

View File

@@ -2,6 +2,7 @@ import UIKit
class TVCRecordingDetails: UITableViewController, EditActionsRemove { class TVCRecordingDetails: UITableViewController, EditActionsRemove {
var record: Recording! var record: Recording!
var noResults: Bool = false
private lazy var isLongRecording: Bool = record.isLongTerm private lazy var isLongRecording: Bool = record.isLongTerm
@IBOutlet private var shareButton: UIBarButtonItem! @IBOutlet private var shareButton: UIBarButtonItem!
@@ -10,7 +11,8 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
/// Sorted by `ts` in ascending order (oldest first) /// Sorted by `ts` in ascending order (oldest first)
private lazy var dataSourceRaw: [DomainTsPair] = { private lazy var dataSourceRaw: [DomainTsPair] = {
let list = RecordingsDB.details(record) let list = RecordingsDB.details(record)
shareButton.isEnabled = list.count > 0 noResults = list.count == 0
shareButton.isEnabled = !noResults
return list return list
}() }()
/// Sorted by `count` (descending), then alphabetically /// Sorted by `count` (descending), then alphabetically
@@ -26,6 +28,14 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
override func viewDidLoad() { override func viewDidLoad() {
title = record.title ?? record.fallbackTitle title = record.title ?? record.fallbackTitle
NotifyRecordingChanged.observe(call: #selector(recordingDidChange(_:)), on: self)
}
@objc private func recordingDidChange(_ notification: Notification) {
let (rec, deleted) = notification.object as! (Recording, Bool)
if rec.id == record.id, !deleted {
record = rec // almost exclusively when 'shared' is set true
}
} }
@IBAction private func toggleDisplayStyle(_ sender: UIBarButtonItem) { @IBAction private func toggleDisplayStyle(_ sender: UIBarButtonItem) {
@@ -34,6 +44,20 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
tableView.reloadData() tableView.reloadData()
} }
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "openContributeSegue" && record.shared {
let alert = Alert(title: nil, text: "You have shared this recording already.")
if let bid = record.appId, bid.isValidBundleId() {
alert.addAction(UIAlertAction.init(title: "Open results", style: .default, handler: { _ in
URL(string: "http://127.0.0.1/redirect.html?id=\(bid)")?.open()
}))
}
alert.presentIn(self)
return false
}
return super.shouldPerformSegue(withIdentifier: identifier, sender: sender)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let tgt = segue.destination as? VCShareRecording { if let tgt = segue.destination as? VCShareRecording {
tgt.record = self.record tgt.record = self.record
@@ -44,12 +68,15 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
// MARK: - Table View Data Source // MARK: - Table View Data Source
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
showRaw ? dataSourceRaw.count : dataSourceSum.count max(1, showRaw ? dataSourceRaw.count : dataSourceSum.count)
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell let cell: UITableViewCell
if showRaw { if noResults {
cell = tableView.dequeueReusableCell(withIdentifier: "RecordNoResultsCell")!
cell.textLabel?.text = " empty recording "
} else if showRaw {
let x = dataSourceRaw[indexPath.row] let x = dataSourceRaw[indexPath.row]
if isLongRecording { if isLongRecording {
cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailLongCell")! cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailLongCell")!
@@ -73,11 +100,11 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
// MARK: - Editing // MARK: - Editing
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
getRowActionsIOS9(indexPath, tableView) noResults ? nil : getRowActionsIOS9(indexPath, tableView)
} }
@available(iOS 11.0, *) @available(iOS 11.0, *)
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
getRowActionsIOS11(indexPath) noResults ? nil : getRowActionsIOS11(indexPath)
} }
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool { func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
@@ -101,7 +128,8 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
tableView.deleteRows(at: [index], with: .automatic) tableView.deleteRows(at: [index], with: .automatic)
} }
} }
shareButton.isEnabled = dataSourceRaw.count > 0 noResults = dataSourceRaw.count == 0
shareButton.isEnabled = !noResults
return true return true
} }
} }

View File

@@ -6,11 +6,14 @@ class VCShareRecording : UIViewController {
private var jsonData: Data? private var jsonData: Data?
@IBOutlet private var text : UITextView! @IBOutlet private var text : UITextView!
@IBOutlet private var sendButton: UIBarButtonItem!
@IBOutlet private var sendActivity : UIActivityIndicatorView! @IBOutlet private var sendActivity : UIActivityIndicatorView!
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
sendButton.isEnabled = !record.shared
let start = record.start let start = record.start
let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start)) let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start))
let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)" let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)"
@@ -68,6 +71,7 @@ class VCShareRecording : UIViewController {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = "POST" request.httpMethod = "POST"
request.httpBody = jsonData request.httpBody = jsonData
var rec = record!
URLSession.shared.dataTask(with: request) { data, response, error in URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
@@ -87,6 +91,12 @@ class VCShareRecording : UIViewController {
self?.banner(.fail, "Server couldn't parse request.\nTry again later.") self?.banner(.fail, "Server couldn't parse request.\nTry again later.")
return return
} }
// update db, mark record as shared
sender.isEnabled = false
rec.shared = true // in case view was closed
self?.record = rec // in case view is still open
RecordingsDB.update(rec) // rec cause self may not be available
// notify user about results
var autoHide = true var autoHide = true
if v == 1, let urlStr = json?["url"] as? String { if v == 1, let urlStr = json?["url"] as? String {
let nextUpdateIn = json?["when"] as? Int let nextUpdateIn = json?["when"] as? Int
@@ -116,12 +126,10 @@ class VCShareRecording : UIViewController {
msg += "shortly. " msg += "shortly. "
} }
msg += "Open results webpage now?" msg += "Open results webpage now?"
let alert = Alert(title: "Thank you", text: msg, buttonText: "Not now") AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in
alert.addAction(UIAlertAction(title: "Show results", style: .default) { _ in
if let url = URL(string: urlStr) { if let url = URL(string: urlStr) {
UIApplication.shared.openURL(url) UIApplication.shared.openURL(url)
} }
}) }.presentIn(self)
alert.presentIn(self)
} }
} }