Show "no results" in recordings + mark recording as shared
This commit is contained in:
@@ -33,3 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// This is a known issue and tolerated.
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@ extension SQLiteDatabase {
|
||||
}
|
||||
if version != 2 {
|
||||
// version 0 -> 1: req(domain) -> heap(fqdn, domain)
|
||||
// version 1 -> 2: rec(+subtitle)
|
||||
// version 1 -> 2: rec(+subtitle, +opt)
|
||||
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;")
|
||||
}
|
||||
@@ -294,11 +297,13 @@ extension CreateTable {
|
||||
appid TEXT,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
notes TEXT
|
||||
notes TEXT,
|
||||
opt INTEGER
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, opt"
|
||||
struct Recording {
|
||||
let id: sqlite3_int64
|
||||
let start: Timestamp
|
||||
@@ -307,6 +312,7 @@ struct Recording {
|
||||
var title: String? = nil
|
||||
var subtitle: String? = nil
|
||||
var notes: String? = nil
|
||||
var shared: Bool = false
|
||||
}
|
||||
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.
|
||||
func recordingUpdate(_ r: Recording) {
|
||||
try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ? WHERE id = ? LIMIT 1;",
|
||||
bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in
|
||||
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), r.shared ? BindInt32(1) : BindNull(), BindInt64(r.id)]) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
@@ -355,18 +362,20 @@ extension SQLiteDatabase {
|
||||
|
||||
private func readRecording(_ stmt: OpaquePointer) -> Recording {
|
||||
let end = col_ts(stmt, 2)
|
||||
let opt = sqlite3_column_int(stmt, 7)
|
||||
return Recording(id: sqlite3_column_int64(stmt, 0),
|
||||
start: col_ts(stmt, 1),
|
||||
stop: end == 0 ? nil : end,
|
||||
appId: col_text(stmt, 3),
|
||||
title: col_text(stmt, 4),
|
||||
subtitle: col_text(stmt, 5),
|
||||
notes: col_text(stmt, 6))
|
||||
notes: col_text(stmt, 6),
|
||||
shared: opt > 0)
|
||||
}
|
||||
|
||||
/// `WHERE stop IS NULL`
|
||||
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)
|
||||
return readRecording($0)
|
||||
}
|
||||
@@ -382,14 +391,14 @@ extension SQLiteDatabase {
|
||||
|
||||
/// `WHERE stop IS NOT NULL`
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
/// `WHERE id = ?`
|
||||
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)
|
||||
return readRecording($0)
|
||||
}
|
||||
|
||||
@@ -169,6 +169,10 @@ protocol DBBinding {
|
||||
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 {
|
||||
let raw: Int32
|
||||
init(_ value: Int32) { raw = value }
|
||||
|
||||
@@ -31,8 +31,8 @@ func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> U
|
||||
/// - Parameters:
|
||||
/// - buttonText: Default: `"Continue"`
|
||||
/// - buttonStyle: Default: `.default`
|
||||
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
|
||||
let alert = Alert(title: title, text: text, buttonText: "Cancel")
|
||||
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: cancelButton)
|
||||
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
|
||||
return alert
|
||||
}
|
||||
@@ -42,9 +42,7 @@ func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
|
||||
AskAlert(title: "Notifications Disabled",
|
||||
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.",
|
||||
buttonText: "Open settings") { _ in
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.openURL(url)
|
||||
}
|
||||
URL(string: UIApplication.openSettingsURLString)?.open()
|
||||
}.presentIn(viewController)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ extension String {
|
||||
let parts = components(separatedBy: ".")
|
||||
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]] = {
|
||||
|
||||
@@ -212,6 +212,23 @@
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</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>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
|
||||
@@ -222,7 +239,7 @@
|
||||
<rightBarButtonItems>
|
||||
<barButtonItem systemItem="action" id="UkE-Wi-JjW">
|
||||
<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>
|
||||
</barButtonItem>
|
||||
<barButtonItem image="line-expand" id="xLc-O7-KVB">
|
||||
@@ -274,8 +291,8 @@
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eWC-xB-CJe">
|
||||
<rect key="frame" x="292" y="12" width="20" height="20"/>
|
||||
<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="275" y="3.5" width="37" height="37"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
@@ -294,6 +311,7 @@
|
||||
</view>
|
||||
<connections>
|
||||
<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"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
|
||||
@@ -2,6 +2,7 @@ import UIKit
|
||||
|
||||
class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
var record: Recording!
|
||||
var noResults: Bool = false
|
||||
private lazy var isLongRecording: Bool = record.isLongTerm
|
||||
|
||||
@IBOutlet private var shareButton: UIBarButtonItem!
|
||||
@@ -10,7 +11,8 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
/// Sorted by `ts` in ascending order (oldest first)
|
||||
private lazy var dataSourceRaw: [DomainTsPair] = {
|
||||
let list = RecordingsDB.details(record)
|
||||
shareButton.isEnabled = list.count > 0
|
||||
noResults = list.count == 0
|
||||
shareButton.isEnabled = !noResults
|
||||
return list
|
||||
}()
|
||||
/// Sorted by `count` (descending), then alphabetically
|
||||
@@ -26,6 +28,14 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
|
||||
override func viewDidLoad() {
|
||||
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) {
|
||||
@@ -34,6 +44,20 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
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?) {
|
||||
if let tgt = segue.destination as? VCShareRecording {
|
||||
tgt.record = self.record
|
||||
@@ -44,12 +68,15 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
// MARK: - Table View Data Source
|
||||
|
||||
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 {
|
||||
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]
|
||||
if isLongRecording {
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailLongCell")!
|
||||
@@ -73,11 +100,11 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
// MARK: - Editing
|
||||
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
noResults ? nil : getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
noResults ? nil : getRowActionsIOS11(indexPath)
|
||||
}
|
||||
|
||||
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
|
||||
@@ -101,7 +128,8 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
tableView.deleteRows(at: [index], with: .automatic)
|
||||
}
|
||||
}
|
||||
shareButton.isEnabled = dataSourceRaw.count > 0
|
||||
noResults = dataSourceRaw.count == 0
|
||||
shareButton.isEnabled = !noResults
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ class VCShareRecording : UIViewController {
|
||||
private var jsonData: Data?
|
||||
|
||||
@IBOutlet private var text : UITextView!
|
||||
@IBOutlet private var sendButton: UIBarButtonItem!
|
||||
@IBOutlet private var sendActivity : UIActivityIndicatorView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
sendButton.isEnabled = !record.shared
|
||||
|
||||
let start = record.start
|
||||
let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start))
|
||||
let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)"
|
||||
@@ -68,6 +71,7 @@ class VCShareRecording : UIViewController {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = jsonData
|
||||
var rec = record!
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error 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.")
|
||||
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
|
||||
if v == 1, let urlStr = json?["url"] as? String {
|
||||
let nextUpdateIn = json?["when"] as? Int
|
||||
@@ -116,12 +126,10 @@ class VCShareRecording : UIViewController {
|
||||
msg += "shortly. "
|
||||
}
|
||||
msg += "Open results webpage now?"
|
||||
let alert = Alert(title: "Thank you", text: msg, buttonText: "Not now")
|
||||
alert.addAction(UIAlertAction(title: "Show results", style: .default) { _ in
|
||||
AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in
|
||||
if let url = URL(string: urlStr) {
|
||||
UIApplication.shared.openURL(url)
|
||||
}
|
||||
})
|
||||
alert.presentIn(self)
|
||||
}.presentIn(self)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user