diff --git a/main/AppDelegate.swift b/main/AppDelegate.swift
index 45001a7..6b4f0c0 100644
--- a/main/AppDelegate.swift
+++ b/main/AppDelegate.swift
@@ -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) }
+}
diff --git a/main/DB/DBAppOnly.swift b/main/DB/DBAppOnly.swift
index 7e10a08..9f62e32 100644
--- a/main/DB/DBAppOnly.swift
+++ b/main/DB/DBAppOnly.swift
@@ -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)
}
diff --git a/main/DB/DBCore.swift b/main/DB/DBCore.swift
index b737d8e..69e43d0 100644
--- a/main/DB/DBCore.swift
+++ b/main/DB/DBCore.swift
@@ -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 }
diff --git a/main/Extensions/AlertSheet.swift b/main/Extensions/AlertSheet.swift
index a290e49..92fb286 100644
--- a/main/Extensions/AlertSheet.swift
+++ b/main/Extensions/AlertSheet.swift
@@ -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)
}
diff --git a/main/Extensions/String.swift b/main/Extensions/String.swift
index 463dd9b..d21775f 100644
--- a/main/Extensions/String.swift
+++ b/main/Extensions/String.swift
@@ -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]] = {
diff --git a/main/GUI/Base.lproj/Recordings.storyboard b/main/GUI/Base.lproj/Recordings.storyboard
index 8018105..123f843 100644
--- a/main/GUI/Base.lproj/Recordings.storyboard
+++ b/main/GUI/Base.lproj/Recordings.storyboard
@@ -212,6 +212,23 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -222,7 +239,7 @@
-
+
@@ -274,8 +291,8 @@
-
-
+
+
@@ -294,6 +311,7 @@
+
diff --git a/main/Recordings/TVCRecordingDetails.swift b/main/Recordings/TVCRecordingDetails.swift
index 4618a83..bb28d93 100644
--- a/main/Recordings/TVCRecordingDetails.swift
+++ b/main/Recordings/TVCRecordingDetails.swift
@@ -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
}
}
diff --git a/main/Recordings/VCShareRecording.swift b/main/Recordings/VCShareRecording.swift
index 5d5fe9d..fb3ef7d 100644
--- a/main/Recordings/VCShareRecording.swift
+++ b/main/Recordings/VCShareRecording.swift
@@ -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)
}
}